hypen_engine/reconcile/
diff.rs

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