///|
pub(all) struct SourceExcerpt {
  line_number : Int
  column : Int
  source_line : String
  marker : String
} derive(Eq, Debug)

///|
fn line_bounds(source : String, requested_line : Int) -> (Int, Int) {
  let target = requested_line.max(1)
  let mut line = 1
  let mut start = 0
  let mut index = 0
  while index < source.length() && line < target {
    if source[index] == '\n' {
      line = line + 1
      start = index + 1
    }
    index = index + 1
  }
  let mut end = start
  while end < source.length() && source[end] != '\n' && source[end] != '\r' {
    end = end + 1
  }
  (start, end)
}

///|
fn marker_prefix(source_line : String, width : Int) -> String {
  let builder = StringBuilder()
  for index = 0
      index < width && index < source_line.length()
      index = index + 1 {
    if source_line[index] == '\t' {
      builder.write_char('\t')
    } else {
      builder.write_char(' ')
    }
  }
  builder.to_string()
}

///|
fn repeat_marker(count : Int) -> String {
  let builder = StringBuilder()
  for index = 0; index < count.max(1); index = index + 1 {
    builder.write_char('^')
  }
  builder.to_string()
}

///|
/// Extract a single-line source frame for a diagnostic span.
pub fn source_excerpt(source : String, span : SourceSpan) -> SourceExcerpt {
  let (start, end) = line_bounds(source, span.line)
  let source_line = source[start:end].to_owned()
  let column = span.column.max(1)
  let offset = (column - 1).min(source_line.length())
  let available = (source_line.length() - offset).max(1)
  let marker_width = span.length().max(1).min(available)
  let marker = marker_prefix(source_line, offset) + repeat_marker(marker_width)
  { line_number: span.line.max(1), column, source_line, marker }
}

///|
pub fn SourceExcerpt::render(self : SourceExcerpt) -> String {
  " \{self.line_number} | \{self.source_line}\n \{blank_gutter(self.line_number)} | \{self.marker}"
}

///|
fn blank_gutter(line_number : Int) -> String {
  let builder = StringBuilder()
  for index = 0; index < line_number.to_string().length(); index = index + 1 {
    builder.write_char(' ')
  }
  builder.to_string()
}

///|
pub fn Diagnostic::render_with_source(
  self : Diagnostic,
  source : String,
) -> String {
  let excerpt = source_excerpt(source, self.span)
  "\{self.severity.label()}[\{self.code}]: \{self.message}\n \{excerpt.line_number} | \{excerpt.source_line}\n \{blank_gutter(excerpt.line_number)} | \{excerpt.marker}"
}

///|
pub fn EvalError::span(self : EvalError) -> SourceSpan {
  match self {
    UnboundVariable(span~, ..)
    | TypeMismatch(span~, ..)
    | NoSuchField(span~, ..)
    | NoSuchKey(span~, ..)
    | IndexOutOfBounds(span~, ..)
    | DivisionByZero(span~)
    | NumericOverflow(span~)
    | UnknownFunction(span~, ..)
    | InvalidArgument(span~, ..)
    | CostLimitExceeded(span~, ..)
    | DepthLimitExceeded(span~, ..)
    | IterationLimitExceeded(span~, ..) => span
  }
}

///|
pub fn EvalError::code(self : EvalError) -> String {
  match self {
    UnboundVariable(..) => "CEL-EVAL-NAME"
    TypeMismatch(..) => "CEL-EVAL-TYPE"
    NoSuchField(..) => "CEL-EVAL-FIELD"
    NoSuchKey(..) => "CEL-EVAL-KEY"
    IndexOutOfBounds(..) => "CEL-EVAL-INDEX"
    DivisionByZero(..) => "CEL-EVAL-DIV-ZERO"
    NumericOverflow(..) => "CEL-EVAL-OVERFLOW"
    UnknownFunction(..) => "CEL-EVAL-FUNCTION"
    InvalidArgument(..) => "CEL-EVAL-ARGUMENT"
    CostLimitExceeded(..) => "CEL-EVAL-COST"
    DepthLimitExceeded(..) => "CEL-EVAL-DEPTH"
    IterationLimitExceeded(..) => "CEL-EVAL-ITERATION"
  }
}

///|
pub fn EvalError::message(self : EvalError) -> String {
  match self {
    UnboundVariable(name~, ..) => "unbound variable '\{name}'"
    TypeMismatch(expected~, actual~, ..) =>
      "expected \{expected}, found \{actual}"
    NoSuchField(field~, ..) => "no such field '\{field}'"
    NoSuchKey(key~, ..) => "no such key \{key}"
    IndexOutOfBounds(index~, length~, ..) =>
      "index \{index} is outside collection length \{length}"
    DivisionByZero(..) => "division by zero"
    NumericOverflow(..) => "numeric overflow"
    UnknownFunction(name~, ..) => "unknown function '\{name}'"
    InvalidArgument(message~, ..) => message
    CostLimitExceeded(limit~, ..) => "evaluation cost exceeded \{limit}"
    DepthLimitExceeded(limit~, ..) => "evaluation depth exceeded \{limit}"
    IterationLimitExceeded(limit~, ..) =>
      "collection iterations exceeded \{limit}"
  }
}

///|
pub fn EvalError::to_diagnostic(self : EvalError) -> Diagnostic {
  Diagnostic::error(self.code(), self.message(), self.span())
}

///|
pub fn EvalError::render_with_source(
  self : EvalError,
  source : String,
) -> String {
  self.to_diagnostic().render_with_source(source)
}