///|
/// Shadow-scoped selector matching (#286, Step 5).
///
/// The shared `mizchi/css/selector` matcher deliberately returns `false` for
/// the shadow-scoping pseudo-classes/elements (`:host`, `:host()`,
/// `:host-context()`, `::slotted()`) because its `Element` view has no shadow
/// tree. Those pseudos can only be resolved against the live DOM, so they are
/// implemented here: the complex selector is walked over the real node tree and
/// each compound is evaluated either against the shadow relationships (host /
/// slot assignment) or, for ordinary compounds, delegated back to
/// `@selector.matches_complex`.
///
/// This is the matching primitive the shadow-tree style cascade uses (#285) and
/// the shadow-scoped `query_selector` (`shadow_query.mbt`, #286) uses for
/// shadow-root queries. A shadow-root query only considers the shadow tree's own
/// descendants, so a bare `:host` selector still matches nothing (the host is
/// not a descendant, matching browser `querySelector` semantics), while
/// `:host(...) x` / `:host-context(...) x` combinators resolve through the host.

///|
/// Match `subject` against `selector` as authored inside the shadow tree whose
/// host is `host`. Returns whether the subject is selected. An unparseable
/// selector yields `InvalidOperation`.
pub fn DomTree::matches_shadow_scoped(
  self : DomTree,
  subject : NodeId,
  selector : String,
  host : NodeId,
) -> Result[Bool, CoreError] {
  guard self.nodes.get(subject.to_int()) is Some(_) else {
    return Err(NodeNotFound(node_id=subject))
  }
  guard self.nodes.get(host.to_int()) is Some(_) else {
    return Err(NodeNotFound(node_id=host))
  }
  let list = match @selector.parse_selector_list_text(selector) {
    Some(list) => list
    None =>
      return Err(InvalidOperation(message="invalid selector: \{selector}"))
  }
  Ok(self.matches_shadow_scoped_list(subject, list, host))
}

///|
/// Match `subject` against an already-parsed selector `list` in the shadow scope
/// whose host is `host`. Shared by the string entry point and the shadow-scoped
/// `query_selector` walk so a query parses the selector once, not per candidate.
fn DomTree::matches_shadow_scoped_list(
  self : DomTree,
  subject : NodeId,
  list : @selector.SelectorList,
  host : NodeId,
) -> Bool {
  for cs in list.selectors {
    if self.complex_matches_scoped(subject, cs, host) {
      return true
    }
  }
  false
}

///|
/// Match a complex selector with `subject` as its rightmost (head) compound,
/// walking the tail leftward/upward over the live tree.
fn DomTree::complex_matches_scoped(
  self : DomTree,
  subject : NodeId,
  cs : @selector.ComplexSelector,
  host : NodeId,
) -> Bool {
  if !self.compound_matches_scoped(subject, cs.head, host) {
    return false
  }
  let mut current = subject
  for step in cs.tail {
    match self.find_step_match(current, step, host) {
      Some(next) => current = next
      None => return false
    }
  }
  true
}

///|
/// Resolve one combinator step, returning the matched relative node if any.
/// Shadow-scoping compounds (`:host` …) can cross the shadow-root boundary to
/// the host; ordinary compounds stay within the shadow tree.
fn DomTree::find_step_match(
  self : DomTree,
  current : NodeId,
  step : @selector.ComplexSelectorStep,
  host : NodeId,
) -> NodeId? {
  let compound = step.selector
  let host_step = is_host_compound(compound)
  match step.combinator {
    @selector.Combinator::Child =>
      match self.light_parent_id(current) {
        Some(p) =>
          if self.is_shadow_root(p) {
            self.host_boundary_match(host_step, compound, host)
          } else if self.compound_matches_scoped(p, compound, host) {
            Some(p)
          } else {
            None
          }
        None => None
      }
    @selector.Combinator::Descendant =>
      // Walk light ancestors within the shadow tree; the chain stops at the
      // shadow root (which has no light parent), then `:host` can cross it.
      match
        first_in_chain(
          self.light_parent_id(current),
          fn(n) { self.light_parent_id(n) },
          fn(a) {
            !self.is_shadow_root(a) &&
            self.compound_matches_scoped(a, compound, host)
          },
        ) {
        Some(a) => Some(a)
        None => self.host_boundary_match(host_step, compound, host)
      }
    @selector.Combinator::NextSibling =>
      match self.prev_element_sibling(current) {
        Some(s) =>
          if self.compound_matches_scoped(s, compound, host) {
            Some(s)
          } else {
            None
          }
        None => None
      }
    @selector.Combinator::SubsequentSibling =>
      first_in_chain(
        self.prev_element_sibling(current),
        fn(s) { self.prev_element_sibling(s) },
        fn(s) { self.compound_matches_scoped(s, compound, host) },
      )
  }
}

///|
/// Cross the shadow-root boundary: a `:host` family step matched against an
/// element whose parent is the shadow root resolves to the host itself.
fn DomTree::host_boundary_match(
  self : DomTree,
  host_step : Bool,
  compound : @selector.CompoundSelector,
  host : NodeId,
) -> NodeId? {
  if host_step && self.compound_matches_scoped(host, compound, host) {
    Some(host)
  } else {
    None
  }
}

///|
/// Return the first node reachable from `start` by repeatedly applying `next`
/// that satisfies `pred`, or `None` when the chain is exhausted.
fn first_in_chain(
  start : NodeId?,
  next : (NodeId) -> NodeId?,
  pred : (NodeId) -> Bool,
) -> NodeId? {
  for cur = start {
    match cur {
      Some(n) => if pred(n) { break Some(n) } else { continue next(n) }
      None => break None
    }
  }
}

///|
/// Match a single compound against `id`, intercepting the shadow-scoping
/// pseudos and delegating everything else to the shared matcher.
fn DomTree::compound_matches_scoped(
  self : DomTree,
  id : NodeId,
  compound : @selector.CompoundSelector,
  host : NodeId,
) -> Bool {
  match slotted_arg_of(compound) {
    Some(args) => return self.matches_slotted(id, args, host)
    None => ()
  }
  match host_pseudo_of(compound) {
    Some(pc) => return self.matches_host_pseudo(id, pc, host)
    None => ()
  }
  self.compound_matches_plain(id, compound)
}

///|
/// Ordinary compound match: project the node into a `@selector.Element` (with
/// element-based sibling index/count for structural pseudos) and delegate.
fn DomTree::compound_matches_plain(
  self : DomTree,
  id : NodeId,
  compound : @selector.CompoundSelector,
) -> Bool {
  match self.nodes.get(id.to_int()) {
    Some(node) if node.node_type == Element => {
      let (index, count) = self.element_sibling_position(id.to_int())
      let element = dom_node_to_selector_element(node, None, None, index, count)
      @selector.matches_complex(
        element,
        @selector.ComplexSelector::simple(compound),
      )
    }
    _ => false
  }
}

///|
/// `:host`, `:host()`, `:host-context()`: the subject must
/// be the host, with the functional argument tested against the host (and, for
/// `:host-context`, its light-tree ancestors).
fn DomTree::matches_host_pseudo(
  self : DomTree,
  id : NodeId,
  pc : @selector.PseudoClass,
  host : NodeId,
) -> Bool {
  if id.to_int() != host.to_int() {
    return false
  }
  match pc {
    @selector.PseudoClass::Host => true
    @selector.PseudoClass::HostFunc(c) => self.compound_matches_plain(host, c)
    @selector.PseudoClass::HostContextFunc(c) =>
      first_in_chain(Some(host), fn(n) { self.light_parent_id(n) }, fn(n) {
        self.compound_matches_plain(n, c)
      })
      is Some(_)
    _ => false
  }
}

///|
/// `::slotted()`: the subject must be a light node assigned to a slot
/// in `host`'s shadow tree and match the compound argument.
fn DomTree::matches_slotted(
  self : DomTree,
  id : NodeId,
  args : Array[@selector.CompoundSelector],
  host : NodeId,
) -> Bool {
  match self.get_assigned_slot_for_light_node(host, id) {
    Some(_) =>
      match args {
        [] => true
        [first, ..] => self.compound_matches_plain(id, first)
      }
    None => false
  }
}

///|
/// The host pseudo-class carried by a compound, if any.
fn host_pseudo_of(
  compound : @selector.CompoundSelector,
) -> @selector.PseudoClass? {
  for sub in compound.subclasses {
    match sub {
      @selector.SimpleSelector::PseudoClass(pc) =>
        match pc {
          @selector.PseudoClass::Host
          | @selector.PseudoClass::HostFunc(_)
          | @selector.PseudoClass::HostContextFunc(_) => return Some(pc)
          _ => ()
        }
      _ => ()
    }
  }
  None
}

///|
fn is_host_compound(compound : @selector.CompoundSelector) -> Bool {
  host_pseudo_of(compound) is Some(_)
}

///|
/// The `::slotted()` argument compounds carried by a compound, if any.
fn slotted_arg_of(
  compound : @selector.CompoundSelector,
) -> Array[@selector.CompoundSelector]? {
  for sub in compound.subclasses {
    match sub {
      @selector.SimpleSelector::PseudoElement(
        @selector.PseudoElement::Slotted(args)
      ) => return Some(args)
      _ => ()
    }
  }
  None
}

///|
fn DomTree::light_parent_id(self : DomTree, id : NodeId) -> NodeId? {
  match self.get_parent(id) {
    Ok(parent) => parent
    Err(_) => None
  }
}

///|
fn DomTree::is_shadow_root(self : DomTree, id : NodeId) -> Bool {
  match self.nodes.get(id.to_int()) {
    Some(node) => node.node_type == ShadowRoot
    None => false
  }
}

///|
/// The previous element sibling of `id` (skipping text/comment nodes).
fn DomTree::prev_element_sibling(self : DomTree, id : NodeId) -> NodeId? {
  guard self.nodes.get(id.to_int()) is Some(node) else { return None }
  guard node.parent_id is Some(pid) else { return None }
  guard self.nodes.get(pid) is Some(parent) else { return None }
  let mut prev : NodeId? = None
  for child_id in parent.children {
    if child_id == id.to_int() {
      break
    }
    if self.nodes.get(child_id) is Some(child) && child.node_type == Element {
      prev = Some(NodeId::from_int(child_id))
    }
  }
  prev
}

///|
/// 1-based index of `node_id` among its element siblings and the element
/// sibling count, defaulting to `(1, 1)` for a root / parentless node.
fn DomTree::element_sibling_position(
  self : DomTree,
  node_id : Int,
) -> (Int, Int) {
  guard self.nodes.get(node_id) is Some(node) else { return (1, 1) }
  guard node.parent_id is Some(pid) else { return (1, 1) }
  guard self.nodes.get(pid) is Some(parent) else { return (1, 1) }
  let mut index = 0
  let mut count = 0
  for child_id in parent.children {
    if self.nodes.get(child_id) is Some(child) && child.node_type == Element {
      count = count + 1
      if child_id == node_id {
        index = count
      }
    }
  }
  if index == 0 {
    (1, 1)
  } else {
    (index, count)
  }
}