///|
/// Error model: traits and concrete type for parse diagnostics.
///
/// Defines the trait hierarchy `Commit` → `CanMerge` → `Positioned` → `ParseFailure`
/// and the default `ParseError` struct. The generic function `commit_then_merge`
/// captures the core choice-combinator logic independently of the concrete error type.

///|
/// Types that track parse commitment — when a parser consumes input
/// and then fails, the error is "committed" to prevent backtracking.
pub(open) trait Commit {
  fn is_committed(Self) -> Bool
  fn mark_committed(Self) -> Self
  fn clear_commit(Self) -> Self
  fn[I, T] reject_if_committed(Self, value : T, rest : I) -> Result[
    (T, I),
    Self,
  ]
}

///|
/// Error types whose alternatives can be merged.
/// Furthest position wins; expected labels are combined.
pub(open) trait CanMerge {
  fn merge(Self, other : Self) -> Self
}

///|
/// Error types that carry a source position (offset, line, column).
pub(open) trait Positioned {
  fn error_offset(Self) -> Int
  fn error_line(Self) -> Int
  fn error_column(Self) -> Int
}

///|
/// Full parsing failure contract. Combines commit tracking, error merging,
/// source positioning, and construction from a cursor.
pub(open) trait ParseFailure: Commit + CanMerge + Positioned {
  fn[I : Cursor] signal(input : I, expected : String) -> Self
  fn[I : Cursor] message(input : I, msg : String) -> Self
}

///|
/// Parse failure metadata. `committed` prevents alternatives from backtracking
/// across input that has already been consumed.
pub(all) struct ParseError {
  offset : Int
  line : Int
  column : Int
  expected : Array[String]
  message : String
  committed : Bool
} derive(Eq, Debug)

///|
pub fn[I, T] ParseError::reject_if_committed(
  self : ParseError,
  value : T,
  rest : I,
) -> Result[(T, I), ParseError] {
  guard self.committed else { Ok((value, rest)) }
  Err(self)
}

///|
pub fn[I : Cursor] ParseError::new(input : I, expected : String) -> ParseError {
  let position = input.position()
  {
    offset: position.offset,
    line: position.line,
    column: position.column,
    expected: [expected],
    message: "",
    committed: false,
  }
}

///|
pub fn[I : Cursor] ParseError::message(input : I, msg : String) -> ParseError {
  let position = input.position()
  {
    offset: position.offset,
    line: position.line,
    column: position.column,
    expected: [],
    message: msg,
    committed: false,
  }
}

///|
pub fn ParseError::offset(self : ParseError) -> Int {
  Positioned::error_offset(self)
}

///|
pub fn ParseError::line(self : ParseError) -> Int {
  Positioned::error_line(self)
}

///|
pub fn ParseError::column(self : ParseError) -> Int {
  Positioned::error_column(self)
}

///|
pub fn ParseError::expected(self : ParseError) -> Array[String] {
  self.expected
}

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

///|
pub fn ParseError::is_committed(self : ParseError) -> Bool {
  Commit::is_committed(self)
}

///|
pub fn ParseError::with_commit(self : ParseError) -> ParseError {
  Commit::mark_committed(self)
}

///|
pub fn ParseError::without_commit(self : ParseError) -> ParseError {
  Commit::clear_commit(self)
}

///|
pub fn ParseError::merge(self : ParseError, other : ParseError) -> ParseError {
  CanMerge::merge(self, other)
}

///|
/// --- Commit impl ---
pub impl Commit for ParseError with fn is_committed(self) {
  self.committed
}

///|
pub impl Commit for ParseError with fn mark_committed(self) {
  { ..self, committed: true }
}

///|
pub impl Commit for ParseError with fn clear_commit(self) {
  { ..self, committed: false }
}

///|
pub impl Commit for ParseError with fn[I, T] reject_if_committed(
  self,
  value : T,
  rest : I,
) -> Result[(T, I), ParseError] {
  guard self.committed else { Ok((value, rest)) }
  Err(self)
}

///|
/// --- CanMerge impl ---
///
/// A committed failure at any offset commits the merged error: the
/// commit flag is OR'd even when the furthest-position error (the one
/// kept for diagnostics) is itself uncommitted. Otherwise a committed
/// failure at a shallower offset would be silently swallowed by a deeper
/// uncommitted one, letting choice backtracks that must not happen.
pub impl CanMerge for ParseError with fn merge(self, other) {
  if self.offset > other.offset {
    { ..self, committed: self.committed || other.committed }
  } else if other.offset > self.offset {
    { ..other, committed: self.committed || other.committed }
  } else {
    let expected = self.expected.copy()
    for item in other.expected {
      if !expected.contains(item) {
        expected.push(item)
      }
    }
    let message = if self.message != "" { self.message } else { other.message }
    {
      offset: self.offset,
      line: self.line,
      column: self.column,
      expected,
      message,
      committed: self.committed || other.committed,
    }
  }
}

///|
/// --- Positioned impl ---
pub impl Positioned for ParseError with fn error_offset(self) {
  self.offset
}

///|
pub impl Positioned for ParseError with fn error_line(self) {
  self.line
}

///|
pub impl Positioned for ParseError with fn error_column(self) {
  self.column
}

///|
/// --- ParseFailure impl ---
pub impl ParseFailure for ParseError with fn[I : Cursor] signal(
  input : I,
  expected : String,
) -> ParseError {
  ParseError::new(input, expected)
}

///|
pub impl ParseFailure for ParseError with fn[I : Cursor] message(
  input : I,
  msg : String,
) -> ParseError {
  ParseError::message(input, msg)
}

///|
pub extend ParseError with Eq::{not_equal, equal}

///|
pub extend ParseError with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ParseError with Commit::{mark_committed, clear_commit}

///|
pub extend ParseError with Positioned::{error_line, error_offset, error_column}

///|
pub extend ParseError with ParseFailure::{signal}

///|
pub fn ParseError::to_string(self : ParseError) -> String {
  let base = "parse error at \{self.line}:\{self.column}"
  if self.message != "" {
    "\{base}: \{self.message}"
  } else if self.expected.length() > 0 {
    "\{base}: expected \{self.expected.join(" or ")}"
  } else {
    base
  }
}

///|
/// Generic error combinator usable with any `E : Commit + CanMerge`.
fn[E : Commit + CanMerge] commit_then_merge(first : E, second : E) -> E {
  guard !Commit::is_committed(first) else { first }
  CanMerge::merge(first, second)
}

///|
test "trait-based generic error handling" {
  let err_a = ParseError::new(Input::new("x"), "a")
  let err_b = ParseError::new(Input::new("y"), "b")
  let committed = ParseError::new(Input::new("x"), "a").with_commit()
  let r1 = commit_then_merge(committed, err_b)
  @test.assert_eq(Commit::is_committed(r1), true)
  let r2 = commit_then_merge(err_a, err_b)
  @test.assert_eq(Positioned::error_offset(r2), 0)
}