ascii_dag/graph.rs
1//! Core DAG (Directed Acyclic Graph) data structure.
2//!
3//! This module provides the fundamental graph structure with nodes and edges.
4//!
5//! ## Performance Characteristics
6//!
7//! - **Node/Edge Insertion**: O(1) amortized with HashMap and cached adjacency lists
8//! - **Child/Parent Lookups**: O(1) via cached adjacency lists (not O(E) iteration)
9//! - **ID→Index Mapping**: O(1) via HashMap (not O(N) scan)
10//! - **Node Width**: O(1) via pre-computed cache
11//!
12//! ## Memory Overhead
13//!
14//! Per node:
15//! - ~100 bytes (node data, caches, adjacency list headers)
16//!
17//! Per edge:
18//! - ~16 bytes (adjacency list entries, both directions)
19//!
20//! ## Security
21//!
22//! - No unsafe code
23//! - For untrusted input, consider limiting maximum nodes/edges to prevent resource exhaustion
24//! - Maximum node ID: `usize::MAX` (up to 20 decimal digits)
25
26use alloc::{string::String, vec::Vec};
27
28#[cfg(feature = "std")]
29use std::collections::{HashMap, HashSet};
30
31#[cfg(not(feature = "std"))]
32use alloc::collections::{BTreeMap as HashMap, BTreeSet as HashSet};
33
34/// Rendering mode for the DAG visualization.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RenderMode {
37    /// Render chains vertically (takes more vertical space)
38    Vertical,
39
40    /// Render chains horizontally when possible (compact, one-line for simple chains)
41    Horizontal,
42
43    /// Auto-detect: horizontal for simple chains, vertical for complex graphs
44    Auto,
45}
46
47impl Default for RenderMode {
48    fn default() -> Self {
49        RenderMode::Auto
50    }
51}
52
53/// A Directed Acyclic Graph (DAG) with ASCII rendering capabilities.
54///
55/// # Examples
56///
57/// ```
58/// use ascii_dag::graph::DAG;
59///
60/// let mut dag = DAG::new();
61/// dag.add_node(1, "Start");
62/// dag.add_node(2, "End");
63/// dag.add_edge(1, 2);
64///
65/// let output = dag.render();
66/// assert!(output.contains("Start"));
67/// assert!(output.contains("End"));
68/// ```
69#[derive(Clone)]
70pub struct DAG<'a> {
71    pub(crate) nodes: Vec<(usize, &'a str)>,
72    pub(crate) edges: Vec<(usize, usize)>,
73    pub(crate) render_mode: RenderMode,
74    pub(crate) auto_created: HashSet<usize>, // Track auto-created nodes for visual distinction (O(1) lookups)
75    pub(crate) id_to_index: HashMap<usize, usize>, // Cache id→index mapping (O(1) lookups)
76    pub(crate) node_widths: Vec<usize>,      // Cached formatted widths
77    pub(crate) children: Vec<Vec<usize>>,    // Adjacency list: children[idx] = child indices
78    pub(crate) parents: Vec<Vec<usize>>,     // Adjacency list: parents[idx] = parent indices
79}
80
81impl<'a> Default for DAG<'a> {
82    fn default() -> Self {
83        Self {
84            nodes: Vec::new(),
85            edges: Vec::new(),
86            render_mode: RenderMode::default(),
87            auto_created: HashSet::new(),
88            id_to_index: HashMap::new(),
89            node_widths: Vec::new(),
90            children: Vec::new(),
91            parents: Vec::new(),
92        }
93    }
94}
95
96impl<'a> DAG<'a> {
97    /// Create a new empty DAG.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use ascii_dag::graph::DAG;
103    /// let dag = DAG::new();
104    /// ```
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Create a DAG from pre-defined nodes and edges (batch construction).
110    ///
111    /// This is more efficient than using the builder API for static graphs.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use ascii_dag::graph::DAG;
117    ///
118    /// let dag = DAG::from_edges(
119    ///     &[(1, "A"), (2, "B"), (3, "C")],
120    ///     &[(1, 2), (2, 3)]
121    /// );
122    /// ```
123    pub fn from_edges(nodes: &[(usize, &'a str)], edges: &[(usize, usize)]) -> Self {
124        let mut dag = Self {
125            nodes: nodes.to_vec(),
126            edges: Vec::new(),
127            render_mode: RenderMode::default(),
128            auto_created: HashSet::new(),
129            id_to_index: HashMap::new(),
130            node_widths: Vec::new(),
131            children: Vec::new(),
132            parents: Vec::new(),
133        };
134
135        // Build id_to_index map and widths cache
136        for (idx, &(id, label)) in dag.nodes.iter().enumerate() {
137            dag.id_to_index.insert(id, idx);
138            let width = dag.compute_node_width(id, label);
139            dag.node_widths.push(width);
140        }
141
142        // Initialize adjacency lists
143        dag.children.resize(dag.nodes.len(), Vec::new());
144        dag.parents.resize(dag.nodes.len(), Vec::new());
145
146        // Add edges (may auto-create missing nodes)
147        for &(from, to) in edges {
148            dag.add_edge(from, to);
149        }
150
151        dag
152    }
153
154    /// Set the rendering mode.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use ascii_dag::graph::{DAG, RenderMode};
160    ///
161    /// let mut dag = DAG::new();
162    /// dag.set_render_mode(RenderMode::Horizontal);
163    /// ```
164    pub fn set_render_mode(&mut self, mode: RenderMode) {
165        self.render_mode = mode;
166    }
167
168    /// Create a DAG with a specific render mode.
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use ascii_dag::graph::{DAG, RenderMode};
174    ///
175    /// let dag = DAG::with_mode(RenderMode::Horizontal);
176    /// ```
177    pub fn with_mode(mode: RenderMode) -> Self {
178        Self {
179            nodes: Vec::new(),
180            edges: Vec::new(),
181            render_mode: mode,
182            auto_created: HashSet::new(),
183            id_to_index: HashMap::new(),
184            node_widths: Vec::new(),
185            children: Vec::new(),
186            parents: Vec::new(),
187        }
188    }
189
190    /// Add a node to the DAG.
191    ///
192    /// If the node was previously auto-created by `add_edge`, this will promote it
193    /// by setting its label and removing the auto-created flag.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use ascii_dag::graph::DAG;
199    ///
200    /// let mut dag = DAG::new();
201    /// dag.add_node(1, "MyNode");
202    /// ```
203    pub fn add_node(&mut self, id: usize, label: &'a str) {
204        // Check if node already exists (could be auto-created) - O(1) with HashMap
205        if let Some(&idx) = self.id_to_index.get(&id) {
206            // Promote auto-created node to explicit node
207            self.nodes[idx] = (id, label);
208            // Remove from auto_created set - O(1)
209            self.auto_created.remove(&id);
210            // Update cached width
211            let width = self.compute_node_width(id, label);
212            self.node_widths[idx] = width;
213        } else {
214            // Brand new node
215            let idx = self.nodes.len();
216            self.nodes.push((id, label));
217            self.id_to_index.insert(id, idx);
218            let width = self.compute_node_width(id, label);
219            self.node_widths.push(width);
220            // Extend adjacency lists
221            self.children.push(Vec::new());
222            self.parents.push(Vec::new());
223        }
224    }
225
226    /// Add an edge from one node to another.
227    ///
228    /// If either node doesn't exist, it will be auto-created as a placeholder.
229    /// You can later call `add_node` to provide a label for auto-created nodes.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use ascii_dag::graph::DAG;
235    ///
236    /// let mut dag = DAG::new();
237    /// dag.add_node(1, "A");
238    /// dag.add_node(2, "B");
239    /// dag.add_edge(1, 2);  // A -> B
240    /// ```
241    pub fn add_edge(&mut self, from: usize, to: usize) {
242        self.ensure_node_exists(from);
243        self.ensure_node_exists(to);
244        self.edges.push((from, to));
245
246        // Update adjacency lists (O(1) lookups)
247        if let (Some(&from_idx), Some(&to_idx)) =
248            (self.id_to_index.get(&from), self.id_to_index.get(&to))
249        {
250            self.children[from_idx].push(to_idx);
251            self.parents[to_idx].push(from_idx);
252        }
253    }
254
255    /// Ensure a node exists, auto-creating if missing.
256    /// Auto-created nodes will be visually distinct (rendered with ⟨⟩ instead of [])
257    /// until explicitly defined with add_node.
258    fn ensure_node_exists(&mut self, id: usize) {
259        // O(1) lookup with HashMap
260        if !self.id_to_index.contains_key(&id) {
261            #[cfg(feature = "warnings")]
262            {
263                eprintln!(
264                    "[ascii-dag] Warning: Node {} missing - auto-creating as placeholder. \
265                     Call add_node({}, \"label\") before add_edge() to provide a label.",
266                    id, id
267                );
268            }
269
270            // Create node with empty label
271            let idx = self.nodes.len();
272            self.nodes.push((id, ""));
273            self.auto_created.insert(id); // O(1) insert
274            self.id_to_index.insert(id, idx); // O(1) insert
275            let width = self.compute_node_width(id, "");
276            self.node_widths.push(width);
277            // Extend adjacency lists
278            self.children.push(Vec::new());
279            self.parents.push(Vec::new());
280        }
281    }
282
283    /// Check if a node was auto-created (for visual distinction)
284    pub(crate) fn is_auto_created(&self, id: usize) -> bool {
285        self.auto_created.contains(&id) // O(1) with HashSet
286    }
287
288    /// Write an unsigned integer to a string buffer without allocation.
289    /// This avoids format! bloat in no_std builds.
290    #[inline]
291    pub(crate) fn write_usize(buf: &mut String, mut n: usize) {
292        if n == 0 {
293            buf.push('0');
294            return;
295        }
296        let mut digits = [0u8; 20]; // Max digits for u64
297        let mut i = 0;
298        while n > 0 {
299            digits[i] = (n % 10) as u8 + b'0';
300            n /= 10;
301            i += 1;
302        }
303        // Write in reverse order
304        while i > 0 {
305            i -= 1;
306            buf.push(digits[i] as char);
307        }
308    }
309
310    /// Count digits in a number (for width calculation)
311    #[inline]
312    fn count_digits(mut n: usize) -> usize {
313        if n == 0 {
314            return 1;
315        }
316        let mut count = 0;
317        while n > 0 {
318            count += 1;
319            n /= 10;
320        }
321        count
322    }
323
324    /// Compute the formatted width of a node
325    pub(crate) fn compute_node_width(&self, id: usize, label: &str) -> usize {
326        if label.is_empty() || self.is_auto_created(id) {
327            // ⟨ID⟩ format
328            2 + Self::count_digits(id) // ⟨ + digits + ⟩
329        } else {
330            // [Label] format
331            2 + label.chars().count() // [ + label + ]
332        }
333    }
334
335    /// Write a formatted node directly to output buffer (avoids intermediate String allocation)
336    #[inline]
337    pub(crate) fn write_node(&self, output: &mut String, id: usize, label: &str) {
338        if label.is_empty() || self.is_auto_created(id) {
339            output.push('⟨');
340            Self::write_usize(output, id);
341            output.push('⟩');
342        } else {
343            output.push('[');
344            output.push_str(label);
345            output.push(']');
346        }
347    }
348
349    /// Get children of a node (returns IDs, not indices).
350    /// Uses cached adjacency lists for O(1) lookup instead of O(E) iteration.
351    pub(crate) fn get_children(&self, node_id: usize) -> Vec<usize> {
352        if let Some(&idx) = self.id_to_index.get(&node_id) {
353            // Convert child indices back to IDs
354            self.children[idx]
355                .iter()
356                .map(|&child_idx| self.nodes[child_idx].0)
357                .collect()
358        } else {
359            Vec::new()
360        }
361    }
362
363    /// Get parents of a node (returns IDs, not indices).
364    /// Uses cached adjacency lists for O(1) lookup instead of O(E) iteration.
365    pub(crate) fn get_parents(&self, node_id: usize) -> Vec<usize> {
366        if let Some(&idx) = self.id_to_index.get(&node_id) {
367            // Convert parent indices back to IDs
368            self.parents[idx]
369                .iter()
370                .map(|&parent_idx| self.nodes[parent_idx].0)
371                .collect()
372        } else {
373            Vec::new()
374        }
375    }
376
377    /// Get children indices directly (no ID conversion) - faster for internal use.
378    ///
379    /// Reserved for future optimization. Currently unused but available for
380    /// performance-critical paths that work with node indices directly.
381    #[inline]
382    #[allow(dead_code)]
383    pub(crate) fn get_children_indices(&self, node_idx: usize) -> &[usize] {
384        &self.children[node_idx]
385    }
386
387    /// Get parent indices directly (no ID conversion) - faster for internal use.
388    ///
389    /// Reserved for future optimization. Currently unused but available for
390    /// performance-critical paths that work with node indices directly.
391    #[inline]
392    #[allow(dead_code)]
393    pub(crate) fn get_parents_indices(&self, node_idx: usize) -> &[usize] {
394        &self.parents[node_idx]
395    }
396
397    /// Get node index from ID using O(1) HashMap lookup
398    #[inline]
399    pub(crate) fn node_index(&self, id: usize) -> Option<usize> {
400        self.id_to_index.get(&id).copied()
401    }
402
403    /// Get cached width for a node index
404    #[inline]
405    pub(crate) fn get_node_width(&self, idx: usize) -> usize {
406        self.node_widths.get(idx).copied().unwrap_or(0)
407    }
408
409    /// Estimate the buffer size needed for rendering.
410    ///
411    /// Use this to pre-allocate a buffer for [`render_to`](Self::render_to).
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// use ascii_dag::graph::DAG;
417    ///
418    /// let dag = DAG::from_edges(
419    ///     &[(1, "A"), (2, "B")],
420    ///     &[(1, 2)]
421    /// );
422    ///
423    /// let size = dag.estimate_size();
424    /// let mut buffer = String::with_capacity(size);
425    /// dag.render_to(&mut buffer);
426    /// ```
427    pub fn estimate_size(&self) -> usize {
428        // Rough estimate: nodes * avg_label_size + edges * connection_chars + box
429        self.nodes.len() * 25 + self.edges.len() * 15 + 200
430    }
431}