///|
#cfg(target="js")
struct VDom {
  mut inode : INode
  target_element : @dom.Element?
  captured_link_listener : @dom.Listener
}

///|
#cfg(not(target="js"))
pub struct VDom {
  mut vnode : VNode
}

///|
#cfg(target="js")
priv struct IProps {
  props : Props
  slots : Map[String, Ref[(Event, &Scheduler) -> Unit]]
}

///|
#cfg(target="js")
priv enum INode {
  Elem(String, IProps, Children[INode], namespace_uri~ : String?, @dom.Element)
  Text(String, @dom.Element)
  Frag(Array[INode], @dom.Comment, @dom.Comment)
  Thunk(Int, INode)
}

///|
#cfg(target="js")
fn INode::start(self : Self) -> @dom.Node {
  match self {
    Elem(_, _, _, element, ..) | Text(_, element) => element.as_node()
    Thunk(_, node) => node.start()
    Frag(_, start, _) => start.as_node()
  }
}

///|
#cfg(target="js")
fn INode::end(self : Self) -> @dom.Node {
  match self {
    Elem(_, _, _, element, ..) | Text(_, element) => element.as_node()
    Thunk(_, node) => node.end()
    Frag(_, _, end) => end.as_node()
  }
}

///|
#cfg(target="js")
fn INode::needs_relocation(
  self : Self,
  anchor : @js.Nullable[@dom.Node],
) -> Bool {
  match (self.end().get_next_sibling().to_option(), anchor.to_option()) {
    (None, None) => false
    (Some(actual), Some(expected)) => !actual.is_same_node(expected)
    _ => true
  }
}

///|
#cfg(target="js")
fn INode::remove(self : Self, parent : @dom.Node) -> Unit {
  match self {
    Elem(_, _, _, element, ..) | Text(_, element) =>
      parent.remove_child(element.as_node())
    Thunk(_, node) => node.remove(parent)
    Frag(_, start, end) => {
      let start = start.as_node()
      let end = end.as_node()
      while start.get_next_sibling().to_option() is Some(node) &&
            !node.is_same_node(end) {
        parent.remove_child(node)
      }
      parent.remove_child(start)
      parent.remove_child(end)
    }
  }
}

///|
#cfg(target="js")
fn INode::relocate(
  self : Self,
  parent : @dom.Node,
  before : @js.Nullable[@dom.Node],
) -> Unit {
  guard self.needs_relocation(before) else { return }
  match self {
    Elem(_, _, _, element, ..) | Text(_, element) =>
      parent.insert_before(element.as_node(), before)
    Thunk(_, node) => node.relocate(parent, before)
    Frag(_, start, end) => {
      {
        guard! start.get_parent_node().to_option() is Some(a)
        guard! end.get_parent_node().to_option() is Some(b)
        guard! a.is_same_node(b)
      }
      let start = start.as_node()
      let end = end.as_node()
      let mut anchor = before
      while end.get_previous_sibling().to_option() is Some(node) &&
            !node.is_same_node(start) {
        parent.insert_before(node, anchor)
        anchor = nullable(node)
      }
      // For an empty fragment, anchor equals before: keep start before end.
      parent.insert_before(start, anchor)
      parent.insert_before(end, before)
    }
  }
}

///|
#cfg(target="js")
fn insert_props(
  element : @dom.Element,
  tag : String,
  props : Props,
  scheduler : &Scheduler,
  captured_link_listener : @dom.Listener,
) -> IProps {
  let { handlers, attrs, props: properties, styles, } = props
  let slots : Map[String, Ref[(Event, &Scheduler) -> Unit]] = Map([])
  for event, handler in handlers {
    let slot = @ref.new(handler)
    element.add_event_listener(event, event => (slot.val)(dom_event(event), scheduler))
    slots[event] = slot
  }
  if is_captured_link_tag(tag) {
    element.add_event_listener("click", captured_link_listener)
  }
  for name, value in attrs {
    element.set_attribute(name, value)
  }
  for name, value in properties {
    element.set_property(name, variant_to_js_value(value))
  }
  let sheet = if element.to_html_element() is Some(html) {
    html.get_style()
  } else if element.to_svg_element() is Some(svg) {
    svg.get_style()
  } else {
    panic()
  }
  for name, value in styles {
    // 类型化样式 → CSSOM。camelCase 键要转成 CSS 语法,长度要补 px。
    sheet.set_property(@style.css_property_name(name), value.to_css())
  }
  { props, slots, }
}

///|
#cfg(target="js")
fn reapply_child_sensitive_properties(
  element : @dom.Element,
  tag : String,
  props : Props,
) -> Unit {
  // Adding