///|
enum VNode[Msg] {
  Text(String)
  Element(String, Array[Attr[Msg]], Array[VNode[Msg]])
  Component(ComponentNode)
  Keyed(String, VNode[Msg])
  Lazy(Int, () -> VNode[Msg])
  Fragment(Array[VNode[Msg]])
  Null
}

///|
pub fn[A, B] VNode::map(self : VNode[A], f : (A) -> B) -> VNode[B] {
  match self {
    Text(s) => Text(s)
    Element(tag, attrs, children) => {
      let new_attrs = attrs.map(fn(a) { a.map(f) })
      let new_children = children.map(fn(c) { c.map(f) })
      Element(tag, new_attrs, new_children)
    }
    Component(comp) => Component(comp)
    Keyed(k, child) => Keyed(k, child.map(f))
    Lazy(hash, thunk) => Lazy(hash, fn() { thunk().map(f) })
    Fragment(children) => Fragment(children.map(fn(c) { c.map(f) }))
    Null => Null
  }
}

///|
/// A VNode that renders nothing. Useful for conditional rendering where a branch
/// should produce no output, e.g. `if show { div([], [text("hi")]) } else { null() }`.
pub fn[Msg] null() -> VNode[Msg] {
  Null
}

///|
pub fn[S : Show, Msg] text(s : S) -> VNode[Msg] {
  Text(s.to_string())
}

///|
/// Convert a typed VNode tree to VNode[Unit] by binding all Event handlers
/// through a dispatch function, turning them into BoundEvent closures.
fn[CMsg] bind_events(
  vnode : VNode[CMsg],
  dispatch : (CMsg) -> Unit,
) -> VNode[Unit] {
  match vnode {
    Text(s) => Text(s)
    Element(tag, attrs, children) => {
      let new_attrs = attrs.map(fn(a) { bind_attr(a, dispatch) })
      let new_children = children.map(fn(c) { bind_events(c, dispatch) })
      Element(tag, new_attrs, new_children)
    }
    Component(comp) => Component(comp)
    Keyed(k, child) => Keyed(k, bind_events(child, dispatch))
    Lazy(hash, thunk) => Lazy(hash, fn() { bind_events(thunk(), dispatch) })
    Fragment(children) =>
      Fragment(children.map(fn(c) { bind_events(c, dispatch) }))
    Null => Null
  }
}

///|
pub fn[Msg] keyed(key : String, child : VNode[Msg]) -> VNode[Msg] {
  Keyed(key, child)
}

///|
/// Wrap an array of (key, vnode) pairs into Keyed vnodes for use as
/// children of any container element: `ul([], keyed_list(items))`
pub fn[Msg] keyed_list(
  items : Array[(String, VNode[Msg])],
) -> Array[VNode[Msg]] {
  items.map(fn(pair) {
    let (k, v) = pair
    Keyed(k, v)
  })
}

///|
/// Skip diffing when `hash` matches the previous render. The `thunk` is only
/// called when the hash changes.
pub fn[Msg] lazy_(hash : Int, thunk : () -> VNode[Msg]) -> VNode[Msg] {
  Lazy(hash, thunk)
}

///|
#cfg(target="js")
extern "js" fn try_render(
  on_ok : () -> Unit,
  on_err : (String) -> Unit,
) -> Unit = "(ok, err) => { try { ok(); } catch(e) { err(String(e)); } }"

///|
#cfg(target="wasm-gc")
fn try_render(on_ok : () -> Unit, _on_err : (String) -> Unit) -> Unit {
  on_ok()
}

///|
/// Wrap a view thunk in an error boundary. If `child` panics, `fallback`
/// receives the error message and renders a replacement VNode.
/// On wasm-gc, only `raise` errors are recoverable; `abort` traps the module.
pub fn[Msg] error_boundary(
  fallback~ : (String) -> VNode[Msg],
  child~ : () -> VNode[Msg],
) -> VNode[Msg] {
  let result : Ref[VNode[Msg]] = Ref::new(Null)
  try_render(fn() { result.val = child() }, fn(msg) {
    result.val = fallback(msg)
  })
  result.val
}

///|
/// Group multiple VNodes without a wrapper element.
pub fn[Msg] fragment(children : Array[VNode[Msg]]) -> VNode[Msg] {
  Fragment(children)
}

///|
/// Flatten Fragment vnodes into a flat array. Returns the original array
/// unchanged (no allocation) when no Fragments are present.
fn[Msg] flatten_children(children : Array[VNode[Msg]]) -> Array[VNode[Msg]] {
  for i, child in children {
    match child {
      Fragment(inner) => {
        // First fragment found — allocate and copy prefix, then flatten the rest
        let result : Array[VNode[Msg]] = Array::new(capacity=children.length())
        for j = 0; j < i; j = j + 1 {
          result.push(children[j])
        }
        flatten_into(inner, result)
        for j = i + 1; j < children.length(); j = j + 1 {
          match children[j] {
            Fragment(inner2) => flatten_into(inner2, result)
            Text(_)
            | Element(_, _, _)
            | Component(_)
            | Keyed(_, _)
            | Lazy(_, _)
            | Null as other => result.push(other)
          }
        }
        return result
      }
      Text(_)
      | Element(_, _, _)
      | Component(_)
      | Keyed(_, _)
      | Lazy(_, _)
      | Null => ()
    }
  }
  children
}

///|
fn[Msg] flatten_into(
  children : Array[VNode[Msg]],
  result : Array[VNode[Msg]],
) -> Unit {
  for child in children {
    match child {
      Fragment(inner) => flatten_into(inner, result)
      Text(_)
      | Element(_, _, _)
      | Component(_)
      | Keyed(_, _)
      | Lazy(_, _)
      | Null => result.push(child)
    }
  }
}

///|
pub fn[Msg] el(
  tag : String,
  attrs : Array[Attr[Msg]],
  children : Array[VNode[Msg]],
) -> VNode[Msg] {
  Element(tag, attrs, children)
}