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