// Main package file for your library code.
// Put public APIs and shared helpers here.
// Keep blocks small and focused for easy refactors.

///|
pub(all) enum Node {
  Element(String, Map[String, String], Array[Node])
  Text(String)
  Empty
  RawHtml(String)
  DynText(() -> String)
  Dynamic(() -> Node)
  WithEvents(Node, Array[(String, (@dom.Event) -> Unit)])
  WithDynAttrs(Node, Array[(String, () -> String?)])
}

///|
pub(open) trait ToArrayNode {
  to_array_node(Self) -> Array[Node]
}

///|
pub impl ToArrayNode for String with to_array_node(self) {
  [Text(self)]
}

///|
pub impl ToArrayNode for Node with to_array_node(self) {
  [self]
}

///|
pub impl ToArrayNode for Array[Node] with to_array_node(self) {
  self
}

///|
pub fn render_to_html(node : Node) -> String {
  let buf = StringBuilder::new()
  render_to_buf(buf, node)
  buf.to_string()
}

///|
fn render_to_buf(buf : StringBuilder, node : Node) -> Unit {
  match node {
    Element(tag, attrs, children) => {
      buf.write_string("<")
      buf.write_string(tag)
      if not(attrs.is_empty()) {
        render_attrs_buf(buf, attrs)
      }
      if is_void_element(tag) {
        buf.write_string("/>")
      } else {
        buf.write_string(">")
        for child in children {
          render_to_buf(buf, child)
        }
        buf.write_string("")
      }
    }
    Text(s) => escape_html_buf(buf, s)
    Empty => ()
    RawHtml(html) => {
      buf.write_string("")
      buf.write_string(html)
      buf.write_string("")
    }
    DynText(f) => escape_html_buf(buf, f())
    Dynamic(f) => {
      buf.write_string("")
      render_to_buf(buf, f())
      buf.write_string("")
    }
    WithEvents(inner, _) => render_to_buf(buf, inner)
    WithDynAttrs(inner, dyn_a) =>
      match inner {
        Element(tag, attrs, children) => {
          let merged = attrs.copy()
          for pair in dyn_a {
            match (pair.1)() {
              Some(v) => merged[pair.0] = v
              None => ()
            }
          }
          render_to_buf(buf, Element(tag, merged, children))
        }
        _ => render_to_buf(buf, inner)
      }
  }
}

///|
fn render_attrs_buf(buf : StringBuilder, attrs : Map[String, String]) -> Unit {
  attrs.each(fn(key, value) {
    buf.write_string(" ")
    buf.write_string(key)
    if value != "" {
      buf.write_string("=\"")
      escape_html_buf(buf, value)
      buf.write_string("\"")
    }
  })
}

///|
extern "js" fn init_escape_html() -> (String) -> String =
  #|() => {
  #|  const map = Object.freeze({"&":"&","<":"<",">":">","\"":""","'":"'"});
  #|  const need = /[&<>"']/;
  #|  const re = /[&<>"']/g;
  #|  return (s) => need.test(s) ? s.replace(re, (c) => map[c]) : s;
  #|}

///|
let escape_html : (String) -> String = init_escape_html()

///|
fn escape_html_buf(buf : StringBuilder, s : String) -> Unit {
  buf.write_string(escape_html(s))
}

///|
fn is_void_element(tag : String) -> Bool {
  match tag {
    "area"
    | "base"
    | "br"
    | "col"
    | "embed"
    | "hr"
    | "img"
    | "input"
    | "link"
    | "meta"
    | "source"
    | "track"
    | "wbr" => true
    _ => false
  }
}

///|
pub fn[C : ToArrayNode] tag(
  tag : String,
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  Element(tag, attrs, children.to_array_node())
}

///|
pub fn[C : ToArrayNode] head(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("head", attrs~, children)
}

///|
pub fn[C : ToArrayNode] body(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("body", attrs~, children)
}

///|
pub fn[C : ToArrayNode] html(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("html", attrs~, children)
}

///|
pub fn[C : ToArrayNode] title(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("title", attrs~, children)
}

///|
pub fn[C : ToArrayNode] meta(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("meta", attrs~, children)
}

///|
pub fn[C : ToArrayNode] div(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("div", attrs~, children)
}

///|
pub fn[C : ToArrayNode] h1(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("h1", attrs~, children)
}

///|
pub fn[C : ToArrayNode] h2(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("h2", attrs~, children)
}

///|
pub fn[C : ToArrayNode] h3(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("h3", attrs~, children)
}

///|
pub fn[C : ToArrayNode] h4(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("h4", attrs~, children)
}

///|
pub fn[C : ToArrayNode] h5(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("h5", attrs~, children)
}

///|
pub fn[C : ToArrayNode] p(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("p", attrs~, children)
}

///|
pub fn[C : ToArrayNode] a(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("a", attrs~, children)
}

///|
pub fn[C : ToArrayNode] ul(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("ul", attrs~, children)
}

///|
pub fn[C : ToArrayNode] ol(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("ol", attrs~, children)
}

///|
pub fn[C : ToArrayNode] li(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("li", attrs~, children)
}

///|
pub fn[C : ToArrayNode] form(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("form", attrs~, children)
}

///|
pub fn input(attrs? : Map[String, String] = {}) -> Node {
  tag("input", attrs~, "")
}

///|
pub fn[C : ToArrayNode] button(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("button", attrs~, children)
}

///|
pub fn[C : ToArrayNode] label(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("label", attrs~, children)
}

///|
pub fn[C : ToArrayNode] select(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("select", attrs~, children)
}

///|
pub fn[C : ToArrayNode] option(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("option", attrs~, children)
}

///|
pub fn[C : ToArrayNode] span(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("span", attrs~, children)
}

///|
pub fn hr(attrs? : Map[String, String] = {}) -> Node {
  tag("hr", attrs~, "")
}

///|
pub fn br(attrs? : Map[String, String] = {}) -> Node {
  tag("br", attrs~, "")
}

///|
pub fn img(attrs? : Map[String, String] = {}) -> Node {
  tag("img", attrs~, "")
}

///|
pub fn[C : ToArrayNode] nav(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("nav", attrs~, children)
}

///|
pub fn[C : ToArrayNode] header(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("header", attrs~, children)
}

///|
pub fn[C : ToArrayNode] footer(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("footer", attrs~, children)
}

///|
pub fn[C : ToArrayNode] main_tag(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("main", attrs~, children)
}

///|
pub fn[C : ToArrayNode] section(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("section", attrs~, children)
}

///|
pub fn[C : ToArrayNode] article(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("article", attrs~, children)
}

///|
pub fn[C : ToArrayNode] code(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("code", attrs~, children)
}

///|
pub fn[C : ToArrayNode] textarea(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("textarea", attrs~, children)
}

///|
pub fn[C : ToArrayNode] pre(
  attrs? : Map[String, String] = {},
  children : C,
) -> Node {
  tag("pre", attrs~, children)
}

///|
pub fn dyn_text(f : () -> String) -> Node {
  DynText(f)
}

///|
pub fn dyn(f : () -> Node) -> Node {
  Dynamic(f)
}

///|
pub fn raw_html(s : String) -> Node {
  RawHtml(s)
}

///|
pub fn on(node : Node, event : String, handler : (@dom.Event) -> Unit) -> Node {
  WithEvents(node, [(event, handler)])
}

///|
pub fn on_click(node : Node, handler : (@dom.Event) -> Unit) -> Node {
  on(node, "click", handler)
}

///|
pub fn on_input(node : Node, handler : (@dom.Event) -> Unit) -> Node {
  on(node, "input", handler)
}

///|
pub fn on_submit(node : Node, handler : (@dom.Event) -> Unit) -> Node {
  on(node, "submit", handler)
}

///|
pub fn dyn_attr(node : Node, name : String, f : () -> String) -> Node {
  WithDynAttrs(node, [(name, fn() { Some(f()) })])
}

///|
pub fn hydrate_dom(parent : @dom.Element, node : Node) -> Unit {
  let _ = hydrate_node(parent, node, 0)
}

///|
/// childNodes[index] から始まる Node を hydrate し、次の index を返す。
fn hydrate_node(parent : @dom.Element, node : Node, index : Int) -> Int {
  match node {
    Element(_, _, children) =>
      match @dom.nth_child_element(parent, index) {
        Some(el) => {
          let mut ci = 0
          for child in children {
            ci = hydrate_node(el, child, ci)
          }
          index + 1
        }
        None => index + 1
      }
    Text(_) => index + 1
    Empty => index
    RawHtml(_) => index + 1 //  から始まる 1 ノード
    DynText(f) =>
      match @dom.nth_child_text(parent, index) {
        Some(tn) => {
          let _ = @signals.render_effect(fn() { @dom.set_node_text(tn, f()) })
          index + 1
        }
        None => index + 1
      }
    Dynamic(f) => {
      // childNodes[index]   =  (comment)
      // childNodes[index+1] =          (content wrapper)
      match @dom.nth_child_element(parent, index + 1) {
        Some(span) => {
          let _ = @signals.render_effect(fn() {
            @dom.set_inner_html(span, "")
            mount_dom(span, f())
          })
        }
        None => ()
      }
      index + 2
    }
    WithEvents(inner, events) => {
      let next = hydrate_node(parent, inner, index)
      match @dom.nth_child_element(parent, index) {
        Some(el) =>
          for pair in events {
            @dom.add_event_listener(el, pair.0, pair.1)
          }
        None => ()
      }
      next
    }
    WithDynAttrs(inner, dyn_attrs) => {
      let next = hydrate_node(parent, inner, index)
      match @dom.nth_child_element(parent, index) {
        Some(el) =>
          for pair in dyn_attrs {
            let _ = @signals.render_effect(fn() {
              match (pair.1)() {
                Some(v) => @dom.set_attribute(el, pair.0, v)
                None => @dom.remove_attribute(el, pair.0)
              }
            })
          }
        None => ()
      }
      next
    }
  }
}

///|
pub fn dyn_bool_attr(node : Node, name : String, f : () -> Bool) -> Node {
  WithDynAttrs(node, [(name, fn() { if f() { Some("") } else { None } })])
}

///|
pub fn mount_dom(parent : @dom.Element, node : Node) -> Unit {
  match node {
    Element(tag, attrs, children) => {
      let el = @dom.create_element(tag)
      attrs.each(fn(k, v) { @dom.set_attribute(el, k, v) })
      for child in children {
        mount_dom(el, child)
      }
      @dom.append_child(parent, el)
    }
    Text(s) => @dom.append_child_node(parent, @dom.create_text_node(s))
    RawHtml(html) => {
      let wrapper = @dom.create_element("span")
      @dom.set_inner_html(wrapper, html)
      @dom.append_child(parent, wrapper)
    }
    Empty => ()
    DynText(f) => {
      let t = @dom.create_text_node(f())
      @dom.append_child_node(parent, t)
      let _ = @signals.render_effect(fn() { @dom.set_node_text(t, f()) })
    }
    Dynamic(f) => {
      let marker = @dom.create_comment("dynamic")
      @dom.append_child_comment(parent, marker)
      let mut current : @dom.Element? = None
      let _ = @signals.render_effect(fn() {
        match current {
          Some(el) => {
            @dom.remove_child(parent, el)
            current = None
          }
          None => ()
        }
        let wrapper = @dom.create_element("span")
        mount_dom(wrapper, f())
        @dom.insert_after(parent, wrapper, marker)
        current = Some(wrapper)
      })
    }
    WithEvents(inner, events) => {
      mount_dom(parent, inner)
      let el = @dom.last_child(parent)
      for pair in events {
        @dom.add_event_listener(el, pair.0, pair.1)
      }
    }
    WithDynAttrs(inner, dyn_attrs) => {
      mount_dom(parent, inner)
      let el = @dom.last_child(parent)
      for pair in dyn_attrs {
        let _ = @signals.render_effect(fn() {
          match (pair.1)() {
            Some(v) => @dom.set_attribute(el, pair.0, v)
            None => @dom.remove_attribute(el, pair.0)
          }
        })
      }
    }
  }
}