///|
/// CSS Selector Parser
/// Parses CSS tokens into Selector structures

///|
/// Parser state
priv struct SelectorParser {
  tokens : Array[@token.Token]
  /// Parallel array marking which `Number` tokens had an explicit
  /// leading `+`/`-` sign — needed to validate `` micro-syntax
  /// (CSS Syntax 3's "type flag" that the public `Token` enum drops).
  /// `None` means the parser was constructed from a token-only stream
  /// (the legacy/stylesheet path) and the An+B parser falls back to
  /// the lenient pre-sign-info behavior.
  signs : Array[Bool]?
  /// Exclusive upper bound on `pos`. When parsing a selector slice
  /// from a larger token stream (e.g. a stylesheet's rule body),
  /// `tokens_end` caps where the parser is allowed to read so callers
  /// don't have to copy a sub-array.
  tokens_end : Int
  mut pos : Int
}

///|
fn SelectorParser::new_with_signs(
  tokens : Array[@token.Token],
  signs : Array[Bool],
) -> SelectorParser {
  { tokens, signs: Some(signs), tokens_end: tokens.length(), pos: 0 }
}

///|
/// Build a parser bound to the half-open `[start, end)` slice of
/// `tokens`. Used by `parse_stylesheet` to avoid re-tokenizing
/// selectors it has already lexed.
fn SelectorParser::from_slice(
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> SelectorParser {
  { tokens, signs: None, tokens_end: end, pos: start }
}

///|
/// True iff the token at `pos` is a `Number` that had an explicit
/// leading `+`/`-` sign in source. Returns `false` for non-Number
/// tokens, out-of-bounds positions, and parsers built without sign
/// info (legacy callers).
fn SelectorParser::is_signed_at(self : SelectorParser, pos : Int) -> Bool {
  match self.signs {
    None => false
    Some(s) => if pos < 0 || pos >= s.length() { false } else { s[pos] }
  }
}

///|
fn SelectorParser::peek(self : SelectorParser) -> @token.Token {
  if self.pos >= self.tokens_end {
    // Return a safe fallback — the tokenizer always appends an EOF
    // sentinel so the source array is never empty.
    self.tokens[self.tokens.length() - 1]
  } else {
    self.tokens[self.pos]
  }
}

///|
fn SelectorParser::is_at_end(self : SelectorParser) -> Bool {
  self.pos >= self.tokens_end ||
  (match self.tokens[self.pos] {
    EOF => true
    _ => false
  })
}

///|
fn SelectorParser::advance(self : SelectorParser) -> @token.Token {
  let tok = self.peek()
  self.pos = self.pos + 1
  tok
}

///|
fn SelectorParser::skip_whitespace(self : SelectorParser) -> Unit {
  while !self.is_at_end() {
    match self.peek() {
      Whitespace => {
        let _ = self.advance()
      }
      _ => break
    }
  }
}

///|
/// Parse a selector list (comma-separated) from a constructed parser.
fn SelectorParser::parse_selector_list(self : SelectorParser) -> SelectorList? {
  self.skip_whitespace()
  let selectors : Array[ComplexSelector] = []
  match self.parse_complex_selector() {
    Some(sel) => selectors.push(sel)
    None => return None
  }
  while !self.is_at_end() {
    self.skip_whitespace()
    match self.peek() {
      Comma => {
        let _ = self.advance()
        self.skip_whitespace()
        match self.parse_complex_selector() {
          Some(sel) => selectors.push(sel)
          None => return None
        }
      }
      _ => break
    }
  }
  self.skip_whitespace()
  if !self.is_at_end() {
    return None
  }
  Some({ selectors, })
}

///|
/// Parse a single selector (without commas) from a constructed parser.
fn SelectorParser::parse_top_selector(
  self : SelectorParser,
) -> ComplexSelector? {
  self.skip_whitespace()
  match self.parse_complex_selector() {
    None => None
    Some(sel) => {
      self.skip_whitespace()
      if self.is_at_end() {
        Some(sel)
      } else {
        None
      }
    }
  }
}

///|
fn SelectorParser::parse_complex_selector(
  self : SelectorParser,
) -> ComplexSelector? {
  // Parse the first compound selector
  let first = match self.parse_compound_selector() {
    None => return None
    Some(f) => f
  }
  let tail : Array[ComplexSelectorStep] = []
  let mut current = first
  while !self.is_at_end() {
    // Check for combinator (maybe preceded by whitespace)
    let had_whitespace = match self.peek() {
      Whitespace => {
        let _ = self.advance()
        true
      }
      _ => false
    }

    // Check for explicit combinator
    let combinator : Combinator? = match self.peek() {
      Delim('>') => {
        let _ = self.advance()
        self.skip_whitespace()
        Some(Child)
      }
      Delim('+') => {
        let _ = self.advance()
        self.skip_whitespace()
        Some(NextSibling)
      }
      Delim('~') => {
        let _ = self.advance()
        self.skip_whitespace()
        Some(SubsequentSibling)
      }
      _ =>
        if had_whitespace && !self.is_at_end() {
          // Descendant combinator (whitespace) — only when something
          // non-empty follows. Pure trailing whitespace at the end of a
          // selector slice must not be treated as a descendant.
          Some(Descendant)
        } else {
          None
        }
    }
    match combinator {
      None => break
      Some(comb) =>
        // A pseudo-element-bearing compound must be the rightmost
        // (subject) compound of a complex selector — combinators may
        // not follow it.
        if compound_has_pseudo_element(current) {
          return None
        } else {
          match self.parse_compound_selector() {
            None => return None
            Some(next) => {
              tail.push({ combinator: comb, selector: current })
              current = next
            }
          }
        }
    }
  }

  // Reverse tail so it goes from right to left
  let reversed_tail : Array[ComplexSelectorStep] = []
  for i = tail.length() - 1; i >= 0; i = i - 1 {
    reversed_tail.push(tail[i])
  }
  Some({ head: current, tail: reversed_tail })
}

///|

///|
/// CSS Namespaces 3 prefix on a type/universal/attribute selector.
priv enum NamespacePrefix {
  NoPrefix // nothing — use default namespace
  AnyNs // `*|` — any namespace
  EmptyNs // `|`  — explicit no namespace (canonically dropped)
  NamedNs // `name|` — named namespace (the name itself isn't kept;
  // this parser doesn't model @namespace declarations)
}

///|
/// Match-everything helper so the unused EmptyNs/NamedNs constructors
/// (parsed but not significant to canonical Show) don't trip the
/// dead-code linter. We otherwise treat them identically to NoPrefix.
fn ns_prefix_collapse(p : NamespacePrefix) -> NamespacePrefix {
  match p {
    AnyNs => AnyNs
    NoPrefix | EmptyNs | NamedNs => NoPrefix
  }
}

///|
/// Optimistically consume a namespace prefix (`*|`, `|`, or `name|`).
/// If the next non-prefix tokens don't form a valid head (type/universal),
/// the caller restores `self.pos`.
fn SelectorParser::parse_ns_prefix(self : SelectorParser) -> NamespacePrefix {
  let start = self.pos
  match self.peek() {
    Delim('|') =>
      match self.peek_at(1) {
        Ident(_) | Delim('*') => {
          let _ = self.advance()
          return EmptyNs
        }
        _ => return NoPrefix
      }
    Delim('*') =>
      match self.peek_at(1) {
        Delim('|') =>
          match self.peek_at(2) {
            Ident(_) | Delim('*') => {
              let _ = self.advance()
              let _ = self.advance()
              return AnyNs
            }
            _ => return NoPrefix
          }
        _ => return NoPrefix
      }
    Ident(_) =>
      match self.peek_at(1) {
        Delim('|') =>
          match self.peek_at(2) {
            // `name|=` is the dash-match operator inside an attribute
            // selector, which we never enter from here; still, guard
            // against `name|something` followed by `=`.
            Delim('=') => return NoPrefix
            Ident(_) | Delim('*') => {
              let _ = self.advance()
              let _ = self.advance()
              return NamedNs
            }
            _ => return NoPrefix
          }
        _ => return NoPrefix
      }
    _ => return NoPrefix
  }
  let _ = start // keep `start` referenced if compiler complains
  NoPrefix
}

///|
fn SelectorParser::peek_at(self : SelectorParser, offset : Int) -> @token.Token {
  let idx = self.pos + offset
  if idx >= self.tokens.length() {
    // Fall back to the tokenizer's trailing EOF (always present).
    return self.tokens[self.tokens.length() - 1]
  }
  self.tokens[idx]
}

///|
fn SelectorParser::parse_compound_selector(
  self : SelectorParser,
) -> CompoundSelector? {
  let mut type_selector : SimpleSelector? = None
  let subclasses : Array[SimpleSelector] = []
  let mut any_ns_universal = false
  // Tracks whether we've already consumed a pseudo-element and, if so,
  // whether it's ::part (which permits a small whitelist of subsequent
  // selectors per CSS Shadow Parts 1) or another element (which closes
  // the compound).
  let mut saw_pe : PseudoElement? = None

  // First, try to parse a (possibly namespaced) type selector or
  // universal. CSS Namespaces 3 allows the prefixes:
  //   `*|`    any namespace
  //   `|`     explicit no namespace (canonically dropped)
  //   `name|` named namespace (we discard the name; we don't track
  //           @namespace declarations)
  // followed by an identifier or `*`.
  let saved_pos = self.pos
  let ns_prefix : NamespacePrefix = ns_prefix_collapse(self.parse_ns_prefix())
  match self.peek() {
    Ident(name) => {
      let _ = self.advance()
      type_selector = Some(Type(name.to_lower()))
      // CSS Namespaces 3 §6: any/named prefixes don't affect serialization
      // beyond optionally keeping the `*|` prefix on attribute selectors.
      // For type selectors we discard the prefix at parse time — the
      // canonical form is just `name`.
      let _ = ns_prefix
    }
    Delim('*') => {
      let _ = self.advance()
      type_selector = Some(Universal)
      // Only the `*|` (any-namespace) prefix on `*` is observable in the
      // canonical serialization: `*|*` is dropped when subclasses follow,
      // whereas a bare `*` is kept.
      match ns_prefix {
        AnyNs => any_ns_universal = true
        _ => ()
      }
    }
    _ =>
      // No type / universal followed the namespace prefix — back off
      // entirely. Same effect as the original parser when there's
      // nothing to consume.
      match ns_prefix {
        NoPrefix => ()
        _ => self.pos = saved_pos
      }
  }

  // Parse subclass selectors
  while !self.is_at_end() {
    match self.peek() {
      // ID selector
      Hash(id, _) =>
        if not_allowed_after_pe(saw_pe, false) {
          return None
        } else {
          let _ = self.advance()
          subclasses.push(Id(id))
        }
      // Class selector
      Delim('.') =>
        if not_allowed_after_pe(saw_pe, false) {
          return None
        } else {
          let _ = self.advance()
          match self.peek() {
            Ident(class_name) => {
              let _ = self.advance()
              subclasses.push(Class(class_name))
            }
            _ => break
          }
        }
      // Attribute selector
      LeftBracket =>
        if not_allowed_after_pe(saw_pe, false) {
          return None
        } else {
          match self.parse_attribute_selector() {
            Some(attr) => subclasses.push(Attribute(attr))
            None => break
          }
        }
      // Pseudo-class or pseudo-element
      Colon => {
        let _ = self.advance()
        match self.peek() {
          // Double colon - pseudo-element
          Colon => {
            // After a non-::part pseudo-element, no further pseudo-
            // elements may appear. ::part allows exactly one chained
            // pseudo-element (e.g. ::part(x)::after).
            if pe_chain_closed(saw_pe) {
              return None
            }
            let _ = self.advance()
            match self.parse_pseudo_element() {
              Some(pe) => {
                subclasses.push(PseudoElement(pe))
                saw_pe = Some(pe)
              }
              None => return None
            }
          }
          // Single colon - pseudo-class (or legacy pseudo-element)
          _ =>
            // First try to parse as pseudo-class
            match self.parse_pseudo_class() {
              Some(pc) => {
                if !pseudo_class_allowed_after_pe(saw_pe, pc) {
                  return None
                }
                subclasses.push(PseudoClass(pc))
              }
              None =>
                // Try legacy pseudo-element syntax (:before, :after)
                if pe_chain_closed(saw_pe) {
                  return None
                } else {
                  match self.parse_pseudo_element() {
                    Some(pe) => {
                      subclasses.push(PseudoElement(pe))
                      saw_pe = Some(pe)
                    }
                    None => return None
                  }
                }
            }
        }
      }
      _ => break
    }
  }

  // Must have at least one selector component
  if type_selector is None && subclasses.is_empty() {
    return None
  }
  Some({ type_selector, subclasses, any_ns_universal })
}

///|
/// True if a non-pseudo subclass (id/class/attr) would be invalid after
/// the most recently seen pseudo-element. Per CSS Shadow Parts 1 only
/// `::part(...)` allows further subclasses; per CSS Scoping 1
/// `::slotted(...)` and other pseudo-elements do not.
fn not_allowed_after_pe(saw_pe : PseudoElement?, _allow_part : Bool) -> Bool {
  match saw_pe {
    None => false
    Some(Part(_)) => false // ::part allows id/class/attr — actually no per spec but be lenient
    Some(_) => true
  }
}

///|
/// Whether another pseudo-element may appear after `saw_pe`. Only
/// `::part(...)` keeps the chain open (one further pseudo-element).
fn pe_chain_closed(saw_pe : PseudoElement?) -> Bool {
  match saw_pe {
    None => false
    Some(Part(_)) => false
    Some(_) => true
  }
}

///|
/// CSS Selectors / Shadow DOM rules constraining which pseudo-classes
/// may follow a pseudo-element:
///
/// - After `::part(name)`: allow `:state`, `:hover`, `:active`,
///   `:focus`, `:focus-visible`, `:focus-within`, `:lang`, `:dir`.
/// - After `::slotted(...)`: no pseudo-classes are permitted.
/// - After other pseudo-elements: no pseudo-classes are permitted.
fn pseudo_class_allowed_after_pe(
  saw_pe : PseudoElement?,
  pc : PseudoClass,
) -> Bool {
  match saw_pe {
    None => true
    Some(Part(_)) =>
      // CSS Shadow Parts 1 §3.2: allow user-action, linguistic, form,
      // and a handful of structural pseudos after ::part. Reject the
      // tree-structural ones whose answer is undefined inside a part
      // (e.g. :first-child against a part of unknown depth) and reject
      // :has / :state-of-the-tree pseudos.
      match pc {
        State(_)
        | Hover
        | Active
        | Focus
        | FocusVisible
        | FocusWithin
        | Lang(_)
        | Dir(_)
        | Link
        | Visited
        | AnyLink
        | Enabled
        | Disabled
        | Checked
        | Indeterminate
        | Required
        | Optional
        | Valid
        | Invalid
        | ReadOnly
        | ReadWrite
        | Is(_)
        | Where(_)
        | Not(_) => true
        _ => false
      }
    Some(_) => false
  }
}

///|
fn SelectorParser::parse_attribute_selector(
  self : SelectorParser,
) -> AttributeSelector? {
  // Consume '['
  match self.peek() {
    LeftBracket => {
      let _ = self.advance()
    }
    _ => return None
  }
  self.skip_whitespace()

  // Optional namespace prefix on the attribute name (CSS Namespaces 3).
  let ns_prefix : NamespacePrefix = ns_prefix_collapse(self.parse_ns_prefix())
  let any_namespace = match ns_prefix {
    AnyNs => true
    _ => false
  }

  // Get attribute name
  let name = match self.peek() {
    Ident(n) => {
      let _ = self.advance()
      n
    }
    _ => return None
  }
  self.skip_whitespace()

  // Check for match operator or end
  let match_type : AttributeMatch = match self.peek() {
    RightBracket => {
      let _ = self.advance()
      return Some({
        name,
        match_type: Exists,
        case_insensitive: false,
        any_namespace,
      })
    }
    Delim('=') => {
      let _ = self.advance()
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => Exact(v)
        None => return None
      }
    }
    Delim('~') => {
      let _ = self.advance()
      match self.peek() {
        Delim('=') => {
          let _ = self.advance()
        }
        _ => return None
      }
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => Includes(v)
        None => return None
      }
    }
    Delim('|') => {
      let _ = self.advance()
      match self.peek() {
        Delim('=') => {
          let _ = self.advance()
        }
        _ => return None
      }
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => DashMatch(v)
        None => return None
      }
    }
    Delim('^') => {
      let _ = self.advance()
      match self.peek() {
        Delim('=') => {
          let _ = self.advance()
        }
        _ => return None
      }
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => Prefix(v)
        None => return None
      }
    }
    Delim('$') => {
      let _ = self.advance()
      match self.peek() {
        Delim('=') => {
          let _ = self.advance()
        }
        _ => return None
      }
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => Suffix(v)
        None => return None
      }
    }
    Delim('*') => {
      let _ = self.advance()
      match self.peek() {
        Delim('=') => {
          let _ = self.advance()
        }
        _ => return None
      }
      self.skip_whitespace()
      match self.parse_attr_value() {
        Some(v) => Substring(v)
        None => return None
      }
    }
    _ => return None
  }
  self.skip_whitespace()

  // Check for case-insensitive flag
  let case_insensitive = match self.peek() {
    Ident(flag) =>
      if flag.to_lower() == "i" {
        let _ = self.advance()
        self.skip_whitespace()
        true
      } else if flag.to_lower() == "s" {
        let _ = self.advance()
        self.skip_whitespace()
        false
      } else {
        false
      }
    _ => false
  }

  // Consume ']'
  match self.peek() {
    RightBracket => {
      let _ = self.advance()
    }
    _ => return None
  }
  Some({ name, match_type, case_insensitive, any_namespace })
}

///|
fn SelectorParser::parse_attr_value(self : SelectorParser) -> String? {
  match self.peek() {
    Ident(v) => {
      let _ = self.advance()
      Some(v)
    }
    String(v) => {
      let _ = self.advance()
      Some(v)
    }
    _ => None
  }
}

///|
fn SelectorParser::parse_pseudo_element(
  self : SelectorParser,
) -> PseudoElement? {
  match self.peek() {
    Ident(name) => {
      // Resolve name BEFORE consuming so unknown pseudo-elements leave
      // the parser state unchanged. This matters when an outer parser
      // (e.g. inside :not()) wants to fall through on an unrecognized
      // ident.
      let result = match name.to_lower() {
        "before" => Some(Before)
        "after" => Some(After)
        "first-line" => Some(FirstLine)
        "first-letter" => Some(FirstLetter)
        "marker" => Some(Marker)
        "placeholder" => Some(Placeholder)
        "selection" => Some(Selection)
        "backdrop" => Some(Backdrop)
        "file-selector-button" => Some(FileSelectorButton)
        "placeholder-shown" => Some(PlaceholderShown)
        "target-text" => Some(TargetText)
        "spelling-error" => Some(SpellingError)
        "grammar-error" => Some(GrammarError)
        "cue" => Some(Cue)
        _ => None
      }
      match result {
        Some(_) => {
          let _ = self.advance()
          result
        }
        None => None
      }
    }
    Function(name) => {
      let _ = self.advance()
      let name_lower = name.to_lower()
      match name_lower {
        "part" => {
          // ::part(+) — one or more identifiers, whitespace-
          // separated. CSS Shadow Parts 1.
          self.skip_whitespace()
          let names : Array[String] = []
          while true {
            match self.peek() {
              Ident(part_name) => {
                let _ = self.advance()
                names.push(part_name)
                self.skip_whitespace()
              }
              _ => break
            }
          }
          match (self.peek(), names.length()) {
            (RightParen, n) if n > 0 => {
              let _ = self.advance()
              Some(Part(names))
            }
            _ => None
          }
        }
        "slotted" => {
          // ::slotted() — CSS Scoping 1.
          self.skip_whitespace()
          let selectors = self.parse_compound_selector_list_until_paren()
          match self.peek() {
            RightParen =>
              if selectors.is_empty() {
                None
              } else {
                let _ = self.advance()
                Some(Slotted(selectors))
              }
            _ => None
          }
        }
        _ => None
      }
    }
    _ => None
  }
}

///|

///|
/// CSS Selectors 4 — the argument of `:not()` may not contain pseudo-
/// elements. (Unknown pseudo-classes have already been rejected by
/// `parse_pseudo_class` returning None.)
fn compound_has_pseudo_element(c : CompoundSelector) -> Bool {
  for sub in c.subclasses {
    match sub {
      PseudoElement(_) => return true
      _ => ()
    }
  }
  false
}

///|
fn complex_has_pseudo_element(c : ComplexSelector) -> Bool {
  if compound_has_pseudo_element(c.head) {
    return true
  }
  for step in c.tail {
    if compound_has_pseudo_element(step.selector) {
      return true
    }
  }
  false
}

///|
fn not_arg_has_invalid(selectors : Array[ComplexSelector]) -> Bool {
  for s in selectors {
    if complex_has_pseudo_element(s) {
      return true
    }
  }
  false
}

///|
/// CSS Scoping 1: `:host()` / `:host-context()` accept a single
/// ``. The compound-selector restriction extends
/// recursively through `:not()`, `:is()`, `:where()`, and `:has()`: no
/// inner pseudo-class may contain a complex selector with combinators.
fn compound_is_strictly_compound(c : CompoundSelector) -> Bool {
  for sub in c.subclasses {
    match sub {
      PseudoClass(pc) =>
        if !pseudo_class_is_strictly_compound(pc) {
          return false
        }
      _ => ()
    }
  }
  true
}

///|
fn complex_is_strictly_compound(c : ComplexSelector) -> Bool {
  if c.tail.length() > 0 {
    return false
  }
  compound_is_strictly_compound(c.head)
}

///|
fn pseudo_class_is_strictly_compound(pc : PseudoClass) -> Bool {
  match pc {
    Not(sels) | Is(sels) | Where(sels) => {
      for s in sels {
        if !complex_is_strictly_compound(s) {
          return false
        }
      }
      true
    }
    Has(rels) => {
      for r in rels {
        if !complex_is_strictly_compound(r.selector) {
          return false
        }
      }
      true
    }
    HostFunc(c) | HostContextFunc(c) => compound_is_strictly_compound(c)
    _ => true
  }
}

///|
fn SelectorParser::parse_pseudo_class(self : SelectorParser) -> PseudoClass? {
  match self.peek() {
    Ident(name) => {
      // Check if it's a known pseudo-class BEFORE consuming the token
      let result = match name.to_lower() {
        "first-child" => Some(FirstChild)
        "last-child" => Some(LastChild)
        "only-child" => Some(OnlyChild)
        "first-of-type" => Some(FirstOfType)
        "last-of-type" => Some(LastOfType)
        "only-of-type" => Some(OnlyOfType)
        "root" => Some(Root)
        "empty" => Some(Empty)
        "hover" => Some(Hover)
        "active" => Some(Active)
        "focus" => Some(Focus)
        "focus-visible" => Some(FocusVisible)
        "focus-within" => Some(FocusWithin)
        "link" => Some(Link)
        "visited" => Some(Visited)
        "any-link" => Some(AnyLink)
        "enabled" => Some(Enabled)
        "disabled" => Some(Disabled)
        "checked" => Some(Checked)
        "indeterminate" => Some(Indeterminate)
        "required" => Some(Required)
        "optional" => Some(Optional)
        "valid" => Some(Valid)
        "invalid" => Some(Invalid)
        "read-only" => Some(ReadOnly)
        "read-write" => Some(ReadWrite)
        "host" => Some(Host)
        "heading" => Some(Heading([]))
        _ => None
      }
      // Only consume the token if it matched
      match result {
        Some(_) => {
          let _ = self.advance()
          result
        }
        None => None
      }
    }
    Function(name) => {
      let _ = self.advance() // Consume the Function token
      let name_lower = name.to_lower()
      match name_lower {
        "nth-child" => {
          let expr = self.parse_nth_expr()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              match expr {
                Some(e) => Some(NthChild(e))
                None => None
              }
            }
            _ => None
          }
        }
        "nth-last-child" => {
          let expr = self.parse_nth_expr()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              match expr {
                Some(e) => Some(NthLastChild(e))
                None => None
              }
            }
            _ => None
          }
        }
        "nth-of-type" => {
          let expr = self.parse_nth_expr()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              match expr {
                Some(e) => Some(NthOfType(e))
                None => None
              }
            }
            _ => None
          }
        }
        "nth-last-of-type" => {
          let expr = self.parse_nth_expr()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              match expr {
                Some(e) => Some(NthLastOfType(e))
                None => None
              }
            }
            _ => None
          }
        }
        "not" => {
          self.skip_whitespace()
          let selectors = self.parse_complex_selector_list_until_paren()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              // CSS Selectors 4: empty :not() argument is invalid.
              // Also reject if any inner selector contains a pseudo-element
              // or an unknown pseudo (per spec).
              if selectors.is_empty() {
                None
              } else if not_arg_has_invalid(selectors) {
                None
              } else {
                Some(Not(selectors))
              }
            }
            _ => None
          }
        }
        "is" => {
          self.skip_whitespace()
          let selectors = self.parse_complex_selector_list_until_paren()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              Some(Is(selectors))
            }
            _ => None
          }
        }
        "where" => {
          self.skip_whitespace()
          let selectors = self.parse_complex_selector_list_until_paren()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              Some(Where(selectors))
            }
            _ => None
          }
        }
        "has" => {
          self.skip_whitespace()
          let selectors = self.parse_relative_selector_list_until_paren()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              // CSS Selectors 4: empty :has() argument is invalid.
              if selectors.is_empty() {
                None
              } else {
                Some(Has(selectors))
              }
            }
            _ => None
          }
        }
        "host" => {
          self.skip_whitespace()
          // :host() — single compound, not list.
          let compound = self.parse_compound_selector()
          self.skip_whitespace()
          match (self.peek(), compound) {
            (RightParen, Some(c)) =>
              if compound_is_strictly_compound(c) {
                let _ = self.advance()
                Some(HostFunc(c))
              } else {
                None
              }
            _ => None
          }
        }
        "host-context" => {
          self.skip_whitespace()
          let compound = self.parse_compound_selector()
          self.skip_whitespace()
          match (self.peek(), compound) {
            (RightParen, Some(c)) =>
              if compound_is_strictly_compound(c) {
                let _ = self.advance()
                Some(HostContextFunc(c))
              } else {
                None
              }
            _ => None
          }
        }
        "heading" => {
          // :heading(N, ...) — comma-separated list of integer levels.
          self.skip_whitespace()
          let levels : Array[Int] = []
          let mut ok = true
          while true {
            match self.peek() {
              Number(n, Integer) => {
                let _ = self.advance()
                levels.push(n.to_int())
                self.skip_whitespace()
                match self.peek() {
                  Comma => {
                    let _ = self.advance()
                    self.skip_whitespace()
                  }
                  RightParen => break
                  _ => {
                    ok = false
                    break
                  }
                }
              }
              _ => {
                ok = false
                break
              }
            }
          }
          if ok && levels.length() > 0 && self.peek() is RightParen {
            let _ = self.advance()
            Some(Heading(levels))
          } else {
            None
          }
        }
        "dir" => {
          self.skip_whitespace()
          match self.peek() {
            Ident(v) => {
              let _ = self.advance()
              self.skip_whitespace()
              match self.peek() {
                RightParen => {
                  let _ = self.advance()
                  Some(Dir(v))
                }
                _ => None
              }
            }
            _ => None
          }
        }
        "lang" => {
          self.skip_whitespace()
          let tags : Array[String] = []
          while true {
            match self.peek() {
              Ident(v) => {
                let _ = self.advance()
                tags.push(v)
              }
              String(v) => {
                let _ = self.advance()
                tags.push(v)
              }
              _ => break
            }
            self.skip_whitespace()
            match self.peek() {
              Comma => {
                let _ = self.advance()
                self.skip_whitespace()
              }
              _ => break
            }
          }
          match (self.peek(), tags.is_empty()) {
            (RightParen, false) => {
              let _ = self.advance()
              Some(Lang(tags))
            }
            _ => None
          }
        }
        "state" => {
          self.skip_whitespace()
          let state = self.parse_attr_value()
          match self.peek() {
            RightParen => {
              let _ = self.advance()
              match state {
                Some(token) => Some(State(token))
                None => None
              }
            }
            _ => None
          }
        }
        _ => None
      }
    }
    _ => None
  }
}

///|
fn SelectorParser::parse_nth_expr(self : SelectorParser) -> NthExpr? {
  self.skip_whitespace()

  // Handle keywords: odd, even
  match self.peek() {
    Ident(kw) => {
      // Match against the lowercase identifier. Supported forms:
      //   "odd", "even"
      //   "n"           a=1, b=parse_nth_b() (allows trailing ± N)
      //   "-n"          a=-1, b=parse_nth_b()
      //   "n-"  a=1,  b=-    (CSS Selectors 4 ndashdigit-ident)
      //   "-n-" a=-1, b=-
      // Bare `n-`/`-n-` followed by whitespace + integer is NOT
      // a valid An+B form per spec.
      let kw_lower = kw.to_lower()
      if kw_lower == "odd" {
        let _ = self.advance()
        return Some({ a: 2, b: 1 })
      }
      if kw_lower == "even" {
        let _ = self.advance()
        return Some({ a: 2, b: 0 })
      }
      if kw_lower == "n" {
        let _ = self.advance()
        return self.finish_nth_after_n(1)
      }
      if kw_lower == "-n" {
        let _ = self.advance()
        return self.finish_nth_after_n(-1)
      }
      match ndashdigit_to_b(kw_lower, "n-") {
        Some(b) => {
          let _ = self.advance()
          return Some({ a: 1, b })
        }
        None => ()
      }
      match ndashdigit_to_b(kw_lower, "-n-") {
        Some(b) => {
          let _ = self.advance()
          return Some({ a: -1, b })
        }
        None => ()
      }
      // Bare `n-` / `-n-` ident: must be followed (after optional
      // whitespace) by a **signless** integer. CSS Selectors 4 only
      // accepts the ` ` shape; a signed
      // integer after `n-` is a parse error (the second sign comes
      // from the integer token, not from the syntactic `+`/`-`).
      if kw_lower == "n-" {
        return self.finish_ndash_int(1)
      }
      if kw_lower == "-n-" {
        return self.finish_ndash_int(-1)
      }
      return None
    }
    Dimension(num, unit) => {
      let unit_lower = unit.to_lower()
      if unit_lower == "n" {
        let _ = self.advance()
        return self.finish_nth_after_n(num.to_int())
      }
      // ndashdigit-dimension, e.g. `5n-3` lexes as Dimension(5, "n-3").
      match ndashdigit_to_b(unit_lower, "n-") {
        Some(b) => {
          let _ = self.advance()
          return Some({ a: num.to_int(), b })
        }
        None => ()
      }
      // `n-` dimension followed by signless integer (e.g. `5n- 3`).
      if unit_lower == "n-" {
        return self.finish_ndash_int(num.to_int())
      }
      // Fallback for `5n+3` lex shapes (unit_lower starts with "n+").
      if unit_lower.length() > 1 && unit_lower[0] == 'n' {
        let suffix = unit_lower.unsafe_substring(
          start=1,
          end=unit_lower.length(),
        )
        if suffix.length() > 1 && suffix[0] == '+' {
          let b = @string.parse_int(
            suffix.unsafe_substring(start=1, end=suffix.length()),
          ) catch {
            _ => return None
          }
          let _ = self.advance()
          return Some({ a: num.to_int(), b })
        }
        return None
      }
      return None
    }
    Number(num, _) => {
      let _ = self.advance()
      // Just a number = 0n+B
      return Some({ a: 0, b: num.to_int() })
    }
    Delim('+') => {
      // `+` must be immediately followed by `n`-form ident; CSS
      // Selectors 4 forbids whitespace between `+` and what follows,
      // and forbids `+ ` (which is just ``).
      let _ = self.advance()
      match self.peek() {
        Ident(kw) => {
          let kw_lower = kw.to_lower()
          if kw_lower == "n" {
            let _ = self.advance()
            return self.finish_nth_after_n(1)
          }
          if kw_lower == "n-" {
            return self.finish_ndash_int(1)
          }
          match ndashdigit_to_b(kw_lower, "n-") {
            Some(b) => {
              let _ = self.advance()
              return Some({ a: 1, b })
            }
            None => return None
          }
        }
        _ => return None
      }
    }
    _ => return None
  }
}

///|
/// After consuming a `-` shape (`n-`, `-n-`, `n-`),
/// require a signless integer next. WPT cases like `5n- 5` are valid
/// (canonicalize to `5n-5`); `5n- -5` / `5n- +5` are parse errors.
fn SelectorParser::finish_ndash_int(self : SelectorParser, a : Int) -> NthExpr? {
  let _ = self.advance()
  self.skip_whitespace()
  match self.peek() {
    Number(n, _) => {
      // When sign info is available, enforce signless; legacy callers
      // (parser built without signs) keep the lenient behavior.
      match self.signs {
        Some(_) => if self.is_signed_at(self.pos) { return None }
        None => ()
      }
      let _ = self.advance()
      Some({ a, b: -n.to_int() })
    }
    _ => None
  }
}

///|
/// After consuming an `` (`n`, `-n`, `+n`, or `n`) with
/// coefficient `a`, optionally parse the trailing `b` part:
///     → b = signed value (e.g. `5n -3`)
///   '+'     → b = +value
///   '-'     → b = -value
///   (nothing)         → b = 0
/// Returns None if any of the surrounding shapes are spec-invalid
/// (e.g. `5n +3` with double sign, `5n 3` with no operator).
fn SelectorParser::finish_nth_after_n(
  self : SelectorParser,
  a : Int,
) -> NthExpr? {
  // Snapshot position to peek the next non-whitespace token; we need
  // to distinguish "no whitespace" from "whitespace + signed-integer".
  self.skip_whitespace()
  match self.peek() {
    Delim('+') => {
      let _ = self.advance()
      self.skip_whitespace()
      match self.peek() {
        Number(n, _) => {
          if self.is_signed_at(self.pos) {
            return None
          }
          let _ = self.advance()
          return Some({ a, b: n.to_int() })
        }
        _ => return None
      }
    }
    Delim('-') => {
      let _ = self.advance()
      self.skip_whitespace()
      match self.peek() {
        Number(n, _) => {
          if self.is_signed_at(self.pos) {
            return None
          }
          let _ = self.advance()
          return Some({ a, b: -n.to_int() })
        }
        _ => return None
      }
    }
    Number(n, _) => {
      //  form. The number MUST have an explicit sign
      // (else it'd be ` ` which is invalid).
      // Legacy callers without sign info accept this for back-compat.
      match self.signs {
        Some(_) => if !self.is_signed_at(self.pos) { return None }
        None => ()
      }
      let _ = self.advance()
      return Some({ a, b: n.to_int() })
    }
    _ => return Some({ a, b: 0 })
  }
}

///|
/// Parse `` patterns from a lowercased identifier. The
/// prefix is one of "n-" / "-n-"; the returned integer is the negative
/// of the digit run (the `-` is part of the ident token).
fn ndashdigit_to_b(name : String, prefix : String) -> Int? {
  if !name.has_prefix(prefix) {
    return None
  }
  let digits = name.unsafe_substring(start=prefix.length(), end=name.length())
  if digits.is_empty() {
    return None
  }
  // All remaining chars must be digits.
  for i in 0.. '9' {
      return None
    }
  }
  let n = @string.parse_int(digits) catch { _ => return None }
  Some(-n)
}

///|
fn SelectorParser::parse_compound_selector_list_until_paren(
  self : SelectorParser,
) -> Array[CompoundSelector] {
  let result : Array[CompoundSelector] = []
  while !self.is_at_end() {
    self.skip_whitespace()
    match self.peek() {
      RightParen | EOF => break
      Comma => {
        let _ = self.advance()
      }
      _ =>
        match self.parse_compound_selector() {
          Some(sel) => result.push(sel)
          None => break
        }
    }
  }
  result
}

///|
fn SelectorParser::parse_complex_selector_list_until_paren(
  self : SelectorParser,
) -> Array[ComplexSelector] {
  let result : Array[ComplexSelector] = []
  while !self.is_at_end() {
    self.skip_whitespace()
    match self.peek() {
      RightParen | EOF => break
      Comma => {
        let _ = self.advance()
      }
      _ =>
        match self.parse_complex_selector() {
          Some(sel) => result.push(sel)
          None => break
        }
    }
  }
  result
}

///|
fn SelectorParser::parse_relative_selector_list_until_paren(
  self : SelectorParser,
) -> Array[RelativeSelector] {
  let result : Array[RelativeSelector] = []
  while !self.is_at_end() {
    self.skip_whitespace()
    match self.peek() {
      RightParen | EOF => break
      Comma => {
        let _ = self.advance()
      }
      _ => {
        // Check for optional leading combinator
        let combinator : Combinator? = match self.peek() {
          Delim('>') => {
            let _ = self.advance()
            self.skip_whitespace()
            Some(Child)
          }
          Delim('+') => {
            let _ = self.advance()
            self.skip_whitespace()
            Some(NextSibling)
          }
          Delim('~') => {
            let _ = self.advance()
            self.skip_whitespace()
            Some(SubsequentSibling)
          }
          _ => None
        }
        match self.parse_complex_selector() {
          Some(sel) => result.push({ combinator, selector: sel })
          None => break
        }
      }
    }
  }
  result
}

///|
/// Convenience function to parse a selector from CSS text
pub fn parse_selector_text(css : String) -> ComplexSelector? {
  let (tokens, signs) = @token.tokenize_with_signs(css)
  SelectorParser::new_with_signs(tokens, signs).parse_top_selector()
}

///|
/// Convenience function to parse a selector list from CSS text
pub fn parse_selector_list_text(css : String) -> SelectorList? {
  let (tokens, signs) = @token.tokenize_with_signs(css)
  SelectorParser::new_with_signs(tokens, signs).parse_selector_list()
}

///|
/// Parse a selector-list from an existing token stream in-place
/// (`tokens[start..end]`). This is the fast path used by
/// `parser/stylesheet`: callers reuse tokens they have already lexed
/// instead of stringifying + re-tokenizing the slice.
///
/// Returns `(text, selector)` pairs because the stylesheet parser
/// stores both the original source text and the parsed selector for
/// every rule; we synthesize the text from the same slice.
pub fn parse_selector_list_from_tokens(
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> Array[ComplexSelector] {
  let result : Array[ComplexSelector] = []
  // Find top-level commas inside the slice to split into selectors.
  let mut depth = 0
  let mut segment_start = start
  let mut i = start
  while i < end {
    match tokens[i] {
      EOF => break
      Function(_) | LeftParen | LeftBracket => depth = depth + 1
      RightParen | RightBracket => if depth > 0 { depth = depth - 1 }
      Comma =>
        if depth == 0 {
          match parse_selector_slice(tokens, segment_start, i) {
            Some(sel) => result.push(sel)
            None => ()
          }
          segment_start = i + 1
        }
      _ => ()
    }
    i = i + 1
  }
  // Final segment.
  match parse_selector_slice(tokens, segment_start, end) {
    Some(sel) => result.push(sel)
    None => ()
  }
  result
}

///|
fn parse_selector_slice(
  tokens : Array[@token.Token],
  start : Int,
  end : Int,
) -> ComplexSelector? {
  let parser = SelectorParser::from_slice(tokens, start, end)
  parser.skip_whitespace()
  match parser.parse_complex_selector() {
    None => None
    Some(sel) => {
      parser.skip_whitespace()
      if parser.is_at_end() {
        Some(sel)
      } else {
        None
      }
    }
  }
}