///|
/// Categories of errors that can occur during NestedText parsing.
///
/// # Variants
/// * `InvalidIndentation` — Indentation uses an unknown mixture of spaces or
///   does not match any previously established indentation level.
/// * `TabInIndentation` — A tab character was used for indentation. NestedText
///   requires spaces.
/// * `UnrecognizedLine` — The line contains characters that cannot be recognised
///   (e.g. invalid UTF-8).
/// * `UnexpectedLineType` — The line type (dict item, list item, string) does
///   not match the expected shape of the enclosing value.
/// * `DuplicateKey` — A dictionary key appears more than once within the same
///   dictionary.
/// * `InvalidIndentLevel` — The indentation level does not correspond to any
///   level established by the containing structure (partial dedent).
/// * `UnterminatedInlineList` — An inline list `[...]` is missing its closing
///   bracket.
/// * `UnterminatedInlineDict` — An inline dictionary `{...}` is missing its
///   closing brace.
/// * `InvalidInlineCharacter` — An unexpected character was found inside an
///   inline list or dictionary.
/// * `TrailingContent` — Extra characters were found after a complete inline
///   value.
/// * `DeserializationError` — A typed-deserialization step failed. The
///   accompanying message provides the detail.
pub(all) enum ErrorKind {
  InvalidIndentation
  TabInIndentation
  UnrecognizedLine
  UnexpectedLineType
  DuplicateKey
  InvalidIndentLevel
  UnterminatedInlineList
  UnterminatedInlineDict
  InvalidInlineCharacter
  TrailingContent
  DeserializationError
} derive(Debug, Eq)

///|
/// A parse or deserialization error with optional source-location context.
///
/// # Fields
/// * `kind` — The category of error.
/// * `message` — A human-readable description of the problem.
/// * `lineno` — The 1-based line number where the error occurred (if known).
/// * `colno` — The 1-based column number where the error occurred (if known).
/// * `line` — The text of the offending line (if available).
pub(all) struct NestedTextError {
  kind : ErrorKind
  message : String
  lineno : Int?
  colno : Int?
  line : String?
} derive(Debug, Eq)

///|
/// Create an error without location information.
pub fn NestedTextError::new(
  kind : ErrorKind,
  message : String,
) -> NestedTextError {
  { kind, message, lineno: None, colno: None, line: None }
}

///|
/// Create an error with full location information.
pub fn NestedTextError::at(
  kind : ErrorKind,
  message : String,
  lineno : Int,
  colno : Int,
  line : String,
) -> NestedTextError {
  { kind, message, lineno: Some(lineno), colno: Some(colno), line: Some(line) }
}

///|
/// Set or replace the line number on an existing error.
pub fn NestedTextError::with_lineno(
  self : NestedTextError,
  lineno : Int,
) -> NestedTextError {
  {
    kind: self.kind,
    message: self.message,
    lineno: Some(lineno),
    colno: self.colno,
    line: self.line,
  }
}

///|
/// Set or replace the column number on an existing error.
pub fn NestedTextError::with_colno(
  self : NestedTextError,
  colno : Int,
) -> NestedTextError {
  {
    kind: self.kind,
    message: self.message,
    lineno: self.lineno,
    colno: Some(colno),
    line: self.line,
  }
}

///|
/// Set or replace the source line on an existing error.
pub fn NestedTextError::with_line(
  self : NestedTextError,
  line : String,
) -> NestedTextError {
  {
    kind: self.kind,
    message: self.message,
    lineno: self.lineno,
    colno: self.colno,
    line: Some(line),
  }
}

///|
/// Format the error as a human-readable string.
///
/// If a line number is available the output is `"line : "`;
/// otherwise it is just ``.
pub fn NestedTextError::to_string(self : NestedTextError) -> String {
  match self.lineno {
    Some(lineno) => "line \{lineno}: \{self.message}"
    None => self.message
  }
}