///|
/// Why a `\u{...}` escape is wrong (`InvalidUnicodeEscapeError` in the
/// compiler's `parse/error.rs`).
pub(all) enum UnicodeEscapeError {
  MissingOpeningBrace
  ExpectedHexDigitOrCloseBrace
  InvalidNumberOfHexDigits
  InvalidCodepoint
} derive(Eq, Debug)

///|
/// The lexical errors of the compiler's `LexicalErrorType`, one variant each.
pub(all) enum LexicalErrorType {
  BadStringEscape
  InvalidUnicodeEscape(UnicodeEscapeError)
  DigitOutOfRadix
  NumTrailingUnderscore
  RadixIntNoValue
  MissingExponent
  UnexpectedStringEnd
  UnrecognizedToken(Char)
  InvalidTripleEqual
  MergeConflictIndicator
  UnsupportedProceduralOperator
  VisuallySimilarInvalidCharacter(name~ : String, correct~ : String)
} derive(Eq, Debug)

///|
/// The variant name, as the rejection check compares it with the oracle.
pub fn LexicalErrorType::kind(self : LexicalErrorType) -> String {
  match self {
    BadStringEscape => "BadStringEscape"
    InvalidUnicodeEscape(MissingOpeningBrace) =>
      "InvalidUnicodeEscape.MissingOpeningBrace"
    InvalidUnicodeEscape(ExpectedHexDigitOrCloseBrace) =>
      "InvalidUnicodeEscape.ExpectedHexDigitOrCloseBrace"
    InvalidUnicodeEscape(InvalidNumberOfHexDigits) =>
      "InvalidUnicodeEscape.InvalidNumberOfHexDigits"
    InvalidUnicodeEscape(InvalidCodepoint) =>
      "InvalidUnicodeEscape.InvalidCodepoint"
    DigitOutOfRadix => "DigitOutOfRadix"
    NumTrailingUnderscore => "NumTrailingUnderscore"
    RadixIntNoValue => "RadixIntNoValue"
    MissingExponent => "MissingExponent"
    UnexpectedStringEnd => "UnexpectedStringEnd"
    UnrecognizedToken(_) => "UnrecognizedToken"
    InvalidTripleEqual => "InvalidTripleEqual"
    MergeConflictIndicator => "MergeConflictIndicator"
    UnsupportedProceduralOperator => "UnsupportedProceduralOperator"
    VisuallySimilarInvalidCharacter(..) => "VisuallySimilarInvalidCharacter"
  }
}

///|
/// The compiler's label text (`LexicalError::to_parse_error_info`).
pub fn LexicalErrorType::label(self : LexicalErrorType) -> String {
  match self {
    BadStringEscape => "I don't understand this escape code"
    DigitOutOfRadix => "This digit is too big for the specified radix"
    NumTrailingUnderscore => "Numbers cannot have a trailing underscore"
    RadixIntNoValue => "This integer has no value"
    MissingExponent => "This float is missing an exponent"
    UnexpectedStringEnd => "The string starting here was left open"
    UnrecognizedToken(';') => "Remove this semicolon"
    UnrecognizedToken('\'') => "Unexpected single quote"
    UnrecognizedToken(_) => "I can't figure out what to do with this character"
    InvalidUnicodeEscape(MissingOpeningBrace) =>
      "Expected '{' in Unicode escape sequence"
    InvalidUnicodeEscape(ExpectedHexDigitOrCloseBrace) =>
      "Expected hex digit or '}' in Unicode escape sequence"
    InvalidUnicodeEscape(InvalidNumberOfHexDigits) =>
      "Expected between 1 and 6 hex digits in Unicode escape sequence"
    InvalidUnicodeEscape(InvalidCodepoint) => "Invalid Unicode codepoint"
    InvalidTripleEqual => "Did you mean `==`?"
    MergeConflictIndicator => "Merge conflict marker found"
    UnsupportedProceduralOperator => "Unknown operator"
    VisuallySimilarInvalidCharacter(..) => "Unexpected character"
  }
}

///|
let merge_conflict_hint : String =
  #|Your code appears to have a merge conflict. Resolve it by removing
  #|the code you do not want to keep.
  #|
  #|If you are in a merge, the top chunk is the checked-out code, and the
  #|bottom chunk is the new code being merged.
  #|
  #|If you are in a rebase, the top chunk is the code being rebased onto,
  #|and the chunk section is the checked-out code which is being rebased.
  #|
  #|More information can be found in the official git documentation:
  #|https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_checking_out_conflicts

///|
let procedural_operator_hint : String =
  #|Gleam is an immutable language and does not have any operators that
  #|change the value of an existing variable. You might want to create a
  #|new variable with the same name as the previous one:
  #|
  #|    let wibble = 10
  #|    let blobby = wibble
  #|    let wibble = wibble + 20
  #|    assert wibble == 30
  #|    assert blobby == 10
  #|

///|
/// The compiler's extra lines (`to_parse_error_info`), in order.
pub fn LexicalErrorType::hints(self : LexicalErrorType) -> Array[String] {
  match self {
    BadStringEscape =>
      [
        "Hint: Add another backslash before it.", "See: https://tour.gleam.run/basics/strings",
      ]
    NumTrailingUnderscore => ["Hint: remove it."]
    MissingExponent => ["Hint: Add an exponent or remove the trailing `e`"]
    UnrecognizedToken(';') =>
      [
        "Hint: Semicolons used to be whitespace and did nothing.", "You can safely remove them without your program changing.",
      ]
    UnrecognizedToken('\'') => ["Hint: Strings are written with double quotes."]
    UnrecognizedToken(_) => ["Hint: Is it a typo?"]
    InvalidUnicodeEscape(MissingOpeningBrace) => ["Hint: Add it."]
    InvalidUnicodeEscape(ExpectedHexDigitOrCloseBrace) =>
      [
        "Hint: Hex digits are digits from 0 to 9 and letters from a to f or A to F.",
      ]
    InvalidTripleEqual =>
      [
        "Gleam uses `==` to check for equality between two values.", "See: https://tour.gleam.run/basics/equality",
      ]
    MergeConflictIndicator => [merge_conflict_hint]
    UnsupportedProceduralOperator => [procedural_operator_hint]
    VisuallySimilarInvalidCharacter(name~, correct~) =>
      [
        wrap(
          "This looks like ascii \{correct}, but it is actually the unicode \{name}.",
        ),
      ]
    DigitOutOfRadix
    | RadixIntNoValue
    | UnexpectedStringEnd
    | InvalidUnicodeEscape(InvalidNumberOfHexDigits)
    | InvalidUnicodeEscape(InvalidCodepoint) => []
  }
}

///|
/// The diagnostic for a lexical error at `location`, in the compiler's shape:
/// title "Syntax error", the label as the message, the compiler's extra
/// lines (`to_parse_error_info`) as the text. The compiler puts its hints
/// for lexical errors in those lines, so `hint` is `None`.
pub fn lexical_diagnostic(
  error : LexicalErrorType,
  location : @span.SrcSpan,
) -> @diagnostic.Diagnostic {
  @diagnostic.Diagnostic::new(
    code="GL-SCAN-001",
    kind=error.kind(),
    severity=Error,
    title="Syntax error",
    message=error.label(),
    span=location,
    text=error.hints().join("\n"),
  )
}

///|
/// The number of UTF-8 bytes of `s` (the compiler measures width in bytes).
fn utf8_length(s : StringView) -> Int32 {
  let mut n = 0
  let mut i = 0
  while i < s.length() {
    let c = s.code_unit_at(i).to_int()
    if c < 0x80 {
      n += 1
      i += 1
    } else if c < 0x800 {
      n += 2
      i += 1
    } else if c >= 0xD800 &&
      c <= 0xDBFF &&
      i + 1 < s.length() &&
      s.code_unit_at(i + 1).to_int() >= 0xDC00 &&
      s.code_unit_at(i + 1).to_int() <= 0xDFFF {
      n += 4
      i += 2
    } else {
      n += 3
      i += 1
    }
  }
  n
}

///|
/// The compiler's `wrap` (`error.rs`): each line longer than 75 bytes is
/// broken at spaces; a word longer than the width is cut into pieces of
/// the width.
pub fn wrap(text : String) -> String {
  let width = 75
  let out : Array[String] = []
  for line in text.split("\n") {
    if utf8_length(line) <= width {
      out.push(line.to_owned())
      continue
    }
    let mut current = ""
    for word in line.split(" ") {
      let word = word.to_owned()
      let is_new_line = current == ""
      let fits = if is_new_line {
        utf8_length(current[:]) + utf8_length(word[:]) <= width
      } else {
        utf8_length(current[:]) + utf8_length(word[:]) + 1 <= width
      }
      if fits {
        current = if is_new_line { word } else { current + " " + word }
      } else {
        if current != "" {
          out.push(current)
          current = ""
        }
        if utf8_length(word[:]) <= width {
          current = word
        } else {
          // Cut a long word into pieces of the width (by code units; the
          // compiler cuts by bytes, which agrees for ASCII).
          let mut rest = word
          while rest.length() > width {
            out.push(rest[:width].to_owned())
            rest = rest[width:].to_owned()
          }
          current = rest
        }
      }
    }
    out.push(current)
  }
  out.join("\n")
}