// Copyright (c) 2024 LinZeming
// Released under the MIT License
//
// Errors — unified error types with source-location tracking.

// ---------------------------------------------------------------------------
// Error kind
// ---------------------------------------------------------------------------

///|
/// Classification of template engine errors.
pub(all) enum ErrorKind {
  LexicalError
  SyntaxError
  EvalError
  RenderError
  FilterError
  NotFoundError
} derive(Debug)

// ---------------------------------------------------------------------------
// Source location
// ---------------------------------------------------------------------------

///|
/// A position in a template source file.
pub(all) struct SourceLocation {
  template : String
  line : Int
  column : Int
} derive(Debug)

/// Create a source location with default template name.
pub fn loc_at(line : Int, column : Int) -> SourceLocation {
  { template: "", line: line, column: column }
}

// ---------------------------------------------------------------------------
// Template error
// ---------------------------------------------------------------------------

///|
/// The primary error type returned by all engine operations.
pub(all) struct TemplateError {
  kind : ErrorKind
  message : String
  location : Option[SourceLocation]
  snippet : Option[String]
} derive(Debug)

/// Create a bare error without location info.
pub fn TemplateError::new(kind : ErrorKind, message : String) -> TemplateError {
  {
    kind: kind,
    message: message,
    location: Option::None,
    snippet: Option::None,
  }
}

/// Attach source location to an error.
pub fn TemplateError::with_location(
  self : TemplateError,
  loc : SourceLocation
) -> TemplateError {
  { ..self, location: Option::Some(loc) }
}

/// Attach a source snippet to an error.
pub fn TemplateError::with_snippet(
  self : TemplateError,
  snippet : String
) -> TemplateError {
  { ..self, snippet: Option::Some(snippet) }
}

///|
/// Format the error as a human-readable string.
pub fn TemplateError::to_string(self : TemplateError) -> String {
  let kind_str = match self.kind {
    ErrorKind::LexicalError => "LexicalError"
    ErrorKind::SyntaxError => "SyntaxError"
    ErrorKind::EvalError => "EvalError"
    ErrorKind::RenderError => "RenderError"
    ErrorKind::FilterError => "FilterError"
    ErrorKind::NotFoundError => "NotFoundError"
  }
  let mut result = "Error: " + kind_str
  match self.location {
    Option::Some(loc) => {
      result = result +
        " in template \"" + loc.template + "\"" +
        " at line " + loc.line.to_string() +
        ", column " + loc.column.to_string()
    }
    Option::None => ()
  }
  result = result + "\n  " + self.message
  match self.snippet {
    Option::Some(snip) => result = result + "\n" + snip
    Option::None => ()
  }
  result
}

// ---------------------------------------------------------------------------
// Convenience constructors
// ---------------------------------------------------------------------------

/// Create a lexer error at a source position.
pub fn lex_error(msg : String, line : Int, col : Int) -> TemplateError {
  TemplateError::new(ErrorKind::LexicalError, msg)
    .with_location(loc_at(line, col))
}

/// Create a syntax error at a source position.
pub fn syntax_error(msg : String, line : Int, col : Int) -> TemplateError {
  TemplateError::new(ErrorKind::SyntaxError, msg)
    .with_location(loc_at(line, col))
}

/// Create an eval error (often without precise location).
pub fn eval_error(msg : String) -> TemplateError {
  TemplateError::new(ErrorKind::EvalError, msg)
}

/// Create a render error.
pub fn render_error(msg : String) -> TemplateError {
  TemplateError::new(ErrorKind::RenderError, msg)
}

/// Create a filter error.
pub fn filter_error(msg : String) -> TemplateError {
  TemplateError::new(ErrorKind::FilterError, msg)
}

/// Create a not-found error (template, variable, field, key).
pub fn not_found_error(msg : String) -> TemplateError {
  TemplateError::new(ErrorKind::NotFoundError, msg)
}

// ---------------------------------------------------------------------------
// Source snippet extraction
// ---------------------------------------------------------------------------

/// Wrap a plain string into a TemplateError (for gradual migration).
pub fn from_string(msg : String) -> TemplateError {
  TemplateError::new(ErrorKind::SyntaxError, msg)
}

///|
/// Extract a single line from *source* for error display.
///
/// Returns a formatted string like: `  5 | {% if x > %}`.
pub fn extract_snippet(source : String, line : Int) -> String {
  if source == "" || line < 1 {
    return ""
  }
  let mut current_line = 1
  let mut line_start = 0
  let len = source.length()
  let mut i = 0
  while i < len {
    let ch = source[i]
    if ch.to_int() == '\n'.to_int() {
      if current_line == line {
        let line_content = source[line_start:i].to_owned()
        return "  " + line.to_string() + " | " + line_content
      }
      current_line = current_line + 1
      line_start = i + 1
    }
    i = i + 1
  }
  // Last line (no trailing newline).
  if current_line == line {
    let line_content = source[line_start:len].to_owned()
    return "  " + line.to_string() + " | " + line_content
  }
  ""
}