///|
/// A complete named shock-tube experiment. Construction validates all inputs.
#derive(Debug)
pub struct Experiment {
  name : String
  gas : Gas
  mesh : Mesh
  left : Primitive
  right : Primitive
  interface_at : Double
  end_time : Double
  config : SolverConfig
}

///|
pub fn Experiment::new(
  name : String,
  gas : Gas,
  mesh : Mesh,
  left : Primitive,
  right : Primitive,
  interface_at : Double,
  end_time : Double,
  config : SolverConfig,
) -> Experiment raise GasError {
  if name.length() == 0 || name.length() > 128 {
    raise Invalid("experiment name needs 1..128 code units")
  }
  if !finite(end_time) || end_time <= 0.0 {
    raise Invalid("experiment end time must be positive and finite")
  }
  if !finite(interface_at) ||
    interface_at <= mesh.left ||
    interface_at >= mesh.right {
    raise Invalid("experiment interface must be interior")
  }
  ignore(gas.riemann(left, right))
  { name, gas, mesh, left, right, interface_at, end_time, config, }
}

///|
#derive(Debug)
pub(all) struct ExperimentResult {
  name : String
  simulation : Simulation
  density_error : ErrorNorm
  reference_unaffected_by_boundaries : Bool
}

///|
/// Runs a gas experiment and labels whether waves have reached either boundary.
/// Exact Riemann comparisons are meaningful only before boundary interactions,
/// and for an outflow experiment; the flag encodes both conditions.
pub fn Experiment::run(self : Experiment) -> ExperimentResult raise GasError {
  let exact = self.gas.riemann(self.left, self.right)
  let initial = self.gas.shock_tube(
    self.mesh,
    self.left,
    self.right,
    self.interface_at,
  )
  let simulation = Simulation::new(self.gas, self.mesh, initial).run_until(
    self.end_time,
    self.config,
  )
  let reference = exact.profile(self.mesh, self.end_time, self.interface_at)
  let speeds = exact.wave_speeds()
  let outflow = self.config.boundaries == Ends(Outflow, Outflow)
  let unaffected = outflow &&
    self.interface_at + speeds[0] * self.end_time > self.mesh.left &&
    self.interface_at + speeds[3] * self.end_time < self.mesh.right
  {
    name: self.name,
    simulation,
    density_error: self.gas.density_error(
      self.mesh,
      simulation.cells,
      reference,
    ),
    reference_unaffected_by_boundaries: unaffected,
  }
}

///|
#derive(Debug)
pub(all) enum BatchOutcome {
  Succeeded(ExperimentResult)
  Failed(String, String)
}

///|
/// Isolated outcomes let an embedding application report a numerical failure
/// without dropping later experiments. Every individual run retains its budgets.
pub fn run_batch(
  experiments : Array[Experiment],
) -> Array[BatchOutcome] raise GasError {
  if experiments.length() > 1000 {
    raise Invalid("batch exceeds 1000 experiments")
  }
  let output = []
  for experiment in experiments {
    let outcome = Succeeded(experiment.run()) catch {
      GasError::Invalid(message) | GasError::Numerical(message) =>
        Failed(experiment.name, message)
    }
    output.push(outcome)
  }
  output
}

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

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

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