///|
fn functional_pseudo_parts(name : StringView) -> (StringView, StringView)? {
  match name.split_once("(") {
    Some((pseudo_name, rest)) => {
      let mut depth = 1
      let mut pos = 0
      while pos < rest.length() {
        guard rest.get_char(pos) is Some(ch) else { return None }
        if ch == '(' {
          depth += 1
        } else if ch == ')' {
          depth -= 1
          if depth == 0 {
            if pos + ch.utf16_len() != rest.length() {
              return None
            }
            return Some((pseudo_name, rest[0:pos]))
          }
        }
        pos += ch.utf16_len()
      }
      None
    }
    None => None
  }
}

///|
fn pseudo_selector_matches(node : @dom.Node, name : StringView) -> Bool {
  let lower_name = @syn.lower_ascii(name)
  match lower_name[:] {
    "comment" => node.kind == Comment
    "first-child" => is_first_element_child(node)
    "last-child" => is_last_element_child(node)
    "only-child" => is_only_element_child(node)
    "empty" => is_empty_for_selector(node)
    "root" => is_root_for_selector(node)
    "first-of-type" => is_first_of_type(node)
    "last-of-type" => is_last_of_type(node)
    "only-of-type" => is_only_of_type(node)
    _ =>
      match functional_pseudo_parts(name) {
        Some((pseudo_name, arg)) =>
          match @syn.lower_ascii(pseudo_name)[:] {
            "nth-child" => is_nth_child(node, arg)
            "nth-of-type" => is_nth_of_type(node, arg)
            "not" => !matches_selector_list(node, arg)
            "contains" => contains_text(node, arg)
            "comment" => node.kind == Comment
            _ => false
          }
        _ => false
      }
  }
}

///|
fn pseudo_selector_is_valid(name : StringView) -> Bool {
  let lower_name = @syn.lower_ascii(name)
  match lower_name[:] {
    "comment"
    | "first-child"
    | "last-child"
    | "only-child"
    | "empty"
    | "root"
    | "first-of-type"
    | "last-of-type"
    | "only-of-type" => true
    _ =>
      match functional_pseudo_parts(name) {
        Some((pseudo_name, arg)) =>
          match @syn.lower_ascii(pseudo_name)[:] {
            "nth-child" | "nth-of-type" | "not" | "comment" => true
            "contains" => !arg.trim().is_empty()
            _ => false
          }
        None => false
      }
  }
}

///|
fn pseudo_selector_matches_with_context(
  node : @dom.Node,
  name : StringView,
  context : SelectorMatchContext,
  depth : Int,
) -> Bool raise @core.HtmlError {
  let lower_name = @syn.lower_ascii(name)
  match lower_name[:] {
    "comment" => node.kind == Comment
    "first-child" => is_first_element_child(node)
    "last-child" => is_last_element_child(node)
    "only-child" => is_only_element_child(node)
    "empty" => is_empty_for_selector(node)
    "root" => is_root_for_selector(node)
    "first-of-type" => is_first_of_type(node)
    "last-of-type" => is_last_of_type(node)
    "only-of-type" => is_only_of_type(node)
    _ =>
      match functional_pseudo_parts(name) {
        Some((pseudo_name, arg)) =>
          match @syn.lower_ascii(pseudo_name)[:] {
            "nth-child" => is_nth_child(node, arg)
            "nth-of-type" => is_nth_of_type(node, arg)
            "not" =>
              !matches_selector_list_with_context(node, arg, context, depth + 1)
            "contains" => contains_text(node, arg)
            "comment" => node.kind == Comment
            _ => false
          }
        _ => false
      }
  }
}