///|
pub(all) struct ReproducibilityManifest {
  name : String
  seed : Int
  cells : Int
  particles : Int
  steps : Int
  dt : Double
  shape : String
  boundary : String
  toolchain : String
} derive(Debug, ToJson)

///|
pub(all) struct ResultFingerprint {
  sample_count : Int
  work_units : Int
  charge_error : Double
  energy_drift : Double
  checksum : Int
} derive(Debug, ToJson)

///|
pub fn manifest_from_scenario(
  scenario : SimulationScenario,
  toolchain : String,
) -> ReproducibilityManifest {
  {
    name: scenario.name,
    seed: scenario.seed,
    cells: scenario.grid.cells,
    particles: scenario.particles,
    steps: scenario.steps,
    dt: scenario.dt,
    shape: shape_name(scenario.shape),
    boundary: boundary_name(scenario.boundary),
    toolchain,
  }
}

///|
pub fn boundary_name(boundary : Boundary1D) -> String {
  match boundary {
    Periodic => "periodic"
    Reflecting => "reflecting"
    Absorbing => "absorbing"
  }
}

///|
pub fn manifest_seed(manifest : ReproducibilityManifest) -> Int {
  deterministic_seed(
    manifest.name,
    manifest.seed + manifest.cells + manifest.particles + manifest.steps,
  )
}

///|
pub fn manifest_work_units(manifest : ReproducibilityManifest) -> Int {
  manifest.cells * manifest.particles * manifest.steps
}

///|
pub fn manifest_to_csv(manifest : ReproducibilityManifest) -> String {
  let output = StringBuilder()
  output.write_string("field,value\n")
  output.write_string("name,\{manifest.name}\n")
  output.write_string("seed,\{manifest.seed}\n")
  output.write_string("cells,\{manifest.cells}\n")
  output.write_string("particles,\{manifest.particles}\n")
  output.write_string("steps,\{manifest.steps}\n")
  output.write_string("dt,\{manifest.dt}\n")
  output.write_string("shape,\{manifest.shape}\n")
  output.write_string("boundary,\{manifest.boundary}\n")
  output.write_string("toolchain,\{manifest.toolchain}\n")
  output.to_string()
}

///|
pub fn fingerprint_integer(value : Double) -> Int {
  let scaled = (value.abs() * 1000000.0).to_int()
  scaled % 2147483647
}

///|
pub fn fingerprint_values(values : ArrayView[Double]) -> Int {
  let mut hash = 17
  for value in values {
    hash = (hash * 31 + fingerprint_integer(value)) % 2147483647
  }
  hash.abs()
}

///|
pub fn fingerprint_diagnostics(values : ArrayView[PicDiagnostics]) -> Int {
  let energies = values.map(fn(value) { value.total_energy() })
  let charges = values.map(fn(value) { value.total_charge })
  (fingerprint_values(energies) * 31 + fingerprint_values(charges)) % 2147483647
}

///|
pub fn fingerprint_from_scenario(
  scenario : SimulationScenario,
) -> ResultFingerprint {
  let diagnostics = scenario_trace(scenario)
  let energies = diagnostics.map(fn(value) { value.total_energy() })
  let charges = diagnostics.map(fn(value) { value.total_charge })
  {
    sample_count: diagnostics.length(),
    work_units: scenario_work_units(scenario),
    charge_error: if charges.length() < 2 {
      0.0
    } else {
      relative_change(charges[0], charges[charges.length() - 1])
    },
    energy_drift: if energies.length() < 2 {
      0.0
    } else {
      relative_change(energies[0], energies[energies.length() - 1])
    },
    checksum: fingerprint_diagnostics(diagnostics),
  }
}

///|
pub fn fingerprint_to_csv(fingerprint : ResultFingerprint) -> String {
  "sample_count,work_units,charge_error,energy_drift,checksum\n\{fingerprint.sample_count},\{fingerprint.work_units},\{fingerprint.charge_error},\{fingerprint.energy_drift},\{fingerprint.checksum}\n"
}

///|
pub fn fingerprints_equal(
  first : ResultFingerprint,
  second : ResultFingerprint,
) -> Bool {
  first.sample_count == second.sample_count &&
  first.work_units == second.work_units &&
  first.checksum == second.checksum &&
  (first.charge_error - second.charge_error).abs() < 1.0e-12 &&
  (first.energy_drift - second.energy_drift).abs() < 1.0e-12
}

///|
pub fn manifest_fingerprint(
  scenario : SimulationScenario,
  toolchain : String,
) -> String {
  let manifest = manifest_from_scenario(scenario, toolchain)
  let fingerprint = fingerprint_from_scenario(scenario)
  let output = StringBuilder()
  output.write_string(manifest_to_csv(manifest))
  output.write_string("\n")
  output.write_string(fingerprint_to_csv(fingerprint))
  output.to_string()
}

///|
pub fn reproducible_run(scenario : SimulationScenario) -> Bool {
  fingerprints_equal(
    fingerprint_from_scenario(scenario),
    fingerprint_from_scenario(scenario),
  )
}

///|
pub fn compare_scenarios(
  first : SimulationScenario,
  second : SimulationScenario,
) -> Bool {
  first.grid.cells == second.grid.cells &&
  first.particles == second.particles &&
  first.steps == second.steps &&
  (first.dt - second.dt).abs() < 1.0e-15 &&
  first.shape == second.shape &&
  first.boundary == second.boundary
}

///|
pub fn scenario_manifest_summary(scenario : SimulationScenario) -> String {
  manifest_to_csv(manifest_from_scenario(scenario, "stable"))
}

///|
pub fn benchmark_fingerprint_suite(
  suite : BenchmarkSuite,
) -> Array[ResultFingerprint] {
  suite.records.map(fn(record) {
    {
      sample_count: record.steps + 1,
      work_units: record.work_units,
      charge_error: record.charge_error,
      energy_drift: record.energy_drift,
      checksum: fingerprint_integer(record.charge_error) +
      fingerprint_integer(record.energy_drift),
    }
  })
}

///|
pub fn fingerprint_suite_to_csv(suite : BenchmarkSuite) -> String {
  let output = StringBuilder()
  output.write_string(
    "benchmark,sample_count,work_units,charge_error,energy_drift,checksum\n",
  )
  let fingerprints = benchmark_fingerprint_suite(suite)
  for i in 0.. Int {
  scenario.steps + 1
}

///|
pub fn expected_work_units(scenario : SimulationScenario) -> Int {
  scenario.grid.cells * scenario.particles * scenario.steps
}

///|
pub fn expected_output_schema() -> String {
  "name,cells,particles,steps,work_units,final_particles,charge_error,energy_drift"
}

///|
pub fn result_is_well_formed(
  scenario : SimulationScenario,
  fingerprint : ResultFingerprint,
) -> Bool {
  fingerprint.sample_count == expected_sample_count(scenario) &&
  fingerprint.work_units == expected_work_units(scenario) &&
  fingerprint.checksum >= 0
}

///|
pub fn reproducibility_report(
  scenario : SimulationScenario,
  toolchain : String,
) -> String {
  let fingerprint = fingerprint_from_scenario(scenario)
  let output = StringBuilder()
  output.write_string("reproducible=\{reproducible_run(scenario)}\n")
  output.write_string(
    "well_formed=\{result_is_well_formed(scenario, fingerprint)}\n",
  )
  output.write_string("\{manifest_fingerprint(scenario, toolchain)}")
  output.to_string()
}