///|
/// One residual/mass observation.
pub(all) struct ConvergenceSample {
  step : Int
  residual : Double
  mass : Double
} derive(Debug)

///|
/// Bounded convergence history for iterative solvers.
pub(all) struct ConvergenceHistory {
  capacity : Int
  samples : Array[ConvergenceSample]
} derive(Debug)

///|
/// Allocate a convergence history.
pub fn ConvergenceHistory::new(capacity? : Int = 128) -> ConvergenceHistory {
  { capacity: capacity.max(1), samples: Array::new() }
}

///|
/// Append an observation and discard the oldest one after reaching capacity.
pub fn ConvergenceHistory::push(
  self : ConvergenceHistory,
  step~ : Int,
  residual~ : Double,
  mass~ : Double,
) -> Unit {
  self.samples.push({ step, residual, mass })
  if self.samples.length() > self.capacity {
    ignore(self.samples.remove(0))
  }
}

///|
/// Number of observations stored.
pub fn ConvergenceHistory::length(self : ConvergenceHistory) -> Int {
  self.samples.length()
}

///|
/// Read a sample safely.
pub fn ConvergenceHistory::get(
  self : ConvergenceHistory,
  index : Int,
) -> ConvergenceSample? {
  self.samples.get(index)
}

///|
/// Ratio between the latest and earliest residual.
pub fn ConvergenceHistory::residual_ratio(self : ConvergenceHistory) -> Double {
  if self.samples.length() < 2 {
    1.0
  } else {
    let first = self.samples[0].residual
    if first == 0.0 {
      0.0
    } else {
      self.samples[self.samples.length() - 1].residual / first
    }
  }
}

///|
/// True when the most recent residual does not increase.
pub fn ConvergenceHistory::is_converging(self : ConvergenceHistory) -> Bool {
  if self.samples.length() < 2 {
    false
  } else {
    let n = self.samples.length()
    self.samples[n - 1].residual <= self.samples[n - 2].residual
  }
}

///|
/// Return the latest residual, or zero for an empty history.
pub fn ConvergenceHistory::latest_residual(self : ConvergenceHistory) -> Double {
  if self.samples.length() == 0 {
    0.0
  } else {
    self.samples[self.samples.length() - 1].residual
  }
}

///|
/// Export a convergence history as CSV.
pub fn ConvergenceHistory::to_csv(self : ConvergenceHistory) -> String {
  let builder = StringBuilder(size_hint=32 + self.samples.length() * 24)
  builder.write_string("step,residual,mass\n")
  for sample in self.samples {
    builder.write_string("\{sample.step},\{sample.residual},\{sample.mass}\n")
  }
  builder.to_string()
}