1use std::collections::{HashMap, HashSet};
26
27use chrono::{DateTime, Utc};
28use serde::{Deserialize, Serialize};
29
30use crate::anchor::ContentAnchor;
31use crate::content::Block;
32use crate::extensions::Collaborator;
33use crate::DocumentState;
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct PhantomClusters {
39 pub version: String,
41
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub clusters: Vec<PhantomCluster>,
45}
46
47impl PhantomClusters {
48 #[must_use]
50 pub fn new() -> Self {
51 Self {
52 version: crate::SPEC_VERSION.to_string(),
53 clusters: Vec::new(),
54 }
55 }
56
57 pub fn add_cluster(&mut self, cluster: PhantomCluster) {
59 self.clusters.push(cluster);
60 }
61
62 #[must_use]
64 pub fn find_cluster(&self, id: &str) -> Option<&PhantomCluster> {
65 self.clusters.iter().find(|c| c.id == id)
66 }
67
68 #[must_use]
70 pub fn find_cluster_mut(&mut self, id: &str) -> Option<&mut PhantomCluster> {
71 self.clusters.iter_mut().find(|c| c.id == id)
72 }
73
74 #[must_use]
76 pub fn len(&self) -> usize {
77 self.clusters.len()
78 }
79
80 #[must_use]
82 pub fn is_empty(&self) -> bool {
83 self.clusters.is_empty()
84 }
85
86 #[must_use]
90 pub fn validate(&self, state: DocumentState) -> Vec<String> {
91 let mut errors = Vec::new();
92
93 for cluster in &self.clusters {
94 errors.extend(cluster.validate_connections(state));
95 }
96
97 errors
98 }
99}
100
101impl Default for PhantomClusters {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct PhantomCluster {
111 pub id: String,
113
114 pub anchor: ContentAnchor,
116
117 pub label: String,
119
120 #[serde(default)]
122 pub scope: PhantomScope,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub author: Option<Collaborator>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub created: Option<DateTime<Utc>>,
131
132 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
134 pub metadata: HashMap<String, serde_json::Value>,
135
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub phantoms: Vec<Phantom>,
139}
140
141impl PhantomCluster {
142 #[must_use]
144 pub fn new(id: impl Into<String>, anchor: ContentAnchor, label: impl Into<String>) -> Self {
145 Self {
146 id: id.into(),
147 anchor,
148 label: label.into(),
149 scope: PhantomScope::default(),
150 author: None,
151 created: Some(Utc::now()),
152 metadata: HashMap::new(),
153 phantoms: Vec::new(),
154 }
155 }
156
157 #[must_use]
159 pub fn with_scope(mut self, scope: PhantomScope) -> Self {
160 self.scope = scope;
161 self
162 }
163
164 #[must_use]
166 pub fn with_author(mut self, author: Collaborator) -> Self {
167 self.author = Some(author);
168 self
169 }
170
171 #[must_use]
173 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
174 self.metadata.insert(key.into(), value);
175 self
176 }
177
178 pub fn add_phantom(&mut self, phantom: Phantom) {
180 self.phantoms.push(phantom);
181 }
182
183 #[must_use]
185 pub fn with_phantom(mut self, phantom: Phantom) -> Self {
186 self.phantoms.push(phantom);
187 self
188 }
189
190 #[must_use]
192 pub fn find_phantom(&self, id: &str) -> Option<&Phantom> {
193 self.phantoms.iter().find(|p| p.id == id)
194 }
195
196 #[must_use]
198 pub fn find_phantom_mut(&mut self, id: &str) -> Option<&mut Phantom> {
199 self.phantoms.iter_mut().find(|p| p.id == id)
200 }
201
202 #[must_use]
210 pub fn validate_connections(&self, state: DocumentState) -> Vec<String> {
211 let mut errors = Vec::new();
212 let phantom_ids: HashSet<_> = self.phantoms.iter().map(|p| p.id.as_str()).collect();
213
214 for phantom in &self.phantoms {
215 for conn in &phantom.connections {
216 if !phantom_ids.contains(conn.target.as_str()) {
218 let msg = format!(
219 "Phantom '{}' has connection to non-existent target '{}' in cluster '{}'",
220 phantom.id, conn.target, self.id
221 );
222 if state.is_immutable() {
223 errors.push(format!("ERROR: {msg}"));
224 } else {
225 errors.push(format!("WARNING: {msg}"));
226 }
227 }
228
229 if conn.target == phantom.id {
231 let msg = format!(
232 "Phantom '{}' has self-referential connection in cluster '{}'",
233 phantom.id, self.id
234 );
235 if state.is_immutable() {
236 errors.push(format!("ERROR: {msg}"));
237 } else {
238 errors.push(format!("WARNING: {msg}"));
239 }
240 }
241 }
242 }
243
244 if let Some(cycle) = self.detect_cycle() {
246 let msg = format!(
247 "Circular connection detected in cluster '{}': {}",
248 self.id,
249 cycle.join(" -> ")
250 );
251 if state.is_immutable() {
252 errors.push(format!("ERROR: {msg}"));
253 } else {
254 errors.push(format!("WARNING: {msg}"));
255 }
256 }
257
258 errors
259 }
260
261 fn detect_cycle(&self) -> Option<Vec<String>> {
264 let mut visited = HashSet::new();
265 let mut rec_stack = HashSet::new();
266 let mut path = Vec::new();
267
268 for phantom in &self.phantoms {
269 if !visited.contains(&phantom.id) {
270 if let Some(cycle) =
271 self.detect_cycle_dfs(&phantom.id, &mut visited, &mut rec_stack, &mut path)
272 {
273 return Some(cycle);
274 }
275 }
276 }
277
278 None
279 }
280
281 fn detect_cycle_dfs(
282 &self,
283 node: &str,
284 visited: &mut HashSet<String>,
285 rec_stack: &mut HashSet<String>,
286 path: &mut Vec<String>,
287 ) -> Option<Vec<String>> {
288 visited.insert(node.to_string());
289 rec_stack.insert(node.to_string());
290 path.push(node.to_string());
291
292 if let Some(phantom) = self.find_phantom(node) {
293 for conn in &phantom.connections {
294 if !visited.contains(&conn.target) {
295 if let Some(cycle) =
296 self.detect_cycle_dfs(&conn.target, visited, rec_stack, path)
297 {
298 return Some(cycle);
299 }
300 } else if rec_stack.contains(&conn.target) {
301 let mut cycle = Vec::new();
303 let mut found = false;
304 for p in path.iter() {
305 if p == &conn.target || found {
306 found = true;
307 cycle.push(p.clone());
308 }
309 }
310 cycle.push(conn.target.clone());
311 return Some(cycle);
312 }
313 }
314 }
315
316 path.pop();
317 rec_stack.remove(node);
318 None
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct Phantom {
326 pub id: String,
328
329 pub position: PhantomPosition,
331
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub size: Option<PhantomSize>,
335
336 pub content: PhantomContent,
338
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
341 pub connections: Vec<PhantomConnection>,
342
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub created: Option<DateTime<Utc>>,
346
347 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub author: Option<Collaborator>,
350}
351
352impl Phantom {
353 #[must_use]
355 pub fn new(id: impl Into<String>, position: PhantomPosition, content: PhantomContent) -> Self {
356 Self {
357 id: id.into(),
358 position,
359 size: None,
360 content,
361 connections: Vec::new(),
362 created: Some(Utc::now()),
363 author: None,
364 }
365 }
366
367 #[must_use]
369 pub fn with_size(mut self, size: PhantomSize) -> Self {
370 self.size = Some(size);
371 self
372 }
373
374 #[must_use]
376 pub fn with_author(mut self, author: Collaborator) -> Self {
377 self.author = Some(author);
378 self
379 }
380
381 #[must_use]
383 pub fn with_connection(mut self, connection: PhantomConnection) -> Self {
384 self.connections.push(connection);
385 self
386 }
387
388 #[must_use]
390 pub fn connect_to(mut self, target_id: impl Into<String>) -> Self {
391 self.connections.push(PhantomConnection::new(target_id));
392 self
393 }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
398pub struct PhantomPosition {
399 pub x: f64,
401 pub y: f64,
403}
404
405impl PhantomPosition {
406 #[must_use]
408 pub fn new(x: f64, y: f64) -> Self {
409 Self { x, y }
410 }
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
415pub struct PhantomSize {
416 pub width: f64,
418 pub height: f64,
420}
421
422impl PhantomSize {
423 #[must_use]
425 pub fn new(width: f64, height: f64) -> Self {
426 Self { width, height }
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct PhantomConnection {
434 pub target: String,
436
437 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub style: Option<ConnectionStyle>,
440
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub label: Option<String>,
444}
445
446impl PhantomConnection {
447 #[must_use]
449 pub fn new(target: impl Into<String>) -> Self {
450 Self {
451 target: target.into(),
452 style: None,
453 label: None,
454 }
455 }
456
457 #[must_use]
459 pub fn with_style(mut self, style: ConnectionStyle) -> Self {
460 self.style = Some(style);
461 self
462 }
463
464 #[must_use]
466 pub fn with_label(mut self, label: impl Into<String>) -> Self {
467 self.label = Some(label.into());
468 self
469 }
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
474#[serde(rename_all = "lowercase")]
475pub enum ConnectionStyle {
476 #[default]
478 Line,
479 Arrow,
481 Dashed,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase", tag = "type")]
488pub enum PhantomScope {
489 #[default]
491 Shared,
492 Private,
494 Role {
496 role: String,
498 },
499}
500
501impl PhantomScope {
502 #[must_use]
504 pub fn role(role: impl Into<String>) -> Self {
505 Self::Role { role: role.into() }
506 }
507}
508
509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct PhantomContent {
513 pub blocks: Vec<Block>,
515}
516
517impl PhantomContent {
518 #[must_use]
520 pub fn new(blocks: Vec<Block>) -> Self {
521 Self { blocks }
522 }
523
524 #[must_use]
526 pub fn paragraph(text: impl Into<String>) -> Self {
527 use crate::content::Text;
528 Self {
529 blocks: vec![Block::paragraph(vec![Text::plain(text)])],
530 }
531 }
532
533 #[must_use]
535 pub fn is_empty(&self) -> bool {
536 self.blocks.is_empty()
537 }
538}
539
540impl From<Vec<Block>> for PhantomContent {
541 fn from(blocks: Vec<Block>) -> Self {
542 Self::new(blocks)
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn test_phantom_clusters_new() {
552 let clusters = PhantomClusters::new();
553 assert_eq!(clusters.version, "0.1");
554 assert!(clusters.is_empty());
555 }
556
557 #[test]
558 fn test_phantom_cluster_creation() {
559 let anchor = ContentAnchor::block("para-1");
560 let cluster = PhantomCluster::new("cluster-1", anchor, "Test Cluster");
561
562 assert_eq!(cluster.id, "cluster-1");
563 assert_eq!(cluster.label, "Test Cluster");
564 assert!(matches!(cluster.scope, PhantomScope::Shared));
565 }
566
567 #[test]
568 fn test_phantom_cluster_with_phantoms() {
569 let anchor = ContentAnchor::range("para-1", 10, 25);
570 let content = PhantomContent::paragraph("Note text");
571 let phantom = Phantom::new("phantom-1", PhantomPosition::new(100.0, 50.0), content);
572
573 let cluster =
574 PhantomCluster::new("cluster-1", anchor, "Notes").with_phantom(phantom.clone());
575
576 assert_eq!(cluster.phantoms.len(), 1);
577 assert!(cluster.find_phantom("phantom-1").is_some());
578 }
579
580 #[test]
581 fn test_phantom_connections() {
582 let anchor = ContentAnchor::block("para-1");
583
584 let phantom1 = Phantom::new(
585 "p1",
586 PhantomPosition::new(0.0, 0.0),
587 PhantomContent::paragraph("First"),
588 )
589 .connect_to("p2");
590
591 let phantom2 = Phantom::new(
592 "p2",
593 PhantomPosition::new(100.0, 0.0),
594 PhantomContent::paragraph("Second"),
595 );
596
597 let cluster = PhantomCluster::new("cluster-1", anchor, "Test")
598 .with_phantom(phantom1)
599 .with_phantom(phantom2);
600
601 assert_eq!(cluster.phantoms[0].connections.len(), 1);
602 assert_eq!(cluster.phantoms[0].connections[0].target, "p2");
603 }
604
605 #[test]
606 fn test_connection_validation_valid() {
607 let anchor = ContentAnchor::block("para-1");
608
609 let phantom1 = Phantom::new(
610 "p1",
611 PhantomPosition::new(0.0, 0.0),
612 PhantomContent::paragraph("First"),
613 )
614 .connect_to("p2");
615
616 let phantom2 = Phantom::new(
617 "p2",
618 PhantomPosition::new(100.0, 0.0),
619 PhantomContent::paragraph("Second"),
620 );
621
622 let cluster = PhantomCluster::new("cluster-1", anchor, "Test")
623 .with_phantom(phantom1)
624 .with_phantom(phantom2);
625
626 let errors = cluster.validate_connections(DocumentState::Draft);
627 assert!(errors.is_empty(), "Valid connections: {errors:?}");
628 }
629
630 #[test]
631 fn test_connection_validation_missing_target() {
632 let anchor = ContentAnchor::block("para-1");
633
634 let phantom1 = Phantom::new(
635 "p1",
636 PhantomPosition::new(0.0, 0.0),
637 PhantomContent::paragraph("First"),
638 )
639 .connect_to("nonexistent");
640
641 let cluster = PhantomCluster::new("cluster-1", anchor, "Test").with_phantom(phantom1);
642
643 let errors = cluster.validate_connections(DocumentState::Draft);
644 assert_eq!(errors.len(), 1);
645 assert!(errors[0].starts_with("WARNING:"));
646
647 let errors = cluster.validate_connections(DocumentState::Frozen);
648 assert_eq!(errors.len(), 1);
649 assert!(errors[0].starts_with("ERROR:"));
650 }
651
652 #[test]
653 fn test_connection_validation_self_reference() {
654 let anchor = ContentAnchor::block("para-1");
655
656 let phantom1 = Phantom::new(
657 "p1",
658 PhantomPosition::new(0.0, 0.0),
659 PhantomContent::paragraph("First"),
660 )
661 .connect_to("p1"); let cluster = PhantomCluster::new("cluster-1", anchor, "Test").with_phantom(phantom1);
664
665 let errors = cluster.validate_connections(DocumentState::Frozen);
666 assert!(!errors.is_empty());
667 assert!(errors.iter().any(|e| e.contains("self-referential")));
668 }
669
670 #[test]
671 fn test_connection_validation_cycle() {
672 let anchor = ContentAnchor::block("para-1");
673
674 let phantom1 = Phantom::new(
675 "p1",
676 PhantomPosition::new(0.0, 0.0),
677 PhantomContent::paragraph("First"),
678 )
679 .connect_to("p2");
680
681 let phantom2 = Phantom::new(
682 "p2",
683 PhantomPosition::new(100.0, 0.0),
684 PhantomContent::paragraph("Second"),
685 )
686 .connect_to("p3");
687
688 let phantom3 = Phantom::new(
689 "p3",
690 PhantomPosition::new(200.0, 0.0),
691 PhantomContent::paragraph("Third"),
692 )
693 .connect_to("p1"); let cluster = PhantomCluster::new("cluster-1", anchor, "Test")
696 .with_phantom(phantom1)
697 .with_phantom(phantom2)
698 .with_phantom(phantom3);
699
700 let errors = cluster.validate_connections(DocumentState::Frozen);
701 assert!(!errors.is_empty());
702 assert!(errors.iter().any(|e| e.contains("Circular")));
703 }
704
705 #[test]
706 fn test_phantom_scope_variants() {
707 let shared = PhantomScope::Shared;
708 let json = serde_json::to_string(&shared).unwrap();
709 assert!(json.contains("\"type\":\"shared\""));
710
711 let private = PhantomScope::Private;
712 let json = serde_json::to_string(&private).unwrap();
713 assert!(json.contains("\"type\":\"private\""));
714
715 let role = PhantomScope::role("editor");
716 let json = serde_json::to_string(&role).unwrap();
717 assert!(json.contains("\"type\":\"role\""));
718 assert!(json.contains("\"role\":\"editor\""));
719 }
720
721 #[test]
722 fn test_phantom_clusters_serialization() {
723 let anchor = ContentAnchor::block("para-1");
724 let content = PhantomContent::paragraph("Note");
725 let phantom = Phantom::new("p1", PhantomPosition::new(50.0, 50.0), content)
726 .with_size(PhantomSize::new(200.0, 100.0));
727
728 let cluster = PhantomCluster::new("cluster-1", anchor, "Notes")
729 .with_scope(PhantomScope::Private)
730 .with_phantom(phantom);
731
732 let mut clusters = PhantomClusters::new();
733 clusters.add_cluster(cluster);
734
735 let json = serde_json::to_string_pretty(&clusters).unwrap();
736 assert!(json.contains("\"version\": \"0.1\""));
737 assert!(json.contains("\"clusters\":"));
738 assert!(json.contains("\"phantoms\":"));
739
740 let parsed: PhantomClusters = serde_json::from_str(&json).unwrap();
742 assert_eq!(parsed.clusters.len(), 1);
743 assert_eq!(parsed.clusters[0].phantoms.len(), 1);
744 }
745
746 #[test]
747 fn test_connection_style() {
748 let conn = PhantomConnection::new("target")
749 .with_style(ConnectionStyle::Arrow)
750 .with_label("relates to");
751
752 let json = serde_json::to_string(&conn).unwrap();
753 assert!(json.contains("\"target\":\"target\""));
754 assert!(json.contains("\"style\":\"arrow\""));
755 assert!(json.contains("\"label\":\"relates to\""));
756 }
757}