///|
/// Specificity.
///
/// Computed on demand rather than stored on the selector, which is the opposite
/// of lightningcss -- it caches specificity on every `Selector`, because a
/// matcher asks for it constantly. Nothing here matches anything, and a stored
/// field would have to be kept correct through every construction and every
/// rewrite for the benefit of a question nobody has asked yet.

///|
/// The three counts CSS compares, most significant first.
///
/// A triple rather than a packed integer: the packed form overflows on absurd
/// input and reads as a magic number when it does not, and comparing three
/// fields is not the expensive part of anything.
pub(all) struct Specificity {
  ids : Int
  classes : Int
  elements : Int
} derive(Eq, Debug)

///|
pub fn Specificity::zero() -> Specificity {
  { ids: 0, classes: 0, elements: 0, }
}

///|
pub fn Specificity::add(self : Specificity, other : Specificity) -> Specificity {
  {
    ids: self.ids + other.ids,
    classes: self.classes + other.classes,
    elements: self.elements + other.elements,
  }
}

///|
/// Whether this selector wins against that one.
pub fn Specificity::beats(self : Specificity, other : Specificity) -> Bool {
  if self.ids != other.ids {
    self.ids > other.ids
  } else if self.classes != other.classes {
    self.classes > other.classes
  } else {
    self.elements > other.elements
  }
}

///|
/// `(a, b, c)`, the way CSS writes it.
pub fn Specificity::to_triple(self : Specificity) -> (Int, Int, Int) {
  (self.ids, self.classes, self.elements)
}

///|
/// The specificity of a selector.
pub fn specificity(s : @ast.Selector) -> Specificity {
  match s {
    Simple(c) => compound_specificity(c)
    Complex(left, _, right) =>
      specificity(left).add(compound_specificity(right))
    // A combinator contributes nothing, so a relative selector is just its
    // right-hand side.
    Relative(_, inner) => specificity(inner)
    Bogus(_) => Specificity::zero()
  }
}

///|
/// The specificity of a selector LIST, which is not the sum.
///
/// A list has no single specificity -- each arm has its own, and the cascade
/// compares whichever one matched. This returns the highest, which is what
/// `:is()` and `:not()` contribute and what a caller asking "how specific is
/// this rule" almost always means.
pub fn max_specificity(sels : Array[@ast.Selector]) -> Specificity {
  let mut best = Specificity::zero()
  for s in sels {
    let sp = specificity(s)
    if sp.beats(best) {
      best = sp
    }
  }
  best
}

///|
fn compound_specificity(c : @ast.Compound) -> Specificity {
  let mut sp = match c.type_sel {
    // A type selector counts as an element; the universal selector counts as
    // nothing at all, which is the one exception in the whole calculation.
    Some(Named(_, _)) => { ids: 0, classes: 0, elements: 1, }
    Some(Universal(_)) => Specificity::zero()
    None => Specificity::zero()
  }
  for q in c.quals {
    sp = sp.add(qualifier_specificity(q))
  }
  sp
}

///|
fn qualifier_specificity(q : @ast.Qualifier) -> Specificity {
  match q {
    Id(_) => { ids: 1, classes: 0, elements: 0, }
    Class(_) => { ids: 0, classes: 1, elements: 0, }
    Attr(_) => { ids: 0, classes: 1, elements: 0, }
    Element(_) => { ids: 0, classes: 0, elements: 1, }
    // `&` takes the specificity of what it stands for, which is not known
    // here: this package sees one selector, not the rule around it. Zero is
    // the honest answer for a lens that cannot see the nesting.
    Nesting => Specificity::zero()
    Bogus(_) => Specificity::zero()
    Pseudo(p) => pseudo_specificity(p)
  }
}

///|
/// A pseudo-class's specificity, which is where CSS stops being uniform.
fn pseudo_specificity(p : @ast.PseudoClass) -> Specificity {
  match p {
    Simple(name) =>
      // The three that count as nothing, and everything else as a class.
      if is_zero_pseudo(name.to_lower()) {
        Specificity::zero()
      } else {
        { ids: 0, classes: 1, elements: 0, }
      }
    Sub(name, sels) => {
      let lower = name.to_lower()
      if lower == "where" {
        // `:where()` is the whole reason this function is not one line: it
        // contributes nothing, whatever is inside it.
        Specificity::zero()
      } else if lower == "is" || lower == "not" || lower == "has" {
        // These take the specificity of their most specific argument -- which
        // is exactly why a selector list may not be spelled `is(...)` in the
        // shrubbery syntax: it would change what the stylesheet matches.
        max_specificity(sels)
      } else {
        { ids: 0, classes: 1, elements: 0, }
      }
    }
    Nth(_, _, of_) =>
      match of_ {
        // `:nth-child(An+B of S)` is a class, plus the most specific arm of S.
        Some(sels) =>
          ({ ids: 0, classes: 1, elements: 0, } : Specificity).add(
            max_specificity(sels),
          )
        None => { ids: 0, classes: 1, elements: 0, }
      }
    Lang(_) | Dir(_) => { ids: 0, classes: 1, elements: 0, }
    Unknown(_, _) => { ids: 0, classes: 1, elements: 0, }
  }
}

///|
/// The pseudo-classes that contribute nothing.
fn is_zero_pseudo(name : String) -> Bool {
  match name {
    "where" | "host" | "host-context" | "slotted" => true
    _ => false
  }
}