///|
/// Everything the three mutually recursive functions share.
priv struct Parser {
  toks : Array[@lexer.Token]
  /// Collected diagnostics. In recovery mode a failure is recorded here and
  /// parsing continues; otherwise the first one is raised.
  diags : Array[@err.Diagnostic]
  recover : Bool
  /// How deep the grammar may recur before it refuses. See
  /// `default_max_depth`.
  max_depth : Int
}

///|
/// Report a failure.
///
/// In recovery mode this records and returns, and the caller carries on --
/// which is what an editor wants, and what makes a file with three mistakes
/// report three. Otherwise it raises, and the first mistake is the answer.
fn Parser::fail(
  self : Parser,
  t : @lexer.Token,
  kind : @err.ErrorKind,
) -> Unit raise @err.ShrubberyError {
  let d = @err.Diagnostic::new(kind, t.span)
  self.diags.push(d)
  if !self.recover {
    raise ShrubberyError(d)
  }
}

///|
/// The mixed-tabs failure, raised where a comparison turns out to have no
/// answer. Passed as a thunk in the reference; here the comparison is checked
/// at each site, which is the same thing written out.
fn Parser::incomparable(
  self : Parser,
  t : @lexer.Token,
) -> Unit raise @err.ShrubberyError {
  self.fail(t, IncomparableIndentation)
}

///|
fn Parser::at_end(self : Parser, i : Int) -> Bool {
  i >= self.toks.length()
}

///|
fn Parser::tok(self : Parser, i : Int) -> @lexer.Token {
  self.toks[i]
}

///|
fn Parser::peek(self : Parser, i : Int) -> @lexer.Token? {
  if i < self.toks.length() {
    Some(self.toks[i])
  } else {
    None
  }
}

///|
/// `a > b`, where an unknown line is never greater.
fn line_gt(a : Int, b : Int?) -> Bool {
  match b {
    Some(v) => a > v
    None => false
  }
}

///|
/// `a == b + n`, where an unknown line matches nothing.
fn line_eq_plus(a : Int, b : Int?, n : Int) -> Bool {
  match b {
    Some(v) => a == v + n
    None => false
  }
}

///|
/// Compare two columns, turning "no answer" into the diagnostic that says so.
fn Parser::col_cmp(
  self : Parser,
  t : @lexer.Token,
  a : @column.Column,
  b : @column.Column,
) -> @column.ColumnOrder raise @err.ShrubberyError {
  let order = a.cmp(b)
  if order is Incomparable {
    self.incomparable(t)
  }
  order
}

///|
fn Parser::col_lt(
  self : Parser,
  t : @lexer.Token,
  a : @column.Column,
  b : @column.Column,
) -> Bool raise @err.ShrubberyError {
  self.col_cmp(t, a, b) is Lt
}

///|
fn Parser::col_gt(
  self : Parser,
  t : @lexer.Token,
  a : @column.Column,
  b : @column.Column,
) -> Bool raise @err.ShrubberyError {
  self.col_cmp(t, a, b) is Gt
}

///|
fn Parser::col_eq(
  self : Parser,
  t : @lexer.Token,
  a : @column.Column,
  b : @column.Column,
) -> Bool raise @err.ShrubberyError {
  self.col_cmp(t, a, b) is Eq
}

///|
/// What `next_of` and its variants answer.
priv struct Advance {
  rest : Int
  last_line : Int?
  delta : Int
  raw : RawList
}

///|
/// Skip whitespace and comments, and apply `\` continuations.
///
/// A `\` joins the next line to this one, which the reference records as a
/// line-span delta rather than by rewriting positions: the column of a token
/// on the continued line is still its own column, but its LINE counts as the
/// original. That is why a continued line does not have to be indented.
fn Parser::next_of(
  self : Parser,
  start : Int,
  last_line : Int?,
  delta : Int,
  raw : RawList,
  count : Bool,
) -> Advance raise @err.ShrubberyError {
  let mut i = start
  let mut ll = last_line
  let mut d = delta
  let mut r = raw
  for ;; {
    if self.at_end(i) {
      return { rest: i, last_line: Some(ll.unwrap_or(0)), delta: d, raw: r, }
    }
    let t = self.tok(i)
    match t.kind {
      Whitespace | Comment => {
        r = r.push(t)
        i = i + 1
      }
      ContinueOperator => {
        let line = t.line()
        // Only whitespace and comments may follow a `\` on its line.
        let mut j = i + 1
        let mut jr = r.push(t)
        while !self.at_end(j) && self.tok(j).is_trivia() {
          jr = jr.push(self.tok(j))
          j = j + 1
        }
        if !self.at_end(j) && (!count || self.tok(j).line() == line) {
          if count {
            self.fail(t, ContinuationFollowedByToken)
          }
          // With counting off, or after reporting, treat it as whitespace.
          i = j
          r = jr
        } else {
          // A line with only whitespace after the `\` does not count as the
          // next line, so any number of them may sit between the two halves.
          let continues = !count || ll is None || line_eq_plus(line, ll, 0)
          ll = if continues { None } else { ll }
          d = d + 1
          i = j
          r = jr
        }
      }
      _ => {
        let line = t.line()
        let new_delta = if !count || ll is None || line_eq_plus(line, ll, 0) {
          d
        } else {
          0
        }
        return {
          rest: i,
          last_line: Some(ll.unwrap_or(line)),
          delta: new_delta,
          raw: r,
        }
      }
    }
  }
}

///|
/// Whether the token at `i` starts a line after `last_line`.
fn Parser::next_line(
  self : Parser,
  i : Int,
  last_line : Int?,
  count : Bool,
) -> Bool {
  count && !self.at_end(i) && line_gt(self.tok(i).line(), last_line)
}

///|
/// Two tokens that must be on one line, such as `:` and the `«` after it.
fn Parser::check_same_line(
  self : Parser,
  t : @lexer.Token,
  next_t : @lexer.Token,
  count : Bool,
) -> Unit raise @err.ShrubberyError {
  if count && t.line() != next_t.line() {
    self.fail(next_t, NotOnSameLine(preceding=t.text))
  }
}

///|
fn Parser::fail_no_comment_group(
  self : Parser,
  t : @lexer.Token,
) -> Unit raise @err.ShrubberyError {
  self.fail(t, NoGroupForTermComment)
}

///|
/// `next_of`, then collect a `#//` that is alone on its line.
fn Parser::next_of_commenting(
  self : Parser,
  start : Int,
  last_line : Int?,
  delta : Int,
  raw : RawList,
  count : Bool,
) -> (@lexer.Token?, Advance) raise @err.ShrubberyError {
  let a = self.next_of(start, last_line, delta, raw, count)
  if self.at_end(a.rest) {
    return (None, a)
  }
  let (commenting, _use_t, use_i, ll, d, r) = self.own_line_group_comment(
    a.rest,
    a.last_line,
    a.delta,
    a.raw,
    count,
  )
  (commenting, { rest: use_i, last_line: ll, delta: d, raw: r, })
}

///|
/// `next_of`, then recognise an immediate `«`.
fn Parser::next_of_opener(
  self : Parser,
  start : Int,
  last_line : Int?,
  delta : Int,
  raw : RawList,
  count : Bool,
) -> (@lexer.Token?, Advance) raise @err.ShrubberyError {
  let a = self.next_of(start, last_line, delta, raw, count)
  if self.at_end(a.rest) {
    return (None, a)
  }
  let t = self.tok(a.rest)
  if t.kind is Opener && t.text == "\u{AB}" {
    (
      Some(t),
      {
        rest: a.rest + 1,
        last_line: Some(t.line()),
        delta: a.delta,
        raw: a.raw.push(t),
      },
    )
  } else {
    (None, a)
  }
}

///|
/// Gather `#//`s that sit alone on their line.
///
/// A `#//` alone on a line does not take part in indentation — it comments out
/// whatever group comes next, wherever that is — while one with more on its
/// line determines that group's column. Deciding which is what this does, and
/// it has to look ahead past whitespace to find out.
///
/// Returns the pending comment, the token now at the front, the index of that
/// token, and the advanced line, delta and raw.
fn Parser::own_line_group_comment(
  self : Parser,
  start : Int,
  line : Int?,
  delta : Int,
  raw : RawList,
  count : Bool,
) -> (@lexer.Token?, @lexer.Token, Int, Int?, Int, RawList) raise @err.ShrubberyError {
  let mut commenting : @lexer.Token? = None
  let mut i = start
  let mut ll = line
  let mut d = delta
  let mut r = raw
  for ;; {
    if self.at_end(i) {
      // Caller checks; there is no token to hand back, so hand back the index.
      return (commenting, self.toks[self.toks.length() - 1], i, ll, d, r)
    }
    let t = self.tok(i)
    if !(t.kind is GroupComment) {
      return (commenting, t, i, ll, d, r)
    }
    match commenting {
      Some(prev) => self.fail_no_comment_group(prev)
      None => ()
    }
    if count && line_gt(t.line(), ll) {
      let a = self.next_of(i + 1, ll, d, r.push(t), count)
      if self.at_end(a.rest) {
        self.fail_no_comment_group(t)
        return (Some(t), t, a.rest, a.last_line, a.delta, a.raw)
      }
      if self.next_line(a.rest, Some(t.line()), count) {
        commenting = Some(t)
        i = a.rest
        ll = a.last_line
        d = a.delta
        r = a.raw
        continue
      }
      // Not on its own line after all: leave it where it is so that it counts
      // toward the next group's indentation.
      return (commenting, t, i, ll, d, r)
    }
    return (commenting, t, i, ll, d, r)
  }
}

///|
/// Consume whitespace and comments that stay on this line.
///
/// "Stay on this line" is decided by looking for a newline in the text, not by
/// comparing line numbers, because the question is whether the comment belongs
/// to the term before it or to whatever comes next.
fn Parser::suffix_comments(
  self : Parser,
  start : Int,
  line : Int?,
  delta : Int,
) -> (RawList, Int, Int?, Int) {
  let mut i = start
  let mut acc : RawList = RNil
  while !self.at_end(i) {
    let t = self.tok(i)
    match t.kind {
      Whitespace | Comment =>
        if t.text.contains("\n") || t.text.contains("\r") {
          break
        } else {
          acc = acc.push(t)
          i = i + 1
        }
      _ => break
    }
  }
  (acc, i, line, delta)
}