// Solid-style reference-based reconciliation for DOM reuse
//
// Unlike React's key-based reconciliation, this approach tracks item references
// using JavaScript's === operator. When items are reordered, their DOM nodes
// are moved rather than recreated.
//

///|
/// Check if two values are the same reference (physical equality)
/// Uses JavaScript's === operator for reference comparison
extern "js" fn ref_equal_any(a : @js.Any, b : @js.Any) -> Bool =
  #| (a, b) => a === b

///|
/// Check if two values are the same reference (type-safe wrapper)
pub fn[T] ref_equal(a : T, b : T) -> Bool {
  ref_equal_any(@js.any(a), @js.any(b))
}

///|
/// Reference-keyed Map helpers for faster lookups (JS Map)
extern "js" fn ref_map_new() -> @js.Any =
  #| () => new Map()

///|
extern "js" fn ref_map_has(map : @js.Any, key : @js.Any) -> Bool =
  #| (map, key) => map.has(key)

///|
extern "js" fn ref_map_set(map : @js.Any, key : @js.Any, value : Int) -> Unit =
  #| (map, key, value) => { map.set(key, value); }

///|
extern "js" fn ref_map_get_or_minus1(map : @js.Any, key : @js.Any) -> Int =
  #| (map, key) => {
  #|   const v = map.get(key);
  #|   return v === undefined ? -1 : v;
  #| }

///|
/// Check if a node's next sibling matches the expected node
extern "js" fn next_sibling_is(
  node : @js_dom.Node,
  expected : @js_dom.Node,
) -> Bool =
  #| (node, expected) => node.nextSibling === expected

///|
/// Item-DOM mapping entry
/// Stores the association between an item reference and its DOM nodes
/// Also stores a dispose function for cleanup when item is removed
pub(all) struct ItemEntry[T] {
  item : T
  dom : @js_dom.Node
  last : @js_dom.Node
  nodes : Array[@js_dom.Node]
  dispose : () -> Unit
}

///|
/// Create a new ItemEntry
pub fn[T] ItemEntry::new(item : T, dom : @js_dom.Node) -> ItemEntry[T] {
  // Default dispose is no-op for backwards compatibility
  { item, dom, last: dom, nodes: [dom], dispose: fn() {  } }
}

///|
/// Create a new ItemEntry with dispose function
pub fn[T] ItemEntry::new_with_dispose(
  item : T,
  dom : @js_dom.Node,
  dispose : () -> Unit,
) -> ItemEntry[T] {
  { item, dom, last: dom, nodes: [dom], dispose }
}

///|
/// Create ItemEntry from multiple DOM nodes
pub fn[T] ItemEntry::new_with_nodes_and_dispose(
  item : T,
  nodes : Array[@js_dom.Node],
  dispose : () -> Unit,
) -> ItemEntry[T] {
  let first = nodes[0]
  let last = nodes[nodes.length() - 1]
  { item, dom: first, last, nodes, dispose }
}

///|
/// Build a reference-keyed index map for old entries
fn[T] build_entry_index_map(entries : Array[ItemEntry[T]]) -> @js.Any {
  let map = ref_map_new()
  for i, entry in entries {
    let key = @js.any(entry.item)
    if !ref_map_has(map, key) {
      ref_map_set(map, key, i)
    }
  }
  map
}

///|
/// Detect duplicate references in a list (JS ===)
fn[T] has_duplicate_refs(items : Array[T]) -> Bool {
  let seen = ref_map_new()
  for item in items {
    let key = @js.any(item)
    if ref_map_has(seen, key) {
      return true
    }
    ref_map_set(seen, key, 1)
  }
  false
}

///|
/// Find an entry index by item reference using a map
fn[T] find_entry_index(
  map : @js.Any,
  item : T,
  used_indices : Array[Bool],
  old_entries : Array[ItemEntry[T]],
) -> Int {
  let key = @js.any(item)
  let first = ref_map_get_or_minus1(map, key)
  if first >= 0 && !used_indices[first] {
    return first
  }
  // Duplicate references: find another unused entry with the same reference
  for i, entry in old_entries {
    if !used_indices[i] && ref_equal(entry.item, item) {
      return i
    }
  }
  -1
}

///|
/// Move a list of nodes before cursor, preserving order
fn move_nodes_before(
  parent : @js_dom.Node,
  nodes : Array[@js_dom.Node],
  cursor : @js_dom.Node,
) -> Unit {
  // Move in order to preserve node sequence
  for node in nodes {
    move_before(parent, node, Some(cursor)) |> ignore
  }
}

///|
/// Safely remove multiple nodes
fn safe_remove_nodes(
  parent : @js_dom.Node,
  nodes : Array[@js_dom.Node],
) -> Unit {
  for node in nodes {
    safe_remove_child(parent, node)
  }
}

///|
/// Reconcile items with DOM nodes using reference equality
///
/// Algorithm:
/// 1. Build a lookup from old entries (item -> entry) using reference equality
/// 2. Process new items in order:
///    - If same reference exists in old entries → reuse DOM, use moveBefore
///    - If not found → create new DOM with render_item
/// 3. Remove DOM nodes for items no longer present
/// 4. Return new ItemEntry array
///
/// Parameters:
/// - parent: The parent DOM node containing the items
/// - old_entries: Previous item-DOM mappings
/// - new_items: New items array
/// - render_item: Function to render a new item to DOM
/// - end_marker: Comment node marking the end of the For region
///
/// Returns: New ItemEntry array reflecting current items
pub fn[T] reconcile_items(
  parent : @js_dom.Node,
  old_entries : Array[ItemEntry[T]],
  new_items : Array[T],
  render_item : (T, Int) -> @js_dom.Node,
  end_marker : @js_dom.Node,
) -> Array[ItemEntry[T]] {
  let n = new_items.length()

  // Handle empty case - dispose and remove all old entries
  if n == 0 {
    for entry in old_entries {
      // Dispose first to run cleanups, then remove DOM
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
    return []
  }

  // Duplicate references are ambiguous for ref-based reconciliation.
  // Fall back to full re-render to keep DOM consistent.
  if has_duplicate_refs(new_items) {
    for entry in old_entries {
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
    let fresh_entries : Array[ItemEntry[T]] = []
    for i, item in new_items {
      let dom = render_item(item, i)
      let nodes = collect_dom_nodes(dom)
      if nodes.length() == 0 {
        let placeholder = @js_dom.document().createComment("empty")
        parent.insertBefore(placeholder, Some(end_marker)) |> ignore
        fresh_entries.push(
          ItemEntry::new_with_nodes_and_dispose(item, [placeholder], fn() {  }),
        )
      } else {
        parent.insertBefore(dom, Some(end_marker)) |> ignore
        fresh_entries.push(
          ItemEntry::new_with_nodes_and_dispose(item, nodes, fn() {  }),
        )
      }
    }
    return fresh_entries
  }
  let new_entries : Array[ItemEntry[T]] = []
  for _ in 0..=..0 {
    let item = new_items[i]
    let old_index = find_entry_index(index_map, item, used_indices, old_entries)
    if old_index >= 0 {
      // Reuse existing DOM node
      let entry = old_entries[old_index]
      used_indices[old_index] = true

      // Only move if not already at correct position
      // The node is at correct position if its nextSibling equals cursor
      if entry.nodes.length() > 0 {
        if !next_sibling_is(entry.last, cursor.val) {
          move_nodes_before(parent, entry.nodes, cursor.val)
        }
        cursor.val = entry.dom
      }
      new_entries[i] = entry
    } else {
      // Create new DOM node
      let dom = render_item(item, i)
      let nodes = collect_dom_nodes(dom)
      if nodes.length() == 0 {
        let placeholder = @js_dom.document().createComment("empty")
        parent.insertBefore(placeholder, Some(cursor.val)) |> ignore
        let placeholder_nodes = [placeholder]
        cursor.val = placeholder
        new_entries[i] = ItemEntry::new_with_nodes_and_dispose(
          item,
          placeholder_nodes,
          fn() {  },
        )
      } else {
        parent.insertBefore(dom, Some(cursor.val)) |> ignore
        cursor.val = nodes[0]
        // New items get no-op dispose - actual dispose is set by for_each
        new_entries[i] = ItemEntry::new_with_nodes_and_dispose(item, nodes, fn() {

        })
      }
    }
  }

  // Remove unused DOM nodes and dispose their owner scopes
  for i, entry in old_entries {
    if !used_indices[i] {
      // Dispose first to run cleanups, then remove DOM
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
  }
  new_entries
}

///|
/// Safely remove a child node, handling cases where the node may have been
/// moved to a different parent (e.g., during moveBefore fallback to insertBefore)
extern "js" fn safe_remove_child(parent : @js_dom.Node, child : @js_dom.Node) =
  #| (parent, child) => {
  #|   try {
  #|     // Check if the child is actually a child of this parent
  #|     if (child.parentNode === parent) {
  #|       parent.removeChild(child);
  #|     } else if (child.parentNode) {
  #|       // Node was moved to a different parent, remove from there
  #|       child.parentNode.removeChild(child);
  #|     }
  #|     // If child.parentNode is null, node is already detached
  #|   } catch (e) {
  #|     // Ignore errors - node may already be removed or in invalid state
  #|   }
  #| }

///|
/// Simplified reconciliation for render.mbt's for_each
/// Uses a DocumentFragment as container with a placeholder comment
pub fn[T] reconcile_for_each(
  entries_ref : Ref[Array[ItemEntry[T]]],
  placeholder : @js_dom.Node,
  new_items : Array[T],
  render_item : (T, Int) -> @js_dom.Node,
) -> Unit {
  if placeholder.parentNode() is Some(parent) {
    entries_ref.val = reconcile_items(
      parent,
      entries_ref.val,
      new_items,
      render_item,
      placeholder,
    )
  }
}

///|
/// Reconciliation with dispose function support
/// render_item returns both the DOM node and a dispose function for cleanup
pub fn[T] reconcile_for_each_with_dispose(
  entries_ref : Ref[Array[ItemEntry[T]]],
  placeholder : @js_dom.Node,
  new_items : Array[T],
  render_item : (T, Int) -> (@js_dom.Node, () -> Unit),
) -> Unit {
  if placeholder.parentNode() is Some(parent) {
    entries_ref.val = reconcile_items_with_dispose(
      parent,
      entries_ref.val,
      new_items,
      render_item,
      placeholder,
    )
  }
}

///|
/// Reconcile items with dispose support
/// Similar to reconcile_items but render_item returns (Node, dispose)
pub fn[T] reconcile_items_with_dispose(
  parent : @js_dom.Node,
  old_entries : Array[ItemEntry[T]],
  new_items : Array[T],
  render_item : (T, Int) -> (@js_dom.Node, () -> Unit),
  end_marker : @js_dom.Node,
) -> Array[ItemEntry[T]] {
  let n = new_items.length()

  // Handle empty case - dispose and remove all old entries
  if n == 0 {
    for entry in old_entries {
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
    return []
  }

  // Duplicate references are ambiguous for ref-based reconciliation.
  // Fall back to full re-render to keep DOM consistent.
  if has_duplicate_refs(new_items) {
    for entry in old_entries {
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
    let fresh_entries : Array[ItemEntry[T]] = []
    for i, item in new_items {
      let (dom, dispose) = render_item(item, i)
      let nodes = collect_dom_nodes(dom)
      if nodes.length() == 0 {
        let placeholder = @js_dom.document().createComment("empty")
        parent.insertBefore(placeholder, Some(end_marker)) |> ignore
        fresh_entries.push(
          ItemEntry::new_with_nodes_and_dispose(item, [placeholder], dispose),
        )
      } else {
        parent.insertBefore(dom, Some(end_marker)) |> ignore
        fresh_entries.push(
          ItemEntry::new_with_nodes_and_dispose(item, nodes, dispose),
        )
      }
    }
    return fresh_entries
  }
  let new_entries : Array[ItemEntry[T]] = []
  for _ in 0..=..0 {
    let item = new_items[i]
    let old_index = find_entry_index(index_map, item, used_indices, old_entries)
    if old_index >= 0 {
      // Reuse existing entry (including its dispose function)
      let entry = old_entries[old_index]
      used_indices[old_index] = true
      if entry.nodes.length() > 0 {
        if !next_sibling_is(entry.last, cursor.val) {
          move_nodes_before(parent, entry.nodes, cursor.val)
        }
        cursor.val = entry.dom
      }
      new_entries[i] = entry
    } else {
      // Create new DOM node with dispose
      let (dom, dispose) = render_item(item, i)
      let nodes = collect_dom_nodes(dom)
      if nodes.length() == 0 {
        let placeholder = @js_dom.document().createComment("empty")
        parent.insertBefore(placeholder, Some(cursor.val)) |> ignore
        let placeholder_nodes = [placeholder]
        cursor.val = placeholder
        new_entries[i] = ItemEntry::new_with_nodes_and_dispose(
          item, placeholder_nodes, dispose,
        )
      } else {
        parent.insertBefore(dom, Some(cursor.val)) |> ignore
        cursor.val = nodes[0]
        new_entries[i] = ItemEntry::new_with_nodes_and_dispose(
          item, nodes, dispose,
        )
      }
    }
  }

  // Remove unused DOM nodes and dispose their owner scopes
  for i, entry in old_entries {
    if !used_indices[i] {
      (entry.dispose)()
      safe_remove_nodes(parent, entry.nodes)
    }
  }
  new_entries
}