///|
/// A spatial cell-average probe with no interpolation across discontinuities.
#derive(Debug, ToJson)
pub(all) struct Probe {
  x : Double
  index : Int32
  state : Primitive
  mach : Double
}

///|
pub fn Simulation::probes(
  self : Simulation,
  coordinates : Array[Double],
) -> Array[Probe] raise GasError {
  if coordinates.length() > 1000000 {
    raise Invalid("too many probe coordinates")
  }
  let output = []
  for x in coordinates {
    let index = self.mesh.locate(x)
    let state = self.gas.primitive(self.cells[index])
    output.push({ x, index, state, mach: self.gas.mach(state), })
  }
  output
}

///|
/// Lightweight monitor entry; no full cell array is retained.
#derive(Debug, ToJson)
pub(all) struct Monitor {
  time : Double
  steps : Int32
  retries : Int32
  diagnostics : Diagnostics
  conservation_residual : Conserved
  probes : Array[Probe]
}

///|
pub fn Simulation::monitor(
  self : Simulation,
  coordinates : Array[Double],
) -> Monitor raise GasError {
  {
    time: self.time,
    steps: self.steps,
    retries: self.retries,
    diagnostics: self.diagnostics(),
    conservation_residual: self.conservation_residual(),
    probes: self.probes(coordinates),
  }
}

///|
/// Ordered output times permit equal entries, but reject backward or nonfinite times.
/// Evolution is continuous between observations; every source snapshot is immutable.
pub fn Simulation::observe(
  self : Simulation,
  times : Array[Double],
  coordinates : Array[Double],
  config : SolverConfig,
) -> (Simulation, Array[Monitor]) raise GasError {
  if times.length() > 10000 ||
    coordinates.length() > 10000 ||
    times.length().to_double() * coordinates.length().to_double() > 1000000.0 {
    raise Invalid("observation request exceeds bounded output budget")
  }
  let mut previous = self.time
  for t in times {
    if !finite(t) || t < previous {
      raise Invalid(
        "observation times must be finite and ordered after snapshot",
      )
    }
    previous = t
  }
  for x in coordinates {
    ignore(self.mesh.locate(x))
  }
  let mut current = self
  let output = []
  for t in times {
    current = current.run_until(t, config)
    output.push(current.monitor(coordinates))
  }
  (current, output)
}

///|
pub fn monitors_json(monitors : Array[Monitor]) -> String {
  Json::array(monitors.map(fn(m) { m.to_json() })).stringify()
}

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

///|
pub extend Probe with ToJson::{to_json}

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

///|
pub extend Monitor with ToJson::{to_json}