Skip to main content

hypen_engine/reconcile/
diff.rs

1use super::conditionals::{evaluate_value, find_matching_branch};
2use super::item_bindings::{replace_ir_node_item_bindings, replace_item_bindings};
3use super::keyed::generate_item_key;
4use super::resolve::{evaluate_binding, resolve_props_full};
5use super::{ControlFlowKind, InstanceTree, Patch};
6use crate::ir::{ConditionalBranch, Element, IRNode, NodeId, Props, Value};
7use crate::reactive::{Binding, DependencyGraph};
8use indexmap::IndexMap;
9
10/// Data sources type alias for readability
11type DataSources = indexmap::IndexMap<String, serde_json::Value>;
12
13/// Shared mutable context threaded through the recursive tree-building and
14/// reconciliation helpers.  Grouping these fields removes the repetitive
15/// parameter tuple that was being copy-pasted across every internal function.
16struct ReconcileCtx<'a> {
17    tree: &'a mut InstanceTree,
18    state: &'a serde_json::Value,
19    patches: &'a mut Vec<Patch>,
20    dependencies: &'a mut DependencyGraph,
21    data_sources: Option<&'a DataSources>,
22}
23
24/// Reconcile an element tree against the instance tree and generate patches
25pub fn reconcile(
26    tree: &mut InstanceTree,
27    element: &Element,
28    parent_id: Option<NodeId>,
29    state: &serde_json::Value,
30    dependencies: &mut DependencyGraph,
31) -> Vec<Patch> {
32    reconcile_with_ds(tree, element, parent_id, state, dependencies, None)
33}
34
35/// Reconcile an element tree with data source context
36pub fn reconcile_with_ds(
37    tree: &mut InstanceTree,
38    element: &Element,
39    parent_id: Option<NodeId>,
40    state: &serde_json::Value,
41    dependencies: &mut DependencyGraph,
42    data_sources: Option<&DataSources>,
43) -> Vec<Patch> {
44    let mut patches = Vec::new();
45
46    // For initial render, just create the tree
47    if tree.root().is_none() {
48        let node_id = create_tree(
49            tree,
50            element,
51            parent_id,
52            state,
53            &mut patches,
54            true,
55            dependencies,
56            data_sources,
57        );
58        tree.set_root(node_id);
59        return patches;
60    }
61
62    // Incremental update: reconcile root against existing tree
63    if let Some(root_id) = tree.root() {
64        reconcile_node(tree, root_id, element, state, &mut patches, dependencies, data_sources);
65    }
66
67    patches
68}
69
70/// Reconcile an IRNode tree against the instance tree and generate patches
71/// This is the primary entry point for the IRNode-based reconciliation system,
72/// which supports first-class ForEach, When/If, and custom item variable names.
73pub fn reconcile_ir(
74    tree: &mut InstanceTree,
75    node: &IRNode,
76    parent_id: Option<NodeId>,
77    state: &serde_json::Value,
78    dependencies: &mut DependencyGraph,
79) -> Vec<Patch> {
80    reconcile_ir_with_ds(tree, node, parent_id, state, dependencies, None)
81}
82
83/// Reconcile an IRNode tree with data source context
84pub fn reconcile_ir_with_ds(
85    tree: &mut InstanceTree,
86    node: &IRNode,
87    parent_id: Option<NodeId>,
88    state: &serde_json::Value,
89    dependencies: &mut DependencyGraph,
90    data_sources: Option<&DataSources>,
91) -> Vec<Patch> {
92    let mut patches = Vec::new();
93
94    // For initial render, create the tree
95    if tree.root().is_none() {
96        let node_id = create_ir_node_tree(
97            tree,
98            node,
99            parent_id,
100            state,
101            &mut patches,
102            true,
103            dependencies,
104            data_sources,
105        );
106        tree.set_root(node_id);
107        return patches;
108    }
109
110    // Incremental update: reconcile root against existing tree
111    if let Some(root_id) = tree.root() {
112        reconcile_ir_node(tree, root_id, node, state, &mut patches, dependencies, data_sources);
113    }
114
115    patches
116}
117
118/// Create a new subtree from an element
119#[allow(clippy::too_many_arguments)]
120pub fn create_tree(
121    tree: &mut InstanceTree,
122    element: &Element,
123    parent_id: Option<NodeId>,
124    state: &serde_json::Value,
125    patches: &mut Vec<Patch>,
126    is_root: bool,
127    dependencies: &mut DependencyGraph,
128    data_sources: Option<&DataSources>,
129) -> NodeId {
130    // Special handling for iterable elements (List, Grid, etc.)
131    // An element is iterable if it has BOTH:
132    // 1. prop "0" with a Binding (like @state.items) - not a static value
133    // 2. children (the template to repeat for each item)
134    // This distinguishes iterables from:
135    // - Text elements (prop "0" but no children)
136    // - Route/Link elements (prop "0" with static path, not a binding)
137    if let Some(Value::Binding(_)) = element.props.get("0") {
138        if !element.children.is_empty() {
139            return create_list_tree(
140                tree,
141                element,
142                parent_id,
143                state,
144                patches,
145                is_root,
146                dependencies,
147                data_sources,
148            );
149        }
150    }
151
152    // Create the node (with data sources for proper binding resolution)
153    let node_id = tree.create_node_full(element, state, data_sources);
154
155    // Register dependencies for this node
156    for value in element.props.values() {
157        match value {
158            Value::Binding(binding) => {
159                dependencies.add_dependency(node_id, binding);
160            }
161            Value::TemplateString { bindings, .. } => {
162                // Register all bindings in the template string
163                for binding in bindings {
164                    dependencies.add_dependency(node_id, binding);
165                }
166            }
167            _ => {}
168        }
169    }
170
171    // Generate Create patch
172    let node = tree.get(node_id).unwrap();
173
174    // For lazy elements, add child component name as a prop
175    let mut props = node.props.clone();
176    if let Some(Value::Static(val)) = element.props.get("__lazy") {
177        if val.as_bool().unwrap_or(false) && !element.children.is_empty() {
178            // Store the first child's element type so renderer knows what component to load
179            let child_component = &element.children[0].element_type;
180            props.insert(
181                "__lazy_child".to_string(),
182                serde_json::json!(child_component),
183            );
184        }
185    }
186
187    patches.push(Patch::create(node_id, node.element_type.clone(), props));
188
189    // Event handling is now done at the renderer level
190
191    // If there's a parent, insert this node
192    if let Some(parent) = parent_id {
193        tree.add_child(parent, node_id, None);
194        patches.push(Patch::insert(parent, node_id, None));
195    } else if is_root {
196        // This is the root node - insert into "root" container
197        patches.push(Patch::insert_root(node_id));
198    }
199
200    // Create children (skip if element is marked as lazy)
201    let is_lazy = element
202        .props
203        .get("__lazy")
204        .and_then(|v| {
205            if let Value::Static(val) = v {
206                val.as_bool()
207            } else {
208                None
209            }
210        })
211        .unwrap_or(false);
212
213    if !is_lazy {
214        for child_element in &element.children {
215            create_tree(
216                tree,
217                child_element,
218                Some(node_id),
219                state,
220                patches,
221                false,
222                dependencies,
223                data_sources,
224            );
225        }
226    }
227
228    node_id
229}
230
231/// Create an iterable element (List, Grid, etc.) that iterates over an array in state
232/// Iterable elements are identified by having prop "0" with an array binding
233#[allow(clippy::too_many_arguments)]
234fn create_list_tree(
235    tree: &mut InstanceTree,
236    element: &Element,
237    parent_id: Option<NodeId>,
238    state: &serde_json::Value,
239    patches: &mut Vec<Patch>,
240    is_root: bool,
241    dependencies: &mut DependencyGraph,
242    data_sources: Option<&DataSources>,
243) -> NodeId {
244    // Get the array binding from first prop (prop "0")
245    let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
246        evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
247    } else {
248        serde_json::Value::Array(vec![])
249    };
250
251    // Create a container element - use the original element type (List, Grid, etc.)
252    // but remove the "0" prop since it's only for iteration, not rendering
253    let mut list_element = Element::new(&element.element_type);
254    for (key, value) in &element.props {
255        // Skip prop "0" - it's only for array binding, not for rendering
256        if key != "0" {
257            list_element.props.insert(key.clone(), value.clone());
258        }
259    }
260
261    let node_id = tree.create_node_full(&list_element, state, data_sources);
262
263    // Register the List node as depending on the array binding
264    // This is critical for reactive updates when the array changes
265    if let Some(Value::Binding(binding)) = element.props.get("0") {
266        dependencies.add_dependency(node_id, binding);
267    }
268
269    // Store the original element template for re-reconciliation
270    // This allows us to rebuild the list when the array changes
271    if let Some(node) = tree.get_mut(node_id) {
272        node.raw_props = element.props.clone();
273        node.element_template = Some(std::sync::Arc::new(element.clone()));
274    }
275
276    // Generate Create patch for container
277    let node = tree.get(node_id).unwrap();
278    patches.push(Patch::create(
279        node_id,
280        node.element_type.clone(),
281        node.props.clone(),
282    ));
283
284    // Insert container
285    if let Some(parent) = parent_id {
286        tree.add_child(parent, node_id, None);
287        patches.push(Patch::insert(parent, node_id, None));
288    } else if is_root {
289        patches.push(Patch::insert_root(node_id));
290    }
291
292    // If array is empty, return early
293    if let serde_json::Value::Array(items) = &array {
294        // Create children for each item in the array
295        for (index, item) in items.iter().enumerate() {
296            for child_template in &element.children {
297                // Clone child and replace ${item.x} bindings with actual item data
298                let child_with_item = replace_item_bindings(child_template, item, index);
299                create_tree(
300                    tree,
301                    &child_with_item,
302                    Some(node_id),
303                    state,
304                    patches,
305                    false,
306                    dependencies,
307                    data_sources,
308                );
309            }
310        }
311    }
312
313    node_id
314}
315
316/// Reconcile a single node with a new element
317pub fn reconcile_node(
318    tree: &mut InstanceTree,
319    node_id: NodeId,
320    element: &Element,
321    state: &serde_json::Value,
322    patches: &mut Vec<Patch>,
323    dependencies: &mut DependencyGraph,
324    data_sources: Option<&DataSources>,
325) {
326    let node = tree.get(node_id).cloned();
327    if node.is_none() {
328        return;
329    }
330    let node = node.unwrap();
331
332    // Special handling for iterable elements (List, Grid, etc.)
333    // An element is iterable if it has BOTH prop "0" (array binding) AND children (template)
334    // This distinguishes iterables from Text elements, which also use prop "0" but have no children
335    let is_iterable = element.props.get("0").is_some() && !element.children.is_empty();
336
337    if is_iterable {
338        // Get the array binding from first prop (prop "0")
339        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
340            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
341        } else {
342            serde_json::Value::Array(vec![])
343        };
344
345        // Regenerate iterable children
346        if let serde_json::Value::Array(items) = &array {
347            let old_children = node.children.clone();
348
349            // Calculate expected number of children
350            let expected_children_count = items.len() * element.children.len();
351
352            // Remove old children if count doesn't match
353            if old_children.len() != expected_children_count {
354                for &old_child_id in &old_children {
355                    patches.push(Patch::remove(old_child_id));
356                }
357
358                // Clear children from node
359                if let Some(node) = tree.get_mut(node_id) {
360                    node.children.clear();
361                }
362
363                // Create new children
364                for (index, item) in items.iter().enumerate() {
365                    for child_template in &element.children {
366                        let child_with_item = replace_item_bindings(child_template, item, index);
367                        create_tree(
368                            tree,
369                            &child_with_item,
370                            Some(node_id),
371                            state,
372                            patches,
373                            false,
374                            dependencies,
375                            data_sources,
376                        );
377                    }
378                }
379            } else {
380                // Reconcile existing children
381                let mut child_index = 0;
382                for (item_index, item) in items.iter().enumerate() {
383                    for child_template in &element.children {
384                        if let Some(&old_child_id) = old_children.get(child_index) {
385                            let child_with_item =
386                                replace_item_bindings(child_template, item, item_index);
387                            reconcile_node(
388                                tree,
389                                old_child_id,
390                                &child_with_item,
391                                state,
392                                patches,
393                                dependencies,
394                                data_sources,
395                            );
396                        }
397                        child_index += 1;
398                    }
399                }
400            }
401        }
402
403        return; // Done with List reconciliation
404    }
405
406    // If element type changed, replace the entire subtree
407    if node.element_type != element.element_type {
408        replace_subtree(tree, node_id, &node, element, state, patches, dependencies, data_sources);
409        return;
410    }
411
412    // Register dependencies for this node (must match create_tree to survive clear+reconcile)
413    for value in element.props.values() {
414        match value {
415            Value::Binding(binding) => {
416                dependencies.add_dependency(node_id, binding);
417            }
418            Value::TemplateString { bindings, .. } => {
419                for binding in bindings {
420                    dependencies.add_dependency(node_id, binding);
421                }
422            }
423            _ => {}
424        }
425    }
426
427    // Diff props (thread data_sources for template strings with data source refs)
428    let new_props = resolve_props_full(&element.props, state, None, data_sources);
429    let prop_patches = diff_props(node_id, &node.props, &new_props);
430    patches.extend(prop_patches);
431
432    // Update node props in tree
433    if let Some(node) = tree.get_mut(node_id) {
434        node.props = new_props.clone();
435        node.raw_props = element.props.clone();
436    }
437
438    // Reconcile children (skip if element is marked as lazy)
439    let is_lazy = element
440        .props
441        .get("__lazy")
442        .and_then(|v| {
443            if let Value::Static(val) = v {
444                val.as_bool()
445            } else {
446                None
447            }
448        })
449        .unwrap_or(false);
450
451    if !is_lazy {
452        let old_children = node.children.clone();
453        let new_children = &element.children;
454
455        // Simple strategy: match children by index
456        for (i, new_child_element) in new_children.iter().enumerate() {
457            if let Some(&old_child_id) = old_children.get(i) {
458                // Reconcile existing child
459                reconcile_node(
460                    tree,
461                    old_child_id,
462                    new_child_element,
463                    state,
464                    patches,
465                    dependencies,
466                    data_sources,
467                );
468            } else {
469                // Create new child
470                let new_child_id = create_tree(
471                    tree,
472                    new_child_element,
473                    Some(node_id),
474                    state,
475                    patches,
476                    false,
477                    dependencies,
478                    data_sources,
479                );
480                if let Some(node) = tree.get_mut(node_id) {
481                    node.children.push_back(new_child_id);
482                }
483            }
484        }
485
486        // Remove extra old children
487        if old_children.len() > new_children.len() {
488            for old_child_id in old_children.iter().skip(new_children.len()).copied() {
489                // Collect all nodes in subtree first (for Remove patches)
490                let subtree_ids = collect_subtree_ids(tree, old_child_id);
491                for &id in &subtree_ids {
492                    patches.push(Patch::remove(id));
493                    dependencies.remove_node(id);
494                }
495                // Remove from parent's children and from tree
496                tree.remove_child(node_id, old_child_id);
497                tree.remove(old_child_id);
498            }
499        }
500    }
501}
502
503/// Reconcile an existing element node against a new Element that has IRNode children.
504/// Handles props diffing like `reconcile_node`, but reconciles children as IRNodes
505/// to support nested ForEach/When/If.
506fn reconcile_element_with_ir_children(
507    tree: &mut InstanceTree,
508    node_id: NodeId,
509    element: &Element,
510    state: &serde_json::Value,
511    patches: &mut Vec<Patch>,
512    dependencies: &mut DependencyGraph,
513    data_sources: Option<&DataSources>,
514) {
515    let node = tree.get(node_id).cloned();
516    if node.is_none() {
517        return;
518    }
519    let node = node.unwrap();
520
521    // If element type changed, replace the entire subtree
522    if node.element_type != element.element_type {
523        replace_subtree(tree, node_id, &node, element, state, patches, dependencies, data_sources);
524        return;
525    }
526
527    // Register dependencies
528    for value in element.props.values() {
529        match value {
530            Value::Binding(binding) => {
531                dependencies.add_dependency(node_id, binding);
532            }
533            Value::TemplateString { bindings, .. } => {
534                for binding in bindings {
535                    dependencies.add_dependency(node_id, binding);
536                }
537            }
538            _ => {}
539        }
540    }
541
542    // Diff props
543    let new_props = resolve_props_full(&element.props, state, None, data_sources);
544    let prop_patches = diff_props(node_id, &node.props, &new_props);
545    patches.extend(prop_patches);
546
547    // Update node props in tree
548    if let Some(n) = tree.get_mut(node_id) {
549        n.props = new_props;
550        n.raw_props = element.props.clone();
551    }
552
553    // Reconcile ir_children as IRNodes
554    let old_children = node.children.clone();
555    let new_children = &element.ir_children;
556    let common = old_children.len().min(new_children.len());
557
558    for (i, child_ir) in new_children.iter().enumerate().take(common) {
559        if let Some(&old_child_id) = old_children.get(i) {
560            reconcile_ir_node(tree, old_child_id, child_ir, state, patches, dependencies, data_sources);
561        }
562    }
563
564    // Create new children beyond old count
565    for child_ir in &new_children[common..] {
566        create_ir_node_tree(
567            tree,
568            child_ir,
569            Some(node_id),
570            state,
571            patches,
572            false,
573            dependencies,
574            data_sources,
575        );
576    }
577
578    // Remove surplus old children
579    for old_child_id in old_children.iter().skip(new_children.len()).copied() {
580        remove_subtree(tree, old_child_id, patches, dependencies);
581        if let Some(n) = tree.get_mut(node_id) {
582            n.children = n
583                .children
584                .iter()
585                .filter(|&&id| id != old_child_id)
586                .copied()
587                .collect();
588        }
589    }
590}
591
592/// Replace an entire subtree when element types don't match.
593/// This removes the old node and its descendants, then creates a new subtree in its place.
594#[allow(clippy::too_many_arguments)]
595fn replace_subtree(
596    tree: &mut InstanceTree,
597    old_node_id: NodeId,
598    old_node: &super::InstanceNode,
599    new_element: &Element,
600    state: &serde_json::Value,
601    patches: &mut Vec<Patch>,
602    dependencies: &mut DependencyGraph,
603    data_sources: Option<&DataSources>,
604) {
605    let parent_id = old_node.parent;
606
607    // Remember the position of the old node in its parent's children list
608    let old_position = if let Some(pid) = parent_id {
609        tree.get(pid)
610            .and_then(|parent| parent.children.iter().position(|&id| id == old_node_id))
611    } else {
612        None
613    };
614
615    // Collect all node IDs in the old subtree (depth-first, children before parents)
616    let ids_to_remove = collect_subtree_ids(tree, old_node_id);
617
618    // Generate Remove patches and clear dependencies in one pass
619    for &id in &ids_to_remove {
620        patches.push(Patch::remove(id));
621        dependencies.remove_node(id);
622    }
623
624    // Remove old node from parent's children list
625    if let Some(pid) = parent_id {
626        if let Some(parent) = tree.get_mut(pid) {
627            parent.children = parent
628                .children
629                .iter()
630                .filter(|&&id| id != old_node_id)
631                .copied()
632                .collect();
633        }
634    }
635
636    // Remove the old subtree from the tree
637    tree.remove(old_node_id);
638
639    // Create the new subtree
640    let is_root = parent_id.is_none();
641    let new_node_id = create_tree(
642        tree,
643        new_element,
644        parent_id,
645        state,
646        patches,
647        is_root,
648        dependencies,
649        data_sources,
650    );
651
652    // If this was the root, update it
653    if is_root {
654        tree.set_root(new_node_id);
655    } else if let Some(pid) = parent_id {
656        if let Some(pos) = old_position {
657            if let Some(parent) = tree.get_mut(pid) {
658                let current_len = parent.children.len();
659                // create_tree appended at end, so new node is at current_len - 1
660                // We want it at position `pos`
661                if pos < current_len - 1 {
662                    // Pop from end (O(log n) for im::Vector) and insert at correct position
663                    let new_id = parent.children.pop_back().unwrap();
664                    parent.children.insert(pos, new_id);
665
666                    // Get the next sibling for the Move patch
667                    let next_sibling = parent.children.get(pos + 1).copied();
668                    patches.push(Patch::move_node(pid, new_node_id, next_sibling));
669                }
670                // If pos == current_len - 1, it's already in the right place (was last child)
671            }
672        }
673    }
674}
675
676/// Collect all node IDs in a subtree (post-order: children before parents)
677/// Uses iterative approach to avoid stack overflow on deep trees
678fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
679    let mut result = Vec::new();
680    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
681
682    while let Some((node_id, children_processed)) = stack.pop() {
683        if children_processed {
684            // Children already processed, add this node
685            result.push(node_id);
686        } else {
687            // Push self back with flag, then push children
688            stack.push((node_id, true));
689            if let Some(node) = tree.get(node_id) {
690                // Push children in reverse order so they're processed left-to-right
691                for &child_id in node.children.iter().rev() {
692                    stack.push((child_id, false));
693                }
694            }
695        }
696    }
697
698    result
699}
700
701/// Diff two sets of props and generate SetProp/RemoveProp patches
702pub fn diff_props(
703    node_id: NodeId,
704    old_props: &IndexMap<String, serde_json::Value>,
705    new_props: &IndexMap<String, serde_json::Value>,
706) -> Vec<Patch> {
707    let mut patches = Vec::new();
708
709    // Check for changed or new props
710    for (key, new_value) in new_props {
711        if old_props.get(key) != Some(new_value) {
712            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
713        }
714    }
715
716    // Remove props that exist in old but not in new
717    for key in old_props.keys() {
718        if !new_props.contains_key(key) {
719            patches.push(Patch::remove_prop(node_id, key.clone()));
720        }
721    }
722
723    patches
724}
725
726// ============================================================================
727// IRNode-based reconciliation (first-class control flow constructs)
728// ============================================================================
729
730/// Create a tree from an IRNode (supports ForEach, When/If, and regular elements)
731#[allow(clippy::too_many_arguments)]
732pub fn create_ir_node_tree(
733    tree: &mut InstanceTree,
734    node: &IRNode,
735    parent_id: Option<NodeId>,
736    state: &serde_json::Value,
737    patches: &mut Vec<Patch>,
738    is_root: bool,
739    dependencies: &mut DependencyGraph,
740    data_sources: Option<&DataSources>,
741) -> NodeId {
742    match node {
743        IRNode::Element(element) => {
744            if element.ir_children.is_empty() {
745                // No control-flow children — use optimized legacy path
746                create_tree(
747                    tree,
748                    element,
749                    parent_id,
750                    state,
751                    patches,
752                    is_root,
753                    dependencies,
754                    data_sources,
755                )
756            } else {
757                // Has IRNode children (may include ForEach/When/If) — process as IRNodes
758                create_element_with_ir_children(
759                    tree,
760                    element,
761                    parent_id,
762                    state,
763                    patches,
764                    is_root,
765                    dependencies,
766                    data_sources,
767                )
768            }
769        }
770        IRNode::ForEach {
771            source,
772            item_name,
773            key_path,
774            template,
775            props,
776        } => {
777            let mut ctx = ReconcileCtx {
778                tree,
779                state,
780                patches,
781                dependencies,
782                data_sources,
783            };
784            create_foreach_ir_tree(
785                &mut ctx,
786                source,
787                item_name,
788                key_path.as_deref(),
789                template,
790                props,
791                node,
792                parent_id,
793                is_root,
794            )
795        }
796        IRNode::Conditional {
797            value,
798            branches,
799            fallback,
800        } => {
801            let mut ctx = ReconcileCtx {
802                tree,
803                state,
804                patches,
805                dependencies,
806                data_sources,
807            };
808            create_conditional_tree(
809                &mut ctx,
810                value,
811                branches,
812                fallback.as_deref(),
813                node,
814                parent_id,
815                is_root,
816            )
817        }
818    }
819}
820
821/// Create a tree from an Element that has `ir_children` (IRNode children).
822/// This handles elements whose children include control-flow nodes (ForEach, When, If).
823/// The element itself is created normally; children are processed as IRNodes.
824fn create_element_with_ir_children(
825    tree: &mut InstanceTree,
826    element: &Element,
827    parent_id: Option<NodeId>,
828    state: &serde_json::Value,
829    patches: &mut Vec<Patch>,
830    is_root: bool,
831    dependencies: &mut DependencyGraph,
832    data_sources: Option<&DataSources>,
833) -> NodeId {
834    // Create the element node (same as create_tree)
835    let node_id = tree.create_node(element, state);
836
837    // Register dependencies
838    for value in element.props.values() {
839        match value {
840            Value::Binding(binding) => {
841                dependencies.add_dependency(node_id, binding);
842            }
843            Value::TemplateString { bindings, .. } => {
844                for binding in bindings {
845                    dependencies.add_dependency(node_id, binding);
846                }
847            }
848            _ => {}
849        }
850    }
851
852    // Generate Create patch
853    let node = tree.get(node_id).unwrap();
854    patches.push(Patch::create(
855        node_id,
856        node.element_type.clone(),
857        node.props.clone(),
858    ));
859
860    // Insert into parent
861    if let Some(parent) = parent_id {
862        tree.add_child(parent, node_id, None);
863        patches.push(Patch::insert(parent, node_id, None));
864    } else if is_root {
865        patches.push(Patch::insert_root(node_id));
866    }
867
868    // Process children as IRNodes (supports ForEach, When, If)
869    for child_ir in &element.ir_children {
870        create_ir_node_tree(
871            tree,
872            child_ir,
873            Some(node_id),
874            state,
875            patches,
876            false,
877            dependencies,
878            data_sources,
879        );
880    }
881
882    node_id
883}
884
885/// Create an IRNode tree with separate logical and render parents.
886///
887/// This is used for control flow children where:
888/// - `logical_parent`: Where the node goes in the InstanceTree (the control flow node)
889/// - `render_parent`: Where Insert patches should target (the grandparent)
890///
891/// Control flow nodes are transparent - they exist in the tree for bookkeeping
892/// but don't create DOM elements. Their children render directly into the grandparent.
893fn create_ir_node_tree_with_render_parent(
894    ctx: &mut ReconcileCtx,
895    node: &IRNode,
896    logical_parent: Option<NodeId>,
897    render_parent: Option<NodeId>,
898    is_root: bool,
899) -> NodeId {
900    match node {
901        IRNode::Element(element) => {
902            // Create element and add to logical parent in tree
903            let node_id = ctx.tree.create_node_full(element, ctx.state, ctx.data_sources);
904
905            // Add to logical parent's children list
906            if let Some(parent) = logical_parent {
907                ctx.tree.add_child(parent, node_id, None);
908            }
909
910            // Emit patches targeting the render parent
911            ctx.patches.push(Patch::create(
912                node_id,
913                element.element_type.clone(),
914                ctx.tree
915                    .get(node_id)
916                    .map(|n| n.props.clone())
917                    .unwrap_or_default(),
918            ));
919
920            if let Some(render_p) = render_parent {
921                ctx.patches.push(Patch::insert(render_p, node_id, None));
922            } else if is_root {
923                ctx.patches.push(Patch::insert_root(node_id));
924            }
925
926            // Register dependencies (must match create_tree to survive clear+reconcile)
927            for (_, value) in &element.props {
928                match value {
929                    Value::Binding(binding) => {
930                        ctx.dependencies.add_dependency(node_id, binding);
931                    }
932                    Value::TemplateString { bindings, .. } => {
933                        for binding in bindings {
934                            ctx.dependencies.add_dependency(node_id, binding);
935                        }
936                    }
937                    _ => {}
938                }
939            }
940
941            // Recursively create children - they use this node as both logical and render parent
942            let children_source: Vec<IRNode> = if !element.ir_children.is_empty() {
943                element.ir_children.clone()
944            } else {
945                element
946                    .children
947                    .iter()
948                    .map(|child| IRNode::Element((**child).clone()))
949                    .collect()
950            };
951            for child_ir in &children_source {
952                create_ir_node_tree(
953                    ctx.tree,
954                    child_ir,
955                    Some(node_id),
956                    ctx.state,
957                    ctx.patches,
958                    false,
959                    ctx.dependencies,
960                    ctx.data_sources,
961                );
962            }
963
964            node_id
965        }
966        IRNode::ForEach {
967            source,
968            item_name,
969            key_path,
970            template,
971            props,
972        } => {
973            // ForEach within control flow - still uses grandparent for its own children
974            create_foreach_ir_tree(
975                ctx,
976                source,
977                item_name,
978                key_path.as_deref(),
979                template,
980                props,
981                node,
982                logical_parent, // ForEach goes into logical parent
983                is_root,
984            )
985        }
986        IRNode::Conditional {
987            value,
988            branches,
989            fallback,
990        } => {
991            // Nested conditional - also transparent
992            create_conditional_tree(
993                ctx,
994                value,
995                branches,
996                fallback.as_deref(),
997                node,
998                logical_parent, // Conditional goes into logical parent
999                is_root,
1000            )
1001        }
1002    }
1003}
1004
1005/// Create a ForEach iteration tree from IRNode::ForEach
1006#[allow(clippy::too_many_arguments)]
1007fn create_foreach_ir_tree(
1008    ctx: &mut ReconcileCtx,
1009    source: &Binding,
1010    item_name: &str,
1011    key_path: Option<&str>,
1012    template: &[IRNode],
1013    props: &Props,
1014    original_node: &IRNode,
1015    parent_id: Option<NodeId>,
1016    is_root: bool,
1017) -> NodeId {
1018    // Evaluate the source array binding
1019    let array = evaluate_binding(source, ctx.state).unwrap_or(serde_json::Value::Array(vec![]));
1020
1021    // Resolve container props (with data sources for template strings)
1022    let resolved_props = resolve_props_full(props, ctx.state, None, ctx.data_sources);
1023
1024    // Create the ForEach container node (internal bookkeeping only - no DOM element)
1025    let node_id = ctx.tree.create_control_flow_node(
1026        "__ForEach",
1027        resolved_props.clone(),
1028        props.clone(),
1029        ControlFlowKind::ForEach {
1030            item_name: item_name.to_string(),
1031            key_path: key_path.map(|s| s.to_string()),
1032        },
1033        original_node.clone(),
1034    );
1035
1036    // Register dependency on the source array binding
1037    ctx.dependencies.add_dependency(node_id, source);
1038
1039    // Add to parent's children list (for tree structure) but NO patches for the container itself
1040    // Control flow nodes are transparent - they don't create DOM elements
1041    if let Some(parent) = parent_id {
1042        ctx.tree.add_child(parent, node_id, None);
1043    }
1044
1045    // Render children for each item in the array
1046    // Children are inserted into the GRANDPARENT for rendering purposes
1047    let render_parent = parent_id; // Children render directly into ForEach's parent
1048
1049    if let serde_json::Value::Array(items) = &array {
1050        for (index, item) in items.iter().enumerate() {
1051            let item_key = generate_item_key(item, key_path, item_name, index);
1052
1053            // Create each template child with item bindings replaced
1054            for child_template in template {
1055                let child_with_item = replace_ir_node_item_bindings(
1056                    child_template,
1057                    item,
1058                    index,
1059                    item_name,
1060                    &item_key,
1061                );
1062                // Children are added to ForEach node in tree, but patches go to grandparent
1063                create_ir_node_tree_with_render_parent(
1064                    ctx,
1065                    &child_with_item,
1066                    Some(node_id), // logical parent (ForEach)
1067                    render_parent, // render parent (grandparent)
1068                    is_root && render_parent.is_none(),
1069                );
1070            }
1071        }
1072    }
1073
1074    node_id
1075}
1076
1077/// Create a Conditional (When/If) tree from IRNode::Conditional
1078fn create_conditional_tree(
1079    ctx: &mut ReconcileCtx,
1080    value: &Value,
1081    branches: &[ConditionalBranch],
1082    fallback: Option<&[IRNode]>,
1083    original_node: &IRNode,
1084    parent_id: Option<NodeId>,
1085    is_root: bool,
1086) -> NodeId {
1087    // Evaluate the condition value (with data sources for template string conditions)
1088    let evaluated_value = evaluate_value(value, ctx.state, ctx.data_sources);
1089
1090    // Create the Conditional container node (internal bookkeeping only - no DOM element)
1091    let mut raw_props = Props::new();
1092    raw_props.insert("__condition".to_string(), value.clone());
1093
1094    let node_id = ctx.tree.create_control_flow_node(
1095        "__Conditional",
1096        IndexMap::new(),
1097        raw_props,
1098        ControlFlowKind::Conditional,
1099        original_node.clone(),
1100    );
1101
1102    // Register dependency on the condition binding
1103    if let Value::Binding(binding) = value {
1104        ctx.dependencies.add_dependency(node_id, binding);
1105    } else if let Value::TemplateString { bindings, .. } = value {
1106        for binding in bindings {
1107            ctx.dependencies.add_dependency(node_id, binding);
1108        }
1109    }
1110
1111    // Add to parent's children list (for tree structure) but NO patches for the container itself
1112    // Control flow nodes are transparent - they don't create DOM elements
1113    if let Some(parent) = parent_id {
1114        ctx.tree.add_child(parent, node_id, None);
1115    }
1116
1117    // Find matching branch (with data sources for pattern matching)
1118    let matched_children =
1119        find_matching_branch(&evaluated_value, branches, fallback, ctx.state, ctx.data_sources);
1120
1121    // Render matched children - insert directly into grandparent for rendering
1122    let render_parent = parent_id; // Children render directly into Conditional's parent
1123
1124    if let Some(children) = matched_children {
1125        for child in children {
1126            // Children are added to Conditional node in tree, but patches go to grandparent
1127            create_ir_node_tree_with_render_parent(
1128                ctx,
1129                child,
1130                Some(node_id), // logical parent (Conditional)
1131                render_parent, // render parent (grandparent)
1132                is_root && render_parent.is_none(),
1133            );
1134        }
1135    }
1136
1137    node_id
1138}
1139
1140/// Reconcile an existing tree against a new IRNode
1141pub fn reconcile_ir_node(
1142    tree: &mut InstanceTree,
1143    node_id: NodeId,
1144    node: &IRNode,
1145    state: &serde_json::Value,
1146    patches: &mut Vec<Patch>,
1147    dependencies: &mut DependencyGraph,
1148    data_sources: Option<&DataSources>,
1149) {
1150    let existing_node = tree.get(node_id).cloned();
1151    if existing_node.is_none() {
1152        return;
1153    }
1154    let existing = existing_node.unwrap();
1155
1156    match node {
1157        IRNode::Element(element) => {
1158            if element.ir_children.is_empty() {
1159                // No control-flow children — use existing reconciliation
1160                reconcile_node(tree, node_id, element, state, patches, dependencies, data_sources);
1161            } else {
1162                // Has IRNode children (may include ForEach/When/If)
1163                reconcile_element_with_ir_children(
1164                    tree, node_id, element, state, patches, dependencies, data_sources,
1165                );
1166            }
1167        }
1168        IRNode::ForEach {
1169            source,
1170            item_name,
1171            key_path,
1172            template,
1173            props: _,
1174        } => {
1175            // ForEach reconciliation
1176            if !existing.is_foreach() {
1177                // Type mismatch - replace entire subtree
1178                let parent_id = existing.parent;
1179                remove_subtree(tree, node_id, patches, dependencies);
1180                create_ir_node_tree(
1181                    tree,
1182                    node,
1183                    parent_id,
1184                    state,
1185                    patches,
1186                    parent_id.is_none(),
1187                    dependencies,
1188                    data_sources,
1189                );
1190                return;
1191            }
1192
1193            // Re-register dependency on the source array binding (cleared before reconcile)
1194            dependencies.add_dependency(node_id, source);
1195
1196            // Re-evaluate the array
1197            let array = evaluate_binding(source, state).unwrap_or(serde_json::Value::Array(vec![]));
1198
1199            if let serde_json::Value::Array(items) = &array {
1200                let old_children = existing.children.clone();
1201                let expected_children_count = items.len() * template.len();
1202
1203                // If child count changed, rebuild
1204                if old_children.len() != expected_children_count {
1205                    // Remove old children
1206                    for &old_child_id in &old_children {
1207                        patches.push(Patch::remove(old_child_id));
1208                    }
1209
1210                    if let Some(node) = tree.get_mut(node_id) {
1211                        node.children.clear();
1212                    }
1213
1214                    // Create new children
1215                    for (index, item) in items.iter().enumerate() {
1216                        let item_key =
1217                            generate_item_key(item, key_path.as_deref(), item_name, index);
1218
1219                        for child_template in template {
1220                            let child_with_item = replace_ir_node_item_bindings(
1221                                child_template,
1222                                item,
1223                                index,
1224                                item_name,
1225                                &item_key,
1226                            );
1227                            create_ir_node_tree(
1228                                tree,
1229                                &child_with_item,
1230                                Some(node_id),
1231                                state,
1232                                patches,
1233                                false,
1234                                dependencies,
1235                                data_sources,
1236                            );
1237                        }
1238                    }
1239                } else {
1240                    // Reconcile existing children
1241                    let mut child_index = 0;
1242                    for (item_index, item) in items.iter().enumerate() {
1243                        let item_key =
1244                            generate_item_key(item, key_path.as_deref(), item_name, item_index);
1245
1246                        for child_template in template {
1247                            if let Some(&old_child_id) = old_children.get(child_index) {
1248                                let child_with_item = replace_ir_node_item_bindings(
1249                                    child_template,
1250                                    item,
1251                                    item_index,
1252                                    item_name,
1253                                    &item_key,
1254                                );
1255                                reconcile_ir_node(
1256                                    tree,
1257                                    old_child_id,
1258                                    &child_with_item,
1259                                    state,
1260                                    patches,
1261                                    dependencies,
1262                                    data_sources,
1263                                );
1264                            }
1265                            child_index += 1;
1266                        }
1267                    }
1268                }
1269            }
1270        }
1271        IRNode::Conditional {
1272            value,
1273            branches,
1274            fallback,
1275        } => {
1276            // Conditional reconciliation
1277            if !existing.is_conditional() {
1278                // Type mismatch - replace entire subtree
1279                let parent_id = existing.parent;
1280                remove_subtree(tree, node_id, patches, dependencies);
1281                create_ir_node_tree(
1282                    tree,
1283                    node,
1284                    parent_id,
1285                    state,
1286                    patches,
1287                    parent_id.is_none(),
1288                    dependencies,
1289                    data_sources,
1290                );
1291                return;
1292            }
1293
1294            // Re-register dependency on the condition binding (cleared before reconcile)
1295            if let Value::Binding(binding) = value {
1296                dependencies.add_dependency(node_id, binding);
1297            } else if let Value::TemplateString { bindings, .. } = value {
1298                for binding in bindings {
1299                    dependencies.add_dependency(node_id, binding);
1300                }
1301            }
1302
1303            // Re-evaluate the condition (with data sources for proper resolution)
1304            let evaluated_value = evaluate_value(value, state, data_sources);
1305            let matched_children =
1306                find_matching_branch(&evaluated_value, branches, fallback.as_deref(), state, data_sources);
1307
1308            let old_children = existing.children.clone();
1309            let old_len = old_children.len();
1310
1311            if let Some(children) = matched_children {
1312                let new_len = children.len();
1313                let common = old_len.min(new_len);
1314
1315                // Reconcile overlapping children (diff reuses DOM nodes where structure matches)
1316                for (i, child) in children.iter().enumerate().take(common) {
1317                    if let Some(&old_child_id) = old_children.get(i) {
1318                        reconcile_ir_node(tree, old_child_id, child, state, patches, dependencies, data_sources);
1319                    }
1320                }
1321
1322                // Remove surplus old children
1323                for i in common..old_len {
1324                    if let Some(&old_child_id) = old_children.get(i) {
1325                        remove_subtree(tree, old_child_id, patches, dependencies);
1326                        if let Some(cond_node) = tree.get_mut(node_id) {
1327                            cond_node.children = cond_node
1328                                .children
1329                                .iter()
1330                                .filter(|&&id| id != old_child_id)
1331                                .copied()
1332                                .collect();
1333                        }
1334                    }
1335                }
1336
1337                // Create new children beyond the old count
1338                for child in &children[common..] {
1339                    create_ir_node_tree(
1340                        tree,
1341                        child,
1342                        Some(node_id),
1343                        state,
1344                        patches,
1345                        false,
1346                        dependencies,
1347                        data_sources,
1348                    );
1349                }
1350            } else {
1351                // No matched branch - remove all children
1352                for &old_child_id in &old_children {
1353                    remove_subtree(tree, old_child_id, patches, dependencies);
1354                }
1355
1356                if let Some(cond_node) = tree.get_mut(node_id) {
1357                    cond_node.children.clear();
1358                }
1359            }
1360        }
1361    }
1362}
1363
1364/// Remove a subtree and generate Remove patches
1365fn remove_subtree(
1366    tree: &mut InstanceTree,
1367    node_id: NodeId,
1368    patches: &mut Vec<Patch>,
1369    dependencies: &mut DependencyGraph,
1370) {
1371    let ids = collect_subtree_ids(tree, node_id);
1372    for &id in &ids {
1373        patches.push(Patch::remove(id));
1374        dependencies.remove_node(id);
1375    }
1376    tree.remove(node_id);
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381    use super::*;
1382    use crate::ir::Value;
1383    use serde_json::json;
1384
1385    #[test]
1386    fn test_create_simple_tree() {
1387        use crate::reactive::DependencyGraph;
1388
1389        let mut tree = InstanceTree::new();
1390        let mut patches = Vec::new();
1391        let mut dependencies = DependencyGraph::new();
1392
1393        let element = Element::new("Column")
1394            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
1395
1396        let state = json!({});
1397        create_tree(
1398            &mut tree,
1399            &element,
1400            None,
1401            &state,
1402            &mut patches,
1403            true,
1404            &mut dependencies,
1405            None,
1406        );
1407
1408        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
1409        // Create Column, Insert Column into root, Create Text, Insert Text into Column
1410        assert_eq!(patches.len(), 4);
1411
1412        // Verify root insert patch exists
1413        let root_insert = patches
1414            .iter()
1415            .find(|p| matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root"));
1416        assert!(root_insert.is_some(), "Root insert patch should exist");
1417    }
1418
1419    #[test]
1420    fn test_diff_props() {
1421        let node_id = NodeId::default();
1422        let old = indexmap::indexmap! {
1423            "color".to_string() => json!("red"),
1424            "size".to_string() => json!(16),
1425        };
1426        let new = indexmap::indexmap! {
1427            "color".to_string() => json!("blue"),
1428            "size".to_string() => json!(16),
1429        };
1430
1431        let patches = diff_props(node_id, &old, &new);
1432
1433        // Only color changed
1434        assert_eq!(patches.len(), 1);
1435    }
1436}