///|
/// Media queries, `@supports` conditions, and the small prelude helpers.
///
/// `@media`, `@supports` and `@container` share one condition grammar, which is
/// why they share one parser here. lightningcss generalises the same way, and
/// it is worth copying: the three diverge only in what a leaf may be.

///|
/// What a leaf of a condition is allowed to be.
priv enum CondKind {
  /// `@media`, `@container`: a leaf is a feature test.
  Query
  /// `@supports`: a leaf may also be a declaration or `selector(...)`.
  Supports
} derive(Eq)

///|
fn Parser::parse_media_list(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
) -> Array[@ast.MediaQuery] raise @err.CssError {
  let out : Array[@ast.MediaQuery] = []
  if only_trivia(toks) {
    return out
  }
  for part in split_top(toks, Comma) {
    out.push(self.parse_media_query(part, start))
  }
  out
}

///|
fn Parser::parse_media_query(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
) -> @ast.MediaQuery raise @err.CssError {
  let c = { toks, pos: 0, }
  let mut qualifier : @ast.MediaQualifier? = None
  let mut media_type : String? = None
  let save = c.pos
  match c.next_meaningful() {
    Some(Ident(w)) => {
      let l = w.to_lower()
      if l == "only" {
        qualifier = Some(Only)
      } else if l == "not" {
        // `not` is a qualifier only when a media type follows it; otherwise it
        // negates a condition and belongs to the condition grammar.
        match c.next_meaningful() {
          Some(Ident(t)) if !is_logical(t) => {
            qualifier = Some(Not)
            media_type = Some(t)
          }
          _ => c.pos = save
        }
      } else if !is_logical(l) {
        media_type = Some(w)
      } else {
        c.pos = save
      }
    }
    _ => c.pos = save
  }
  if qualifier is Some(Only) {
    match c.next_meaningful() {
      Some(Ident(t)) => media_type = Some(t)
      _ => ()
    }
  }
  // What is left is the condition, minus a leading `and`.
  let mut rest = c.rest()
  match first_meaningful(rest) {
    Some(Ident(w)) if w.to_lower() == "and" =>
      rest = drop_first_meaningful(rest)
    _ => ()
  }
  let condition = if only_trivia(rest) {
    None
  } else {
    Some(self.parse_cond(rest, start, Query))
  }
  { qualifier, media_type, condition, }
}

///|
fn is_logical(w : String) -> Bool {
  let l = w.to_lower()
  l == "and" || l == "or" || l == "not"
}

///|
/// A `@supports` condition.
fn Parser::parse_condition(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
) -> @ast.Condition raise @err.CssError {
  self.parse_cond(toks, start, Supports)
}

///|
fn Parser::parse_cond(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
  kind : CondKind,
) -> @ast.Condition raise @err.CssError {
  if only_trivia(toks) {
    return Bogus(self.bogus_view(BadSupportsCondition, toks, start))
  }
  // `and` and `or` may not be mixed without parentheses, and CSS says so too,
  // so this is a diagnostic rather than a precedence decision.
  let ands = split_on_word(toks, "and")
  let ors = split_on_word(toks, "or")
  if ands.length() > 1 && ors.length() > 1 {
    self.error(MixedLogicalOps, span_of_view(toks, start))
    return Bogus(self.bogus_view(MixedLogicalOps, toks, start))
  }
  if ands.length() > 1 {
    let conds = []
    for part in ands {
      flatten_into(conds, self.parse_cond(part, start, kind), And)
    }
    return Operation(And, conds)
  }
  if ors.length() > 1 {
    let conds = []
    for part in ors {
      flatten_into(conds, self.parse_cond(part, start, kind), Or)
    }
    return Operation(Or, conds)
  }
  // A single term, possibly negated.
  match first_meaningful(toks) {
    Some(Ident(w)) if w.to_lower() == "not" => {
      let inner = drop_first_meaningful(toks)
      return Operation(Not, [self.parse_cond(inner, start, kind)])
    }
    _ => ()
  }
  self.parse_cond_leaf(toks, start, kind)
}

///|
/// Fold a nested operation of the same operator into its parent.
///
/// Keeping the tree flat is what makes the round-trip property an equality
/// rather than an equality-up-to-associativity, which would need a normaliser
/// in every test.
fn flatten_into(
  out : Array[@ast.Condition],
  c : @ast.Condition,
  op : @ast.LogicalOp,
) -> Unit {
  match c {
    Operation(o, inner) if o == op =>
      for x in inner {
        out.push(x)
      }
    _ => out.push(c)
  }
}

///|
fn Parser::parse_cond_leaf(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
  kind : CondKind,
) -> @ast.Condition raise @err.CssError {
  let c = { toks, pos: 0, }
  match c.next_meaningful() {
    Some(LParen) => {
      let inner = c.take_group_paren()
      if !only_trivia(c.rest()) {
        // Something after the closing paren that is not a logical operator.
        return Unknown(values_of_view(toks))
      }
      // A parenthesised group is either a nested condition or a feature; a
      // nested one is recognised by containing a `(` or a logical word.
      if contains_group_or_logical(inner) {
        return self.parse_cond(inner, start, kind)
      }
      self.parse_feature(inner, start, kind)
    }
    Some(Function(f)) => {
      let args = c.take_group()
      let l = f.to_lower()
      if kind == Supports && l == "selector" {
        return SelectorFn(self.parse_selector_list(args, start))
      }
      Unknown(values_of_view(toks))
    }
    _ => Unknown(values_of_view(toks))
  }
}

///|
/// Whether a parenthesised body is itself a condition rather than a feature.
fn contains_group_or_logical(toks : ArrayView[@token.Token]) -> Bool {
  let mut depth = 0
  for t in toks {
    let k = t.kind
    if k == LParen {
      if depth == 0 {
        return true
      }
      depth = depth + 1
    } else if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket {
      if depth > 0 {
        depth = depth - 1
      }
    } else if depth == 0 {
      match k {
        Ident(w) => if is_logical(w) { return true }
        _ => ()
      }
    }
  }
  false
}

///|
/// The four shapes a feature test comes in.
fn Parser::parse_feature(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
  kind : CondKind,
) -> @ast.Condition raise @err.CssError {
  // `(display: grid)` inside `@supports` is a declaration, not a feature.
  if kind == Supports && has_top_colon(toks) {
    let sub = {
      src: self.src,
      toks: to_owned_with_eof(toks),
      pos: 0,
      diags: self.diags,
      strict: self.strict,
    }
    match sub.parse_declaration() {
      Some(d) => return Decl(d)
      None => return Unknown(values_of_view(toks))
    }
  }
  // `(name: value)`
  let colon = top_index(toks, Colon)
  if colon >= 0 {
    let name = match first_meaningful(toks[0:colon]) {
      Some(Ident(n)) => n
      _ => return Unknown(values_of_view(toks))
    }
    return Feature(Plain(name, values_of_view(toks[colon + 1:])))
  }
  // A range: one or two comparison operators around a name.
  let ops = range_ops(toks)
  match ops.length() {
    0 =>
      match first_meaningful(toks) {
        Some(Ident(n)) if only_one_meaningful(toks) => Feature(Boolean(n))
        _ => Unknown(values_of_view(toks))
      }
    1 => {
      let (at, op, width) = ops[0]
      let left = toks[0:at]
      let right = toks[at + width:]
      match first_meaningful(left) {
        Some(Ident(n)) if only_one_meaningful(left) =>
          Feature(Range(n, op, values_of_view(right)))
        _ =>
          // The name is on the right: `40rem >= width` means `width <= 40rem`.
          match first_meaningful(right) {
            Some(Ident(n)) if only_one_meaningful(right) =>
              Feature(Range(n, flip(op), values_of_view(left)))
            _ => Unknown(values_of_view(toks))
          }
      }
    }
    2 => {
      let (a1, op1, w1) = ops[0]
      let (a2, op2, w2) = ops[1]
      let lo = toks[0:a1]
      let mid = toks[a1 + w1:a2]
      let hi = toks[a2 + w2:]
      match first_meaningful(mid) {
        Some(Ident(n)) =>
          Feature(Interval(values_of_view(lo), op1, n, op2, values_of_view(hi)))
        _ => Unknown(values_of_view(toks))
      }
    }
    _ => {
      self.error(BadMediaQuery, span_of_view(toks, start))
      Unknown(values_of_view(toks))
    }
  }
}

///|
fn flip(op : @ast.RangeOp) -> @ast.RangeOp {
  match op {
    Lt => Gt
    Le => Ge
    Gt => Lt
    Ge => Le
    Eq => Eq
  }
}

///|
/// The comparison operators at the top level, with their index and token width.
///
/// `<=` is two delimiters, because the tokenizer does not fuse them -- outside
/// a media query those two characters are unrelated.
fn range_ops(toks : ArrayView[@token.Token]) -> Array[(Int, @ast.RangeOp, Int)] {
  let out : Array[(Int, @ast.RangeOp, Int)] = []
  let mut depth = 0
  let mut i = 0
  while i < toks.length() {
    let k = toks[i].kind
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket {
      if depth > 0 {
        depth = depth - 1
      }
    } else if depth == 0 {
      let nxt = if i + 1 < toks.length() { toks[i + 1].kind } else { Eof }
      match k {
        Delim('<') =>
          if nxt == Delim('=') {
            out.push((i, Le, 2))
            i = i + 1
          } else {
            out.push((i, Lt, 1))
          }
        Delim('>') =>
          if nxt == Delim('=') {
            out.push((i, Ge, 2))
            i = i + 1
          } else {
            out.push((i, Gt, 1))
          }
        Delim('=') => out.push((i, Eq, 1))
        _ => ()
      }
    }
    i = i + 1
  }
  out
}

// ------------------------------------------------------------------ helpers

///|
fn only_trivia(toks : ArrayView[@token.Token]) -> Bool {
  for t in toks {
    if !t.is_trivia() {
      return false
    }
  }
  true
}

///|
fn only_one_meaningful(toks : ArrayView[@token.Token]) -> Bool {
  let mut n = 0
  for t in toks {
    if !t.is_trivia() {
      n = n + 1
    }
  }
  n == 1
}

///|
fn has_top_colon(toks : ArrayView[@token.Token]) -> Bool {
  top_index(toks, Colon) >= 0
}

///|
fn top_index(toks : ArrayView[@token.Token], what : @token.TokenKind) -> Int {
  let mut depth = 0
  for i, t in toks {
    let k = t.kind
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket || k == RBrace {
      if depth > 0 {
        depth = depth - 1
      }
    } else if depth == 0 && k == what {
      return i
    }
  }
  -1
}

///|
fn split_top(
  toks : ArrayView[@token.Token],
  sep : @token.TokenKind,
) -> Array[ArrayView[@token.Token]] {
  let out : Array[ArrayView[@token.Token]] = []
  let mut from = 0
  let mut depth = 0
  for i, t in toks {
    let k = t.kind
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket || k == RBrace {
      if depth > 0 {
        depth = depth - 1
      }
    } else if depth == 0 && k == sep {
      out.push(toks[from:i])
      from = i + 1
    }
  }
  out.push(toks[from:])
  out
}

///|
fn split_on_word(
  toks : ArrayView[@token.Token],
  word : String,
) -> Array[ArrayView[@token.Token]] {
  let out : Array[ArrayView[@token.Token]] = []
  let mut from = 0
  let mut depth = 0
  for i, t in toks {
    let k = t.kind
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket || k == RBrace {
      if depth > 0 {
        depth = depth - 1
      }
    } else if depth == 0 {
      match k {
        Ident(w) =>
          if w.to_lower() == word {
            out.push(toks[from:i])
            from = i + 1
          }
        _ => ()
      }
    }
  }
  out.push(toks[from:])
  out
}

///|
fn drop_first_meaningful(
  toks : ArrayView[@token.Token],
) -> ArrayView[@token.Token] {
  for i, t in toks {
    if !t.is_trivia() {
      return toks[i + 1:]
    }
  }
  toks[toks.length():]
}

///|
fn Parser::bogus_view(
  self : Parser,
  kind : @kind.ErrorKind,
  toks : ArrayView[@token.Token],
  start : Int,
) -> @ast.Bogus raise @err.CssError {
  let sp = span_of_view(toks, start)
  self.bogus(kind, sp.start, sp.end)
}

///|
/// A token view, copied into an owned array with an `Eof` on the end.
///
/// Needed where a sub-parser has to run over a slice: `Parser` reads
/// `self.toks[self.pos]` unguarded and relies on `Eof` being there, which a
/// bare slice does not have.
fn to_owned_with_eof(toks : ArrayView[@token.Token]) -> Array[@token.Token] {
  let out = []
  for t in toks {
    out.push(t)
  }
  let at = if toks.length() == 0 { 0 } else { toks[toks.length() - 1].span.end }
  out.push({ kind: Eof, span: @span.Span::at(at), })
  out
}

// ------------------------------------------------------- ValCursor extras

///|
fn ValCursor::next_meaningful(self : ValCursor) -> @token.TokenKind? {
  while !self.at_end() {
    let t = self.toks[self.pos]
    self.pos = self.pos + 1
    if !t.is_trivia() {
      return Some(t.kind)
    }
  }
  None
}

///|
fn ValCursor::rest(self : ValCursor) -> ArrayView[@token.Token] {
  self.toks[self.pos:]
}

///|
/// Positioned just after a `Function` token: its arguments, up to the `)`.
fn ValCursor::take_group(self : ValCursor) -> ArrayView[@token.Token] {
  let from = self.pos
  let mut depth = 1
  while !self.at_end() {
    let k = self.toks[self.pos].kind
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen {
      depth = depth - 1
      if depth == 0 {
        let out = self.toks[from:self.pos]
        self.pos = self.pos + 1
        return out
      }
    }
    self.pos = self.pos + 1
  }
  self.toks[from:self.pos]
}

///|
/// Positioned just after a `LParen`.
fn ValCursor::take_group_paren(self : ValCursor) -> ArrayView[@token.Token] {
  self.take_group()
}

// --------------------------------------------------- small prelude readers

///|
fn split_container_name(
  toks : ArrayView[@token.Token],
) -> (String?, ArrayView[@token.Token]) {
  for i, t in toks {
    if t.is_trivia() {
      continue
    }
    match t.kind {
      // A bare leading identifier that is not a logical word is the container
      // name; anything else means the prelude is all condition.
      Ident(w) =>
        if is_logical(w) {
          return (None, toks)
        } else {
          return (Some(w), toks[i + 1:])
        }
      _ => return (None, toks)
    }
  }
  (None, toks)
}

///|
fn page_selectors(toks : ArrayView[@token.Token]) -> Array[String] {
  let out = []
  let buf = StringBuilder()
  for t in toks {
    match t.kind {
      Comma => {
        let s = buf.to_string()
        if s != "" {
          out.push(s)
        }
        buf.reset()
      }
      Ident(n) => buf.write_string(n)
      Colon => buf.write_string(":")
      Whitespace | Comment(_) => ()
      _ => ()
    }
  }
  let s = buf.to_string()
  if s != "" {
    out.push(s)
  }
  out
}

///|
fn layer_names(toks : ArrayView[@token.Token]) -> Array[@ast.LayerName] {
  let out : Array[@ast.LayerName] = []
  for part in split_top(toks, Comma) {
    let parts = layer_parts(part)
    if parts.length() > 0 {
      out.push({ parts, })
    }
  }
  out
}

///|
fn layer_parts(toks : ArrayView[@token.Token]) -> Array[String] {
  let out = []
  for t in toks {
    match t.kind {
      Ident(n) => out.push(n)
      _ => ()
    }
  }
  out
}

///|
fn namespace_parts(toks : ArrayView[@token.Token]) -> (String?, String?) {
  let c = { toks, pos: 0, }
  match c.next_meaningful() {
    Some(Str(u)) => (None, Some(u))
    Some(Url(u)) => (None, Some(u))
    Some(Ident(p)) =>
      match c.next_meaningful() {
        Some(Str(u)) => (Some(p), Some(u))
        Some(Url(u)) => (Some(p), Some(u))
        _ => (Some(p), None)
      }
    _ => (None, None)
  }
}

///|
/// `@scope (start) to (end)`, either half optional.
fn Parser::split_scope(
  self : Parser,
  toks : ArrayView[@token.Token],
  start : Int,
) -> (Array[@ast.Selector]?, Array[@ast.Selector]?) raise @err.CssError {
  let c = { toks, pos: 0, }
  let mut s : Array[@ast.Selector]? = None
  let mut e : Array[@ast.Selector]? = None
  while true {
    match c.next_meaningful() {
      None => break
      Some(LParen) => {
        let inner = c.take_group_paren()
        if s is None {
          s = Some(self.parse_selector_list(inner, start))
        } else {
          e = Some(self.parse_selector_list(inner, start))
        }
      }
      Some(Ident(_)) => ()
      Some(_) => ()
    }
  }
  (s, e)
}