Skip to main content

hypen_engine/reconcile/
diff.rs

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