Skip to main content

hypen_engine/reconcile/
diff.rs

1use super::{InstanceTree, Patch, ControlFlowKind};
2use super::resolve::{evaluate_binding, resolve_props};
3use crate::ir::{Element, NodeId, Value, IRNode, ConditionalBranch, Props};
4use crate::reactive::{DependencyGraph, evaluate_template_string, Binding};
5use indexmap::IndexMap;
6
7/// Shared mutable context threaded through the recursive tree-building and
8/// reconciliation helpers.  Grouping these four fields removes the repetitive
9/// `(tree, state, patches, dependencies)` parameter tuple that was being
10/// copy-pasted across every internal function.
11struct ReconcileCtx<'a> {
12    tree: &'a mut InstanceTree,
13    state: &'a serde_json::Value,
14    patches: &'a mut Vec<Patch>,
15    dependencies: &'a mut DependencyGraph,
16}
17
18/// Reconcile an element tree against the instance tree and generate patches
19pub fn reconcile(
20    tree: &mut InstanceTree,
21    element: &Element,
22    parent_id: Option<NodeId>,
23    state: &serde_json::Value,
24    dependencies: &mut DependencyGraph,
25) -> Vec<Patch> {
26    let mut patches = Vec::new();
27
28    // For initial render, just create the tree
29    if tree.root().is_none() {
30        let node_id = create_tree(tree, element, parent_id, state, &mut patches, true, dependencies);
31        tree.set_root(node_id);
32        return patches;
33    }
34
35    // Incremental update: reconcile root against existing tree
36    if let Some(root_id) = tree.root() {
37        reconcile_node(tree, root_id, element, state, &mut patches, dependencies);
38    }
39
40    patches
41}
42
43/// Reconcile an IRNode tree against the instance tree and generate patches
44/// This is the primary entry point for the IRNode-based reconciliation system,
45/// which supports first-class ForEach, When/If, and custom item variable names.
46pub fn reconcile_ir(
47    tree: &mut InstanceTree,
48    node: &IRNode,
49    parent_id: Option<NodeId>,
50    state: &serde_json::Value,
51    dependencies: &mut DependencyGraph,
52) -> Vec<Patch> {
53    let mut patches = Vec::new();
54
55    // For initial render, create the tree
56    if tree.root().is_none() {
57        let node_id = create_ir_node_tree(tree, node, parent_id, state, &mut patches, true, dependencies);
58        tree.set_root(node_id);
59        return patches;
60    }
61
62    // Incremental update: reconcile root against existing tree
63    if let Some(root_id) = tree.root() {
64        reconcile_ir_node(tree, root_id, node, state, &mut patches, dependencies);
65    }
66
67    patches
68}
69
70/// Create a new subtree from an element
71pub fn create_tree(
72    tree: &mut InstanceTree,
73    element: &Element,
74    parent_id: Option<NodeId>,
75    state: &serde_json::Value,
76    patches: &mut Vec<Patch>,
77    is_root: bool,
78    dependencies: &mut DependencyGraph,
79) -> NodeId {
80    // Special handling for iterable elements (List, Grid, etc.)
81    // An element is iterable if it has BOTH:
82    // 1. prop "0" with a Binding (like @state.items) - not a static value
83    // 2. children (the template to repeat for each item)
84    // This distinguishes iterables from:
85    // - Text elements (prop "0" but no children)
86    // - Route/Link elements (prop "0" with static path, not a binding)
87    if let Some(Value::Binding(_)) = element.props.get("0") {
88        if !element.children.is_empty() {
89            return create_list_tree(tree, element, parent_id, state, patches, is_root, dependencies);
90        }
91    }
92
93    // Create the node
94    let node_id = tree.create_node(element, state);
95
96    // Register dependencies for this node
97    for value in element.props.values() {
98        match value {
99            Value::Binding(binding) => {
100                dependencies.add_dependency(node_id, binding);
101            }
102            Value::TemplateString { bindings, .. } => {
103                // Register all bindings in the template string
104                for binding in bindings {
105                    dependencies.add_dependency(node_id, binding);
106                }
107            }
108            _ => {}
109        }
110    }
111
112    // Generate Create patch
113    let node = tree.get(node_id).unwrap();
114    
115    // For lazy elements, add child component name as a prop
116    let mut props = node.props.clone();
117    if let Some(Value::Static(val)) = element.props.get("__lazy") {
118        if val.as_bool().unwrap_or(false) && !element.children.is_empty() {
119            // Store the first child's element type so renderer knows what component to load
120            let child_component = &element.children[0].element_type;
121            props.insert("__lazy_child".to_string(), serde_json::json!(child_component));
122        }
123    }
124    
125    patches.push(Patch::create(
126        node_id,
127        node.element_type.clone(),
128        props,
129    ));
130
131    // Event handling is now done at the renderer level
132
133    // If there's a parent, insert this node
134    if let Some(parent) = parent_id {
135        tree.add_child(parent, node_id, None);
136        patches.push(Patch::insert(parent, node_id, None));
137    } else if is_root {
138        // This is the root node - insert into "root" container
139        patches.push(Patch::insert_root(node_id));
140    }
141
142    // Create children (skip if element is marked as lazy)
143    let is_lazy = element.props.get("__lazy")
144        .and_then(|v| {
145            if let Value::Static(val) = v {
146                val.as_bool()
147            } else {
148                None
149            }
150        })
151        .unwrap_or(false);
152    
153    if !is_lazy {
154        for child_element in &element.children {
155            create_tree(tree, child_element, Some(node_id), state, patches, false, dependencies);
156        }
157    }
158
159    node_id
160}
161
162/// Create an iterable element (List, Grid, etc.) that iterates over an array in state
163/// Iterable elements are identified by having prop "0" with an array binding
164fn create_list_tree(
165    tree: &mut InstanceTree,
166    element: &Element,
167    parent_id: Option<NodeId>,
168    state: &serde_json::Value,
169    patches: &mut Vec<Patch>,
170    is_root: bool,
171    dependencies: &mut DependencyGraph,
172) -> NodeId {
173    // Get the array binding from first prop (prop "0")
174    let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
175        evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
176    } else {
177        serde_json::Value::Array(vec![])
178    };
179
180    // Create a container element - use the original element type (List, Grid, etc.)
181    // but remove the "0" prop since it's only for iteration, not rendering
182    let mut list_element = Element::new(&element.element_type);
183    for (key, value) in &element.props {
184        // Skip prop "0" - it's only for array binding, not for rendering
185        if key != "0" {
186            list_element.props.insert(key.clone(), value.clone());
187        }
188    }
189    
190    let node_id = tree.create_node(&list_element, state);
191
192    // Register the List node as depending on the array binding
193    // This is critical for reactive updates when the array changes
194    if let Some(Value::Binding(binding)) = element.props.get("0") {
195        dependencies.add_dependency(node_id, binding);
196    }
197
198    // Store the original element template for re-reconciliation
199    // This allows us to rebuild the list when the array changes
200    if let Some(node) = tree.get_mut(node_id) {
201        node.raw_props = element.props.clone();
202        node.element_template = Some(std::sync::Arc::new(element.clone()));
203    }
204
205    // Generate Create patch for container
206    let node = tree.get(node_id).unwrap();
207    patches.push(Patch::create(
208        node_id,
209        node.element_type.clone(),
210        node.props.clone(),
211    ));
212
213    // Insert container
214    if let Some(parent) = parent_id {
215        tree.add_child(parent, node_id, None);
216        patches.push(Patch::insert(parent, node_id, None));
217    } else if is_root {
218        patches.push(Patch::insert_root(node_id));
219    }
220
221    // If array is empty, return early
222    if let serde_json::Value::Array(items) = &array {
223        // Create children for each item in the array
224        for (index, item) in items.iter().enumerate() {
225            for child_template in &element.children {
226                // Clone child and replace ${item.x} bindings with actual item data
227                let child_with_item = replace_item_bindings(child_template, item, index);
228                create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
229            }
230        }
231    }
232
233    node_id
234}
235
236/// Navigate a nested path in a JSON value (e.g., "images.0" or "category.name")
237/// Supports both object keys and array indices
238fn navigate_item_path<'a>(item: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
239    let mut current = item;
240
241    for segment in path.split('.') {
242        // Try to parse as array index first
243        if let Ok(index) = segment.parse::<usize>() {
244            current = current.get(index)?;
245        } else {
246            // Otherwise treat as object key
247            current = current.get(segment)?;
248        }
249    }
250
251    Some(current)
252}
253
254/// A replacement to be applied: (start_index, end_index, replacement_string)
255/// Using a struct for clarity
256#[derive(Debug)]
257struct Replacement {
258    start: usize,
259    end: usize,
260    text: String,
261}
262
263/// Apply all collected replacements to a string in a single pass.
264/// Builds result by copying segments between replacements - true O(n + m) complexity.
265fn apply_replacements(s: &str, mut replacements: Vec<Replacement>) -> String {
266    if replacements.is_empty() {
267        return s.to_string();
268    }
269
270    // Sort by start position ascending
271    replacements.sort_by(|a, b| a.start.cmp(&b.start));
272
273    // Filter invalid replacements and calculate result size
274    let valid_replacements: Vec<_> = replacements
275        .into_iter()
276        .filter(|r| r.start <= s.len() && r.end <= s.len() && r.start <= r.end)
277        .collect();
278
279    if valid_replacements.is_empty() {
280        return s.to_string();
281    }
282
283    // Calculate total size: original - removed + added
284    let removed: usize = valid_replacements.iter().map(|r| r.end - r.start).sum();
285    let added: usize = valid_replacements.iter().map(|r| r.text.len()).sum();
286    let capacity = s.len() - removed + added;
287
288    let mut result = String::with_capacity(capacity);
289    let mut pos = 0;
290
291    for r in valid_replacements {
292        // Push the segment before this replacement
293        if r.start > pos {
294            result.push_str(&s[pos..r.start]);
295        }
296        // Push the replacement text
297        result.push_str(&r.text);
298        pos = r.end;
299    }
300
301    // Push remaining segment after last replacement
302    if pos < s.len() {
303        result.push_str(&s[pos..]);
304    }
305
306    result
307}
308
309/// Find the end of a path (valid identifier chars: alphanumeric, _, and . for nesting)
310fn find_path_end(s: &str, start: usize) -> usize {
311    let chars: Vec<char> = s[start..].chars().collect();
312    let mut end = start;
313
314    for (i, ch) in chars.iter().enumerate() {
315        if ch.is_alphanumeric() || *ch == '_' {
316            end = start + i + 1;
317        } else if *ch == '.' {
318            // Dot is valid only if followed by alphanumeric or underscore
319            if i + 1 < chars.len() {
320                let next = chars[i + 1];
321                if next.is_alphanumeric() || next == '_' {
322                    end = start + i + 1;
323                    continue;
324                }
325            }
326            break;
327        } else {
328            break;
329        }
330    }
331
332    end
333}
334
335/// Format a JSON value for replacement in expressions
336fn format_value_for_replacement(val: &serde_json::Value, quote_strings: bool) -> String {
337    match val {
338        serde_json::Value::String(s) => {
339            if quote_strings {
340                format!("'{}'", s)
341            } else {
342                s.clone()
343            }
344        }
345        serde_json::Value::Number(n) => n.to_string(),
346        serde_json::Value::Bool(b) => b.to_string(),
347        serde_json::Value::Null => "null".to_string(),
348        _ => serde_json::to_string(val).unwrap_or_default(),
349    }
350}
351
352/// Replace item bindings (Value::Binding with is_item() or TemplateString with item bindings) with actual item values
353/// OPTIMIZED: Uses single-pass replacement instead of O(n²) repeated scans
354/// This is a convenience wrapper that uses "item" as the default item name.
355pub fn replace_item_bindings(element: &Element, item: &serde_json::Value, index: usize) -> Element {
356    replace_item_bindings_with_name(element, item, index, "item")
357}
358
359/// Reconcile a single node with a new element
360pub fn reconcile_node(
361    tree: &mut InstanceTree,
362    node_id: NodeId,
363    element: &Element,
364    state: &serde_json::Value,
365    patches: &mut Vec<Patch>,
366    dependencies: &mut DependencyGraph,
367) {
368    let node = tree.get(node_id).cloned();
369    if node.is_none() {
370        return;
371    }
372    let node = node.unwrap();
373
374    // Special handling for iterable elements (List, Grid, etc.)
375    // An element is iterable if it has BOTH prop "0" (array binding) AND children (template)
376    // This distinguishes iterables from Text elements, which also use prop "0" but have no children
377    let is_iterable = element.props.get("0").is_some() && !element.children.is_empty();
378    
379    if is_iterable {
380        // Get the array binding from first prop (prop "0")
381        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
382            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
383        } else {
384            serde_json::Value::Array(vec![])
385        };
386
387        // Regenerate iterable children
388        if let serde_json::Value::Array(items) = &array {
389            let old_children = node.children.clone();
390            
391            // Calculate expected number of children
392            let expected_children_count = items.len() * element.children.len();
393            
394            // Remove old children if count doesn't match
395            if old_children.len() != expected_children_count {
396                for &old_child_id in &old_children {
397                    patches.push(Patch::remove(old_child_id));
398                }
399                
400                // Clear children from node
401                if let Some(node) = tree.get_mut(node_id) {
402                    node.children.clear();
403                }
404                
405                // Create new children
406                for (index, item) in items.iter().enumerate() {
407                    for child_template in &element.children {
408                        let child_with_item = replace_item_bindings(child_template, item, index);
409                        create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
410                    }
411                }
412            } else {
413                // Reconcile existing children
414                let mut child_index = 0;
415                for (item_index, item) in items.iter().enumerate() {
416                    for child_template in &element.children {
417                        if let Some(&old_child_id) = old_children.get(child_index) {
418                            let child_with_item = replace_item_bindings(child_template, item, item_index);
419                            reconcile_node(tree, old_child_id, &child_with_item, state, patches, dependencies);
420                        }
421                        child_index += 1;
422                    }
423                }
424            }
425        }
426        
427        return; // Done with List reconciliation
428    }
429
430    // If element type changed, replace the entire subtree
431    if node.element_type != element.element_type {
432        replace_subtree(tree, node_id, &node, element, state, patches, dependencies);
433        return;
434    }
435
436    // Register dependencies for this node (same as create_tree)
437    for value in element.props.values() {
438        if let Value::Binding(binding) = value {
439            dependencies.add_dependency(node_id, binding);
440        }
441    }
442
443    // Diff props
444    let new_props = resolve_props(&element.props, state);
445    let prop_patches = diff_props(node_id, &node.props, &new_props);
446    patches.extend(prop_patches);
447
448    // Update node props in tree
449    if let Some(node) = tree.get_mut(node_id) {
450        node.props = new_props.clone();
451        node.raw_props = element.props.clone();
452    }
453
454    // Reconcile children (skip if element is marked as lazy)
455    let is_lazy = element.props.get("__lazy")
456        .and_then(|v| {
457            if let Value::Static(val) = v {
458                val.as_bool()
459            } else {
460                None
461            }
462        })
463        .unwrap_or(false);
464    
465    if !is_lazy {
466        let old_children = node.children.clone();
467        let new_children = &element.children;
468
469        // Simple strategy: match children by index
470        for (i, new_child_element) in new_children.iter().enumerate() {
471            if let Some(&old_child_id) = old_children.get(i) {
472                // Reconcile existing child
473                reconcile_node(tree, old_child_id, new_child_element, state, patches, dependencies);
474            } else {
475                // Create new child
476                let new_child_id = create_tree(tree, new_child_element, Some(node_id), state, patches, false, dependencies);
477                if let Some(node) = tree.get_mut(node_id) {
478                    node.children.push_back(new_child_id);
479                }
480            }
481        }
482
483        // Remove extra old children
484        if old_children.len() > new_children.len() {
485            for old_child_id in old_children.iter().skip(new_children.len()).copied() {
486                // Collect all nodes in subtree first (for Remove patches)
487                let subtree_ids = collect_subtree_ids(tree, old_child_id);
488                for &id in &subtree_ids {
489                    patches.push(Patch::remove(id));
490                    dependencies.remove_node(id);
491                }
492                // Remove from parent's children and from tree
493                tree.remove_child(node_id, old_child_id);
494                tree.remove(old_child_id);
495            }
496        }
497    }
498}
499
500/// Replace an entire subtree when element types don't match.
501/// This removes the old node and its descendants, then creates a new subtree in its place.
502fn replace_subtree(
503    tree: &mut InstanceTree,
504    old_node_id: NodeId,
505    old_node: &super::InstanceNode,
506    new_element: &Element,
507    state: &serde_json::Value,
508    patches: &mut Vec<Patch>,
509    dependencies: &mut DependencyGraph,
510) {
511    let parent_id = old_node.parent;
512
513    // Remember the position of the old node in its parent's children list
514    let old_position = if let Some(pid) = parent_id {
515        tree.get(pid).and_then(|parent| {
516            parent.children.iter().position(|&id| id == old_node_id)
517        })
518    } else {
519        None
520    };
521
522    // Collect all node IDs in the old subtree (depth-first, children before parents)
523    let ids_to_remove = collect_subtree_ids(tree, old_node_id);
524
525    // Generate Remove patches and clear dependencies in one pass
526    for &id in &ids_to_remove {
527        patches.push(Patch::remove(id));
528        dependencies.remove_node(id);
529    }
530
531    // Remove old node from parent's children list
532    if let Some(pid) = parent_id {
533        if let Some(parent) = tree.get_mut(pid) {
534            parent.children = parent.children.iter().filter(|&&id| id != old_node_id).copied().collect();
535        }
536    }
537
538    // Remove the old subtree from the tree
539    tree.remove(old_node_id);
540
541    // Create the new subtree
542    let is_root = parent_id.is_none();
543    let new_node_id = create_tree(tree, new_element, parent_id, state, patches, is_root, dependencies);
544
545    // If this was the root, update it
546    if is_root {
547        tree.set_root(new_node_id);
548    } else if let Some(pid) = parent_id {
549        if let Some(pos) = old_position {
550            if let Some(parent) = tree.get_mut(pid) {
551                let current_len = parent.children.len();
552                // create_tree appended at end, so new node is at current_len - 1
553                // We want it at position `pos`
554                if pos < current_len - 1 {
555                    // Pop from end (O(log n) for im::Vector) and insert at correct position
556                    let new_id = parent.children.pop_back().unwrap();
557                    parent.children.insert(pos, new_id);
558
559                    // Get the next sibling for the Move patch
560                    let next_sibling = parent.children.get(pos + 1).copied();
561                    patches.push(Patch::move_node(pid, new_node_id, next_sibling));
562                }
563                // If pos == current_len - 1, it's already in the right place (was last child)
564            }
565        }
566    }
567}
568
569/// Collect all node IDs in a subtree (post-order: children before parents)
570/// Uses iterative approach to avoid stack overflow on deep trees
571fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
572    let mut result = Vec::new();
573    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
574
575    while let Some((node_id, children_processed)) = stack.pop() {
576        if children_processed {
577            // Children already processed, add this node
578            result.push(node_id);
579        } else {
580            // Push self back with flag, then push children
581            stack.push((node_id, true));
582            if let Some(node) = tree.get(node_id) {
583                // Push children in reverse order so they're processed left-to-right
584                for &child_id in node.children.iter().rev() {
585                    stack.push((child_id, false));
586                }
587            }
588        }
589    }
590
591    result
592}
593
594/// Generate a stable key for a list item.
595/// Tries to use a key_path (e.g., "id") from the item, falling back to index-based key.
596fn generate_item_key(item: &serde_json::Value, key_path: Option<&str>, item_name: &str, index: usize) -> String {
597    if let Some(kp) = key_path {
598        item.get(kp)
599            .map(|v| match v {
600                serde_json::Value::String(s) => s.clone(),
601                serde_json::Value::Number(n) => n.to_string(),
602                _ => format!("{}-{}", item_name, index),
603            })
604            .unwrap_or_else(|| format!("{}-{}", item_name, index))
605    } else {
606        format!("{}-{}", item_name, index)
607    }
608}
609
610/// Reconcile children using keys for efficient updates
611/// Uses LIS (Longest Increasing Subsequence) algorithm to minimize DOM moves
612pub fn reconcile_keyed_children(
613    tree: &mut InstanceTree,
614    parent_id: NodeId,
615    old_children: &[NodeId],
616    new_children: &[Element],
617    state: &serde_json::Value,
618    dependencies: &mut DependencyGraph,
619) -> Vec<Patch> {
620    let mut patches = Vec::new();
621
622    // Build a map of old children by key
623    let mut old_keyed: IndexMap<String, NodeId> = IndexMap::new();
624    let mut old_unkeyed: Vec<NodeId> = Vec::new();
625
626    for &child_id in old_children {
627        if let Some(node) = tree.get(child_id) {
628            if let Some(key) = &node.key {
629                old_keyed.insert(key.clone(), child_id);
630            } else {
631                old_unkeyed.push(child_id);
632            }
633        }
634    }
635
636    let mut new_child_ids: Vec<NodeId> = Vec::new();
637    let mut unkeyed_idx = 0;
638
639    // First pass: match children by key or create new ones
640    for new_element in new_children {
641        let new_key = new_element.key.as_ref();
642
643        let child_id = if let Some(key) = new_key {
644            // Try to find matching keyed child
645            if let Some(&old_id) = old_keyed.get(key) {
646                // Found a match - reconcile it
647                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
648                old_keyed.shift_remove(key); // Mark as used
649                old_id
650            } else {
651                // No match - create new child
652                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
653            }
654        } else {
655            // Unkeyed child - try to reuse unkeyed old child
656            if let Some(&old_id) = old_unkeyed.get(unkeyed_idx) {
657                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
658                unkeyed_idx += 1;
659                old_id
660            } else {
661                // No unkeyed child available - create new
662                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
663            }
664        };
665
666        new_child_ids.push(child_id);
667    }
668
669    // Second pass: generate move/insert patches using LIS algorithm
670    // Build list of old positions for children that were reused
671    let mut old_positions: Vec<Option<usize>> = Vec::new();
672    for &new_id in &new_child_ids {
673        let old_pos = old_children.iter().position(|&old_id| old_id == new_id);
674        old_positions.push(old_pos);
675    }
676
677    // Find longest increasing subsequence to minimize moves
678    let lis = longest_increasing_subsequence(&old_positions);
679
680    // Update parent's children list
681    if let Some(parent_node) = tree.get_mut(parent_id) {
682        parent_node.children = new_child_ids.iter().copied().collect();
683    }
684
685    // Generate move/insert patches for children not in LIS
686    for (new_idx, &child_id) in new_child_ids.iter().enumerate() {
687        // Check if this child needs to be moved
688        if !lis.contains(&new_idx) {
689            // Calculate before_id (the next child in the list, or None for last position)
690            let before_id = new_child_ids.get(new_idx + 1).copied();
691            patches.push(Patch::move_node(parent_id, child_id, before_id));
692        }
693    }
694
695    // Third pass: remove old children that weren't reused
696    // Also clean up their dependencies to prevent memory leaks
697    for &old_id in old_keyed.values() {
698        dependencies.remove_node(old_id);
699        tree.remove(old_id);
700        patches.push(Patch::remove(old_id));
701    }
702    for &old_id in &old_unkeyed[unkeyed_idx..] {
703        dependencies.remove_node(old_id);
704        tree.remove(old_id);
705        patches.push(Patch::remove(old_id));
706    }
707
708    patches
709}
710
711/// Find the longest increasing subsequence (LIS) indices.
712/// Uses the patience-sort / binary-search algorithm for O(n log n) performance.
713/// Returns indices (into the original `positions` slice) of elements that are
714/// already in correct relative order, minimizing DOM moves during reconciliation.
715fn longest_increasing_subsequence(positions: &[Option<usize>]) -> Vec<usize> {
716    // Extract valid positions with their original indices
717    let valid: Vec<(usize, usize)> = positions
718        .iter()
719        .enumerate()
720        .filter_map(|(idx, &pos)| pos.map(|p| (idx, p)))
721        .collect();
722
723    if valid.is_empty() {
724        return Vec::new();
725    }
726
727    let n = valid.len();
728    // `tails[i]` holds the index (into `valid`) of the smallest tail element for
729    // an increasing subsequence of length `i + 1`.
730    let mut tails: Vec<usize> = Vec::with_capacity(n);
731    // `prev[i]` links back to the predecessor of valid[i] in the LIS.
732    let mut prev: Vec<Option<usize>> = vec![None; n];
733
734    for i in 0..n {
735        let val = valid[i].1;
736        // Binary search for the leftmost tail >= val
737        let pos = tails.partition_point(|&t| valid[t].1 < val);
738
739        if pos == tails.len() {
740            tails.push(i);
741        } else {
742            tails[pos] = i;
743        }
744
745        if pos > 0 {
746            prev[i] = Some(tails[pos - 1]);
747        }
748    }
749
750    // Reconstruct: walk backwards from the last tail entry
751    let mut result = Vec::with_capacity(tails.len());
752    let mut idx = Some(*tails.last().unwrap());
753    while let Some(i) = idx {
754        result.push(valid[i].0);
755        idx = prev[i];
756    }
757    result.reverse();
758
759    result
760}
761
762/// Diff two sets of props and generate SetProp/RemoveProp patches
763pub fn diff_props(
764    node_id: NodeId,
765    old_props: &IndexMap<String, serde_json::Value>,
766    new_props: &IndexMap<String, serde_json::Value>,
767) -> Vec<Patch> {
768    let mut patches = Vec::new();
769
770    // Check for changed or new props
771    for (key, new_value) in new_props {
772        if old_props.get(key) != Some(new_value) {
773            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
774        }
775    }
776
777    // Remove props that exist in old but not in new
778    for key in old_props.keys() {
779        if !new_props.contains_key(key) {
780            patches.push(Patch::remove_prop(node_id, key.clone()));
781        }
782    }
783
784    patches
785}
786
787// ============================================================================
788// IRNode-based reconciliation (first-class control flow constructs)
789// ============================================================================
790
791/// Create a tree from an IRNode (supports ForEach, When/If, and regular elements)
792pub fn create_ir_node_tree(
793    tree: &mut InstanceTree,
794    node: &IRNode,
795    parent_id: Option<NodeId>,
796    state: &serde_json::Value,
797    patches: &mut Vec<Patch>,
798    is_root: bool,
799    dependencies: &mut DependencyGraph,
800) -> NodeId {
801    match node {
802        IRNode::Element(element) => {
803            // Regular element - use existing create_tree logic
804            create_tree(tree, element, parent_id, state, patches, is_root, dependencies)
805        }
806        IRNode::ForEach { source, item_name, key_path, template, props } => {
807            let mut ctx = ReconcileCtx { tree, state, patches, dependencies };
808            create_foreach_ir_tree(
809                &mut ctx,
810                source,
811                item_name,
812                key_path.as_deref(),
813                template,
814                props,
815                node,
816                parent_id,
817                is_root,
818            )
819        }
820        IRNode::Conditional { value, branches, fallback } => {
821            let mut ctx = ReconcileCtx { tree, state, patches, dependencies };
822            create_conditional_tree(
823                &mut ctx,
824                value,
825                branches,
826                fallback.as_deref(),
827                node,
828                parent_id,
829                is_root,
830            )
831        }
832    }
833}
834
835/// Create an IRNode tree with separate logical and render parents.
836///
837/// This is used for control flow children where:
838/// - `logical_parent`: Where the node goes in the InstanceTree (the control flow node)
839/// - `render_parent`: Where Insert patches should target (the grandparent)
840///
841/// Control flow nodes are transparent - they exist in the tree for bookkeeping
842/// but don't create DOM elements. Their children render directly into the grandparent.
843fn create_ir_node_tree_with_render_parent(
844    ctx: &mut ReconcileCtx,
845    node: &IRNode,
846    logical_parent: Option<NodeId>,
847    render_parent: Option<NodeId>,
848    is_root: bool,
849) -> NodeId {
850    match node {
851        IRNode::Element(element) => {
852            // Create element and add to logical parent in tree
853            let node_id = ctx.tree.create_node(element, ctx.state);
854
855            // Add to logical parent's children list
856            if let Some(parent) = logical_parent {
857                ctx.tree.add_child(parent, node_id, None);
858            }
859
860            // Emit patches targeting the render parent
861            ctx.patches.push(Patch::create(
862                node_id,
863                element.element_type.clone(),
864                ctx.tree.get(node_id).map(|n| n.props.clone()).unwrap_or_default(),
865            ));
866
867            if let Some(render_p) = render_parent {
868                ctx.patches.push(Patch::insert(render_p, node_id, None));
869            } else if is_root {
870                ctx.patches.push(Patch::insert_root(node_id));
871            }
872
873            // Register dependencies
874            for (_, value) in &element.props {
875                if let Value::Binding(binding) = value {
876                    ctx.dependencies.add_dependency(node_id, binding);
877                }
878            }
879
880            // Recursively create children - they use this node as both logical and render parent
881            for child in &element.children {
882                let child_ir = IRNode::Element((**child).clone());
883                create_ir_node_tree(ctx.tree, &child_ir, Some(node_id), ctx.state, ctx.patches, false, ctx.dependencies);
884            }
885
886            node_id
887        }
888        IRNode::ForEach { source, item_name, key_path, template, props } => {
889            // ForEach within control flow - still uses grandparent for its own children
890            create_foreach_ir_tree(
891                ctx,
892                source,
893                item_name,
894                key_path.as_deref(),
895                template,
896                props,
897                node,
898                logical_parent,  // ForEach goes into logical parent
899                is_root,
900            )
901        }
902        IRNode::Conditional { value, branches, fallback } => {
903            // Nested conditional - also transparent
904            create_conditional_tree(
905                ctx,
906                value,
907                branches,
908                fallback.as_deref(),
909                node,
910                logical_parent,  // Conditional goes into logical parent
911                is_root,
912            )
913        }
914    }
915}
916
917/// Create a ForEach iteration tree from IRNode::ForEach
918#[allow(clippy::too_many_arguments)]
919fn create_foreach_ir_tree(
920    ctx: &mut ReconcileCtx,
921    source: &Binding,
922    item_name: &str,
923    key_path: Option<&str>,
924    template: &[IRNode],
925    props: &Props,
926    original_node: &IRNode,
927    parent_id: Option<NodeId>,
928    is_root: bool,
929) -> NodeId {
930    // Evaluate the source array binding
931    let array = evaluate_binding(source, ctx.state).unwrap_or(serde_json::Value::Array(vec![]));
932
933    // Resolve container props
934    let resolved_props = resolve_props(props, ctx.state);
935
936    // Create the ForEach container node (internal bookkeeping only - no DOM element)
937    let node_id = ctx.tree.create_control_flow_node(
938        "__ForEach",
939        resolved_props.clone(),
940        props.clone(),
941        ControlFlowKind::ForEach {
942            item_name: item_name.to_string(),
943            key_path: key_path.map(|s| s.to_string()),
944        },
945        original_node.clone(),
946    );
947
948    // Register dependency on the source array binding
949    ctx.dependencies.add_dependency(node_id, source);
950
951    // Add to parent's children list (for tree structure) but NO patches for the container itself
952    // Control flow nodes are transparent - they don't create DOM elements
953    if let Some(parent) = parent_id {
954        ctx.tree.add_child(parent, node_id, None);
955    }
956
957    // Render children for each item in the array
958    // Children are inserted into the GRANDPARENT for rendering purposes
959    let render_parent = parent_id; // Children render directly into ForEach's parent
960
961    if let serde_json::Value::Array(items) = &array {
962        for (index, item) in items.iter().enumerate() {
963            let item_key = generate_item_key(item, key_path, item_name, index);
964
965            // Create each template child with item bindings replaced
966            for child_template in template {
967                let child_with_item = replace_ir_node_item_bindings(
968                    child_template,
969                    item,
970                    index,
971                    item_name,
972                    &item_key,
973                );
974                // Children are added to ForEach node in tree, but patches go to grandparent
975                create_ir_node_tree_with_render_parent(
976                    ctx,
977                    &child_with_item,
978                    Some(node_id),  // logical parent (ForEach)
979                    render_parent,   // render parent (grandparent)
980                    is_root && render_parent.is_none(),
981                );
982            }
983        }
984    }
985
986    node_id
987}
988
989/// Create a Conditional (When/If) tree from IRNode::Conditional
990fn create_conditional_tree(
991    ctx: &mut ReconcileCtx,
992    value: &Value,
993    branches: &[ConditionalBranch],
994    fallback: Option<&[IRNode]>,
995    original_node: &IRNode,
996    parent_id: Option<NodeId>,
997    is_root: bool,
998) -> NodeId {
999    // Evaluate the condition value
1000    let evaluated_value = evaluate_value(value, ctx.state);
1001
1002    // Create the Conditional container node (internal bookkeeping only - no DOM element)
1003    let mut raw_props = Props::new();
1004    raw_props.insert("__condition".to_string(), value.clone());
1005
1006    let node_id = ctx.tree.create_control_flow_node(
1007        "__Conditional",
1008        IndexMap::new(),
1009        raw_props,
1010        ControlFlowKind::Conditional,
1011        original_node.clone(),
1012    );
1013
1014    // Register dependency on the condition binding
1015    if let Value::Binding(binding) = value {
1016        ctx.dependencies.add_dependency(node_id, binding);
1017    } else if let Value::TemplateString { bindings, .. } = value {
1018        for binding in bindings {
1019            ctx.dependencies.add_dependency(node_id, binding);
1020        }
1021    }
1022
1023    // Add to parent's children list (for tree structure) but NO patches for the container itself
1024    // Control flow nodes are transparent - they don't create DOM elements
1025    if let Some(parent) = parent_id {
1026        ctx.tree.add_child(parent, node_id, None);
1027    }
1028
1029    // Find matching branch
1030    let matched_children = find_matching_branch(&evaluated_value, branches, fallback, ctx.state);
1031
1032    // Render matched children - insert directly into grandparent for rendering
1033    let render_parent = parent_id; // Children render directly into Conditional's parent
1034
1035    if let Some(children) = matched_children {
1036        for child in children {
1037            // Children are added to Conditional node in tree, but patches go to grandparent
1038            create_ir_node_tree_with_render_parent(
1039                ctx,
1040                child,
1041                Some(node_id),  // logical parent (Conditional)
1042                render_parent,   // render parent (grandparent)
1043                is_root && render_parent.is_none(),
1044            );
1045        }
1046    }
1047
1048    node_id
1049}
1050
1051/// Find the matching branch for a conditional value
1052fn find_matching_branch<'a>(
1053    evaluated_value: &serde_json::Value,
1054    branches: &'a [ConditionalBranch],
1055    fallback: Option<&'a [IRNode]>,
1056    state: &serde_json::Value,
1057) -> Option<&'a [IRNode]> {
1058    for branch in branches {
1059        if pattern_matches(evaluated_value, &branch.pattern, state) {
1060            return Some(&branch.children);
1061        }
1062    }
1063
1064    fallback
1065}
1066
1067/// Check if a pattern matches an evaluated value
1068/// Supports:
1069/// - Static values: exact equality matching
1070/// - Bindings: evaluate the binding against state and check truthiness
1071/// - TemplateStrings: evaluate as expression with `value` available
1072fn pattern_matches(evaluated_value: &serde_json::Value, pattern: &Value, state: &serde_json::Value) -> bool {
1073    match pattern {
1074        Value::Static(pattern_value) => {
1075            values_match(evaluated_value, pattern_value)
1076        }
1077        Value::Binding(binding) => {
1078            // Evaluate the binding against state and check if the result is truthy.
1079            // This allows: Case(match: @state.isAdmin)
1080            let resolved = evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null);
1081            is_truthy(&resolved)
1082        }
1083        Value::TemplateString { template, .. } => {
1084            // Expression pattern: evaluate with `value` variable available
1085            // This allows: Case(when: "${value > 100}") or Case(match: "${value == 'loading'}")
1086            evaluate_expression_pattern(template, evaluated_value)
1087        }
1088        Value::Action(_) => false, // Actions don't make sense as patterns
1089    }
1090}
1091
1092/// Evaluate an expression pattern against a value
1093/// The expression has access to `value` which is the evaluated When condition
1094fn evaluate_expression_pattern(template: &str, value: &serde_json::Value) -> bool {
1095    use crate::reactive::evaluate_expression;
1096    use std::collections::HashMap;
1097
1098    // Build a context with the value available
1099    let mut context: HashMap<String, serde_json::Value> = HashMap::new();
1100    context.insert("value".to_string(), value.clone());
1101
1102    // Extract the expression from the template (e.g., "${value > 100}" -> "value > 100")
1103    let expr = if template.starts_with("${") && template.ends_with("}") {
1104        &template[2..template.len()-1]
1105    } else if template.contains("${") {
1106        // Template with embedded expression - evaluate the whole thing
1107        // Build a JSON context for evaluate_template_string
1108        let json_context = serde_json::json!({ "value": value });
1109        match evaluate_template_string(template, &json_context, None) {
1110            Ok(result) => {
1111                // Check if result is truthy
1112                return is_truthy_string(&result);
1113            }
1114            Err(_) => return false,
1115        }
1116    } else {
1117        // Plain expression without ${} wrapper
1118        template
1119    };
1120
1121    // Evaluate the expression
1122    match evaluate_expression(expr, &context) {
1123        Ok(result) => is_truthy(&result),
1124        Err(_) => false,
1125    }
1126}
1127
1128/// Check if a string value is truthy
1129fn is_truthy_string(s: &str) -> bool {
1130    !s.is_empty() && s != "false" && s != "0" && s != "null" && s != "undefined"
1131}
1132
1133/// Check if a JSON value is truthy
1134fn is_truthy(value: &serde_json::Value) -> bool {
1135    match value {
1136        serde_json::Value::Null => false,
1137        serde_json::Value::Bool(b) => *b,
1138        serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
1139        serde_json::Value::String(s) => is_truthy_string(s),
1140        serde_json::Value::Array(arr) => !arr.is_empty(),
1141        serde_json::Value::Object(obj) => !obj.is_empty(),
1142    }
1143}
1144
1145/// Check if two JSON values match for conditional branching
1146/// Supports:
1147/// - Exact equality: Case(match: "loading"), Case(match: 42), Case(match: true), Case(match: null)
1148/// - Wildcard patterns: Case(match: "_") or Case(match: "*") matches anything
1149/// - Multiple values: Case(match: ["loading", "pending"]) matches any in the array
1150/// - Expression patterns: Case(match: "${value > 100}")
1151fn values_match(value: &serde_json::Value, pattern: &serde_json::Value) -> bool {
1152    match pattern {
1153        // Array pattern - match any value in the array
1154        serde_json::Value::Array(patterns) => {
1155            patterns.iter().any(|p| values_match(value, p))
1156        }
1157
1158        // String pattern with special syntax
1159        serde_json::Value::String(pattern_str) => {
1160            // Wildcard patterns
1161            if pattern_str == "_" || pattern_str == "*" {
1162                return true;
1163            }
1164
1165            // Expression pattern (${...})
1166            if pattern_str.contains("${") {
1167                return evaluate_expression_pattern(pattern_str, value);
1168            }
1169
1170            // Exact string equality
1171            matches!(value, serde_json::Value::String(s) if s == pattern_str)
1172        }
1173
1174        // Exact equality for non-string primitives
1175        _ => {
1176            if value == pattern {
1177                return true;
1178            }
1179
1180            // Boolean coercion for truthy/falsy matching
1181            match (value, pattern) {
1182                (serde_json::Value::Bool(b), serde_json::Value::Bool(p)) => b == p,
1183                (serde_json::Value::Null, serde_json::Value::Null) => true,
1184                _ => false,
1185            }
1186        }
1187    }
1188}
1189
1190/// Evaluate a Value against state
1191fn evaluate_value(value: &Value, state: &serde_json::Value) -> serde_json::Value {
1192    match value {
1193        Value::Static(v) => v.clone(),
1194        Value::Binding(binding) => {
1195            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
1196        }
1197        Value::TemplateString { template, .. } => {
1198            match evaluate_template_string(template, state, None) {
1199                Ok(result) => serde_json::Value::String(result),
1200                Err(_) => serde_json::Value::String(template.clone()),
1201            }
1202        }
1203        Value::Action(action) => serde_json::Value::String(format!("@{}", action)),
1204    }
1205}
1206
1207/// Replace item bindings in an IRNode with actual item values
1208/// Supports configurable item variable names (e.g., "todo", "user" instead of "item")
1209fn replace_ir_node_item_bindings(
1210    node: &IRNode,
1211    item: &serde_json::Value,
1212    index: usize,
1213    item_name: &str,
1214    item_key: &str,
1215) -> IRNode {
1216    match node {
1217        IRNode::Element(element) => {
1218            let mut new_element = replace_item_bindings_with_name(element, item, index, item_name);
1219            // Override key with the computed item key
1220            new_element.key = Some(item_key.to_string());
1221            IRNode::Element(new_element)
1222        }
1223        IRNode::ForEach { source, item_name: inner_item_name, key_path, template, props } => {
1224            // Note: source binding is kept as-is; nested ForEach maintains its own iteration context
1225            // Replace in props
1226            let new_props = replace_props_item_bindings(props, item, item_name);
1227
1228            // Recursively replace in template (but inner ForEach has its own item context)
1229            let new_template: Vec<IRNode> = template.iter()
1230                .map(|child| replace_ir_node_item_bindings(child, item, index, item_name, item_key))
1231                .collect();
1232
1233            IRNode::ForEach {
1234                source: source.clone(),
1235                item_name: inner_item_name.clone(),
1236                key_path: key_path.clone(),
1237                template: new_template,
1238                props: new_props,
1239            }
1240        }
1241        IRNode::Conditional { value, branches, fallback } => {
1242            // Replace in condition value
1243            let new_value = replace_value_item_bindings(value, item, item_name);
1244
1245            // Replace in branches
1246            let new_branches: Vec<ConditionalBranch> = branches.iter()
1247                .map(|branch| {
1248                    let new_pattern = replace_value_item_bindings(&branch.pattern, item, item_name);
1249                    let new_children: Vec<IRNode> = branch.children.iter()
1250                        .map(|child| replace_ir_node_item_bindings(child, item, index, item_name, item_key))
1251                        .collect();
1252                    ConditionalBranch::new(new_pattern, new_children)
1253                })
1254                .collect();
1255
1256            // Replace in fallback
1257            let new_fallback = fallback.as_ref().map(|f| {
1258                f.iter()
1259                    .map(|child| replace_ir_node_item_bindings(child, item, index, item_name, item_key))
1260                    .collect()
1261            });
1262
1263            IRNode::Conditional {
1264                value: new_value,
1265                branches: new_branches,
1266                fallback: new_fallback,
1267            }
1268        }
1269    }
1270}
1271
1272/// Replace item bindings in Props
1273fn replace_props_item_bindings(props: &Props, item: &serde_json::Value, item_name: &str) -> Props {
1274    let mut new_props = Props::new();
1275    for (key, value) in props {
1276        new_props.insert(key.clone(), replace_value_item_bindings(value, item, item_name));
1277    }
1278    new_props
1279}
1280
1281/// Replace item bindings in a Value
1282/// Uses optimized single-pass replacement to avoid O(n²) string operations.
1283/// `item_name` allows custom iteration variable names (e.g., "todo", "user" instead of "item").
1284fn replace_value_item_bindings(value: &Value, item: &serde_json::Value, item_name: &str) -> Value {
1285    match value {
1286        Value::Binding(binding) => {
1287            if binding.is_item() {
1288                if binding.path.is_empty() {
1289                    Value::Static(item.clone())
1290                } else {
1291                    let path = binding.full_path();
1292                    if let Some(val) = navigate_item_path(item, &path) {
1293                        Value::Static(val.clone())
1294                    } else {
1295                        value.clone()
1296                    }
1297                }
1298            } else {
1299                value.clone()
1300            }
1301        }
1302        Value::TemplateString { template, bindings } => {
1303            let has_item_bindings = bindings.iter().any(|b| b.is_item());
1304            if !has_item_bindings {
1305                return value.clone();
1306            }
1307
1308            // PHASE 1: Collect all replacements for explicit bindings (single pass)
1309            let mut replacements = Vec::new();
1310            for binding in bindings {
1311                if binding.is_item() {
1312                    let pattern = format!("${{{}}}", binding.full_path_with_source());
1313                    if let Some(start) = template.find(&pattern) {
1314                        let replacement = if binding.path.is_empty() {
1315                            format_value_for_replacement(item, false)
1316                        } else if let Some(val) = navigate_item_path(item, &binding.full_path()) {
1317                            format_value_for_replacement(val, false)
1318                        } else {
1319                            continue;
1320                        };
1321                        replacements.push(Replacement {
1322                            start,
1323                            end: start + pattern.len(),
1324                            text: replacement,
1325                        });
1326                    }
1327                }
1328            }
1329
1330            // PHASE 2: Apply explicit binding replacements
1331            let mut result = apply_replacements(template, replacements);
1332
1333            // PHASE 3: Replace item.xxx references in expressions (e.g., ternary operators)
1334            // Uses the configurable item_name for custom iteration variables
1335            let expr_replacements = collect_item_replacements_with_name(&result, item, true, item_name);
1336            result = apply_replacements(&result, expr_replacements);
1337
1338            // Filter out resolved item bindings, keep state bindings
1339            let remaining_bindings: Vec<_> = bindings.iter()
1340                .filter(|b| b.is_state())
1341                .cloned()
1342                .collect();
1343
1344            if remaining_bindings.is_empty() {
1345                if result.contains("${") {
1346                    match evaluate_template_string(&result, &serde_json::Value::Null, None) {
1347                        Ok(evaluated) => Value::Static(serde_json::Value::String(evaluated)),
1348                        Err(_) => Value::Static(serde_json::Value::String(result)),
1349                    }
1350                } else {
1351                    Value::Static(serde_json::Value::String(result))
1352                }
1353            } else {
1354                Value::TemplateString {
1355                    template: result,
1356                    bindings: remaining_bindings,
1357                }
1358            }
1359        }
1360        // Handle static strings containing ${item.xxx} pattern (legacy/fallback)
1361        Value::Static(serde_json::Value::String(s)) if s.contains(&format!("${{{}.", item_name)) || s.contains(&format!("${{{}}}", item_name)) => {
1362            replace_static_item_bindings_with_name(s, item, item_name)
1363        }
1364        _ => value.clone(),
1365    }
1366}
1367
1368/// Find all item.xxx references in a string with configurable item name.
1369fn collect_item_replacements_with_name(s: &str, item: &serde_json::Value, quote_strings: bool, item_name: &str) -> Vec<Replacement> {
1370    let mut replacements = Vec::new();
1371    let mut pos = 0;
1372
1373    while pos < s.len() {
1374        if let Some(rel_start) = s[pos..].find(item_name) {
1375            let abs_start = pos + rel_start;
1376            let after_item = abs_start + item_name.len();
1377
1378            if after_item < s.len() && s.as_bytes()[after_item] == b'.' {
1379                let path_start = after_item + 1;
1380                let path_end = find_path_end(s, path_start);
1381
1382                if path_end > path_start {
1383                    let path = &s[path_start..path_end];
1384                    if let Some(val) = navigate_item_path(item, path) {
1385                        let replacement = format_value_for_replacement(val, quote_strings);
1386                        replacements.push(Replacement {
1387                            start: abs_start,
1388                            end: path_end,
1389                            text: replacement,
1390                        });
1391                    }
1392                }
1393                pos = path_end.max(after_item + 1);
1394            } else {
1395                pos = after_item;
1396            }
1397        } else {
1398            break;
1399        }
1400    }
1401
1402    replacements
1403}
1404
1405/// Replace item bindings in a static string value with configurable item name
1406fn replace_static_item_bindings_with_name(s: &str, item: &serde_json::Value, item_name: &str) -> Value {
1407    let mut replacements = Vec::new();
1408    let mut pos = 0;
1409
1410    while let Some(start) = s[pos..].find("${") {
1411        let abs_start = pos + start;
1412
1413        if let Some(end) = s[abs_start..].find('}') {
1414            let abs_end = abs_start + end;
1415            let content = &s[abs_start + 2..abs_end];
1416
1417            if content == item_name {
1418                // Bare ${item}
1419                replacements.push(Replacement {
1420                    start: abs_start,
1421                    end: abs_end + 1,
1422                    text: format_value_for_replacement(item, false),
1423                });
1424                pos = abs_end + 1;
1425            } else if content.starts_with(&format!("{}.", item_name)) && is_simple_path_with_name(content, item_name) {
1426                // Simple ${item.path}
1427                let path = &content[item_name.len() + 1..];
1428                if let Some(val) = navigate_item_path(item, path) {
1429                    replacements.push(Replacement {
1430                        start: abs_start,
1431                        end: abs_end + 1,
1432                        text: format_value_for_replacement(val, false),
1433                    });
1434                }
1435                pos = abs_end + 1;
1436            } else if content.contains(&format!("{}.", item_name)) || content.contains(&format!("{} ", item_name)) {
1437                // Complex expression - replace item refs within, then evaluate
1438                let expr_replacements = collect_item_replacements_with_name(content, item, true, item_name);
1439                let substituted_content = apply_replacements(content, expr_replacements);
1440                let new_expr = format!("${{{}}}", substituted_content);
1441
1442                if let Ok(evaluated) = evaluate_template_string(&new_expr, &serde_json::Value::Null, None) {
1443                    replacements.push(Replacement {
1444                        start: abs_start,
1445                        end: abs_end + 1,
1446                        text: evaluated,
1447                    });
1448                }
1449                pos = abs_end + 1;
1450            } else {
1451                pos = abs_end + 1;
1452            }
1453        } else {
1454            break;
1455        }
1456    }
1457
1458    let result = apply_replacements(s, replacements);
1459    Value::Static(serde_json::Value::String(result))
1460}
1461
1462/// Check if a string is a simple path with configurable item name
1463fn is_simple_path_with_name(s: &str, item_name: &str) -> bool {
1464    if !s.starts_with(item_name) {
1465        return false;
1466    }
1467
1468    let after_item = &s[item_name.len()..];
1469    if after_item.is_empty() {
1470        return true;
1471    }
1472
1473    if !after_item.starts_with('.') {
1474        return false;
1475    }
1476
1477    after_item[1..].chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.')
1478}
1479
1480/// Replace item bindings in an Element with a configurable item name
1481/// This is the full implementation that supports custom iteration variable names.
1482pub fn replace_item_bindings_with_name(
1483    element: &Element,
1484    item: &serde_json::Value,
1485    index: usize,
1486    item_name: &str,
1487) -> Element {
1488    let mut new_element = element.clone();
1489
1490    // Replace bindings in props using the unified Value replacement logic
1491    let mut new_props = Props::new();
1492    for (key, value) in &element.props {
1493        new_props.insert(key.clone(), replace_value_item_bindings(value, item, item_name));
1494    }
1495    new_element.props = new_props;
1496
1497    // Generate key using the item name
1498    let key = item.get("id")
1499        .and_then(|v| v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string())))
1500        .or_else(|| item.get("key").and_then(|v| v.as_str().map(|s| s.to_string())))
1501        .map(|id| format!("{}-{}", item_name, id))
1502        .unwrap_or_else(|| format!("{}-{}", item_name, index));
1503    new_element.key = Some(key);
1504
1505    // Recursively replace in children
1506    new_element.children = element.children.iter()
1507        .map(|child| std::sync::Arc::new(replace_item_bindings_with_name(child, item, index, item_name)))
1508        .collect();
1509
1510    new_element
1511}
1512
1513/// Reconcile an existing tree against a new IRNode
1514pub fn reconcile_ir_node(
1515    tree: &mut InstanceTree,
1516    node_id: NodeId,
1517    node: &IRNode,
1518    state: &serde_json::Value,
1519    patches: &mut Vec<Patch>,
1520    dependencies: &mut DependencyGraph,
1521) {
1522    let existing_node = tree.get(node_id).cloned();
1523    if existing_node.is_none() {
1524        return;
1525    }
1526    let existing = existing_node.unwrap();
1527
1528    match node {
1529        IRNode::Element(element) => {
1530            // Regular element reconciliation
1531            reconcile_node(tree, node_id, element, state, patches, dependencies);
1532        }
1533        IRNode::ForEach { source, item_name, key_path, template, props: _ } => {
1534            // ForEach reconciliation
1535            if !existing.is_foreach() {
1536                // Type mismatch - replace entire subtree
1537                let parent_id = existing.parent;
1538                remove_subtree(tree, node_id, patches, dependencies);
1539                create_ir_node_tree(tree, node, parent_id, state, patches, parent_id.is_none(), dependencies);
1540                return;
1541            }
1542
1543            // Re-evaluate the array
1544            let array = evaluate_binding(source, state).unwrap_or(serde_json::Value::Array(vec![]));
1545
1546            if let serde_json::Value::Array(items) = &array {
1547                let old_children = existing.children.clone();
1548                let expected_children_count = items.len() * template.len();
1549
1550                // If child count changed, rebuild
1551                if old_children.len() != expected_children_count {
1552                    // Remove old children
1553                    for &old_child_id in &old_children {
1554                        patches.push(Patch::remove(old_child_id));
1555                    }
1556
1557                    if let Some(node) = tree.get_mut(node_id) {
1558                        node.children.clear();
1559                    }
1560
1561                    // Create new children
1562                    for (index, item) in items.iter().enumerate() {
1563                        let item_key = generate_item_key(item, key_path.as_deref(), item_name, index);
1564
1565                        for child_template in template {
1566                            let child_with_item = replace_ir_node_item_bindings(
1567                                child_template,
1568                                item,
1569                                index,
1570                                item_name,
1571                                &item_key,
1572                            );
1573                            create_ir_node_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
1574                        }
1575                    }
1576                } else {
1577                    // Reconcile existing children
1578                    let mut child_index = 0;
1579                    for (item_index, item) in items.iter().enumerate() {
1580                        let item_key = generate_item_key(item, key_path.as_deref(), item_name, item_index);
1581
1582                        for child_template in template {
1583                            if let Some(&old_child_id) = old_children.get(child_index) {
1584                                let child_with_item = replace_ir_node_item_bindings(
1585                                    child_template,
1586                                    item,
1587                                    item_index,
1588                                    item_name,
1589                                    &item_key,
1590                                );
1591                                reconcile_ir_node(tree, old_child_id, &child_with_item, state, patches, dependencies);
1592                            }
1593                            child_index += 1;
1594                        }
1595                    }
1596                }
1597            }
1598        }
1599        IRNode::Conditional { value, branches, fallback } => {
1600            // Conditional reconciliation
1601            if !existing.is_conditional() {
1602                // Type mismatch - replace entire subtree
1603                let parent_id = existing.parent;
1604                remove_subtree(tree, node_id, patches, dependencies);
1605                create_ir_node_tree(tree, node, parent_id, state, patches, parent_id.is_none(), dependencies);
1606                return;
1607            }
1608
1609            // Re-evaluate the condition
1610            let evaluated_value = evaluate_value(value, state);
1611            let matched_children = find_matching_branch(&evaluated_value, branches, fallback.as_deref(), state);
1612
1613            let old_children = existing.children.clone();
1614            let old_len = old_children.len();
1615
1616            if let Some(children) = matched_children {
1617                let new_len = children.len();
1618                let common = old_len.min(new_len);
1619
1620                // Reconcile overlapping children (diff reuses DOM nodes where structure matches)
1621                for (i, child) in children.iter().enumerate().take(common) {
1622                    if let Some(&old_child_id) = old_children.get(i) {
1623                        reconcile_ir_node(tree, old_child_id, child, state, patches, dependencies);
1624                    }
1625                }
1626
1627                // Remove surplus old children
1628                for i in common..old_len {
1629                    if let Some(&old_child_id) = old_children.get(i) {
1630                        remove_subtree(tree, old_child_id, patches, dependencies);
1631                        if let Some(cond_node) = tree.get_mut(node_id) {
1632                            cond_node.children = cond_node.children.iter()
1633                                .filter(|&&id| id != old_child_id)
1634                                .copied()
1635                                .collect();
1636                        }
1637                    }
1638                }
1639
1640                // Create new children beyond the old count
1641                for child in &children[common..] {
1642                    create_ir_node_tree(tree, child, Some(node_id), state, patches, false, dependencies);
1643                }
1644            } else {
1645                // No matched branch - remove all children
1646                for &old_child_id in &old_children {
1647                    remove_subtree(tree, old_child_id, patches, dependencies);
1648                }
1649
1650                if let Some(cond_node) = tree.get_mut(node_id) {
1651                    cond_node.children.clear();
1652                }
1653            }
1654        }
1655    }
1656}
1657
1658/// Remove a subtree and generate Remove patches
1659fn remove_subtree(
1660    tree: &mut InstanceTree,
1661    node_id: NodeId,
1662    patches: &mut Vec<Patch>,
1663    dependencies: &mut DependencyGraph,
1664) {
1665    let ids = collect_subtree_ids(tree, node_id);
1666    for &id in &ids {
1667        patches.push(Patch::remove(id));
1668        dependencies.remove_node(id);
1669    }
1670    tree.remove(node_id);
1671}
1672
1673#[cfg(test)]
1674mod tests {
1675    use super::*;
1676    use crate::ir::Value;
1677    use serde_json::json;
1678
1679    #[test]
1680    fn test_create_simple_tree() {
1681        use crate::reactive::DependencyGraph;
1682
1683        let mut tree = InstanceTree::new();
1684        let mut patches = Vec::new();
1685        let mut dependencies = DependencyGraph::new();
1686
1687        let element = Element::new("Column")
1688            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
1689
1690        let state = json!({});
1691        create_tree(&mut tree, &element, None, &state, &mut patches, true, &mut dependencies);
1692
1693        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
1694        // Create Column, Insert Column into root, Create Text, Insert Text into Column
1695        assert_eq!(patches.len(), 4);
1696
1697        // Verify root insert patch exists
1698        let root_insert = patches.iter().find(|p| {
1699            matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root")
1700        });
1701        assert!(root_insert.is_some(), "Root insert patch should exist");
1702    }
1703
1704    #[test]
1705    fn test_diff_props() {
1706        let node_id = NodeId::default();
1707        let old = indexmap::indexmap! {
1708            "color".to_string() => json!("red"),
1709            "size".to_string() => json!(16),
1710        };
1711        let new = indexmap::indexmap! {
1712            "color".to_string() => json!("blue"),
1713            "size".to_string() => json!(16),
1714        };
1715
1716        let patches = diff_props(node_id, &old, &new);
1717
1718        // Only color changed
1719        assert_eq!(patches.len(), 1);
1720    }
1721
1722    #[test]
1723    fn test_longest_increasing_subsequence_basic() {
1724        // Test case: [0, 1, 2, 3] - already in order, should select all
1725        let positions = vec![Some(0), Some(1), Some(2), Some(3)];
1726        let lis = longest_increasing_subsequence(&positions);
1727        assert_eq!(lis, vec![0, 1, 2, 3]);
1728    }
1729
1730    #[test]
1731    fn test_longest_increasing_subsequence_reverse() {
1732        // Test case: [3, 2, 1, 0] - reversed, should select only one element
1733        let positions = vec![Some(3), Some(2), Some(1), Some(0)];
1734        let lis = longest_increasing_subsequence(&positions);
1735        assert_eq!(lis.len(), 1);
1736    }
1737
1738    #[test]
1739    fn test_longest_increasing_subsequence_mixed() {
1740        // Test case: [0, 3, 1, 2] - should select [0, 1, 2]
1741        let positions = vec![Some(0), Some(3), Some(1), Some(2)];
1742        let lis = longest_increasing_subsequence(&positions);
1743        assert_eq!(lis.len(), 3);
1744        // Should contain indices of 0, 1, 2 in the original array
1745        assert!(lis.contains(&0));
1746        assert!(lis.contains(&2));
1747        assert!(lis.contains(&3));
1748    }
1749
1750    #[test]
1751    fn test_longest_increasing_subsequence_with_new_items() {
1752        // Test case: [None, Some(0), None, Some(1)] - new items mixed with existing
1753        let positions = vec![None, Some(0), None, Some(1)];
1754        let lis = longest_increasing_subsequence(&positions);
1755        assert_eq!(lis, vec![1, 3]); // Only indices with existing positions
1756    }
1757
1758    #[test]
1759    fn test_longest_increasing_subsequence_empty() {
1760        let positions: Vec<Option<usize>> = vec![];
1761        let lis = longest_increasing_subsequence(&positions);
1762        assert_eq!(lis, Vec::<usize>::new());
1763    }
1764
1765    #[test]
1766    fn test_longest_increasing_subsequence_all_new() {
1767        // All new items (no old positions)
1768        let positions = vec![None, None, None];
1769        let lis = longest_increasing_subsequence(&positions);
1770        assert_eq!(lis, Vec::<usize>::new());
1771    }
1772
1773    #[test]
1774    fn test_reconcile_keyed_children_basic() {
1775        let mut tree = InstanceTree::new();
1776        let mut dependencies = DependencyGraph::new();
1777        let state = json!({});
1778
1779        // Create a parent node
1780        let parent = Element::new("Column");
1781        let parent_id = tree.create_node(&parent, &state);
1782
1783        // Create old children
1784        let mut old_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1785        old_child_1.key = Some("item-1".to_string());
1786        let old_id_1 = tree.create_node(&old_child_1, &state);
1787
1788        let mut old_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1789        old_child_2.key = Some("item-2".to_string());
1790        let old_id_2 = tree.create_node(&old_child_2, &state);
1791
1792        tree.add_child(parent_id, old_id_1, None);
1793        tree.add_child(parent_id, old_id_2, None);
1794
1795        // New children in reversed order
1796        let mut new_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1797        new_child_2.key = Some("item-2".to_string());
1798
1799        let mut new_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1800        new_child_1.key = Some("item-1".to_string());
1801
1802        let new_children = vec![new_child_2, new_child_1];
1803        let old_children = vec![old_id_1, old_id_2];
1804
1805        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1806
1807        // Should have move patches but no create/remove patches (children are reused)
1808        let move_count = patches.iter().filter(|p| matches!(p, Patch::Move { .. })).count();
1809        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1810        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1811
1812        assert_eq!(create_count, 0, "Should not create new children");
1813        assert_eq!(remove_count, 0, "Should not remove any children");
1814        assert!(move_count > 0, "Should have move patches for reordering");
1815    }
1816
1817    #[test]
1818    fn test_reconcile_keyed_children_add_remove() {
1819        let mut tree = InstanceTree::new();
1820        let mut dependencies = DependencyGraph::new();
1821        let state = json!({});
1822
1823        let parent = Element::new("Column");
1824        let parent_id = tree.create_node(&parent, &state);
1825
1826        // Old: item-1, item-2
1827        let mut old_child_1 = Element::new("Text");
1828        old_child_1.key = Some("item-1".to_string());
1829        let old_id_1 = tree.create_node(&old_child_1, &state);
1830
1831        let mut old_child_2 = Element::new("Text");
1832        old_child_2.key = Some("item-2".to_string());
1833        let old_id_2 = tree.create_node(&old_child_2, &state);
1834
1835        tree.add_child(parent_id, old_id_1, None);
1836        tree.add_child(parent_id, old_id_2, None);
1837
1838        // New: item-2, item-3 (removed item-1, added item-3)
1839        let mut new_child_2 = Element::new("Text");
1840        new_child_2.key = Some("item-2".to_string());
1841
1842        let mut new_child_3 = Element::new("Text");
1843        new_child_3.key = Some("item-3".to_string());
1844
1845        let new_children = vec![new_child_2, new_child_3];
1846        let old_children = vec![old_id_1, old_id_2];
1847
1848        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1849
1850        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1851        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1852
1853        assert_eq!(create_count, 1, "Should create item-3");
1854        assert_eq!(remove_count, 1, "Should remove item-1");
1855    }
1856
1857    #[test]
1858    fn test_pattern_matches_binding_truthy() {
1859        use crate::reactive::Binding;
1860
1861        let state = json!({"isAdmin": true, "count": 0});
1862        let evaluated = json!("anything"); // The When() condition value
1863
1864        // Truthy binding -> should match
1865        let pattern = Value::Binding(Binding::state(vec!["isAdmin".to_string()]));
1866        assert!(pattern_matches(&evaluated, &pattern, &state));
1867
1868        // Falsy binding (number 0) -> should not match
1869        let pattern = Value::Binding(Binding::state(vec!["count".to_string()]));
1870        assert!(!pattern_matches(&evaluated, &pattern, &state));
1871
1872        // Missing path -> null -> should not match
1873        let pattern = Value::Binding(Binding::state(vec!["missing".to_string()]));
1874        assert!(!pattern_matches(&evaluated, &pattern, &state));
1875    }
1876
1877    #[test]
1878    fn test_lis_large_sequence() {
1879        // Test that the O(n log n) algorithm handles longer sequences correctly
1880        // [3, 1, 4, 1, 5, 9, 2, 6] has LIS of length 4: 1,4,5,9 or 1,4,5,6
1881        let positions: Vec<Option<usize>> = vec![
1882            Some(3), Some(1), Some(4), Some(1), Some(5), Some(9), Some(2), Some(6),
1883        ];
1884        let lis = longest_increasing_subsequence(&positions);
1885        assert_eq!(lis.len(), 4);
1886        // Verify the subsequence is actually increasing
1887        let values: Vec<usize> = lis.iter().map(|&i| positions[i].unwrap()).collect();
1888        for w in values.windows(2) {
1889            assert!(w[0] < w[1], "LIS must be strictly increasing: {:?}", values);
1890        }
1891    }
1892}