// The front door: source text in, AST and diagnostics out.
//
// Mirrors the reference's `Parsing.Make.parse_diagnostics`, which returns data
// and prints nothing -- the shape a differential harness needs. Errors are
// never raised out of here: the caller inspects the reports.

///|
/// The result of parsing one file.
pub struct ParseResult {
  /// The module, or None when the parse failed.
  ///
  /// There is no partial tree: MoonYacc has no error recovery, and the
  /// reference's default path likewise aborts at the first syntax error.
  module_ : @ast.LocModule?
  /// Diagnostics, in the order found. At most one, for the same reason.
  errors : Array[@basic.Report]
  /// Comments and blank lines, for the printer.
  trivia : @trivia.Context
}

///|
/// Parse `src`.
///
/// Three failure paths, all funnelled into `errors`:
///
///   * a LEXICAL error, which stops the scan (`lexer.tokens_from_string`);
///   * a SEMANTIC error raised from a grammar action -- recorded rather than
///     raised, since MoonYacc's actions cannot propagate one;
///   * a SYNTAX error from the automaton itself, `ParseError`.
pub fn parse_string(src : String, fname? : String = "") -> ParseResult {
  let ctx = @trivia.Context::new()
  set_context(ctx)
  let lexed = @lexer.tokens_from_string(src, fname~, trivia_ctx=ctx)
  if lexed.errors.length() > 0 {
    // The reference's parser PULLS tokens, so a syntax error earlier in the
    // file is reported before its lexer ever reaches the bad character. This
    // one lexes the whole file first, which would otherwise let a stray `…` on
    // line 2 hide the real error on line 1. Parse what did lex and prefer an
    // error that comes first in the file.
    let errors = match earlier_syntax_error(lexed.tokens, lexed.errors[0]) {
      Some(report) => [report]
      None => lexed.errors
    }
    return { module_: None, errors, trivia: ctx }
  }
  let module_ = parse(lexed.tokens) catch {
    e => {
      // A semantic error recorded EARLIER in the file wins, for the same
      // reason the lexical case above does. The reference raises from the
      // action and stops right there; MoonYacc's actions cannot raise
      // (test/UPSTREAM-FINDINGS.md finding 3), so the parse runs on and can
      // reach a later syntax error that would otherwise be reported instead.
      let syntax = report_of_parse_error(e, lexed.tokens)
      let report : @basic.Report = match taken_error() {
        Some(SyntaxError(loc~, message~, hint~, fix~)) =>
          if loc.start.cnum < syntax.loc.start.cnum {
            { ..@basic.Report::error(loc, message), hint, edit: fix }
          } else {
            syntax
          }
        None => syntax
      }
      return { module_: None, errors: [report], trivia: ctx }
    }
  }
  // A semantic error is recorded rather than raised, so the parse "succeeds"
  // with a tree built partly from placeholders. Discard it.
  match taken_error() {
    Some(SyntaxError(loc~, message~, hint~, fix~)) =>
      {
        module_: None,
        errors: [
          {
            loc,
            severity: Error,
            message,
            warning: None,
            hint,
            edit: fix,
            related: [],
          },
        ],
        trivia: ctx,
      }
    None => { module_: Some(module_), errors: [], trivia: ctx }
  }
}

///|
/// A parse error strictly before where the scan stopped, if there is one.
///
/// The tokens that did lex are a real prefix of the file, so parsing them says
/// what a lazy lexer's parser would have hit first. Only an error at a token
/// BEFORE the lexical one counts: running out of tokens is an artifact of the
/// truncation, not something the reference would report.
fn earlier_syntax_error(
  tokens : Array[(Token, Position, Position)],
  lexical : @basic.Report,
) -> @basic.Report? {
  let cut = lexical.loc.start
  // The scan stops without emitting EOF, and the automaton needs one to make
  // any decision at the end of the prefix.
  let prefix = tokens.copy()
  prefix.push((EOF, cut, cut))
  let found : @basic.Report? = try {
    parse(prefix) |> ignore
    (None : @basic.Report?)
  } catch {
    UnexpectedToken(_, (start, _), _) as e =>
      if start.cnum < cut.cnum {
        Some(report_of_parse_error(e, prefix))
      } else {
        None
      }
    // Out of tokens: the file was cut short by the scan, so this says nothing.
    UnexpectedEndOfInput(_) => None
  } noraise {
    _ => None
  }
  // A grammar action may have recorded a semantic error along the way; it is
  // reported on the same first-in-the-file terms. Draining it either way keeps
  // it from surfacing on the next parse.
  match (found, taken_error()) {
    (Some(_), _) => found
    (None, Some(SyntaxError(loc~, message~, hint~, fix~))) =>
      if loc.start.cnum < cut.cnum {
        Some({
          loc,
          severity: Error,
          message,
          warning: None,
          hint,
          edit: fix,
          related: [],
        })
      } else {
        None
      }
    (None, None) => None
  }
}

///|
/// Turn the automaton's own error into a diagnostic.
fn report_of_parse_error(
  e : ParseError,
  tokens : Array[(Token, Position, Position)],
) -> @basic.Report {
  let (loc, expected) : (@basic.Location, Array[TokenKind]) = match e {
    UnexpectedToken(_, (start, end), expected) => ({ start, end }, expected)
    UnexpectedEndOfInput(pos, expected) => ({ start: pos, end: pos }, expected)
  }
  // The state comes from replaying the parse, so it is only trusted when the
  // replay stopped where the parse did -- a disagreement means the replay no
  // longer mirrors the engine, and a message keyed on the wrong state would be
  // worse than no message at all.
  guard error_state(tokens) is Some(st) else {
    return @basic.Report::error(loc, expecting_message(expected))
  }
  let agrees = match e {
    UnexpectedToken(_, (start, end), _) =>
      st.token_index < tokens.length() &&
      tokens[st.token_index].1 == start &&
      tokens[st.token_index].2 == end
    UnexpectedEndOfInput(_) => st.token_index >= tokens.length()
  }
  guard agrees else {
    return @basic.Report::error(loc, expecting_message(expected))
  }
  syntax_report(st, tokens, loc, expected)
}

///|
/// The diagnostic for a failure the parser has already located.
///
/// The message is the reference's own where the state it failed in is one the
/// reference has a message for -- that is what `parser_messages.mbt` is, and it
/// is the only way to reach the "Assuming that the X is complete" subject,
/// which names a grammar symbol nothing in MoonYacc's error value identifies.
/// Otherwise it is the locally rendered acceptable-token list: less specific,
/// never wrong.
///
/// Shared with recovery (`recover.mbt`), which reaches the same situation
/// without an exception to catch: it holds the stack already.
fn syntax_report(
  st : ErrorState,
  tokens : Array[(Token, Position, Position)],
  loc : @basic.Location,
  expected : Array[TokenKind],
) -> @basic.Report {
  match reference_message(st.stack[:], expected_signature(expected)) {
    None => @basic.Report::error(loc, expecting_message(expected))
    Some(m) => {
      let report = @basic.Report::error(loc, m.text)
      // Emission order, as the reference's runtime resolves them: the subject
      // first, the delimiter hint second.
      if m.subject != "" && st.subject_span() is Some((start, end)) {
        report.related.push({ loc: { start, end }, message: m.subject })
      }
      if m.opener != "" &&
        enclosing_opener(tokens, st.token_index) is Some((start, end)) {
        report.related.push({ loc: { start, end }, message: m.opener })
      }
      report
    }
  }
}

///|
/// A signature of the acceptable-token set, for the message table.
///
/// A stack suffix alone is not enough to borrow the reference's message: the
/// same top frames occur in contexts whose continuations differ (a `type`
/// definition inside a `rec { … }` group accepts `'}'`, the same construct at
/// the top level does not), and a message from the wrong one claims a token is
/// legal when it is not. The acceptable set IS that difference, so the table
/// records it and the lookup checks it.
///
/// Deliberately the raw spellings rather than the rendered message: the
/// rendering has opinions (class collapse, ordering) that may change, and this
/// wants to change only when the automaton does.
pub fn expected_signature(expected : Array[TokenKind]) -> String {
  let names = []
  for k in expected {
    names.push(k.to_expect_string())
  }
  names.sort_by((a, b) => a.lexical_compare(b))
  names.join(" ")
}

///|
/// Does this borrowed message claim only tokens this state really accepts?
///
/// The second half of the message-table lookup. A recorded signature that
/// matches is proof the context is the same one; this is the weaker evidence
/// used when it does not, and it is what the reader would check: every token
/// the message NAMES must be legal here. It lets a message through when the
/// recorded context differed in a token the message never mentions (the
/// function-type state reached with `->` still ahead of it, say), and keeps out
/// the one that matters -- a message from elsewhere in the grammar, offering a
/// `'}'` that would close nothing.
///
/// Says nothing about a message with no quoted token in it ("Expecting an
/// expression."), which is then borrowed on the stack key alone.
fn message_fits(message : String, signature : String) -> Bool {
  let padded = " " + signature + " "
  let chars = message.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] != '\'' {
      i += 1
      continue
    }
    // `'''` is the QUOTE token, whose spelling is a quote between quotes.
    let (spelling, next) = if i + 2 < chars.length() &&
      chars[i + 1] == '\'' &&
      chars[i + 2] == '\'' {
      ("'''", i + 3)
    } else {
      let mut j = i + 1
      while j < chars.length() && chars[j] != '\'' {
        j += 1
      }
      if j >= chars.length() {
        return true // an unpaired quote is prose, not a token
      }
      (String::from_array(chars[i:j + 1].to_owned()), j + 1)
    }
    if !padded.contains(" " + spelling + " ") {
      return false
    }
    i = next
  }
  true
}

///|
/// Render the automaton's acceptable-token set.
///
/// MoonYacc hands back the exact set of token kinds that could have been
/// shifted. The reference's generator (`stele`) works from a richer set -- the
/// acceptable *symbols*, nonterminals included, so it says "an expression"
/// where we can only enumerate what an expression may start with -- but the
/// rendering of the list itself is reproduced here: the class collapse, then
/// sort-and-dedupe on the rendered text, then the Oxford-comma join.
///
/// Two pieces of the reference's rendering are deliberately absent:
///
///   * its `<=5` cap, which degrades a longer list to a bare "Syntax error".
///     Without nonterminal names our lists routinely exceed 5 -- a set the
///     reference renders as "an expression" reaches us as its 31-token FIRST
///     set -- so applying the cap would erase almost every message we have.
///     It belongs with the nonterminal names, not before them.
///   * the `Assuming that the X is complete` subject, which needs the error
///     state's item set. See task 4 of implementation-plan.md.
fn expecting_message(expected : Array[TokenKind]) -> String {
  if expected.is_empty() {
    return "Syntax error."
  }
  // A class collapses only when >=2 of its members are legal here, so count
  // before rendering. Counting distinct SPELLINGS, since the collapse is about
  // how many entries the list would otherwise carry.
  let counts : Map[String, Int] = Map([])
  let seen_spellings = []
  for k in expected {
    let spelling = k.to_expect_string()
    if seen_spellings.contains(spelling) {
      continue
    }
    seen_spellings.push(spelling)
    match k.expect_class() {
      Some(c) => counts[c] = counts.get(c).unwrap_or(0) + 1
      None => ()
    }
  }
  let names = []
  for k in expected {
    let name = match k.expect_class() {
      Some(c) if counts.get(c).unwrap_or(0) >= 2 => c
      _ => k.to_expect_string()
    }
    if !names.contains(name) {
      names.push(name)
    }
  }
  // Sorted by the RENDERED text, as the reference does: quoted spellings sort
  // ahead of readable phrases because `'` precedes every letter.
  //
  // NOT `sort()`: MoonBit's `Compare` for String is SHORTLEX -- it orders by
  // length first -- which would put `'{'` before `'do'`. OCaml's
  // `String.compare`, which the reference sorts with, is plain lexicographic,
  // and that is `lexical_compare` here.
  names.sort_by((a, b) => a.lexical_compare(b))
  "Expecting \{human_list(names)}."
}

///|
/// Join as English, with the Oxford comma: `a`, `a, or b`, `a, b, or c`.
///
/// Mirrors `format_human_list` in the reference's generator, including the
/// comma before `or` in the two-item case.
fn human_list(items : Array[String]) -> String {
  match items {
    [] => ""
    [x] => x
    [.. rest, last] => rest.join(", ") + ", or " + last
  }
}