// DOM Rendering - Fine-grained reactive DOM bindings
//

///|
/// Create a reactive text node that updates when the content changes
pub fn text_node(content : () -> String) -> DomNode {
  let doc = @js_dom.document()
  let initial = content()
  let node = doc.createTextNode(initial)

  // RenderEffect for DOM updates (synchronous)
  let _ = @resource.render_effect(fn() {
    let new_content = content()
    node.as_node().setTextContent(new_content)
  })
  Txt(node)
}

///|
/// Create a reactive text node from a signal
pub fn[T : Show] text_from_signal(sig : @resource.Signal[T]) -> DomNode {
  text_node(fn() { sig.get().to_string() })
}

///|
/// Reactive attribute value type
pub(all) enum AttrValue {
  Static(String)
  Dynamic(() -> String)
  Handler((@js.Any) -> Unit)
}

///|
/// SVG namespace constant
pub let svg_ns : String = "http://www.w3.org/2000/svg"

///|
/// MathML namespace constant
pub let mathml_ns : String = "http://www.w3.org/1998/Math/MathML"

///|
/// FFI for createElementNS
extern "js" fn create_element_ns_ffi(
  ns : String,
  tag : String,
) -> @js_dom.Element =
  #| (ns, tag) => document.createElementNS(ns, tag)

///|
/// Create an element with namespace (for SVG, MathML, etc.)
/// Use this for SVG elements since they require the SVG namespace.
///
/// Example:
/// ```moonbit nocheck
/// // Create an SVG rectangle
/// let rect = create_element_ns(
///   svg_ns,
///   "rect",
///   [
///     ("x", Static("10")),
///     ("y", Static("10")),
///     ("width", Static("100")),
///     ("height", Static("50")),
///     ("fill", Static("blue")),
///   ],
///   [],
/// )
/// ```
pub fn create_element_ns(
  ns : String,
  tag : String,
  attrs : Array[(String, AttrValue)],
  children : Array[DomNode],
) -> DomNode {
  let elem = create_element_ns_ffi(ns, tag)

  // Apply attributes
  for attr in attrs {
    let (name, value) = attr
    apply_attribute(elem, name, value)
  }

  // Append children
  for child in children {
    elem.as_node().appendChild(child.to_dom()) |> ignore
  }
  El(DomElement::from_dom(elem))
}

///|
/// Create an element with reactive attributes (returns Node for easy composition)
pub fn create_element(
  tag : String,
  attrs : Array[(String, AttrValue)],
  children : Array[DomNode],
) -> DomNode {
  let doc = @js_dom.document()
  let elem = doc.createElement(tag)

  // Apply attributes
  for attr in attrs {
    let (name, value) = attr
    apply_attribute(elem, name, value)
  }

  // Append children
  for child in children {
    elem.as_node().appendChild(child.to_dom()) |> ignore
  }
  El(DomElement::from_dom(elem))
}

///|
/// Apply a single attribute to an element
fn apply_attribute(
  elem : @js_dom.Element,
  name : String,
  value : AttrValue,
) -> Unit {
  match value {
    Static(s) =>
      if name == "style" {
        apply_style_string(elem, s)
      } else {
        apply_static_attr(elem, name, s)
      }
    Dynamic(getter) => {
      // RenderEffect for DOM updates (synchronous)
      let _ = @resource.render_effect(fn() {
        let new_value = getter()
        if name == "style" {
          apply_style_string(elem, new_value)
        } else {
          apply_static_attr(elem, name, new_value)
        }
      })
    }
    Handler(handler) =>
      if name == "__ref" {
        // Call ref callback with element (not an event listener)
        handler(elem.as_any())
      } else {
        apply_event_handler(elem, name, handler)
      }
  }
}

///|
/// Apply a static attribute value
fn apply_static_attr(
  elem : @js_dom.Element,
  name : String,
  value : String,
) -> Unit {
  if name == "className" || name == "class" {
    elem.setClassName(value)
  } else if name == "__innerHTML" {
    // dangerouslySetInnerHTML - set innerHTML as property
    elem.as_any()._set("innerHTML", @js.any(value)) |> ignore
  } else if name == "value" {
    // Special handling for input value
    elem.as_any()._set("value", @js.any(value)) |> ignore
  } else if name == "checked" {
    elem.as_any()._set("checked", @js.any(value == "true" || value == ""))
    |> ignore
  } else if name == "disabled" {
    if value == "true" || value == "" {
      elem.setAttribute("disabled", "")
    } else {
      elem.removeAttribute("disabled")
    }
  } else {
    elem.setAttribute(name, value)
  }
}

///|
/// Apply an event handler
/// Event names are already lowercase (click, input, etc.) - no conversion needed
extern "js" fn apply_event_handler(
  elem : @js_dom.Element,
  name : String,
  handler : (@js.Any) -> Unit,
) -> Unit =
  #|(elem, name, handler) => elem.addEventListener(name, handler)

///|
/// Apply style string (e.g. "color: red; margin: 10px")
fn apply_style_string(elem : @js_dom.Element, style : String) -> Unit {
  elem.setAttribute("style", style)
}

///|
/// Mount a node to a container
pub fn mount(container : DomElement, n : DomNode) -> Unit {
  container.to_dom().as_node().appendChild(n.to_dom()) |> ignore
}

///|
/// Mount to a jsdom container (for tests)
pub fn mount_to(container : @js_dom.Element, n : DomNode) -> Unit {
  container.as_node().appendChild(n.to_dom()) |> ignore
}

///|
/// Clear a container
pub fn clear(container : DomElement) -> Unit {
  container.to_dom().as_node().setTextContent("")
}

///|
/// Clear a jsdom container (for tests)
pub fn clear_jsdom(container : @js_dom.Element) -> Unit {
  container.as_node().setTextContent("")
}

///|
/// Render to a container (clear and mount)
pub fn render(container : DomElement, n : DomNode) -> Unit {
  clear(container)
  mount(container, n)
}

///|
/// Render to a jsdom container (for tests)
pub fn render_to(container : @js_dom.Element, n : DomNode) -> Unit {
  clear_jsdom(container)
  mount_to(container, n)
}

///|
/// Helper to collect child nodes from a DomNode.
/// If it's a DocumentFragment, collects all children; otherwise returns single node.
fn collect_child_nodes(node : @js_dom.Node) -> Array[@js_dom.Node] {
  // Check if node is a DocumentFragment by nodeType (11 = DocumentFragment)
  if node.nodeType() == 11 {
    // DocumentFragment: collect all children before they are moved
    let children : Array[@js_dom.Node] = []
    while node.firstChild() is Some(child) {
      children.push(child)
      node.removeChild(child) |> ignore
    }
    children
  } else {
    [node]
  }
}

///|
/// Collect DOM nodes without detaching them.
/// For DocumentFragment, returns its children (before they get moved).
/// For regular nodes, returns the node itself in an array.
fn collect_dom_nodes(node : @js_dom.Node) -> Array[@js_dom.Node] {
  if node.nodeType() == 11 {
    let children : Array[@js_dom.Node] = []
    let child_nodes = node.childNodes()
    for i in 0..