///|
/// At-rules.
///
/// The shape is decided before the name is: an at-rule runs to either a `;` or
/// a `{ ... }`, and which one it is has to be known before the prelude can be
/// interpreted. That is biome's split -- `UnknownBlockAtRule` versus
/// `UnknownValueAtRule` -- and it is the reason an at-rule nobody knows can
/// still be printed back correctly.

///|
fn Parser::parse_at_rule(
  self : Parser,
  name : String,
) -> @ast.CssRule? raise @err.CssError {
  let start = self.cur().span.start
  self.pos = self.pos + 1
  let prelude_start = self.pos
  let mut depth = 0
  let mut has_block = false
  while !self.at_eof() {
    let k = self.peek()
    if depth == 0 {
      if k == LBrace {
        has_block = true
        break
      }
      if k == Semicolon {
        break
      }
      if k == RBrace {
        break
      }
    }
    if k.is_opener() {
      depth = depth + 1
    } else if k == RParen || k == RBracket || k == RBrace {
      if depth > 0 {
        depth = depth - 1
      }
    }
    self.pos = self.pos + 1
  }
  let prelude = self.toks[prelude_start:self.pos]
  let lower = name.to_lower()
  let rule = self.build_at_rule(name, lower, prelude, has_block, start)
  // Consume whatever ended the prelude.
  if !has_block && self.peek() == Semicolon {
    self.pos = self.pos + 1
  }
  rule
}

///|
fn Parser::build_at_rule(
  self : Parser,
  name : String,
  lower : String,
  prelude : ArrayView[@token.Token],
  has_block : Bool,
  start : Int,
) -> @ast.CssRule? raise @err.CssError {
  match (lower, has_block) {
    ("media", true) => {
      let queries = self.parse_media_list(prelude, start)
      let body = self.parse_block()
      Some(Media({ queries, body, span: self.span_from(start), }))
    }
    ("supports", true) => {
      let condition = self.parse_condition(prelude, start)
      let body = self.parse_block()
      Some(Supports({ condition, body, span: self.span_from(start), }))
    }
    ("container", true) => {
      let (cname, rest) = split_container_name(prelude)
      let condition = self.parse_condition(rest, start)
      let body = self.parse_block()
      Some(
        Container({ name: cname, condition, body, span: self.span_from(start), }),
      )
    }
    ("keyframes", true) | ("-webkit-keyframes", true) => {
      let kname : @ast.KeyframesName = match first_meaningful(prelude) {
        Some(Ident(n)) => Ident(n)
        Some(Str(s)) => Str(s)
        _ => {
          self.error(BadAtRulePrelude(name), span_of_view(prelude, start))
          Ident("")
        }
      }
      let frames = self.parse_keyframes_body(start)
      Some(Keyframes({ name: kname, frames, span: self.span_from(start), }))
    }
    ("font-face", true) => Some(FontFace(self.parse_decl_block(start)))
    ("counter-style", true) => {
      let cname = match first_meaningful(prelude) {
        Some(Ident(n)) => n
        _ => ""
      }
      Some(CounterStyle(cname, self.parse_decl_block(start)))
    }
    ("property", true) => {
      let pname = match first_meaningful(prelude) {
        Some(Ident(n)) => n
        _ => ""
      }
      Some(
        Property({
          name: pname,
          decls: self.parse_decl_block(start),
          span: self.span_from(start),
        }),
      )
    }
    ("page", true) => {
      let selectors = page_selectors(prelude)
      let body = self.parse_block()
      Some(Page({ selectors, body, span: self.span_from(start), }))
    }
    ("starting-style", true) => {
      let body = self.parse_block()
      Some(StartingStyle(body, self.span_from(start)))
    }
    ("scope", true) => {
      let (s, e) = self.split_scope(prelude, start)
      let body = self.parse_block()
      Some(Scope({ start: s, end: e, body, span: self.span_from(start), }))
    }
    ("layer", true) => {
      let names = layer_names(prelude)
      let body = self.parse_block()
      Some(Layer({ names, body: Some(body), span: self.span_from(start), }))
    }
    ("layer", false) =>
      Some(
        Layer({
          names: layer_names(prelude),
          body: None,
          span: self.span_from(start),
        }),
      )
    ("charset", false) =>
      match first_meaningful(prelude) {
        Some(Str(s)) => Some(Charset(s, self.span_from(start)))
        _ => {
          self.error(BadAtRulePrelude(name), span_of_view(prelude, start))
          Some(
            Unknown({
              name,
              prelude: self.values_of(prelude),
              block: None,
              span: self.span_from(start),
            }),
          )
        }
      }
    ("namespace", false) => {
      let (prefix, url) = namespace_parts(prelude)
      match url {
        Some(u) =>
          Some(Namespace({ prefix, url: u, span: self.span_from(start), }))
        None => {
          self.error(BadAtRulePrelude(name), span_of_view(prelude, start))
          Some(
            Unknown({
              name,
              prelude: self.values_of(prelude),
              block: None,
              span: self.span_from(start),
            }),
          )
        }
      }
    }
    ("import", false) => self.parse_import(name, prelude, start)
    _ => {
      // Not a name this library knows, or a known name in the wrong shape.
      // Either way the rule is kept verbatim -- the diagnostic says which.
      if is_known_at_rule(lower) {
        self.error(BadAtRuleShape(name), self.span_from(start))
      } else {
        self.error(
          UnknownAtRule(name),
          @span.Span::new(start, start + 1 + name.length()),
        )
      }
      let block = if has_block { Some(self.parse_block()) } else { None }
      Some(
        Unknown({
          name,
          prelude: self.values_of(prelude),
          block,
          span: self.span_from(start),
        }),
      )
    }
  }
}

///|
fn Parser::span_from(self : Parser, start : Int) -> @span.Span {
  @span.Span::new(start, self.prev_end())
}

///|
fn is_known_at_rule(lower : String) -> Bool {
  lower == "media" ||
  lower == "supports" ||
  lower == "container" ||
  lower == "keyframes" ||
  lower == "font-face" ||
  lower == "counter-style" ||
  lower == "property" ||
  lower == "page" ||
  lower == "starting-style" ||
  lower == "scope" ||
  lower == "layer" ||
  lower == "charset" ||
  lower == "namespace" ||
  lower == "import"
}

///|
fn first_meaningful(toks : ArrayView[@token.Token]) -> @token.TokenKind? {
  for t in toks {
    if !t.is_trivia() {
      return Some(t.kind)
    }
  }
  None
}

///|
/// A block that may hold only declarations.
fn Parser::parse_decl_block(
  self : Parser,
  start : Int,
) -> @ast.DeclBlock raise @err.CssError {
  let items = self.parse_block()
  // A nested rule here is not a CSS error the parser can fix, but it is one it
  // must not silently accept: the printer would emit something invalid.
  for it in items {
    match it {
      Rule(_) => self.error(RuleInDeclarationContext, self.span_from(start))
      _ => ()
    }
  }
  { decls: items, span: self.span_from(start), }
}

///|
fn Parser::parse_keyframes_body(
  self : Parser,
  start : Int,
) -> Array[@ast.KeyframeBlock] raise @err.CssError {
  let frames : Array[@ast.KeyframeBlock] = []
  if self.peek() != LBrace {
    return frames
  }
  self.pos = self.pos + 1
  while true {
    self.skip_trivia()
    if self.at_eof() {
      self.error(UnclosedBlock("{"), self.span_from(start))
      return frames
    }
    if self.peek() == RBrace {
      self.pos = self.pos + 1
      return frames
    }
    let fstart = self.cur().span.start
    let sel_start = self.pos
    while !self.at_eof() && self.peek() != LBrace && self.peek() != RBrace {
      self.pos = self.pos + 1
    }
    let selectors = self.keyframe_selectors(
      self.toks[sel_start:self.pos],
      fstart,
    )
    if self.peek() != LBrace {
      continue
    }
    let decls = self.parse_decl_block(fstart)
    frames.push({ selectors, decls, span: self.span_from(fstart), })
  }
  frames
}

///|
fn Parser::keyframe_selectors(
  self : Parser,
  toks : ArrayView[@token.Token],
  at : Int,
) -> Array[@ast.KeyframeSelector] raise @err.CssError {
  let out : Array[@ast.KeyframeSelector] = []
  for t in toks {
    match t.kind {
      Ident(w) => {
        let l = w.to_lower()
        if l == "from" {
          out.push(From)
        } else if l == "to" {
          out.push(To)
        } else {
          self.error(BadKeyframeSelector, t.span)
        }
      }
      Percentage(r, v) =>
        out.push(Percentage({ repr: r, value: v, is_int: false, }))
      Comma | Whitespace | Comment(_) => ()
      _ => self.error(BadKeyframeSelector, t.span)
    }
  }
  if out.length() == 0 {
    self.error(BadKeyframeSelector, @span.Span::at(at))
  }
  out
}

///|
fn Parser::parse_import(
  self : Parser,
  name : String,
  prelude : ArrayView[@token.Token],
  start : Int,
) -> @ast.CssRule? raise @err.CssError {
  let c = { toks: prelude, pos: 0, }
  let url = match c.next_meaningful() {
    Some(Str(s)) => s
    Some(Url(u)) => u
    // `url("a.css")` -- a quoted body makes this a function call, not a `Url`
    // token, so both spellings have to be unwrapped to the same string.
    Some(Function(f)) if f.to_lower() == "url" => {
      let args = c.take_group()
      match first_meaningful(args) {
        Some(Str(s)) => s
        Some(Url(u)) => u
        _ => ""
      }
    }
    _ => {
      self.error(BadAtRulePrelude(name), span_of_view(prelude, start))
      return Some(
        Unknown({
          name,
          prelude: self.values_of(prelude),
          block: None,
          span: self.span_from(start),
        }),
      )
    }
  }
  let mut layer : @ast.LayerName?? = None
  let mut supports : @ast.Condition? = None
  let media_from = c.pos
  while true {
    match c.next_meaningful() {
      None => break
      Some(Ident(w)) =>
        if w.to_lower() == "layer" && layer is None {
          layer = Some(None)
        } else {
          c.pos = c.pos - 1
          break
        }
      Some(Function(f)) => {
        let l = f.to_lower()
        let args = c.take_group()
        if l == "layer" {
          layer = Some(Some({ parts: layer_parts(args), }))
        } else if l == "supports" {
          supports = Some(self.parse_condition(args, start))
        } else {
          break
        }
      }
      Some(_) => {
        c.pos = c.pos - 1
        break
      }
    }
  }
  ignore(media_from)
  let media = self.parse_media_list(c.rest(), start)
  Some(Import({ url, layer, supports, media, span: self.span_from(start), }))
}