// Public MoonBit API around the ported Kiwi 1.4.9 tableau.
// Copyright (c) 2013-2025, Nucleic Development Team.
// Copyright (c) 2026, MoonCassowary contributors. BSD-3-Clause; see LICENSE.

///|
/// An incremental solver. Mutation is transactional: an error restores the
/// previous tableau. Snapshots cost O(tableau size) memory and time per edit;
/// optimization still reuses the existing basis instead of rebuilding it.
pub struct Solver {
  priv mut state : Tableau
}

///|
pub struct Statistics {
  /// Explicit constraints, excluding edit-variable constraints.
  constraints : Int
  edits : Int
  /// Variable symbols are retained until reset, as in Kiwi.
  variables : Int
  rows : Int
  /// Successful optimization/removal pivots since reset.
  pivots : Int
} derive(Eq, @debug.Debug)

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

///|
pub extend Statistics with @debug.Debug::{to_repr}

///|
pub fn Solver::new() -> Solver {
  { state: Tableau::new(), }
}

///|
fn Solver::mutate(
  self : Solver,
  operation : (Tableau) -> Unit raise SolverError,
) -> Unit raise SolverError {
  let backup = self.state.copy()
  errdefer {
    self.state = backup
  }
  self.state.operation_pivots = 0
  operation(self.state)
  self.state.check_finite()
}

///|
fn validate_constraint(constraint : Constraint) -> Unit raise SolverError {
  check_strength(constraint.strength, false)
  if !is_finite(constraint.expression.constant) {
    raise InvalidNumber
  }
  for _, coefficient in constraint.expression.terms {
    if !is_finite(coefficient) {
      raise InvalidNumber
    }
  }
}

///|
pub fn Solver::add_constraint(
  self : Solver,
  constraint : Constraint,
) -> Unit raise SolverError {
  validate_constraint(constraint)
  self.mutate(state => state.add(constraint))
}

///|
pub fn Solver::remove_constraint(
  self : Solver,
  constraint : Constraint,
) -> Unit raise SolverError {
  self.mutate(state => state.remove(constraint))
}

///|
pub fn Solver::has_constraint(self : Solver, constraint : Constraint) -> Bool {
  self.state.constraints.contains(constraint.id)
}

///|
/// Register a variable that can be interactively suggested. Required strength
/// is not allowed: a suggestion is a soft preference, not an assignment.
pub fn Solver::add_edit_variable(
  self : Solver,
  variable : Variable,
  strength : Double,
) -> Unit raise SolverError {
  check_strength(strength, true)
  self.mutate(state => state.add_edit(variable, strength))
}

///|
pub fn Solver::remove_edit_variable(
  self : Solver,
  variable : Variable,
) -> Unit raise SolverError {
  self.mutate(state => state.remove_edit(variable))
}

///|
pub fn Solver::has_edit_variable(self : Solver, variable : Variable) -> Bool {
  self.state.edits.contains(variable)
}

///|
/// Reoptimizes the existing basis using dual simplex. May be clamped by hard
/// constraints or outweighed by competing soft preferences.
pub fn Solver::suggest_value(
  self : Solver,
  variable : Variable,
  value : Double,
) -> Unit raise SolverError {
  if !is_finite(value) {
    raise InvalidNumber
  }
  self.mutate(state => state.suggest(variable, value))
}

///|
/// Current solved value. Nonbasic and unknown variables return zero. Reading
/// a variable from another solver never shares or mutates its solved value.
pub fn Solver::value(self : Solver, variable : Variable) -> Double {
  match self.state.variables.get(variable) {
    Some(symbol) =>
      match self.state.rows.get(symbol) {
        Some(row) => row.constant
        None => 0.0
      }
    None => 0.0
  }
}

///|
/// Discard constraints, edit registrations, symbols, and pivot counts.
pub fn Solver::reset(self : Solver) -> Unit {
  self.state = Tableau::new()
}

///|
pub fn Solver::statistics(self : Solver) -> Statistics {
  {
    constraints: self.state.constraints.length() - self.state.edits.length(),
    edits: self.state.edits.length(),
    variables: self.state.variables.length(),
    rows: self.state.rows.length(),
    pivots: self.state.pivots,
  }
}