///|
pub(all) suberror JinjaError {
  LexerError(String)
  ParseError(String)
  RenderError(String)
}

///|
/// UTF-8 byte and human-readable source range.
pub(all) struct SourceSpan {
  start : Int
  end : Int
  line : Int
  column : Int
  end_line : Int
  end_column : Int
} derive(Eq, Debug)

///|
/// Structured view over a lexer, parser, or render failure.
pub struct ErrorDiagnostic {
  priv stage : ErrorStage
  priv message : String
  priv template_name : String?
  priv span : SourceSpan?
  priv template_trace : Array[String]
}

///|
pub fn ErrorDiagnostic::stage(self : ErrorDiagnostic) -> ErrorStage {
  self.stage
}

///|
pub fn ErrorDiagnostic::message(self : ErrorDiagnostic) -> String {
  self.message
}

///|
pub fn ErrorDiagnostic::template_name(self : ErrorDiagnostic) -> String? {
  self.template_name
}

///|
pub fn ErrorDiagnostic::span(self : ErrorDiagnostic) -> SourceSpan? {
  self.span
}

///|
pub fn ErrorDiagnostic::template_trace(self : ErrorDiagnostic) -> Array[String] {
  self.template_trace.copy()
}

///|
pub(all) enum ErrorStage {
  Lexer
  Parser
  Render
} derive(Eq, Debug)

///|
pub fn JinjaError::stage(self : JinjaError) -> ErrorStage {
  match self {
    LexerError(_) => Lexer
    ParseError(_) => Parser
    RenderError(_) => Render
  }
}

///|
pub fn JinjaError::message(self : JinjaError) -> String {
  match self {
    LexerError(message) | ParseError(message) | RenderError(message) => message
  }
}

///|
pub fn JinjaError::diagnostic(self : JinjaError) -> ErrorDiagnostic {
  let message = self.message()
  let template_trace = diagnostic_template_trace(message)
  let template_name = template_trace.last()
  {
    stage: self.stage(),
    message,
    template_name,
    span: diagnostic_span(message),
    template_trace,
  }
}

///|
fn diagnostic_template_trace(message : String) -> Array[String] {
  let marker = "Template '"
  let trace : Array[String] = []
  let mut remaining = message
  while remaining.find(marker) is Some(offset) {
    let start = offset + marker.length()
    let tail = remaining[start:].to_owned()
    guard tail.find("'") is Some(length) else { break }
    trace.push(tail[:length].to_owned())
    remaining = tail[length + 1:].to_owned()
  }
  trace
}

///|
fn parse_positive_int(text : String) -> Int? {
  if text == "" {
    return None
  }
  let mut value = 0
  for char in text {
    if char < '0' || char > '9' {
      return None
    }
    value = value * 10 + char.to_int() - '0'.to_int()
  }
  Some(value)
}

///|
fn diagnostic_span(message : String) -> SourceSpan? {
  guard message.find(" (bytes ") is Some(bytes_marker) else { return None }
  let bytes_tail = message[bytes_marker + 8:].to_owned()
  guard bytes_tail.find("..") is Some(separator) else { return None }
  guard bytes_tail.find(")") is Some(close) else { return None }
  guard parse_positive_int(bytes_tail[:separator].to_owned()) is Some(start) else {
    return None
  }
  guard parse_positive_int(bytes_tail[separator + 2:close].to_owned())
    is Some(end) else {
    return None
  }
  let before_bytes = message[:bytes_marker].to_owned()
  guard before_bytes.rev_find(" at ") is Some(at_marker) else { return None }
  let position = before_bytes[at_marker + 4:].to_owned()
  guard position.find(":") is Some(colon) else { return None }
  guard parse_positive_int(position[:colon].to_owned()) is Some(line) else {
    return None
  }
  guard parse_positive_int(position[colon + 1:].to_owned()) is Some(column) else {
    return None
  }
  Some({ start, end, line, column, end_line: line, end_column: column })
}