///|
pub(all) struct ExperimentConfig {
  name : String
  grid : Grid1D
  steps : Int
  dt : Double
} derive(Debug, ToJson)

///|
pub fn ExperimentConfig::new(
  name : String,
  grid : Grid1D,
  steps : Int,
  dt : Double,
) -> ExperimentConfig {
  { name, grid, steps: steps.max(0), dt }
}

///|
pub(all) struct ExperimentSample {
  step : Int
  time : Double
  particles : Int
  total_charge : Double
  kinetic_energy : Double
  field_energy : Double
  total_energy : Double
  charge_error : Double
} derive(Debug, ToJson)

///|
pub(all) struct ExperimentReport {
  name : String
  samples : Array[ExperimentSample]
  final_state : PicState
} derive(Debug, ToJson)

///|
fn experiment_sample(
  step : Int,
  state : PicState,
  initial_charge : Double,
) -> ExperimentSample {
  let diagnostics = PicDiagnostics::from_state(state)
  {
    step,
    time: diagnostics.time,
    particles: diagnostics.particles,
    total_charge: diagnostics.total_charge,
    kinetic_energy: diagnostics.kinetic_energy,
    field_energy: diagnostics.field_energy,
    total_energy: diagnostics.total_energy(),
    charge_error: (diagnostics.total_charge - initial_charge).abs(),
  }
}

///|
pub fn run_experiment(config : ExperimentConfig) -> ExperimentReport {
  let particles = two_stream_particles(
    config.grid.cells,
    config.grid.length,
    1.0e5,
  )
  let initial = PicState::new(config.grid, particles)
  let initial_charge = total_particle_charge(initial.particles)
  let samples : Array[ExperimentSample] = []
  let mut state = initial
  samples.push(experiment_sample(0, state, initial_charge))
  for step in 0.. String {
  let output = StringBuilder()
  output.write_string(
    "step,time,particles,total_charge,kinetic_energy,field_energy,total_energy,charge_error\n",
  )
  for sample in report.samples {
    output.write_string(
      "\{sample.step},\{sample.time},\{sample.particles},\{sample.total_charge},\{sample.kinetic_energy},\{sample.field_energy},\{sample.total_energy},\{sample.charge_error}\n",
    )
  }
  output.to_string()
}

///|
pub fn experiment_summary(report : ExperimentReport) -> String {
  let output = StringBuilder()
  output.write_string("experiment=\{report.name}\n")
  output.write_string("samples=\{report.samples.length()}\n")
  output.write_string("final_time=\{report.final_state.time}\n")
  output.write_string(
    "final_particles=\{report.final_state.particles.length()}\n",
  )
  output.to_string()
}

///|
pub fn experiment_charge_error(report : ExperimentReport) -> Double {
  if report.samples.length() == 0 {
    0.0
  } else {
    report.samples[report.samples.length() - 1].charge_error
  }
}

///|
pub fn experiment_energy_drift(report : ExperimentReport) -> Double {
  if report.samples.length() < 2 {
    0.0
  } else {
    let first = report.samples[0].total_energy
    let last = report.samples[report.samples.length() - 1].total_energy
    safe_ratio((last - first).abs(), first.abs().max(1.0e-30), 0.0)
  }
}

///|
pub fn experiment_times(report : ExperimentReport) -> Array[Double] {
  report.samples.map(fn(sample) { sample.time })
}

///|
pub fn experiment_particle_counts(report : ExperimentReport) -> Array[Int] {
  report.samples.map(fn(sample) { sample.particles })
}

///|
pub fn experiment_energy_series(report : ExperimentReport) -> Array[Double] {
  report.samples.map(fn(sample) { sample.total_energy })
}

///|
pub fn experiment_passes_charge_tolerance(
  report : ExperimentReport,
  tolerance : Double,
) -> Bool {
  experiment_charge_error(report) <= tolerance
}

///|
pub fn experiment_passes_energy_tolerance(
  report : ExperimentReport,
  tolerance : Double,
) -> Bool {
  experiment_energy_drift(report) <= tolerance
}