///|
/// 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,
  message : String,
) -> ParseError {
  let position = input.position()
  {
    offset: position.offset,
    line: position.line,
    column: position.column,
    expected: [],
    message,
    committed: false,
  }
}

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

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

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

///|
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 {
  self.committed
}

///|
pub fn ParseError::with_commit(self : ParseError) -> ParseError {
  {
    offset: self.offset,
    line: self.line,
    column: self.column,
    expected: self.expected,
    message: self.message,
    committed: true,
  }
}

///|
pub fn ParseError::without_commit(self : ParseError) -> ParseError {
  {
    offset: self.offset,
    line: self.line,
    column: self.column,
    expected: self.expected,
    message: self.message,
    committed: false,
  }
}

///|
pub fn ParseError::merge(self : ParseError, other : ParseError) -> ParseError {
  if self.offset > other.offset {
    self
  } else if other.offset > self.offset {
    other
  } 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,
    }
  }
}

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