///|
/// What kind of failure a `PeleError` reports, as pele's `PeleErrorCode`.
pub(all) enum ErrorCode {
  UnsupportedDiagram
  Syntax
  Semantic
  Limit
}

///|
/// The code as pele spells it.
pub fn ErrorCode::to_string(self : ErrorCode) -> String {
  match self {
    UnsupportedDiagram => "unsupported-diagram"
    Syntax => "syntax"
    Semantic => "semantic"
    Limit => "limit"
  }
}

///|
/// The details of a rejected diagram.
/// `kind` and `mermaid_type` are `None` until the diagram type is known.
pub(all) struct Failure {
  message : String
  code : ErrorCode
  kind : String?
  mermaid_type : String?
  line : Int32
  column : Int32
  snippet : String
}

///|
/// A diagram that pele cannot render, with the same codes and messages as pele's `PeleError`.
pub(all) suberror PeleError {
  PeleError(Failure)
}

///|
/// Builds a `PeleError`; `code` defaults to `Syntax` as in pele.
pub fn pele_error(
  message : String,
  code~ : ErrorCode = Syntax,
  kind? : String,
  mermaid_type? : String,
  line~ : Int32 = 0,
  column~ : Int32 = 0,
  snippet~ : String = "",
) -> PeleError {
  PeleError({ message, code, kind, mermaid_type, line, column, snippet, })
}

///|
/// An input range `[from, to)` replaced by `length` output characters.
pub type Edit = (Int32, Int32, Int32)

///|
/// Where the output offset `at` of one pass that made `edits` comes from in that pass's input.
/// An offset inside a replacement maps into what it replaced, no further than its last character.
fn step_input(edits : Array[Edit], at : Int32) -> Int32 {
  let mut shift = 0
  for edit in edits {
    let (from, to, length) = edit
    let start = from - shift
    if at < start {
      break
    }
    if at < start + length {
      let inside = at - start
      return from +
        (if inside < to - from - 1 { inside } else { to - from - 1 })
    }
    shift += to - from - length
  }
  at + shift
}

///|
/// Where the output offset `at` comes from in the input of the first of `steps`, each of which
/// lists the edits one pass made, in order.
pub fn input_offset(steps : Array[Array[Edit]], at : Int32) -> Int32 {
  let mut at = at
  for s = steps.length() - 1; s >= 0; s = s - 1 {
    at = step_input(steps[s], at)
  }
  at
}

///|
/// The text being parsed, and how to find an offset of it in the source as written.
pub(all) struct Origin {
  parsed : String
  find : (Int32) -> (String, Int32)
}

///|
let origin : Ref[Origin?] = { val: None, }

///|
/// Runs `run` with positions in `next.parsed` reported in the source it was made from.
pub fn[T] locating_in(
  next : Origin,
  run : () -> T raise PeleError,
) -> T raise PeleError {
  let outer = origin.val
  origin.val = Some(next)
  defer {
    origin.val = outer
  }
  run()
}

///|
/// The source as written and the offset in it that `offset` in `src` comes from, when `src` is
/// exactly the text being parsed; otherwise `src` and `offset` themselves.
pub fn written(src : String, offset : Int32) -> (String, Int32) {
  match origin.val {
    Some(found) if src.length() == found.parsed.length() && src == found.parsed =>
      (found.find)(offset)
    _ => (src, offset)
  }
}

///|
/// Where `offset` is in `src`: its line and column from 1, and its line with a caret under it.
pub fn locate(src : String, offset : Int32) -> (Int32, Int32, String) {
  let mut end = src.length()
  while end > 0 && @util.is_space(src.unsafe_get(end - 1).to_int()) {
    end -= 1
  }
  // The end of input is reported on the last line with content rather than after the trailing
  // blank lines, which would show an empty snippet.
  let (src, offset) = if end > 0 && offset >= end {
    let (src, last) = written(src, end - 1)
    (src, last + 1)
  } else {
    written(src, offset)
  }
  let mut line = 1
  let mut line_start = 0
  let mut i = @util.index_of(src, "\n")
  while i != -1 && i < offset {
    line += 1
    line_start = i + 1
    i = @util.index_of(src, "\n", from=i + 1)
  }
  let found_end = @util.index_of(src, "\n", from=offset)
  let line_end = if found_end == -1 { src.length() } else { found_end }
  let column = offset - line_start + 1
  let text = src.view(start_offset=line_start, end_offset=line_end)
  // A line of a hundred million characters copied once for each piece added to it would be held
  // several times over, so the snippet is built at its final size, which needs no copy at the end.
  let dashes = if column > 1 { column - 1 } else { 0 }
  let snippet = StringBuilder(size_hint=(text.length() + 1 + dashes + 1) * 2)
  snippet.write_view(text)
  snippet.write_char('\n')
  for _ in 0.. PeleError {
  let (line, column, snippet) = locate(src, offset)
  let (head, tail) = if lexical {
    ("Lexical error on line \{line}. Unrecognized text.\n", "")
  } else {
    ("Parse error on line \{line}:\n", "\n\{detail}")
  }
  let message = StringBuilder(
    size_hint=(head.length() + snippet.length() + tail.length()) * 2,
  )
  message.write_string(head)
  message.write_string(snippet)
  message.write_string(tail)
  let message = message.to_string()
  pele_error(message, code=Syntax, kind~, line~, column~, snippet~)
}