///|
/// Validated policy for a bounded simulation. Read-only after construction.
#derive(Debug)
pub struct SolverConfig {
  boundaries : Boundaries
  reconstruction : Reconstruction
  flux : FluxMethod
  integrator : Integrator
  cfl : Double
  max_steps : Int32
  max_retries : Int32
}

///|
pub fn SolverConfig::new(
  boundaries : Boundaries,
  reconstruction : Reconstruction,
  flux : FluxMethod,
  integrator : Integrator,
  cfl~ : Double = 0.4,
  max_steps~ : Int32 = 100000,
  max_retries~ : Int32 = 20,
) -> SolverConfig raise GasError {
  if !finite(cfl) ||
    cfl <= 0.0 ||
    cfl > 0.5 ||
    max_steps < 1 ||
    max_steps > 10000000 ||
    max_retries < 0 ||
    max_retries > 40 {
    raise Invalid("invalid CFL, step budget or retry budget")
  }
  { boundaries, reconstruction, flux, integrator, cfl, max_steps, max_retries, }
}

///|
pub fn SolverConfig::standard() -> SolverConfig {
  {
    boundaries: Ends(Outflow, Outflow),
    reconstruction: MonotonizedCentral,
    flux: Hll,
    integrator: SspRk2,
    cfl: 0.4,
    max_steps: 100000,
    max_retries: 20,
  }
}

///|
/// An immutable snapshot accessor always returns a copy of its mutable array.
#derive(Debug)
pub struct Simulation {
  gas : Gas
  mesh : Mesh
  cells : Array[Conserved]
  time : Double
  steps : Int32
  retries : Int32
  boundary_change : Conserved
  initial_totals : Conserved
}

///|
pub fn Simulation::new(
  gas : Gas,
  mesh : Mesh,
  cells : Array[Conserved],
  time~ : Double = 0.0,
) -> Simulation raise GasError {
  if !finite(time) || time < 0.0 {
    raise Invalid("initial time must be finite and nonnegative")
  }
  let initial = gas.diagnostics(mesh, cells)
  {
    gas,
    mesh,
    cells: cells.copy(),
    time,
    steps: 0,
    retries: 0,
    boundary_change: Conserved::zero(),
    initial_totals: initial.totals,
  }
}

///|
pub fn Simulation::cells(self : Simulation) -> Array[Conserved] {
  self.cells.copy()
}

///|
pub fn Simulation::time(self : Simulation) -> Double {
  self.time
}

///|
pub fn Simulation::steps(self : Simulation) -> Int32 {
  self.steps
}

///|
pub fn Simulation::retries(self : Simulation) -> Int32 {
  self.retries
}

///|
pub fn Simulation::mesh(self : Simulation) -> Mesh {
  self.mesh
}

///|
pub fn Simulation::gas(self : Simulation) -> Gas {
  self.gas
}

///|
pub fn Simulation::diagnostics(self : Simulation) -> Diagnostics raise GasError {
  self.gas.diagnostics(self.mesh, self.cells)
}

///|
pub fn Simulation::conservation_residual(
  self : Simulation,
) -> Conserved raise GasError {
  let result = self.diagnostics().totals
    .minus(self.initial_totals)
    .minus(self.boundary_change)
  if !finite(result.mass) || !finite(result.momentum) || !finite(result.energy) {
    raise Numerical("conservation ledger overflow")
  }
  result
}

///|
/// Runs transactionally: errors do not mutate the source simulation. Step budget
/// applies to this call; cumulative accepted steps remain in the returned value.
pub fn Simulation::run_until(
  self : Simulation,
  end_time : Double,
  config : SolverConfig,
) -> Simulation raise GasError {
  if !finite(end_time) || end_time < self.time {
    raise Invalid("end time precedes snapshot or is nonfinite")
  }
  let mut cells = self.cells.copy()
  let mut time = self.time
  let mut steps = 0
  let mut retries = 0
  let mut boundary_change = self.boundary_change
  while time < end_time {
    if self.steps + steps >= 1000000000 {
      raise Numerical("cumulative step counter exhausted")
    }
    if steps >= config.max_steps {
      raise Numerical("accepted-step budget exhausted")
    }
    let remaining = end_time - time
    let mut dt = self.gas.stable_dt(self.mesh, cells, config.cfl).min(remaining)
    let mut attempt = 0
    let mut accepted : StepResult? = None
    while accepted is None {
      if time + dt <= time || dt <= 0.0 {
        raise Numerical("time cannot advance at machine precision")
      }
      accepted = Some(
        self.gas.advance(
          self.mesh,
          cells,
          dt,
          config.boundaries,
          config.reconstruction,
          config.flux,
          config.integrator,
        ),
      ) catch {
        GasError::Numerical(_) | GasError::Invalid(_) => None
      }
      if accepted is None {
        if attempt >= config.max_retries {
          raise Numerical("physical-state/CFL retry budget exhausted")
        }
        dt = dt * 0.5
        attempt = attempt + 1
        if self.retries + retries >= 1000000000 {
          raise Numerical("cumulative retry counter exhausted")
        }
        retries = retries + 1
      }
    }
    match accepted {
      Some(next) => {
        cells = next.cells
        boundary_change = boundary_change.plus(next.boundary_change)
      }
      None => raise Numerical("internal step acceptance failure")
    }
    time = if dt == remaining { end_time } else { time + dt }
    steps = steps + 1
  }
  {
    gas: self.gas,
    mesh: self.mesh,
    cells,
    time,
    steps: self.steps + steps,
    retries: self.retries + retries,
    boundary_change,
    initial_totals: self.initial_totals,
  }
}

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

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