///|
/// The parser: CSS text to a CSS syntax tree.
///
/// Tolerant by default, which is not a convenience but a requirement. Real
/// stylesheets contain vendor hacks, at-rules newer than any given parser, and
/// the residue of preprocessors. The CSS specification says what to do with all
/// of it -- skip a bad declaration to the next `;`, skip a bad rule to the end
/// of its block, keep going -- and following that is what lets this library be
/// handed Bootstrap and give back a stylesheet rather than an error.
///
/// Strict mode turns the first `Error`-severity diagnostic into a raise. It
/// exists for the caller who is checking their own file rather than consuming
/// someone else's, and for the round-trip property: whatever this library
/// prints must parse strictly.
///
/// Nothing here fabricates. When a construct cannot be read, its source text
/// goes into a `Bogus` node, so the printer can echo it and a stylesheet does
/// not quietly lose the one line the parser did not understand.

///|
/// A parse: the tree, and everything noticed on the way.
pub struct Parsed {
  sheet : @ast.Stylesheet
  diagnostics : Array[@err.Diagnostic]
}

///|
pub fn Parsed::sheet(self : Parsed) -> @ast.Stylesheet {
  self.sheet
}

///|
pub fn Parsed::diagnostics(self : Parsed) -> Array[@err.Diagnostic] {
  self.diagnostics
}

///|
/// Whether anything of `Error` severity was found. Warnings do not count: an
/// unknown at-rule is kept, not failed.
pub fn Parsed::has_errors(self : Parsed) -> Bool {
  for d in self.diagnostics {
    if d.is_error() {
      return true
    }
  }
  false
}

///|
priv struct Parser {
  src : String
  toks : Array[@token.Token]
  mut pos : Int
  diags : Array[@err.Diagnostic]
  strict : Bool
}

///|
/// Parse a stylesheet.
///
/// In strict mode the first error is raised instead of recorded; in tolerant
/// mode this never raises, and `Parsed::has_errors` is how a caller asks.
pub fn parse(
  src : String,
  strict? : Bool = false,
) -> Parsed raise @err.CssError {
  let p = { src, toks: @token.tokenize(src), pos: 0, diags: [], strict, }
  let items = []
  while !p.at_eof() {
    p.skip_trivia_collecting(items)
    if p.at_eof() {
      break
    }
    match p.peek() {
      // `` are legal noise at the top level and mean nothing.
      Cdo | Cdc => p.pos = p.pos + 1
      RBrace | RParen | RBracket => {
        let t = p.cur()
        p.error(UnexpectedCloser(closer_text(t.kind)), t.span)
        p.pos = p.pos + 1
      }
      _ =>
        match p.parse_rule(top=true) {
          Some(r) => items.push(@ast.TopItem::Rule(r))
          None => ()
        }
    }
  }
  let span = @span.Span::new(0, src.length())
  { sheet: { items, span, }, diagnostics: p.diags, }
}

///|
/// Parse and raise on the first error, whatever the mode.
///
/// A convenience for the caller who wants a `Stylesheet` and not a pair.
pub fn parse_strict(src : String) -> @ast.Stylesheet raise @err.CssError {
  parse(src, strict=true).sheet
}

// -------------------------------------------------------------------- cursor

///|
fn Parser::cur(self : Parser) -> @token.Token {
  self.toks[self.pos]
}

///|
fn Parser::peek(self : Parser) -> @token.TokenKind {
  self.toks[self.pos].kind
}

///|
fn Parser::at_eof(self : Parser) -> Bool {
  self.peek() == Eof
}

///|
/// Skip whitespace and comments, discarding both.
fn Parser::skip_trivia(self : Parser) -> Unit {
  while self.toks[self.pos].is_trivia() {
    self.pos = self.pos + 1
  }
}

///|
/// Skip trivia, but keep comments as items.
///
/// Only used where a comment has somewhere to go -- between rules, and between
/// declarations. Inside a value a comment is a token separator and nothing
/// more, so there it is dropped.
fn Parser::skip_trivia_collecting(
  self : Parser,
  items : Array[@ast.TopItem],
) -> Unit {
  while self.toks[self.pos].is_trivia() {
    match self.peek() {
      Comment(body) =>
        items.push(Comment({ text: body, span: self.cur().span, }))
      _ => ()
    }
    self.pos = self.pos + 1
  }
}

///|
fn Parser::skip_trivia_collecting_block(
  self : Parser,
  items : Array[@ast.BlockItem],
) -> Unit {
  while self.toks[self.pos].is_trivia() {
    match self.peek() {
      Comment(body) =>
        items.push(Comment({ text: body, span: self.cur().span, }))
      _ => ()
    }
    self.pos = self.pos + 1
  }
}

///|
fn Parser::text_between(self : Parser, from : Int, to : Int) -> String {
  @span.Span::new(from, to).slice(self.src)
}

///|
fn closer_text(k : @token.TokenKind) -> String {
  match k {
    RBrace => "}"
    RParen => ")"
    RBracket => "]"
    _ => "?"
  }
}

// --------------------------------------------------------------- diagnostics

///|
/// Record a problem. In strict mode this raises instead.
fn Parser::error(
  self : Parser,
  kind : @kind.ErrorKind,
  span : @span.Span,
) -> Unit raise @err.CssError {
  let d = @err.Diagnostic::of_kind(kind, span)
  self.diags.push(d)
  if self.strict && d.is_error() {
    d.raise_()
  }
}

///|
/// Record a problem and build the `Bogus` that stands in its place.
fn Parser::bogus(
  self : Parser,
  kind : @kind.ErrorKind,
  from : Int,
  to : Int,
) -> @ast.Bogus raise @err.CssError {
  let span = @span.Span::new(from, to)
  self.error(kind, span)
  @ast.Bogus::new(kind, span, text=self.text_between(from, to))
}

// ------------------------------------------------------------------ recovery

///|
/// Discard the remnants of a bad declaration: up to a `;`, or to the `}` that
/// closes the block this is inside, whichever comes first. The `}` is left for
/// the caller, because it is theirs.
fn Parser::recover_declaration(self : Parser) -> Unit {
  let mut depth = 0
  while !self.at_eof() {
    let k = self.peek()
    if depth == 0 {
      match k {
        Semicolon => {
          self.pos = self.pos + 1
          return
        }
        RBrace => return
        _ => ()
      }
    }
    if k.is_opener() {
      depth = depth + 1
    } else if k == RBrace || k == RParen || k == RBracket {
      if depth > 0 {
        depth = depth - 1
      }
    }
    self.pos = self.pos + 1
  }
}