///|
/// Query Selector Implementation
///
/// CSS selector matching for DOM queries. Parsing and matching are delegated to
/// `mizchi/css/selector`: each candidate element in the queried subtree is
/// projected into a `@selector.Element` (with parent / previous-sibling /
/// sibling-index wiring) and tested with `@selector.matches_selector_list`.
/// This reuses the selector grammar and matcher — selector lists, combinators,
/// attribute operators and structural pseudo-classes — without duplicating it.
///
/// Scoping note: matching is restricted to the queried subtree (the root's
/// ancestors are not consulted) and the context node itself is never returned,
/// per `querySelectorAll` semantics.

///|
/// Query selector — returns the first matching node in document order.
pub fn DomTree::query_selector(
  self : DomTree,
  root : NodeId,
  selector : String,
) -> Result[NodeId?, CoreError] {
  match self.query_selector_all(root, selector) {
    Ok(results) =>
      if results.length() > 0 {
        Ok(Some(results[0]))
      } else {
        Ok(None)
      }
    Err(e) => Err(e)
  }
}

///|
/// Query selector all — returns every matching node in document order.
pub fn DomTree::query_selector_all(
  self : DomTree,
  root : NodeId,
  selector : String,
) -> Result[Array[NodeId], CoreError] {
  let root_id = root.to_int()
  guard self.nodes.get(root_id) is Some(_) else {
    return Err(NodeNotFound(node_id=root))
  }
  let list = match @selector.parse_selector_list_text(selector) {
    Some(list) => list
    None =>
      return Err(InvalidOperation(message="invalid selector: \{selector}"))
  }
  let results : Array[NodeId] = []
  let _ = self.query_match_node(root_id, None, 1, 1, None, true, list, results)
  Ok(results)
}

///|
/// Project the node at `node_id` into a `@selector.Element`, test it against
/// `list` (unless it is the scope root), then recurse into its element
/// children. Returns the built element so the caller can thread it as the
/// parent / previous-sibling of the next node. Non-element nodes are not tested
/// but still propagate scope to their element children.
fn DomTree::query_match_node(
  self : DomTree,
  node_id : Int,
  parent_sel : @selector.Element?,
  sibling_index : Int,
  sibling_count : Int,
  prev_sel : @selector.Element?,
  is_root : Bool,
  list : @selector.SelectorList,
  results : Array[NodeId],
) -> @selector.Element? {
  let node = self.nodes.get(node_id)
  guard node is Some(node) else { return None }
  let my_sel : @selector.Element? = if node.node_type == Element {
    let element = dom_node_to_selector_element(
      node, parent_sel, prev_sel, sibling_index, sibling_count,
    )
    if !is_root && @selector.matches_selector_list(element, list) {
      results.push(NodeId(node_id))
    }
    Some(element)
  } else {
    None
  }
  // Element children inherit this element as their parent; a non-element node
  // (document / shadow root) passes the inherited scope through unchanged.
  let child_parent = match my_sel {
    Some(_) => my_sel
    None => parent_sel
  }
  let element_children : Array[Int] = []
  for child_id in node.children {
    match self.nodes.get(child_id) {
      Some(child) if child.node_type == Element =>
        element_children.push(child_id)
      _ => ()
    }
  }
  let count = element_children.length()
  let mut prev : @selector.Element? = None
  for i, child_id in element_children {
    let child_sel = self.query_match_node(
      child_id,
      child_parent,
      i + 1,
      count,
      prev,
      false,
      list,
      results,
    )
    prev = child_sel
  }
  my_sel
}

///|
/// Build a `@selector.Element` view of a DOM node, wiring the parent and
/// previous-sibling links and the element-based sibling index/count that the
/// selector matcher uses for combinators and structural pseudo-classes.
/// Shared read-only empties for projected `@selector.Element` fields, so each
/// per-query node projection does not allocate fresh empty arrays. The selector
/// matcher only reads these fields; `children` is never populated by the query.
let empty_selector_strings : Array[String] = []

///|
let empty_selector_attrs : Array[@selector.Attribute] = []

///|
let empty_selector_children : Array[@selector.Element] = []

///|
fn dom_node_to_selector_element(
  node : DomNode,
  parent : @selector.Element?,
  prev_sibling : @selector.Element?,
  sibling_index : Int,
  sibling_count : Int,
) -> @selector.Element {
  // Reuse the parsed class list / attribute array across queries; both are
  // invalidated when the node's attributes change.
  let classes : Array[String] = match node.sel_classes {
    Some(cached) => cached
    None => {
      let computed = match node.attributes.get("class") {
        Some(value) => split_whitespace(value)
        None => empty_selector_strings
      }
      node.sel_classes = Some(computed)
      computed
    }
  }
  let attributes : Array[@selector.Attribute] = match node.sel_attrs {
    Some(cached) => cached
    None => {
      let computed : Array[@selector.Attribute] = if node.attributes.is_empty() {
        empty_selector_attrs
      } else {
        let attrs = []
        node.attributes.each(fn(name, value) {
          attrs.push(@selector.Attribute::{ name, value })
        })
        attrs
      }
      node.sel_attrs = Some(computed)
      computed
    }
  }
  // The projection is transient and read-only, so the node's custom_states can
  // be aliased (an empty set is shared) instead of copied.
  let custom_states = if node.custom_states.is_empty() {
    empty_selector_strings
  } else {
    node.custom_states
  }
  {
    tag_name: node.tag_name,
    id: node.attributes.get("id"),
    classes,
    attributes,
    custom_states,
    parent,
    prev_sibling,
    next_sibling: None,
    children: empty_selector_children,
    sibling_index,
    sibling_count,
  }
}

///|
fn is_whitespace_code(code : Int) -> Bool {
  code == 32 || code == 9 || code == 10 || code == 13
}

///|
/// Split a string on ASCII whitespace, dropping empty segments (used for the
/// `class` attribute's token list). Index-scans the input and slices each token
/// instead of materializing a char array (`to_array`) and a `StringBuilder` per
/// token, which dominated allocation in `querySelectorAll` node projection.
fn split_whitespace(s : String) -> Array[String] {
  let n = s.length()
  let tokens : Array[String] = []
  let mut i = 0
  while i < n {
    while i < n && is_whitespace_code(s[i].to_int()) {
      i = i + 1
    }
    let start = i
    while i < n && !is_whitespace_code(s[i].to_int()) {
      i = i + 1
    }
    if i > start {
      tokens.push(s[start:i].to_owned())
    }
  }
  tokens
}