hypen_engine/reconcile/
diff.rs

1use super::{InstanceTree, Patch};
2use crate::ir::{Element, NodeId, Value};
3use crate::reactive::{DependencyGraph, evaluate_template_string};
4use indexmap::IndexMap;
5
6/// Reconcile an element tree against the instance tree and generate patches
7pub fn reconcile(
8    tree: &mut InstanceTree,
9    element: &Element,
10    parent_id: Option<NodeId>,
11    state: &serde_json::Value,
12    dependencies: &mut DependencyGraph,
13) -> Vec<Patch> {
14    let mut patches = Vec::new();
15
16    // For initial render, just create the tree
17    if tree.root().is_none() {
18        let node_id = create_tree(tree, element, parent_id, state, &mut patches, true, dependencies);
19        tree.set_root(node_id);
20        return patches;
21    }
22
23    // Incremental update: reconcile root against existing tree
24    if let Some(root_id) = tree.root() {
25        reconcile_node(tree, root_id, element, state, &mut patches, dependencies);
26    }
27
28    patches
29}
30
31/// Create a new subtree from an element
32pub fn create_tree(
33    tree: &mut InstanceTree,
34    element: &Element,
35    parent_id: Option<NodeId>,
36    state: &serde_json::Value,
37    patches: &mut Vec<Patch>,
38    is_root: bool,
39    dependencies: &mut DependencyGraph,
40) -> NodeId {
41    // Special handling for iterable elements (List, Grid, etc.)
42    // An element is iterable if it has BOTH:
43    // 1. prop "0" with a Binding (like @state.items) - not a static value
44    // 2. children (the template to repeat for each item)
45    // This distinguishes iterables from:
46    // - Text elements (prop "0" but no children)
47    // - Route/Link elements (prop "0" with static path, not a binding)
48    if let Some(Value::Binding(_)) = element.props.get("0") {
49        if !element.children.is_empty() {
50            return create_list_tree(tree, element, parent_id, state, patches, is_root, dependencies);
51        }
52    }
53
54    // Create the node
55    let node_id = tree.create_node(element, state);
56
57    // Register dependencies for this node
58    for value in element.props.values() {
59        match value {
60            Value::Binding(binding) => {
61                dependencies.add_dependency(node_id, binding);
62            }
63            Value::TemplateString { bindings, .. } => {
64                // Register all bindings in the template string
65                for binding in bindings {
66                    dependencies.add_dependency(node_id, binding);
67                }
68            }
69            _ => {}
70        }
71    }
72
73    // Generate Create patch
74    let node = tree.get(node_id).unwrap();
75    
76    // For lazy elements, add child component name as a prop
77    let mut props = node.props.clone();
78    if let Some(Value::Static(val)) = element.props.get("__lazy") {
79        if val.as_bool().unwrap_or(false) && !element.children.is_empty() {
80            // Store the first child's element type so renderer knows what component to load
81            let child_component = &element.children[0].element_type;
82            props.insert("__lazy_child".to_string(), serde_json::json!(child_component));
83        }
84    }
85    
86    patches.push(Patch::create(
87        node_id,
88        node.element_type.clone(),
89        props,
90    ));
91
92    // Event handling is now done at the renderer level
93
94    // If there's a parent, insert this node
95    if let Some(parent) = parent_id {
96        tree.add_child(parent, node_id, None);
97        patches.push(Patch::insert(parent, node_id, None));
98    } else if is_root {
99        // This is the root node - insert into "root" container
100        patches.push(Patch::insert_root(node_id));
101    }
102
103    // Create children (skip if element is marked as lazy)
104    let is_lazy = element.props.get("__lazy")
105        .and_then(|v| {
106            if let Value::Static(val) = v {
107                val.as_bool()
108            } else {
109                None
110            }
111        })
112        .unwrap_or(false);
113    
114    if !is_lazy {
115        for child_element in &element.children {
116            create_tree(tree, child_element, Some(node_id), state, patches, false, dependencies);
117        }
118    }
119
120    node_id
121}
122
123/// Create an iterable element (List, Grid, etc.) that iterates over an array in state
124/// Iterable elements are identified by having prop "0" with an array binding
125fn create_list_tree(
126    tree: &mut InstanceTree,
127    element: &Element,
128    parent_id: Option<NodeId>,
129    state: &serde_json::Value,
130    patches: &mut Vec<Patch>,
131    is_root: bool,
132    dependencies: &mut DependencyGraph,
133) -> NodeId {
134    // Get the array binding from first prop (prop "0")
135    let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
136        evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
137    } else {
138        serde_json::Value::Array(vec![])
139    };
140
141    // Create a container element - use the original element type (List, Grid, etc.)
142    // but remove the "0" prop since it's only for iteration, not rendering
143    let mut list_element = Element::new(&element.element_type);
144    for (key, value) in &element.props {
145        // Skip prop "0" - it's only for array binding, not for rendering
146        if key != "0" {
147            list_element.props.insert(key.clone(), value.clone());
148        }
149    }
150    
151    let node_id = tree.create_node(&list_element, state);
152
153    // Register the List node as depending on the array binding
154    // This is critical for reactive updates when the array changes
155    if let Some(Value::Binding(binding)) = element.props.get("0") {
156        dependencies.add_dependency(node_id, binding);
157    }
158
159    // Store the original element template for re-reconciliation
160    // This allows us to rebuild the list when the array changes
161    if let Some(node) = tree.get_mut(node_id) {
162        node.raw_props = element.props.clone();
163        node.element_template = Some(element.clone());
164    }
165
166    // Generate Create patch for container
167    let node = tree.get(node_id).unwrap();
168    patches.push(Patch::create(
169        node_id,
170        node.element_type.clone(),
171        node.props.clone(),
172    ));
173
174    // Insert container
175    if let Some(parent) = parent_id {
176        tree.add_child(parent, node_id, None);
177        patches.push(Patch::insert(parent, node_id, None));
178    } else if is_root {
179        patches.push(Patch::insert_root(node_id));
180    }
181
182    // If array is empty, return early
183    if let serde_json::Value::Array(items) = &array {
184        // Create children for each item in the array
185        for (index, item) in items.iter().enumerate() {
186            for child_template in &element.children {
187                // Clone child and replace ${item.x} bindings with actual item data
188                let child_with_item = replace_item_bindings(child_template, item, index);
189                create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
190            }
191        }
192    }
193
194    node_id
195}
196
197/// Navigate a nested path in a JSON value (e.g., "images.0" or "category.name")
198/// Supports both object keys and array indices
199fn navigate_item_path<'a>(item: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
200    let mut current = item;
201
202    for segment in path.split('.') {
203        // Try to parse as array index first
204        if let Ok(index) = segment.parse::<usize>() {
205            current = current.get(index)?;
206        } else {
207            // Otherwise treat as object key
208            current = current.get(segment)?;
209        }
210    }
211
212    Some(current)
213}
214
215/// Check if a string is a simple item path (e.g., "item.name" or "item.user.profile")
216/// Simple paths contain only alphanumeric chars, underscores, and dots for nesting.
217/// Complex expressions contain operators like ?, :, +, -, etc.
218fn is_simple_path(s: &str) -> bool {
219    // Must start with "item" followed by valid path characters
220    if !s.starts_with("item") {
221        return false;
222    }
223
224    let after_item = &s[4..]; // Skip "item"
225    if after_item.is_empty() {
226        return true; // Just "item"
227    }
228
229    // Must be "item.something" - check rest is valid path
230    if !after_item.starts_with('.') {
231        return false;
232    }
233
234    // Check all characters after "item." are valid path chars
235    after_item[1..].chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.')
236}
237
238/// Replace item bindings (Value::Binding with is_item() or TemplateString with item bindings) with actual item values
239pub fn replace_item_bindings(element: &Element, item: &serde_json::Value, index: usize) -> Element {
240    let mut new_element = element.clone();
241
242    // Replace bindings in props
243    let mut new_props = IndexMap::new();
244    for (key, value) in &element.props {
245        let new_value = match value {
246            Value::Binding(binding) => {
247                // Check if this is an item binding
248                if binding.is_item() {
249                    // For bare @item, return the whole item
250                    if binding.path.is_empty() {
251                        Value::Static(item.clone())
252                    } else {
253                        // Navigate the path (e.g., ["name"] or ["user", "profile"])
254                        let prop_path = binding.full_path();
255                        if let Some(val) = navigate_item_path(item, &prop_path) {
256                            Value::Static(val.clone())
257                        } else {
258                            value.clone()
259                        }
260                    }
261                } else {
262                    value.clone()
263                }
264            }
265            Value::TemplateString { template, bindings } => {
266                // Check if any bindings are item bindings
267                let has_item_bindings = bindings.iter().any(|b| b.is_item());
268
269                if has_item_bindings {
270                    let mut result = template.clone();
271
272                    // First, try exact pattern replacement for simple bindings like ${item.name}
273                    for binding in bindings {
274                        if binding.is_item() {
275                            let pattern = format!("${{{}}}", binding.full_path_with_source());
276                            let replacement = if binding.path.is_empty() {
277                                // Bare ${item}
278                                match item {
279                                    serde_json::Value::String(s) => s.clone(),
280                                    serde_json::Value::Number(n) => n.to_string(),
281                                    serde_json::Value::Bool(b) => b.to_string(),
282                                    _ => serde_json::to_string(item).unwrap_or_default(),
283                                }
284                            } else if let Some(val) = navigate_item_path(item, &binding.full_path()) {
285                                match val {
286                                    serde_json::Value::String(s) => s.clone(),
287                                    serde_json::Value::Number(n) => n.to_string(),
288                                    serde_json::Value::Bool(b) => b.to_string(),
289                                    _ => serde_json::to_string(val).unwrap_or_default(),
290                                }
291                            } else {
292                                continue; // Skip if path not found
293                            };
294                            result = result.replace(&pattern, &replacement);
295                        }
296                    }
297
298                    // Second pass: replace item.xxx identifiers within expressions
299                    // This handles cases like ${item.selected ? 'a' : 'b'} where item.selected
300                    // is part of an expression, not the entire binding
301                    let mut pos = 0;
302                    while let Some(item_start) = result[pos..].find("item.") {
303                        let item_abs_start = pos + item_start;
304                        let path_start = item_abs_start + 5; // Skip "item."
305
306                        // Find the end of the path (valid identifier chars only)
307                        let mut path_end = path_start;
308                        let chars: Vec<char> = result[path_start..].chars().collect();
309
310                        for (i, ch) in chars.iter().enumerate() {
311                            if ch.is_alphanumeric() || *ch == '_' || *ch == '.' {
312                                if *ch == '.' {
313                                    if i + 1 >= chars.len() || {
314                                        let next = chars[i + 1];
315                                        !next.is_alphanumeric() && next != '_'
316                                    } {
317                                        break;
318                                    }
319                                }
320                                path_end = path_start + i + 1;
321                            } else {
322                                break;
323                            }
324                        }
325
326                        if path_end > path_start {
327                            let path = &result[path_start..path_end];
328                            if let Some(val) = navigate_item_path(item, path) {
329                                let replacement = match val {
330                                    serde_json::Value::String(s) => format!("'{}'", s),
331                                    serde_json::Value::Number(n) => n.to_string(),
332                                    serde_json::Value::Bool(b) => b.to_string(),
333                                    serde_json::Value::Null => "null".to_string(),
334                                    _ => serde_json::to_string(val).unwrap_or_default(),
335                                };
336                                result = format!(
337                                    "{}{}{}",
338                                    &result[..item_abs_start],
339                                    replacement,
340                                    &result[path_end..]
341                                );
342                                pos = 0; // Start over after replacement
343                                continue;
344                            }
345                        }
346                        pos = item_abs_start + 5;
347                    }
348
349                    // Filter out resolved item bindings, keep state bindings
350                    let remaining_bindings: Vec<_> = bindings.iter()
351                        .filter(|b| b.is_state())
352                        .cloned()
353                        .collect();
354
355                    if remaining_bindings.is_empty() {
356                        // Check if result still contains ${...} expressions that need evaluation
357                        if result.contains("${") {
358                            // Evaluate the expression (item values have been substituted)
359                            match evaluate_template_string(&result, &serde_json::Value::Null, None) {
360                                Ok(evaluated) => Value::Static(serde_json::Value::String(evaluated)),
361                                Err(_) => Value::Static(serde_json::Value::String(result)),
362                            }
363                        } else {
364                            Value::Static(serde_json::Value::String(result))
365                        }
366                    } else {
367                        Value::TemplateString {
368                            template: result,
369                            bindings: remaining_bindings,
370                        }
371                    }
372                } else {
373                    value.clone()
374                }
375            }
376            // Handle static strings containing ${item.xxx} pattern (legacy/fallback)
377            // This handles both simple patterns like ${item.name} and complex expressions
378            // like ${item.selected ? '2px' : '1px'}
379            Value::Static(serde_json::Value::String(s)) if s.contains("${item.") || s.contains("${item}") => {
380                let mut result = s.clone();
381
382                // Strategy: Process ${...} blocks one at a time
383                // - If block is simple (just ${item} or ${item.path}), replace entire block with value
384                // - If block is complex (contains operators like ?, :, +, etc.), replace item refs then evaluate
385
386                let mut pos = 0;
387                while let Some(start) = result[pos..].find("${") {
388                    let abs_start = pos + start;
389
390                    // Find matching closing brace
391                    if let Some(end) = result[abs_start..].find('}') {
392                        let abs_end = abs_start + end;
393                        let content = &result[abs_start + 2..abs_end]; // Content between ${ and }
394
395                        // Check if this is a simple item reference or a complex expression
396                        if content == "item" {
397                            // Bare ${item} - replace with full item value
398                            let replacement = match item {
399                                serde_json::Value::String(s) => s.clone(),
400                                serde_json::Value::Number(n) => n.to_string(),
401                                serde_json::Value::Bool(b) => b.to_string(),
402                                _ => serde_json::to_string(item).unwrap_or_default(),
403                            };
404                            result = format!("{}{}{}", &result[..abs_start], replacement, &result[abs_end + 1..]);
405                            pos = 0;
406                            continue;
407                        } else if content.starts_with("item.") && is_simple_path(content) {
408                            // Simple ${item.path.to.value} - replace entire block with value
409                            let path = &content[5..]; // Skip "item."
410                            if let Some(val) = navigate_item_path(item, path) {
411                                let replacement = match val {
412                                    serde_json::Value::String(s) => s.clone(),
413                                    serde_json::Value::Number(n) => n.to_string(),
414                                    serde_json::Value::Bool(b) => b.to_string(),
415                                    serde_json::Value::Null => "null".to_string(),
416                                    _ => serde_json::to_string(val).unwrap_or_default(),
417                                };
418                                result = format!("{}{}{}", &result[..abs_start], replacement, &result[abs_end + 1..]);
419                                pos = 0;
420                                continue;
421                            }
422                        } else if content.contains("item.") || content.contains("item ") {
423                            // Complex expression with item references - replace item refs within the expression
424                            let mut expr_content = content.to_string();
425
426                            // Replace all item.xxx references within this expression
427                            let mut expr_pos = 0;
428                            while let Some(item_start) = expr_content[expr_pos..].find("item.") {
429                                let item_abs_start = expr_pos + item_start;
430                                let path_start = item_abs_start + 5; // Skip "item."
431
432                                // Find the end of the path (valid identifier chars only)
433                                let mut path_end = path_start;
434                                let chars: Vec<char> = expr_content[path_start..].chars().collect();
435
436                                for (i, ch) in chars.iter().enumerate() {
437                                    if ch.is_alphanumeric() || *ch == '_' || *ch == '.' {
438                                        if *ch == '.' {
439                                            if i + 1 >= chars.len() || {
440                                                let next = chars[i + 1];
441                                                !next.is_alphanumeric() && next != '_'
442                                            } {
443                                                break;
444                                            }
445                                        }
446                                        path_end = path_start + i + 1;
447                                    } else {
448                                        break;
449                                    }
450                                }
451
452                                if path_end > path_start {
453                                    let path = &expr_content[path_start..path_end];
454                                    if let Some(val) = navigate_item_path(item, path) {
455                                        let replacement = match val {
456                                            serde_json::Value::String(s) => format!("'{}'", s),
457                                            serde_json::Value::Number(n) => n.to_string(),
458                                            serde_json::Value::Bool(b) => b.to_string(),
459                                            serde_json::Value::Null => "null".to_string(),
460                                            _ => serde_json::to_string(val).unwrap_or_default(),
461                                        };
462                                        expr_content = format!(
463                                            "{}{}{}",
464                                            &expr_content[..item_abs_start],
465                                            replacement,
466                                            &expr_content[path_end..]
467                                        );
468                                        expr_pos = 0;
469                                        continue;
470                                    }
471                                }
472                                expr_pos = item_abs_start + 5;
473                            }
474
475                            // Reconstruct the expression block and evaluate
476                            let new_expr = format!("${{{}}}", expr_content);
477                            match evaluate_template_string(&new_expr, &serde_json::Value::Null, None) {
478                                Ok(evaluated) => {
479                                    result = format!("{}{}{}", &result[..abs_start], evaluated, &result[abs_end + 1..]);
480                                    pos = 0;
481                                    continue;
482                                }
483                                Err(_) => {
484                                    // If evaluation fails, leave as-is and move on
485                                    pos = abs_end + 1;
486                                    continue;
487                                }
488                            }
489                        }
490
491                        // Not an item-related block, skip it
492                        pos = abs_end + 1;
493                    } else {
494                        break;
495                    }
496                }
497
498                Value::Static(serde_json::Value::String(result))
499            }
500            _ => value.clone(),
501        };
502        new_props.insert(key.clone(), new_value);
503    }
504    new_element.props = new_props;
505
506    // Add a key for reconciliation (use array index)
507    new_element.key = Some(format!("list-item-{}", index));
508
509    // Recursively replace in children
510    new_element.children = element.children.iter()
511        .map(|child| replace_item_bindings(child, item, index))
512        .collect();
513
514    new_element
515}
516
517/// Reconcile a single node with a new element
518pub fn reconcile_node(
519    tree: &mut InstanceTree,
520    node_id: NodeId,
521    element: &Element,
522    state: &serde_json::Value,
523    patches: &mut Vec<Patch>,
524    dependencies: &mut DependencyGraph,
525) {
526    let node = tree.get(node_id).cloned();
527    if node.is_none() {
528        return;
529    }
530    let node = node.unwrap();
531
532    // Special handling for iterable elements (List, Grid, etc.)
533    // An element is iterable if it has BOTH prop "0" (array binding) AND children (template)
534    // This distinguishes iterables from Text elements, which also use prop "0" but have no children
535    let is_iterable = element.props.get("0").is_some() && !element.children.is_empty();
536    
537    if is_iterable {
538        // Get the array binding from first prop (prop "0")
539        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
540            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
541        } else {
542            serde_json::Value::Array(vec![])
543        };
544
545        // Regenerate iterable children
546        if let serde_json::Value::Array(items) = &array {
547            let old_children = node.children.clone();
548            
549            // Calculate expected number of children
550            let expected_children_count = items.len() * element.children.len();
551            
552            // Remove old children if count doesn't match
553            if old_children.len() != expected_children_count {
554                for &old_child_id in &old_children {
555                    patches.push(Patch::remove(old_child_id));
556                }
557                
558                // Clear children from node
559                if let Some(node) = tree.get_mut(node_id) {
560                    node.children.clear();
561                }
562                
563                // Create new children
564                for (index, item) in items.iter().enumerate() {
565                    for child_template in &element.children {
566                        let child_with_item = replace_item_bindings(child_template, item, index);
567                        create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
568                    }
569                }
570            } else {
571                // Reconcile existing children
572                let mut child_index = 0;
573                for (item_index, item) in items.iter().enumerate() {
574                    for child_template in &element.children {
575                        if let Some(&old_child_id) = old_children.get(child_index) {
576                            let child_with_item = replace_item_bindings(child_template, item, item_index);
577                            reconcile_node(tree, old_child_id, &child_with_item, state, patches, dependencies);
578                        }
579                        child_index += 1;
580                    }
581                }
582            }
583        }
584        
585        return; // Done with List reconciliation
586    }
587
588    // If element type changed, replace the entire subtree
589    if node.element_type != element.element_type {
590        replace_subtree(tree, node_id, &node, element, state, patches, dependencies);
591        return;
592    }
593
594    // Register dependencies for this node (same as create_tree)
595    for value in element.props.values() {
596        if let Value::Binding(binding) = value {
597            dependencies.add_dependency(node_id, binding);
598        }
599    }
600
601    // Diff props
602    let new_props = resolve_props(&element.props, state);
603    let prop_patches = diff_props(node_id, &node.props, &new_props);
604    patches.extend(prop_patches);
605
606    // Update node props in tree
607    if let Some(node) = tree.get_mut(node_id) {
608        node.props = new_props.clone();
609        node.raw_props = element.props.clone();
610    }
611
612    // Reconcile children (skip if element is marked as lazy)
613    let is_lazy = element.props.get("__lazy")
614        .and_then(|v| {
615            if let Value::Static(val) = v {
616                val.as_bool()
617            } else {
618                None
619            }
620        })
621        .unwrap_or(false);
622    
623    if !is_lazy {
624        let old_children = node.children.clone();
625        let new_children = &element.children;
626
627        // Simple strategy: match children by index
628        for (i, new_child_element) in new_children.iter().enumerate() {
629            if let Some(&old_child_id) = old_children.get(i) {
630                // Reconcile existing child
631                reconcile_node(tree, old_child_id, new_child_element, state, patches, dependencies);
632            } else {
633                // Create new child
634                let new_child_id = create_tree(tree, new_child_element, Some(node_id), state, patches, false, dependencies);
635                if let Some(node) = tree.get_mut(node_id) {
636                    node.children.push(new_child_id);
637                }
638            }
639        }
640
641        // Remove extra old children
642        if old_children.len() > new_children.len() {
643            for &old_child_id in &old_children[new_children.len()..] {
644                patches.push(Patch::remove(old_child_id));
645                // TODO: Remove from tree
646            }
647        }
648    }
649}
650
651/// Replace an entire subtree when element types don't match.
652/// This removes the old node and its descendants, then creates a new subtree in its place.
653fn replace_subtree(
654    tree: &mut InstanceTree,
655    old_node_id: NodeId,
656    old_node: &super::InstanceNode,
657    new_element: &Element,
658    state: &serde_json::Value,
659    patches: &mut Vec<Patch>,
660    dependencies: &mut DependencyGraph,
661) {
662    let parent_id = old_node.parent;
663
664    // Remember the position of the old node in its parent's children list
665    let old_position = if let Some(pid) = parent_id {
666        tree.get(pid).and_then(|parent| {
667            parent.children.iter().position(|&id| id == old_node_id)
668        })
669    } else {
670        None
671    };
672
673    // Collect all node IDs in the old subtree (depth-first, children before parents)
674    let ids_to_remove = collect_subtree_ids(tree, old_node_id);
675
676    // Generate Remove patches and clear dependencies in one pass
677    for &id in &ids_to_remove {
678        patches.push(Patch::remove(id));
679        dependencies.remove_node(id);
680    }
681
682    // Remove old node from parent's children list
683    if let Some(pid) = parent_id {
684        if let Some(parent) = tree.get_mut(pid) {
685            parent.children.retain(|&id| id != old_node_id);
686        }
687    }
688
689    // Remove the old subtree from the tree
690    tree.remove(old_node_id);
691
692    // Create the new subtree
693    let is_root = parent_id.is_none();
694    let new_node_id = create_tree(tree, new_element, parent_id, state, patches, is_root, dependencies);
695
696    // If this was the root, update it
697    if is_root {
698        tree.set_root(new_node_id);
699    } else if let Some(pid) = parent_id {
700        if let Some(pos) = old_position {
701            if let Some(parent) = tree.get_mut(pid) {
702                let current_len = parent.children.len();
703                // create_tree appended at end, so new node is at current_len - 1
704                // We want it at position `pos`
705                if pos < current_len - 1 {
706                    // Pop from end (O(1)) and insert at correct position (O(n))
707                    let new_id = parent.children.pop().unwrap();
708                    parent.children.insert(pos, new_id);
709
710                    // Get the next sibling for the Move patch
711                    let next_sibling = parent.children.get(pos + 1).copied();
712                    patches.push(Patch::move_node(pid, new_node_id, next_sibling));
713                }
714                // If pos == current_len - 1, it's already in the right place (was last child)
715            }
716        }
717    }
718}
719
720/// Collect all node IDs in a subtree (post-order: children before parents)
721/// Uses iterative approach to avoid stack overflow on deep trees
722fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
723    let mut result = Vec::new();
724    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
725
726    while let Some((node_id, children_processed)) = stack.pop() {
727        if children_processed {
728            // Children already processed, add this node
729            result.push(node_id);
730        } else {
731            // Push self back with flag, then push children
732            stack.push((node_id, true));
733            if let Some(node) = tree.get(node_id) {
734                // Push children in reverse order so they're processed left-to-right
735                for &child_id in node.children.iter().rev() {
736                    stack.push((child_id, false));
737                }
738            }
739        }
740    }
741
742    result
743}
744
745/// Resolve props by evaluating bindings against state
746fn resolve_props(props: &IndexMap<String, crate::ir::Value>, state: &serde_json::Value) -> IndexMap<String, serde_json::Value> {
747    resolve_props_with_item(props, state, None)
748}
749
750/// Resolve props with optional item context (for list iteration)
751fn resolve_props_with_item(
752    props: &IndexMap<String, crate::ir::Value>,
753    state: &serde_json::Value,
754    item: Option<&serde_json::Value>,
755) -> IndexMap<String, serde_json::Value> {
756    let mut resolved = IndexMap::new();
757
758    for (key, value) in props {
759        let resolved_value = match value {
760            crate::ir::Value::Static(v) => v.clone(),
761            crate::ir::Value::Binding(binding) => {
762                if binding.is_item() {
763                    // Evaluate item binding
764                    if let Some(item_value) = item {
765                        evaluate_item_binding(binding, item_value).unwrap_or(serde_json::Value::Null)
766                    } else {
767                        serde_json::Value::Null
768                    }
769                } else {
770                    // Evaluate state binding
771                    evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
772                }
773            }
774            crate::ir::Value::TemplateString { template, .. } => {
775                // Use expression evaluation for template strings - handles both
776                // simple bindings and complex expressions like ternaries
777                match crate::reactive::evaluate_template_string(template, state, item) {
778                    Ok(result) => serde_json::Value::String(result),
779                    Err(_) => serde_json::Value::String(template.clone()),
780                }
781            }
782            crate::ir::Value::Action(action) => {
783                // Actions are serialized with @ prefix for renderer to detect
784                serde_json::Value::String(format!("@{}", action))
785            }
786        };
787        resolved.insert(key.clone(), resolved_value);
788    }
789
790    resolved
791}
792
793/// Evaluate an item binding against the item object
794fn evaluate_item_binding(binding: &crate::reactive::Binding, item: &serde_json::Value) -> Option<serde_json::Value> {
795    if binding.path.is_empty() {
796        return Some(item.clone());
797    }
798
799    let mut current = item;
800    for segment in &binding.path {
801        current = current.get(segment)?;
802    }
803    Some(current.clone())
804}
805
806/// Evaluate a binding against state using its parsed path
807fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
808    let mut current = state;
809
810    for segment in &binding.path {
811        current = current.get(segment)?;
812    }
813
814    Some(current.clone())
815}
816
817/// Reconcile children using keys for efficient updates
818fn reconcile_keyed_children(
819    tree: &mut InstanceTree,
820    parent_id: NodeId,
821    old_children: &[NodeId],
822    new_children: &[Element],
823    state: &serde_json::Value,
824    dependencies: &mut DependencyGraph,
825) -> Vec<Patch> {
826    let mut patches = Vec::new();
827
828    // Build a map of old children by key
829    let mut old_keyed: IndexMap<String, NodeId> = IndexMap::new();
830    let mut old_unkeyed: Vec<NodeId> = Vec::new();
831
832    for &child_id in old_children {
833        if let Some(node) = tree.get(child_id) {
834            if let Some(key) = &node.key {
835                old_keyed.insert(key.clone(), child_id);
836            } else {
837                old_unkeyed.push(child_id);
838            }
839        }
840    }
841
842    let mut new_child_ids: Vec<NodeId> = Vec::new();
843    let mut unkeyed_idx = 0;
844
845    // First pass: match children by key or create new ones
846    for new_element in new_children {
847        let new_key = new_element.key.as_ref();
848
849        let child_id = if let Some(key) = new_key {
850            // Try to find matching keyed child
851            if let Some(&old_id) = old_keyed.get(key) {
852                // Found a match - reconcile it
853                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
854                old_keyed.shift_remove(key); // Mark as used
855                old_id
856            } else {
857                // No match - create new child
858                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
859            }
860        } else {
861            // Unkeyed child - try to reuse unkeyed old child
862            if let Some(&old_id) = old_unkeyed.get(unkeyed_idx) {
863                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
864                unkeyed_idx += 1;
865                old_id
866            } else {
867                // No unkeyed child available - create new
868                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
869            }
870        };
871
872        new_child_ids.push(child_id);
873    }
874
875    // Second pass: generate move/insert patches using LIS algorithm
876    // Build list of old positions for children that were reused
877    let mut old_positions: Vec<Option<usize>> = Vec::new();
878    for &new_id in &new_child_ids {
879        let old_pos = old_children.iter().position(|&old_id| old_id == new_id);
880        old_positions.push(old_pos);
881    }
882
883    // Find longest increasing subsequence to minimize moves
884    let lis = longest_increasing_subsequence(&old_positions);
885
886    // Update parent's children list
887    if let Some(parent_node) = tree.get_mut(parent_id) {
888        parent_node.children = new_child_ids.clone();
889    }
890
891    // Generate move/insert patches for children not in LIS
892    for (new_idx, &child_id) in new_child_ids.iter().enumerate() {
893        // Check if this child needs to be moved
894        if !lis.contains(&new_idx) {
895            // Calculate before_id (the next child in the list, or None for last position)
896            let before_id = new_child_ids.get(new_idx + 1).copied();
897            patches.push(Patch::move_node(parent_id, child_id, before_id));
898        }
899    }
900
901    // Third pass: remove old children that weren't reused
902    for old_id in old_keyed.values() {
903        patches.push(Patch::remove(*old_id));
904    }
905    for &old_id in &old_unkeyed[unkeyed_idx..] {
906        patches.push(Patch::remove(old_id));
907    }
908
909    patches
910}
911
912/// Find the longest increasing subsequence (LIS) indices
913/// This is used to minimize DOM moves during reconciliation
914/// Returns indices of elements that are already in correct relative order
915fn longest_increasing_subsequence(positions: &[Option<usize>]) -> Vec<usize> {
916    // Extract valid positions with their indices
917    let valid: Vec<(usize, usize)> = positions
918        .iter()
919        .enumerate()
920        .filter_map(|(idx, &pos)| pos.map(|p| (idx, p)))
921        .collect();
922
923    if valid.is_empty() {
924        return Vec::new();
925    }
926
927    // Use dynamic programming to find LIS
928    let n = valid.len();
929    let mut dp: Vec<usize> = vec![1; n];
930    let mut prev: Vec<Option<usize>> = vec![None; n];
931
932    for i in 1..n {
933        for j in 0..i {
934            if valid[j].1 < valid[i].1 && dp[j] + 1 > dp[i] {
935                dp[i] = dp[j] + 1;
936                prev[i] = Some(j);
937            }
938        }
939    }
940
941    // Find the index with maximum LIS length
942    let mut max_idx = 0;
943    for i in 1..n {
944        if dp[i] > dp[max_idx] {
945            max_idx = i;
946        }
947    }
948
949    // Reconstruct the LIS indices
950    let mut result = Vec::new();
951    let mut idx = Some(max_idx);
952    while let Some(i) = idx {
953        result.push(valid[i].0);
954        idx = prev[i];
955    }
956    result.reverse();
957
958    result
959}
960
961/// Diff two sets of props and generate SetProp patches
962pub fn diff_props(
963    node_id: NodeId,
964    old_props: &IndexMap<String, serde_json::Value>,
965    new_props: &IndexMap<String, serde_json::Value>,
966) -> Vec<Patch> {
967    let mut patches = Vec::new();
968
969    // Check for changed or new props
970    for (key, new_value) in new_props {
971        if old_props.get(key) != Some(new_value) {
972            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
973        }
974    }
975
976    // Note: We don't remove old props, just set new ones
977    // Platforms should handle prop removal if needed
978
979    patches
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985    use crate::ir::Value;
986    use serde_json::json;
987
988    #[test]
989    fn test_create_simple_tree() {
990        use crate::reactive::DependencyGraph;
991
992        let mut tree = InstanceTree::new();
993        let mut patches = Vec::new();
994        let mut dependencies = DependencyGraph::new();
995
996        let element = Element::new("Column")
997            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
998
999        let state = json!({});
1000        create_tree(&mut tree, &element, None, &state, &mut patches, true, &mut dependencies);
1001
1002        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
1003        // Create Column, Insert Column into root, Create Text, Insert Text into Column
1004        assert_eq!(patches.len(), 4);
1005
1006        // Verify root insert patch exists
1007        let root_insert = patches.iter().find(|p| {
1008            matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root")
1009        });
1010        assert!(root_insert.is_some(), "Root insert patch should exist");
1011    }
1012
1013    #[test]
1014    fn test_diff_props() {
1015        let node_id = NodeId::default();
1016        let old = indexmap::indexmap! {
1017            "color".to_string() => json!("red"),
1018            "size".to_string() => json!(16),
1019        };
1020        let new = indexmap::indexmap! {
1021            "color".to_string() => json!("blue"),
1022            "size".to_string() => json!(16),
1023        };
1024
1025        let patches = diff_props(node_id, &old, &new);
1026
1027        // Only color changed
1028        assert_eq!(patches.len(), 1);
1029    }
1030
1031    #[test]
1032    fn test_longest_increasing_subsequence_basic() {
1033        // Test case: [0, 1, 2, 3] - already in order, should select all
1034        let positions = vec![Some(0), Some(1), Some(2), Some(3)];
1035        let lis = longest_increasing_subsequence(&positions);
1036        assert_eq!(lis, vec![0, 1, 2, 3]);
1037    }
1038
1039    #[test]
1040    fn test_longest_increasing_subsequence_reverse() {
1041        // Test case: [3, 2, 1, 0] - reversed, should select only one element
1042        let positions = vec![Some(3), Some(2), Some(1), Some(0)];
1043        let lis = longest_increasing_subsequence(&positions);
1044        assert_eq!(lis.len(), 1);
1045    }
1046
1047    #[test]
1048    fn test_longest_increasing_subsequence_mixed() {
1049        // Test case: [0, 3, 1, 2] - should select [0, 1, 2]
1050        let positions = vec![Some(0), Some(3), Some(1), Some(2)];
1051        let lis = longest_increasing_subsequence(&positions);
1052        assert_eq!(lis.len(), 3);
1053        // Should contain indices of 0, 1, 2 in the original array
1054        assert!(lis.contains(&0));
1055        assert!(lis.contains(&2));
1056        assert!(lis.contains(&3));
1057    }
1058
1059    #[test]
1060    fn test_longest_increasing_subsequence_with_new_items() {
1061        // Test case: [None, Some(0), None, Some(1)] - new items mixed with existing
1062        let positions = vec![None, Some(0), None, Some(1)];
1063        let lis = longest_increasing_subsequence(&positions);
1064        assert_eq!(lis, vec![1, 3]); // Only indices with existing positions
1065    }
1066
1067    #[test]
1068    fn test_longest_increasing_subsequence_empty() {
1069        let positions: Vec<Option<usize>> = vec![];
1070        let lis = longest_increasing_subsequence(&positions);
1071        assert_eq!(lis, Vec::<usize>::new());
1072    }
1073
1074    #[test]
1075    fn test_longest_increasing_subsequence_all_new() {
1076        // All new items (no old positions)
1077        let positions = vec![None, None, None];
1078        let lis = longest_increasing_subsequence(&positions);
1079        assert_eq!(lis, Vec::<usize>::new());
1080    }
1081
1082    #[test]
1083    fn test_reconcile_keyed_children_basic() {
1084        let mut tree = InstanceTree::new();
1085        let mut dependencies = DependencyGraph::new();
1086        let state = json!({});
1087
1088        // Create a parent node
1089        let parent = Element::new("Column");
1090        let parent_id = tree.create_node(&parent, &state);
1091
1092        // Create old children
1093        let mut old_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1094        old_child_1.key = Some("item-1".to_string());
1095        let old_id_1 = tree.create_node(&old_child_1, &state);
1096
1097        let mut old_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1098        old_child_2.key = Some("item-2".to_string());
1099        let old_id_2 = tree.create_node(&old_child_2, &state);
1100
1101        tree.add_child(parent_id, old_id_1, None);
1102        tree.add_child(parent_id, old_id_2, None);
1103
1104        // New children in reversed order
1105        let mut new_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1106        new_child_2.key = Some("item-2".to_string());
1107
1108        let mut new_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1109        new_child_1.key = Some("item-1".to_string());
1110
1111        let new_children = vec![new_child_2, new_child_1];
1112        let old_children = vec![old_id_1, old_id_2];
1113
1114        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1115
1116        // Should have move patches but no create/remove patches (children are reused)
1117        let move_count = patches.iter().filter(|p| matches!(p, Patch::Move { .. })).count();
1118        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1119        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1120
1121        assert_eq!(create_count, 0, "Should not create new children");
1122        assert_eq!(remove_count, 0, "Should not remove any children");
1123        assert!(move_count > 0, "Should have move patches for reordering");
1124    }
1125
1126    #[test]
1127    fn test_reconcile_keyed_children_add_remove() {
1128        let mut tree = InstanceTree::new();
1129        let mut dependencies = DependencyGraph::new();
1130        let state = json!({});
1131
1132        let parent = Element::new("Column");
1133        let parent_id = tree.create_node(&parent, &state);
1134
1135        // Old: item-1, item-2
1136        let mut old_child_1 = Element::new("Text");
1137        old_child_1.key = Some("item-1".to_string());
1138        let old_id_1 = tree.create_node(&old_child_1, &state);
1139
1140        let mut old_child_2 = Element::new("Text");
1141        old_child_2.key = Some("item-2".to_string());
1142        let old_id_2 = tree.create_node(&old_child_2, &state);
1143
1144        tree.add_child(parent_id, old_id_1, None);
1145        tree.add_child(parent_id, old_id_2, None);
1146
1147        // New: item-2, item-3 (removed item-1, added item-3)
1148        let mut new_child_2 = Element::new("Text");
1149        new_child_2.key = Some("item-2".to_string());
1150
1151        let mut new_child_3 = Element::new("Text");
1152        new_child_3.key = Some("item-3".to_string());
1153
1154        let new_children = vec![new_child_2, new_child_3];
1155        let old_children = vec![old_id_1, old_id_2];
1156
1157        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1158
1159        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1160        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1161
1162        assert_eq!(create_count, 1, "Should create item-3");
1163        assert_eq!(remove_count, 1, "Should remove item-1");
1164    }
1165}