Skip to main content

wow_alchemy/utils/
tree.rs

1//! Tree structure rendering utilities for file format visualization
2
3use console::Style;
4use std::collections::HashMap;
5
6/// Represents a node in a tree structure
7#[derive(Debug, Clone)]
8pub struct TreeNode {
9    pub name: String,
10    pub node_type: NodeType,
11    pub size: Option<u64>,
12    pub children: Vec<TreeNode>,
13    pub metadata: HashMap<String, String>,
14    pub external_refs: Vec<ExternalRef>,
15}
16
17/// Types of nodes in the tree
18#[derive(Debug, Clone, PartialEq)]
19pub enum NodeType {
20    Root,
21    Header,
22    #[allow(dead_code)]
23    Chunk,
24    #[allow(dead_code)]
25    Table,
26    File,
27    Directory,
28    #[allow(dead_code)] // Reserved for future use
29    Reference,
30    #[allow(dead_code)]
31    Property,
32    #[allow(dead_code)]
33    Data,
34}
35
36/// External file reference
37#[derive(Debug, Clone)]
38pub struct ExternalRef {
39    pub path: String,
40    pub ref_type: RefType,
41    pub exists: Option<bool>,
42}
43
44/// Types of external references
45#[derive(Debug, Clone, PartialEq)]
46pub enum RefType {
47    Texture,
48    Model,
49    Animation,
50    Map,
51    Database,
52    Sound,
53    Script,
54    Unknown,
55}
56
57/// Options for tree rendering
58#[derive(Debug, Clone)]
59pub struct TreeOptions {
60    pub max_depth: Option<usize>,
61    pub show_external_refs: bool,
62    pub no_color: bool,
63    pub show_metadata: bool,
64    pub compact: bool,
65}
66
67impl Default for TreeOptions {
68    fn default() -> Self {
69        Self {
70            max_depth: None,
71            show_external_refs: true,
72            no_color: false,
73            show_metadata: true,
74            compact: false,
75        }
76    }
77}
78
79impl TreeNode {
80    /// Create a new tree node
81    pub fn new(name: String, node_type: NodeType) -> Self {
82        Self {
83            name,
84            node_type,
85            size: None,
86            children: Vec::new(),
87            metadata: HashMap::new(),
88            external_refs: Vec::new(),
89        }
90    }
91
92    /// Add a child node
93    pub fn add_child(mut self, child: TreeNode) -> Self {
94        self.children.push(child);
95        self
96    }
97
98    /// Set the size of this node
99    pub fn with_size(mut self, size: u64) -> Self {
100        self.size = Some(size);
101        self
102    }
103
104    /// Add metadata
105    pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
106        self.metadata.insert(key.to_string(), value.to_string());
107        self
108    }
109
110    /// Add external reference
111    pub fn with_external_ref(mut self, path: &str, ref_type: RefType) -> Self {
112        self.external_refs.push(ExternalRef {
113            path: path.to_string(),
114            ref_type,
115            exists: None,
116        });
117        self
118    }
119}
120
121impl ExternalRef {
122    /// Get emoji icon for reference type
123    pub fn icon(&self) -> &'static str {
124        match self.ref_type {
125            RefType::Texture => "πŸ–ΌοΈ",
126            RefType::Model => "πŸ—οΈ",
127            RefType::Animation => "πŸ“½οΈ",
128            RefType::Map => "πŸ—ΊοΈ",
129            RefType::Database => "πŸ“Š",
130            RefType::Sound => "πŸ”Š",
131            RefType::Script => "πŸ“„",
132            RefType::Unknown => "πŸ“",
133        }
134    }
135
136    /// Get color style based on existence
137    pub fn style(&self, no_color: bool) -> Style {
138        if no_color {
139            Style::new()
140        } else {
141            match self.exists {
142                Some(true) => Style::new().green(),
143                Some(false) => Style::new().red(),
144                None => Style::new().yellow(),
145            }
146        }
147    }
148}
149
150impl NodeType {
151    /// Get emoji icon for node type
152    pub fn icon(&self) -> &'static str {
153        match self {
154            NodeType::Root => "πŸ“",
155            NodeType::Header => "πŸ“‹",
156            NodeType::Chunk => "πŸ“¦",
157            NodeType::Table => "πŸ“Š",
158            NodeType::File => "πŸ“„",
159            NodeType::Directory => "πŸ“",
160            NodeType::Reference => "πŸ”—",
161            NodeType::Property => "🏷️",
162            NodeType::Data => "πŸ’Ύ",
163        }
164    }
165
166    /// Get color style for node type
167    pub fn style(&self, no_color: bool) -> Style {
168        if no_color {
169            Style::new()
170        } else {
171            match self {
172                NodeType::Root => Style::new().bold().cyan(),
173                NodeType::Header => Style::new().bold().yellow(),
174                NodeType::Chunk => Style::new().blue(),
175                NodeType::Table => Style::new().magenta(),
176                NodeType::File => Style::new().green(),
177                NodeType::Directory => Style::new().cyan(),
178                NodeType::Reference => Style::new().yellow(),
179                NodeType::Property => Style::new().dim(),
180                NodeType::Data => Style::new().white(),
181            }
182        }
183    }
184}
185
186/// Render a tree structure to string
187pub fn render_tree(root: &TreeNode, options: &TreeOptions) -> String {
188    let mut output = String::new();
189    render_node(root, &mut output, "", true, 0, options);
190    output
191}
192
193/// Render a single node and its children
194fn render_node(
195    node: &TreeNode,
196    output: &mut String,
197    prefix: &str,
198    is_last: bool,
199    depth: usize,
200    options: &TreeOptions,
201) {
202    // Check depth limit
203    if let Some(max_depth) = options.max_depth {
204        if depth > max_depth {
205            return;
206        }
207    }
208
209    // Node icon and name
210    let icon = node.node_type.icon();
211    let style = node.node_type.style(options.no_color);
212    let connector = if depth == 0 {
213        ""
214    } else if is_last {
215        "└── "
216    } else {
217        "β”œβ”€β”€ "
218    };
219
220    let mut line = format!(
221        "{}{}{} {}",
222        prefix,
223        connector,
224        icon,
225        style.apply_to(&node.name)
226    );
227
228    // Add size if available
229    if let Some(size) = node.size {
230        line.push_str(&format!(" ({})", format_bytes(size)));
231    }
232
233    // Add metadata if enabled and available
234    if options.show_metadata && !node.metadata.is_empty() && options.compact {
235        // Show key metadata inline
236        let mut meta_parts = Vec::new();
237        for (key, value) in &node.metadata {
238            if ["version", "count", "flags", "type"].contains(&key.as_str()) {
239                meta_parts.push(format!("{key}:{value}"));
240            }
241        }
242        if !meta_parts.is_empty() {
243            line.push_str(&format!(" [{}]", meta_parts.join(", ")));
244        }
245    }
246
247    output.push_str(&line);
248    output.push('\n');
249
250    // Show detailed metadata on separate lines (non-compact mode)
251    if options.show_metadata && !options.compact && !node.metadata.is_empty() {
252        let child_prefix = if depth == 0 {
253            ""
254        } else if is_last {
255            "    "
256        } else {
257            "β”‚   "
258        };
259        let meta_prefix = format!("{prefix}{child_prefix}    ");
260
261        for (key, value) in &node.metadata {
262            let meta_style = Style::new().dim();
263            output.push_str(&format!(
264                "{}🏷️  {}: {}\n",
265                meta_prefix,
266                meta_style.apply_to(key),
267                value
268            ));
269        }
270    }
271
272    // Show external references
273    if options.show_external_refs && !node.external_refs.is_empty() {
274        let child_prefix = if depth == 0 {
275            ""
276        } else if is_last {
277            "    "
278        } else {
279            "β”‚   "
280        };
281        let ref_prefix = format!("{prefix}{child_prefix}    ");
282
283        for ext_ref in &node.external_refs {
284            let icon = ext_ref.icon();
285            let style = ext_ref.style(options.no_color);
286            output.push_str(&format!(
287                "{}└─→ {} {}\n",
288                ref_prefix,
289                icon,
290                style.apply_to(&ext_ref.path)
291            ));
292        }
293    }
294
295    // Render children
296    if !node.children.is_empty() {
297        let new_prefix = if depth == 0 {
298            String::new()
299        } else {
300            format!("{}{}", prefix, if is_last { "    " } else { "β”‚   " })
301        };
302
303        for (i, child) in node.children.iter().enumerate() {
304            let is_last_child = i == node.children.len() - 1;
305            render_node(
306                child,
307                output,
308                &new_prefix,
309                is_last_child,
310                depth + 1,
311                options,
312            );
313        }
314    }
315}
316
317/// Format bytes in human-readable format
318fn format_bytes(bytes: u64) -> String {
319    const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
320    let mut size = bytes as f64;
321    let mut unit_index = 0;
322
323    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
324        size /= 1024.0;
325        unit_index += 1;
326    }
327
328    if unit_index == 0 {
329        format!("{} {}", bytes, UNITS[unit_index])
330    } else {
331        format!("{:.1} {}", size, UNITS[unit_index])
332    }
333}
334
335/// Detect reference type from file extension
336pub fn detect_ref_type(path: &str) -> RefType {
337    let path_lower = path.to_lowercase();
338
339    if path_lower.ends_with(".blp") {
340        RefType::Texture
341    } else if path_lower.ends_with(".m2") || path_lower.ends_with(".mdx") {
342        RefType::Model
343    } else if path_lower.ends_with(".anim") || path_lower.ends_with(".bone") {
344        RefType::Animation
345    } else if path_lower.ends_with(".wdt")
346        || path_lower.ends_with(".adt")
347        || path_lower.ends_with(".wdl")
348    {
349        RefType::Map
350    } else if path_lower.ends_with(".dbc") || path_lower.ends_with(".db2") {
351        RefType::Database
352    } else if path_lower.ends_with(".wav") || path_lower.ends_with(".mp3") {
353        RefType::Sound
354    } else if path_lower.ends_with(".lua") || path_lower.ends_with(".xml") {
355        RefType::Script
356    } else {
357        RefType::Unknown
358    }
359}