///|
pub(all) struct PipelineConfig {
  name : String
  grid : Grid1D
  particles : Int
  steps : Int
  dt : Double
  shape : ShapeKind
  boundary : Boundary1D
} derive(Debug, ToJson)

///|
pub(all) struct PipelineFrame {
  step : Int
  state : PicState
  diagnostics : PicDiagnostics
} derive(Debug, ToJson)

///|
pub(all) struct PipelineResult {
  config : PipelineConfig
  frames : Array[PipelineFrame]
} derive(Debug, ToJson)

///|
pub fn PipelineConfig::new(
  name : String,
  grid : Grid1D,
  particles : Int,
  steps : Int,
  dt : Double,
) -> PipelineConfig {
  {
    name,
    grid,
    particles: particles.max(0),
    steps: steps.max(0),
    dt,
    shape: CIC,
    boundary: Periodic,
  }
}

///|
pub fn PipelineConfig::with_shape(
  config : PipelineConfig,
  shape : ShapeKind,
) -> PipelineConfig {
  { ..config, shape, }
}

///|
pub fn PipelineConfig::with_boundary(
  config : PipelineConfig,
  boundary : Boundary1D,
) -> PipelineConfig {
  { ..config, boundary, }
}

///|
pub fn pipeline_initial_state(config : PipelineConfig) -> PicState {
  let particles = sample_particles(
    config.grid,
    config.particles,
    deterministic_seed(config.name, 11),
    0.0,
    1.0e4,
  )
  PicState::new(config.grid, particles)
}

///|
pub fn pipeline_frame(
  _config : PipelineConfig,
  step : Int,
  state : PicState,
) -> PipelineFrame {
  { step, state, diagnostics: PicDiagnostics::from_state(state) }
}

///|
pub fn pipeline_step(config : PipelineConfig, state : PicState) -> PicState {
  let stepped = state.step(config.dt)
  let bounded = apply_particle_boundary_batch(
      config.grid,
      config.boundary,
      stepped.particles,
    ).alive
  let charge = deposit_charge_with_shape(config.grid, bounded, config.shape)
  {
    grid: config.grid,
    particles: bounded,
    field: solve_periodic_field(config.grid, charge),
    time: stepped.time,
  }
}

///|
pub fn run_pipeline(config : PipelineConfig) -> PipelineResult {
  let frames : Array[PipelineFrame] = []
  let mut state = pipeline_initial_state(config)
  frames.push(pipeline_frame(config, 0, state))
  for step in 0.. PipelineFrame? {
  if result.frames.length() == 0 {
    None
  } else {
    Some(result.frames[result.frames.length() - 1])
  }
}

///|
pub fn PipelineResult::times(result : PipelineResult) -> Array[Double] {
  result.frames.map(fn(frame) { frame.diagnostics.time })
}

///|
pub fn PipelineResult::energies(result : PipelineResult) -> Array[Double] {
  result.frames.map(fn(frame) { frame.diagnostics.total_energy() })
}

///|
pub fn PipelineResult::charge_errors(result : PipelineResult) -> Array[Double] {
  if result.frames.length() == 0 {
    []
  } else {
    let initial = result.frames[0].diagnostics.total_charge
    result.frames.map(fn(frame) {
      (frame.diagnostics.total_charge - initial).abs()
    })
  }
}

///|
pub fn pipeline_to_csv(result : PipelineResult) -> String {
  let output = StringBuilder()
  output.write_string(
    "step,time,particles,total_charge,kinetic_energy,field_energy,total_energy\n",
  )
  for frame in result.frames {
    let d = frame.diagnostics
    output.write_string(
      "\{frame.step},\{d.time},\{d.particles},\{d.total_charge},\{d.kinetic_energy},\{d.field_energy},\{d.total_energy()}\n",
    )
  }
  output.to_string()
}

///|
pub fn pipeline_summary(result : PipelineResult) -> String {
  "name=\{result.config.name}\nframes=\{result.frames.length()}\nwork_units=\{result.config.grid.cells * result.config.particles * result.config.steps}\n"
}