use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use ucm_core::{BlockId, EdgeType};
#[derive(Debug, Clone)]
pub struct TraversalCursor {
pub position: BlockId,
pub neighborhood: CursorNeighborhood,
pub breadcrumbs: VecDeque<BlockId>,
pub view_mode: ViewMode,
max_breadcrumbs: usize,
}
impl TraversalCursor {
pub fn new(position: BlockId, max_breadcrumbs: usize) -> Self {
Self {
position,
neighborhood: CursorNeighborhood::default(),
breadcrumbs: VecDeque::new(),
view_mode: ViewMode::default(),
max_breadcrumbs,
}
}
pub fn move_to(&mut self, new_position: BlockId) {
self.breadcrumbs.push_back(self.position);
if self.breadcrumbs.len() > self.max_breadcrumbs {
self.breadcrumbs.pop_front();
}
self.position = new_position;
self.neighborhood.stale = true;
}
pub fn go_back(&mut self, steps: usize) -> Option<BlockId> {
for _ in 0..steps {
if let Some(prev) = self.breadcrumbs.pop_back() {
self.position = prev;
self.neighborhood.stale = true;
} else {
return None;
}
}
Some(self.position)
}
pub fn can_go_back(&self) -> bool {
!self.breadcrumbs.is_empty()
}
pub fn history_depth(&self) -> usize {
self.breadcrumbs.len()
}
pub fn clear_history(&mut self) {
self.breadcrumbs.clear();
}
pub fn update_neighborhood(&mut self, neighborhood: CursorNeighborhood) {
self.neighborhood = neighborhood;
self.neighborhood.stale = false;
self.neighborhood.computed_at = Utc::now();
}
pub fn needs_refresh(&self) -> bool {
self.neighborhood.stale
}
}
#[derive(Debug, Clone)]
pub struct CursorNeighborhood {
pub ancestors: Vec<BlockId>,
pub children: Vec<BlockId>,
pub siblings: Vec<BlockId>,
pub connections: Vec<(BlockId, EdgeType)>,
pub computed_at: DateTime<Utc>,
pub stale: bool,
}
impl Default for CursorNeighborhood {
fn default() -> Self {
Self::new()
}
}
impl CursorNeighborhood {
pub fn new() -> Self {
Self {
ancestors: Vec::new(),
children: Vec::new(),
siblings: Vec::new(),
connections: Vec::new(),
computed_at: Utc::now(),
stale: true,
}
}
pub fn total_blocks(&self) -> usize {
self.ancestors.len() + self.children.len() + self.siblings.len() + self.connections.len()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum ViewMode {
IdsOnly,
Preview { length: usize },
#[default]
Full,
Metadata,
Adaptive { interest_threshold: f32 },
}
impl ViewMode {
pub fn preview(length: usize) -> Self {
Self::Preview { length }
}
pub fn adaptive(threshold: f32) -> Self {
Self::Adaptive {
interest_threshold: threshold,
}
}
}
impl From<ucl_parser::ast::ViewMode> for ViewMode {
fn from(mode: ucl_parser::ast::ViewMode) -> Self {
match mode {
ucl_parser::ast::ViewMode::Full => ViewMode::Full,
ucl_parser::ast::ViewMode::Preview { length } => ViewMode::Preview { length },
ucl_parser::ast::ViewMode::Metadata => ViewMode::Metadata,
ucl_parser::ast::ViewMode::IdsOnly => ViewMode::IdsOnly,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block_id(s: &str) -> BlockId {
s.parse().unwrap_or_else(|_| {
let mut bytes = [0u8; 12];
let s_bytes = s.as_bytes();
for (i, b) in s_bytes.iter().enumerate() {
bytes[i % 12] ^= *b;
}
BlockId::from_bytes(bytes)
})
}
#[test]
fn test_cursor_movement() {
let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 10);
cursor.move_to(block_id("blk_000000000002"));
assert_eq!(cursor.position, block_id("blk_000000000002"));
assert_eq!(cursor.breadcrumbs.len(), 1);
cursor.move_to(block_id("blk_000000000003"));
assert_eq!(cursor.position, block_id("blk_000000000003"));
assert_eq!(cursor.breadcrumbs.len(), 2);
cursor.go_back(1);
assert_eq!(cursor.position, block_id("blk_000000000002"));
assert_eq!(cursor.breadcrumbs.len(), 1);
}
#[test]
fn test_cursor_history_limit() {
let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 3);
for i in 2..=5 {
cursor.move_to(block_id(&format!("blk_00000000000{}", i)));
}
assert_eq!(cursor.breadcrumbs.len(), 3);
}
#[test]
fn test_neighborhood_staleness() {
let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 10);
assert!(cursor.needs_refresh());
cursor.update_neighborhood(CursorNeighborhood::new());
assert!(!cursor.needs_refresh());
cursor.move_to(block_id("blk_000000000002"));
assert!(cursor.needs_refresh());
}
}