hypen_engine/reconcile/
diff.rs

1use super::{InstanceTree, Patch};
2use crate::ir::{Element, NodeId, Value};
3use crate::reactive::{DependencyGraph, evaluate_template_string};
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/// Check if a string is a simple item path (e.g., "item.name" or "item.user.profile")
216/// Simple paths contain only alphanumeric chars, underscores, and dots for nesting.
217/// Complex expressions contain operators like ?, :, +, -, etc.
218fn is_simple_path(s: &str) -> bool {
219    // Must start with "item" followed by valid path characters
220    if !s.starts_with("item") {
221        return false;
222    }
223
224    let after_item = &s[4..]; // Skip "item"
225    if after_item.is_empty() {
226        return true; // Just "item"
227    }
228
229    // Must be "item.something" - check rest is valid path
230    if !after_item.starts_with('.') {
231        return false;
232    }
233
234    // Check all characters after "item." are valid path chars
235    after_item[1..].chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.')
236}
237
238/// Replace item bindings (Value::Binding with is_item() or TemplateString with item bindings) with actual item values
239pub fn replace_item_bindings(element: &Element, item: &serde_json::Value, index: usize) -> Element {
240    let mut new_element = element.clone();
241
242    // Replace bindings in props
243    let mut new_props = IndexMap::new();
244    for (key, value) in &element.props {
245        let new_value = match value {
246            Value::Binding(binding) => {
247                // Check if this is an item binding
248                if binding.is_item() {
249                    // For bare @item, return the whole item
250                    if binding.path.is_empty() {
251                        Value::Static(item.clone())
252                    } else {
253                        // Navigate the path (e.g., ["name"] or ["user", "profile"])
254                        let prop_path = binding.full_path();
255                        if let Some(val) = navigate_item_path(item, &prop_path) {
256                            Value::Static(val.clone())
257                        } else {
258                            value.clone()
259                        }
260                    }
261                } else {
262                    value.clone()
263                }
264            }
265            Value::TemplateString { template, bindings } => {
266                // Check if any bindings are item bindings
267                let has_item_bindings = bindings.iter().any(|b| b.is_item());
268
269                if has_item_bindings {
270                    let mut result = template.clone();
271
272                    for binding in bindings {
273                        if binding.is_item() {
274                            let pattern = format!("${{{}}}", binding.full_path_with_source());
275                            let replacement = if binding.path.is_empty() {
276                                // Bare ${item}
277                                match item {
278                                    serde_json::Value::String(s) => s.clone(),
279                                    serde_json::Value::Number(n) => n.to_string(),
280                                    serde_json::Value::Bool(b) => b.to_string(),
281                                    _ => serde_json::to_string(item).unwrap_or_default(),
282                                }
283                            } else if let Some(val) = navigate_item_path(item, &binding.full_path()) {
284                                match val {
285                                    serde_json::Value::String(s) => s.clone(),
286                                    serde_json::Value::Number(n) => n.to_string(),
287                                    serde_json::Value::Bool(b) => b.to_string(),
288                                    _ => serde_json::to_string(val).unwrap_or_default(),
289                                }
290                            } else {
291                                continue; // Skip if path not found
292                            };
293                            result = result.replace(&pattern, &replacement);
294                        }
295                    }
296
297                    // Filter out resolved item bindings, keep state bindings
298                    let remaining_bindings: Vec<_> = bindings.iter()
299                        .filter(|b| b.is_state())
300                        .cloned()
301                        .collect();
302
303                    if remaining_bindings.is_empty() {
304                        // Check if result still contains ${...} expressions that need evaluation
305                        if result.contains("${") {
306                            // Evaluate the expression (item values have been substituted)
307                            match evaluate_template_string(&result, &serde_json::Value::Null, None) {
308                                Ok(evaluated) => Value::Static(serde_json::Value::String(evaluated)),
309                                Err(_) => Value::Static(serde_json::Value::String(result)),
310                            }
311                        } else {
312                            Value::Static(serde_json::Value::String(result))
313                        }
314                    } else {
315                        Value::TemplateString {
316                            template: result,
317                            bindings: remaining_bindings,
318                        }
319                    }
320                } else {
321                    value.clone()
322                }
323            }
324            // Handle static strings containing ${item.xxx} pattern (legacy/fallback)
325            // This handles both simple patterns like ${item.name} and complex expressions
326            // like ${item.selected ? '2px' : '1px'}
327            Value::Static(serde_json::Value::String(s)) if s.contains("${item.") || s.contains("${item}") => {
328                let mut result = s.clone();
329
330                // Strategy: Process ${...} blocks one at a time
331                // - If block is simple (just ${item} or ${item.path}), replace entire block with value
332                // - If block is complex (contains operators like ?, :, +, etc.), replace item refs then evaluate
333
334                let mut pos = 0;
335                while let Some(start) = result[pos..].find("${") {
336                    let abs_start = pos + start;
337
338                    // Find matching closing brace
339                    if let Some(end) = result[abs_start..].find('}') {
340                        let abs_end = abs_start + end;
341                        let content = &result[abs_start + 2..abs_end]; // Content between ${ and }
342
343                        // Check if this is a simple item reference or a complex expression
344                        if content == "item" {
345                            // Bare ${item} - replace with full item value
346                            let replacement = match item {
347                                serde_json::Value::String(s) => s.clone(),
348                                serde_json::Value::Number(n) => n.to_string(),
349                                serde_json::Value::Bool(b) => b.to_string(),
350                                _ => serde_json::to_string(item).unwrap_or_default(),
351                            };
352                            result = format!("{}{}{}", &result[..abs_start], replacement, &result[abs_end + 1..]);
353                            pos = 0;
354                            continue;
355                        } else if content.starts_with("item.") && is_simple_path(content) {
356                            // Simple ${item.path.to.value} - replace entire block with value
357                            let path = &content[5..]; // Skip "item."
358                            if let Some(val) = navigate_item_path(item, path) {
359                                let replacement = match val {
360                                    serde_json::Value::String(s) => s.clone(),
361                                    serde_json::Value::Number(n) => n.to_string(),
362                                    serde_json::Value::Bool(b) => b.to_string(),
363                                    serde_json::Value::Null => "null".to_string(),
364                                    _ => serde_json::to_string(val).unwrap_or_default(),
365                                };
366                                result = format!("{}{}{}", &result[..abs_start], replacement, &result[abs_end + 1..]);
367                                pos = 0;
368                                continue;
369                            }
370                        } else if content.contains("item.") || content.contains("item ") {
371                            // Complex expression with item references - replace item refs within the expression
372                            let mut expr_content = content.to_string();
373
374                            // Replace all item.xxx references within this expression
375                            let mut expr_pos = 0;
376                            while let Some(item_start) = expr_content[expr_pos..].find("item.") {
377                                let item_abs_start = expr_pos + item_start;
378                                let path_start = item_abs_start + 5; // Skip "item."
379
380                                // Find the end of the path (valid identifier chars only)
381                                let mut path_end = path_start;
382                                let chars: Vec<char> = expr_content[path_start..].chars().collect();
383
384                                for (i, ch) in chars.iter().enumerate() {
385                                    if ch.is_alphanumeric() || *ch == '_' || *ch == '.' {
386                                        if *ch == '.' {
387                                            if i + 1 >= chars.len() || {
388                                                let next = chars[i + 1];
389                                                !next.is_alphanumeric() && next != '_'
390                                            } {
391                                                break;
392                                            }
393                                        }
394                                        path_end = path_start + i + 1;
395                                    } else {
396                                        break;
397                                    }
398                                }
399
400                                if path_end > path_start {
401                                    let path = &expr_content[path_start..path_end];
402                                    if let Some(val) = navigate_item_path(item, path) {
403                                        let replacement = match val {
404                                            serde_json::Value::String(s) => format!("'{}'", s),
405                                            serde_json::Value::Number(n) => n.to_string(),
406                                            serde_json::Value::Bool(b) => b.to_string(),
407                                            serde_json::Value::Null => "null".to_string(),
408                                            _ => serde_json::to_string(val).unwrap_or_default(),
409                                        };
410                                        expr_content = format!(
411                                            "{}{}{}",
412                                            &expr_content[..item_abs_start],
413                                            replacement,
414                                            &expr_content[path_end..]
415                                        );
416                                        expr_pos = 0;
417                                        continue;
418                                    }
419                                }
420                                expr_pos = item_abs_start + 5;
421                            }
422
423                            // Reconstruct the expression block and evaluate
424                            let new_expr = format!("${{{}}}", expr_content);
425                            match evaluate_template_string(&new_expr, &serde_json::Value::Null, None) {
426                                Ok(evaluated) => {
427                                    result = format!("{}{}{}", &result[..abs_start], evaluated, &result[abs_end + 1..]);
428                                    pos = 0;
429                                    continue;
430                                }
431                                Err(_) => {
432                                    // If evaluation fails, leave as-is and move on
433                                    pos = abs_end + 1;
434                                    continue;
435                                }
436                            }
437                        }
438
439                        // Not an item-related block, skip it
440                        pos = abs_end + 1;
441                    } else {
442                        break;
443                    }
444                }
445
446                Value::Static(serde_json::Value::String(result))
447            }
448            _ => value.clone(),
449        };
450        new_props.insert(key.clone(), new_value);
451    }
452    new_element.props = new_props;
453
454    // Add a key for reconciliation (use array index)
455    new_element.key = Some(format!("list-item-{}", index));
456
457    // Recursively replace in children
458    new_element.children = element.children.iter()
459        .map(|child| replace_item_bindings(child, item, index))
460        .collect();
461
462    new_element
463}
464
465/// Reconcile a single node with a new element
466pub fn reconcile_node(
467    tree: &mut InstanceTree,
468    node_id: NodeId,
469    element: &Element,
470    state: &serde_json::Value,
471    patches: &mut Vec<Patch>,
472    dependencies: &mut DependencyGraph,
473) {
474    let node = tree.get(node_id).cloned();
475    if node.is_none() {
476        return;
477    }
478    let node = node.unwrap();
479
480    // Special handling for iterable elements (List, Grid, etc.)
481    // An element is iterable if it has BOTH prop "0" (array binding) AND children (template)
482    // This distinguishes iterables from Text elements, which also use prop "0" but have no children
483    let is_iterable = element.props.get("0").is_some() && !element.children.is_empty();
484    
485    if is_iterable {
486        // Get the array binding from first prop (prop "0")
487        let array = if let Some(Value::Binding(binding)) = element.props.get("0") {
488            evaluate_binding(binding, state).unwrap_or(serde_json::Value::Array(vec![]))
489        } else {
490            serde_json::Value::Array(vec![])
491        };
492
493        // Regenerate iterable children
494        if let serde_json::Value::Array(items) = &array {
495            let old_children = node.children.clone();
496            
497            // Calculate expected number of children
498            let expected_children_count = items.len() * element.children.len();
499            
500            // Remove old children if count doesn't match
501            if old_children.len() != expected_children_count {
502                for &old_child_id in &old_children {
503                    patches.push(Patch::remove(old_child_id));
504                }
505                
506                // Clear children from node
507                if let Some(node) = tree.get_mut(node_id) {
508                    node.children.clear();
509                }
510                
511                // Create new children
512                for (index, item) in items.iter().enumerate() {
513                    for child_template in &element.children {
514                        let child_with_item = replace_item_bindings(child_template, item, index);
515                        create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
516                    }
517                }
518            } else {
519                // Reconcile existing children
520                let mut child_index = 0;
521                for (item_index, item) in items.iter().enumerate() {
522                    for child_template in &element.children {
523                        if let Some(&old_child_id) = old_children.get(child_index) {
524                            let child_with_item = replace_item_bindings(child_template, item, item_index);
525                            reconcile_node(tree, old_child_id, &child_with_item, state, patches, dependencies);
526                        }
527                        child_index += 1;
528                    }
529                }
530            }
531        }
532        
533        return; // Done with List reconciliation
534    }
535
536    // If element type changed, replace the entire subtree
537    if node.element_type != element.element_type {
538        replace_subtree(tree, node_id, &node, element, state, patches, dependencies);
539        return;
540    }
541
542    // Register dependencies for this node (same as create_tree)
543    for value in element.props.values() {
544        if let Value::Binding(binding) = value {
545            dependencies.add_dependency(node_id, binding);
546        }
547    }
548
549    // Diff props
550    let new_props = resolve_props(&element.props, state);
551    let prop_patches = diff_props(node_id, &node.props, &new_props);
552    patches.extend(prop_patches);
553
554    // Update node props in tree
555    if let Some(node) = tree.get_mut(node_id) {
556        node.props = new_props.clone();
557        node.raw_props = element.props.clone();
558    }
559
560    // Reconcile children (skip if element is marked as lazy)
561    let is_lazy = element.props.get("__lazy")
562        .and_then(|v| {
563            if let Value::Static(val) = v {
564                val.as_bool()
565            } else {
566                None
567            }
568        })
569        .unwrap_or(false);
570    
571    if !is_lazy {
572        let old_children = node.children.clone();
573        let new_children = &element.children;
574
575        // Simple strategy: match children by index
576        for (i, new_child_element) in new_children.iter().enumerate() {
577            if let Some(&old_child_id) = old_children.get(i) {
578                // Reconcile existing child
579                reconcile_node(tree, old_child_id, new_child_element, state, patches, dependencies);
580            } else {
581                // Create new child
582                let new_child_id = create_tree(tree, new_child_element, Some(node_id), state, patches, false, dependencies);
583                if let Some(node) = tree.get_mut(node_id) {
584                    node.children.push(new_child_id);
585                }
586            }
587        }
588
589        // Remove extra old children
590        if old_children.len() > new_children.len() {
591            for &old_child_id in &old_children[new_children.len()..] {
592                patches.push(Patch::remove(old_child_id));
593                // TODO: Remove from tree
594            }
595        }
596    }
597}
598
599/// Replace an entire subtree when element types don't match.
600/// This removes the old node and its descendants, then creates a new subtree in its place.
601fn replace_subtree(
602    tree: &mut InstanceTree,
603    old_node_id: NodeId,
604    old_node: &super::InstanceNode,
605    new_element: &Element,
606    state: &serde_json::Value,
607    patches: &mut Vec<Patch>,
608    dependencies: &mut DependencyGraph,
609) {
610    let parent_id = old_node.parent;
611
612    // Remember the position of the old node in its parent's children list
613    let old_position = if let Some(pid) = parent_id {
614        tree.get(pid).and_then(|parent| {
615            parent.children.iter().position(|&id| id == old_node_id)
616        })
617    } else {
618        None
619    };
620
621    // Collect all node IDs in the old subtree (depth-first, children before parents)
622    let ids_to_remove = collect_subtree_ids(tree, old_node_id);
623
624    // Generate Remove patches and clear dependencies in one pass
625    for &id in &ids_to_remove {
626        patches.push(Patch::remove(id));
627        dependencies.remove_node(id);
628    }
629
630    // Remove old node from parent's children list
631    if let Some(pid) = parent_id {
632        if let Some(parent) = tree.get_mut(pid) {
633            parent.children.retain(|&id| id != old_node_id);
634        }
635    }
636
637    // Remove the old subtree from the tree
638    tree.remove(old_node_id);
639
640    // Create the new subtree
641    let is_root = parent_id.is_none();
642    let new_node_id = create_tree(tree, new_element, parent_id, state, patches, is_root, dependencies);
643
644    // If this was the root, update it
645    if is_root {
646        tree.set_root(new_node_id);
647    } else if let Some(pid) = parent_id {
648        if let Some(pos) = old_position {
649            if let Some(parent) = tree.get_mut(pid) {
650                let current_len = parent.children.len();
651                // create_tree appended at end, so new node is at current_len - 1
652                // We want it at position `pos`
653                if pos < current_len - 1 {
654                    // Pop from end (O(1)) and insert at correct position (O(n))
655                    let new_id = parent.children.pop().unwrap();
656                    parent.children.insert(pos, new_id);
657
658                    // Get the next sibling for the Move patch
659                    let next_sibling = parent.children.get(pos + 1).copied();
660                    patches.push(Patch::move_node(pid, new_node_id, next_sibling));
661                }
662                // If pos == current_len - 1, it's already in the right place (was last child)
663            }
664        }
665    }
666}
667
668/// Collect all node IDs in a subtree (post-order: children before parents)
669/// Uses iterative approach to avoid stack overflow on deep trees
670fn collect_subtree_ids(tree: &InstanceTree, root_id: NodeId) -> Vec<NodeId> {
671    let mut result = Vec::new();
672    let mut stack: Vec<(NodeId, bool)> = vec![(root_id, false)];
673
674    while let Some((node_id, children_processed)) = stack.pop() {
675        if children_processed {
676            // Children already processed, add this node
677            result.push(node_id);
678        } else {
679            // Push self back with flag, then push children
680            stack.push((node_id, true));
681            if let Some(node) = tree.get(node_id) {
682                // Push children in reverse order so they're processed left-to-right
683                for &child_id in node.children.iter().rev() {
684                    stack.push((child_id, false));
685                }
686            }
687        }
688    }
689
690    result
691}
692
693/// Resolve props by evaluating bindings against state
694fn resolve_props(props: &IndexMap<String, crate::ir::Value>, state: &serde_json::Value) -> IndexMap<String, serde_json::Value> {
695    resolve_props_with_item(props, state, None)
696}
697
698/// Resolve props with optional item context (for list iteration)
699fn resolve_props_with_item(
700    props: &IndexMap<String, crate::ir::Value>,
701    state: &serde_json::Value,
702    item: Option<&serde_json::Value>,
703) -> IndexMap<String, serde_json::Value> {
704    let mut resolved = IndexMap::new();
705
706    for (key, value) in props {
707        let resolved_value = match value {
708            crate::ir::Value::Static(v) => v.clone(),
709            crate::ir::Value::Binding(binding) => {
710                if binding.is_item() {
711                    // Evaluate item binding
712                    if let Some(item_value) = item {
713                        evaluate_item_binding(binding, item_value).unwrap_or(serde_json::Value::Null)
714                    } else {
715                        serde_json::Value::Null
716                    }
717                } else {
718                    // Evaluate state binding
719                    evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
720                }
721            }
722            crate::ir::Value::TemplateString { template, .. } => {
723                // Use expression evaluation for template strings - handles both
724                // simple bindings and complex expressions like ternaries
725                match crate::reactive::evaluate_template_string(template, state, item) {
726                    Ok(result) => serde_json::Value::String(result),
727                    Err(_) => serde_json::Value::String(template.clone()),
728                }
729            }
730            crate::ir::Value::Action(action) => {
731                // Actions are serialized with @ prefix for renderer to detect
732                serde_json::Value::String(format!("@{}", action))
733            }
734        };
735        resolved.insert(key.clone(), resolved_value);
736    }
737
738    resolved
739}
740
741/// Evaluate an item binding against the item object
742fn evaluate_item_binding(binding: &crate::reactive::Binding, item: &serde_json::Value) -> Option<serde_json::Value> {
743    if binding.path.is_empty() {
744        return Some(item.clone());
745    }
746
747    let mut current = item;
748    for segment in &binding.path {
749        current = current.get(segment)?;
750    }
751    Some(current.clone())
752}
753
754/// Evaluate a binding against state using its parsed path
755fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
756    let mut current = state;
757
758    for segment in &binding.path {
759        current = current.get(segment)?;
760    }
761
762    Some(current.clone())
763}
764
765/// Reconcile children using keys for efficient updates
766fn reconcile_keyed_children(
767    tree: &mut InstanceTree,
768    parent_id: NodeId,
769    old_children: &[NodeId],
770    new_children: &[Element],
771    state: &serde_json::Value,
772    dependencies: &mut DependencyGraph,
773) -> Vec<Patch> {
774    let mut patches = Vec::new();
775
776    // Build a map of old children by key
777    let mut old_keyed: IndexMap<String, NodeId> = IndexMap::new();
778    let mut old_unkeyed: Vec<NodeId> = Vec::new();
779
780    for &child_id in old_children {
781        if let Some(node) = tree.get(child_id) {
782            if let Some(key) = &node.key {
783                old_keyed.insert(key.clone(), child_id);
784            } else {
785                old_unkeyed.push(child_id);
786            }
787        }
788    }
789
790    let mut new_child_ids: Vec<NodeId> = Vec::new();
791    let mut unkeyed_idx = 0;
792
793    // First pass: match children by key or create new ones
794    for new_element in new_children {
795        let new_key = new_element.key.as_ref();
796
797        let child_id = if let Some(key) = new_key {
798            // Try to find matching keyed child
799            if let Some(&old_id) = old_keyed.get(key) {
800                // Found a match - reconcile it
801                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
802                old_keyed.shift_remove(key); // Mark as used
803                old_id
804            } else {
805                // No match - create new child
806                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
807            }
808        } else {
809            // Unkeyed child - try to reuse unkeyed old child
810            if let Some(&old_id) = old_unkeyed.get(unkeyed_idx) {
811                reconcile_node(tree, old_id, new_element, state, &mut patches, dependencies);
812                unkeyed_idx += 1;
813                old_id
814            } else {
815                // No unkeyed child available - create new
816                create_tree(tree, new_element, Some(parent_id), state, &mut patches, false, dependencies)
817            }
818        };
819
820        new_child_ids.push(child_id);
821    }
822
823    // Second pass: generate move/insert patches using LIS algorithm
824    // Build list of old positions for children that were reused
825    let mut old_positions: Vec<Option<usize>> = Vec::new();
826    for &new_id in &new_child_ids {
827        let old_pos = old_children.iter().position(|&old_id| old_id == new_id);
828        old_positions.push(old_pos);
829    }
830
831    // Find longest increasing subsequence to minimize moves
832    let lis = longest_increasing_subsequence(&old_positions);
833
834    // Update parent's children list
835    if let Some(parent_node) = tree.get_mut(parent_id) {
836        parent_node.children = new_child_ids.clone();
837    }
838
839    // Generate move/insert patches for children not in LIS
840    for (new_idx, &child_id) in new_child_ids.iter().enumerate() {
841        // Check if this child needs to be moved
842        if !lis.contains(&new_idx) {
843            // Calculate before_id (the next child in the list, or None for last position)
844            let before_id = new_child_ids.get(new_idx + 1).copied();
845            patches.push(Patch::move_node(parent_id, child_id, before_id));
846        }
847    }
848
849    // Third pass: remove old children that weren't reused
850    for old_id in old_keyed.values() {
851        patches.push(Patch::remove(*old_id));
852    }
853    for &old_id in &old_unkeyed[unkeyed_idx..] {
854        patches.push(Patch::remove(old_id));
855    }
856
857    patches
858}
859
860/// Find the longest increasing subsequence (LIS) indices
861/// This is used to minimize DOM moves during reconciliation
862/// Returns indices of elements that are already in correct relative order
863fn longest_increasing_subsequence(positions: &[Option<usize>]) -> Vec<usize> {
864    // Extract valid positions with their indices
865    let valid: Vec<(usize, usize)> = positions
866        .iter()
867        .enumerate()
868        .filter_map(|(idx, &pos)| pos.map(|p| (idx, p)))
869        .collect();
870
871    if valid.is_empty() {
872        return Vec::new();
873    }
874
875    // Use dynamic programming to find LIS
876    let n = valid.len();
877    let mut dp: Vec<usize> = vec![1; n];
878    let mut prev: Vec<Option<usize>> = vec![None; n];
879
880    for i in 1..n {
881        for j in 0..i {
882            if valid[j].1 < valid[i].1 && dp[j] + 1 > dp[i] {
883                dp[i] = dp[j] + 1;
884                prev[i] = Some(j);
885            }
886        }
887    }
888
889    // Find the index with maximum LIS length
890    let mut max_idx = 0;
891    for i in 1..n {
892        if dp[i] > dp[max_idx] {
893            max_idx = i;
894        }
895    }
896
897    // Reconstruct the LIS indices
898    let mut result = Vec::new();
899    let mut idx = Some(max_idx);
900    while let Some(i) = idx {
901        result.push(valid[i].0);
902        idx = prev[i];
903    }
904    result.reverse();
905
906    result
907}
908
909/// Diff two sets of props and generate SetProp patches
910pub fn diff_props(
911    node_id: NodeId,
912    old_props: &IndexMap<String, serde_json::Value>,
913    new_props: &IndexMap<String, serde_json::Value>,
914) -> Vec<Patch> {
915    let mut patches = Vec::new();
916
917    // Check for changed or new props
918    for (key, new_value) in new_props {
919        if old_props.get(key) != Some(new_value) {
920            patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
921        }
922    }
923
924    // Note: We don't remove old props, just set new ones
925    // Platforms should handle prop removal if needed
926
927    patches
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use crate::ir::Value;
934    use serde_json::json;
935
936    #[test]
937    fn test_create_simple_tree() {
938        use crate::reactive::DependencyGraph;
939
940        let mut tree = InstanceTree::new();
941        let mut patches = Vec::new();
942        let mut dependencies = DependencyGraph::new();
943
944        let element = Element::new("Column")
945            .with_child(Element::new("Text").with_prop("text", Value::Static(json!("Hello"))));
946
947        let state = json!({});
948        create_tree(&mut tree, &element, None, &state, &mut patches, true, &mut dependencies);
949
950        // Should create 2 nodes (Column + Text) + 2 Inserts (root + child)
951        // Create Column, Insert Column into root, Create Text, Insert Text into Column
952        assert_eq!(patches.len(), 4);
953
954        // Verify root insert patch exists
955        let root_insert = patches.iter().find(|p| {
956            matches!(p, Patch::Insert { parent_id, .. } if parent_id == "root")
957        });
958        assert!(root_insert.is_some(), "Root insert patch should exist");
959    }
960
961    #[test]
962    fn test_diff_props() {
963        let node_id = NodeId::default();
964        let old = indexmap::indexmap! {
965            "color".to_string() => json!("red"),
966            "size".to_string() => json!(16),
967        };
968        let new = indexmap::indexmap! {
969            "color".to_string() => json!("blue"),
970            "size".to_string() => json!(16),
971        };
972
973        let patches = diff_props(node_id, &old, &new);
974
975        // Only color changed
976        assert_eq!(patches.len(), 1);
977    }
978
979    #[test]
980    fn test_longest_increasing_subsequence_basic() {
981        // Test case: [0, 1, 2, 3] - already in order, should select all
982        let positions = vec![Some(0), Some(1), Some(2), Some(3)];
983        let lis = longest_increasing_subsequence(&positions);
984        assert_eq!(lis, vec![0, 1, 2, 3]);
985    }
986
987    #[test]
988    fn test_longest_increasing_subsequence_reverse() {
989        // Test case: [3, 2, 1, 0] - reversed, should select only one element
990        let positions = vec![Some(3), Some(2), Some(1), Some(0)];
991        let lis = longest_increasing_subsequence(&positions);
992        assert_eq!(lis.len(), 1);
993    }
994
995    #[test]
996    fn test_longest_increasing_subsequence_mixed() {
997        // Test case: [0, 3, 1, 2] - should select [0, 1, 2]
998        let positions = vec![Some(0), Some(3), Some(1), Some(2)];
999        let lis = longest_increasing_subsequence(&positions);
1000        assert_eq!(lis.len(), 3);
1001        // Should contain indices of 0, 1, 2 in the original array
1002        assert!(lis.contains(&0));
1003        assert!(lis.contains(&2));
1004        assert!(lis.contains(&3));
1005    }
1006
1007    #[test]
1008    fn test_longest_increasing_subsequence_with_new_items() {
1009        // Test case: [None, Some(0), None, Some(1)] - new items mixed with existing
1010        let positions = vec![None, Some(0), None, Some(1)];
1011        let lis = longest_increasing_subsequence(&positions);
1012        assert_eq!(lis, vec![1, 3]); // Only indices with existing positions
1013    }
1014
1015    #[test]
1016    fn test_longest_increasing_subsequence_empty() {
1017        let positions: Vec<Option<usize>> = vec![];
1018        let lis = longest_increasing_subsequence(&positions);
1019        assert_eq!(lis, Vec::<usize>::new());
1020    }
1021
1022    #[test]
1023    fn test_longest_increasing_subsequence_all_new() {
1024        // All new items (no old positions)
1025        let positions = vec![None, None, None];
1026        let lis = longest_increasing_subsequence(&positions);
1027        assert_eq!(lis, Vec::<usize>::new());
1028    }
1029
1030    #[test]
1031    fn test_reconcile_keyed_children_basic() {
1032        let mut tree = InstanceTree::new();
1033        let mut dependencies = DependencyGraph::new();
1034        let state = json!({});
1035
1036        // Create a parent node
1037        let parent = Element::new("Column");
1038        let parent_id = tree.create_node(&parent, &state);
1039
1040        // Create old children
1041        let mut old_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1042        old_child_1.key = Some("item-1".to_string());
1043        let old_id_1 = tree.create_node(&old_child_1, &state);
1044
1045        let mut old_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1046        old_child_2.key = Some("item-2".to_string());
1047        let old_id_2 = tree.create_node(&old_child_2, &state);
1048
1049        tree.add_child(parent_id, old_id_1, None);
1050        tree.add_child(parent_id, old_id_2, None);
1051
1052        // New children in reversed order
1053        let mut new_child_2 = Element::new("Text").with_prop("text", Value::Static(json!("Item 2")));
1054        new_child_2.key = Some("item-2".to_string());
1055
1056        let mut new_child_1 = Element::new("Text").with_prop("text", Value::Static(json!("Item 1")));
1057        new_child_1.key = Some("item-1".to_string());
1058
1059        let new_children = vec![new_child_2, new_child_1];
1060        let old_children = vec![old_id_1, old_id_2];
1061
1062        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1063
1064        // Should have move patches but no create/remove patches (children are reused)
1065        let move_count = patches.iter().filter(|p| matches!(p, Patch::Move { .. })).count();
1066        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1067        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1068
1069        assert_eq!(create_count, 0, "Should not create new children");
1070        assert_eq!(remove_count, 0, "Should not remove any children");
1071        assert!(move_count > 0, "Should have move patches for reordering");
1072    }
1073
1074    #[test]
1075    fn test_reconcile_keyed_children_add_remove() {
1076        let mut tree = InstanceTree::new();
1077        let mut dependencies = DependencyGraph::new();
1078        let state = json!({});
1079
1080        let parent = Element::new("Column");
1081        let parent_id = tree.create_node(&parent, &state);
1082
1083        // Old: item-1, item-2
1084        let mut old_child_1 = Element::new("Text");
1085        old_child_1.key = Some("item-1".to_string());
1086        let old_id_1 = tree.create_node(&old_child_1, &state);
1087
1088        let mut old_child_2 = Element::new("Text");
1089        old_child_2.key = Some("item-2".to_string());
1090        let old_id_2 = tree.create_node(&old_child_2, &state);
1091
1092        tree.add_child(parent_id, old_id_1, None);
1093        tree.add_child(parent_id, old_id_2, None);
1094
1095        // New: item-2, item-3 (removed item-1, added item-3)
1096        let mut new_child_2 = Element::new("Text");
1097        new_child_2.key = Some("item-2".to_string());
1098
1099        let mut new_child_3 = Element::new("Text");
1100        new_child_3.key = Some("item-3".to_string());
1101
1102        let new_children = vec![new_child_2, new_child_3];
1103        let old_children = vec![old_id_1, old_id_2];
1104
1105        let patches = reconcile_keyed_children(&mut tree, parent_id, &old_children, &new_children, &state, &mut dependencies);
1106
1107        let create_count = patches.iter().filter(|p| matches!(p, Patch::Create { .. })).count();
1108        let remove_count = patches.iter().filter(|p| matches!(p, Patch::Remove { .. })).count();
1109
1110        assert_eq!(create_count, 1, "Should create item-3");
1111        assert_eq!(remove_count, 1, "Should remove item-1");
1112    }
1113}