///|
/// A timestamped action that can be applied to a linear filter.  Keeping the
/// action separate from the filter makes offline reproduction and incident
/// investigation deterministic: the same event stream produces the same
/// state stream.
pub(all) enum ReplayEvent {
  Predict(Int)
  Measure(Int, Array[Double])
  Missing(Int)
  Restore(FilterCheckpoint)
} derive(Debug)

///|
/// One durable observation of a replay step.
pub struct ReplayRecord {
  timestamp : Int
  result : UpdateResult
  state : Array[Double]
  covariance : Matrix
  nis : Double
} derive(Debug)

///|
pub fn ReplayRecord::new(
  timestamp : Int,
  result : UpdateResult,
  state : Array[Double],
  covariance : Matrix,
  nis : Double,
) -> ReplayRecord {
  { timestamp, result, state: state.copy(), covariance: covariance.copy(), nis }
}

///|
pub fn ReplayRecord::timestamp(self : ReplayRecord) -> Int {
  self.timestamp
}

///|
pub fn ReplayRecord::result(self : ReplayRecord) -> UpdateResult {
  self.result
}

///|
pub fn ReplayRecord::state(self : ReplayRecord) -> Array[Double] {
  self.state.copy()
}

///|
pub fn ReplayRecord::covariance(self : ReplayRecord) -> Matrix {
  self.covariance.copy()
}

///|
pub fn ReplayRecord::nis(self : ReplayRecord) -> Double {
  self.nis
}

///|
/// Counts and records accumulated by a replay session.
pub struct ReplayReport {
  steps : Int
  accepted : Int
  rejected : Int
  missing : Int
  invalid : Int
  final_timestamp : Int
  final_state : Array[Double]
  final_covariance : Matrix
} derive(Debug)

///|
pub fn ReplayReport::steps(self : ReplayReport) -> Int {
  self.steps
}

///|
pub fn ReplayReport::accepted(self : ReplayReport) -> Int {
  self.accepted
}

///|
pub fn ReplayReport::rejected(self : ReplayReport) -> Int {
  self.rejected
}

///|
pub fn ReplayReport::missing(self : ReplayReport) -> Int {
  self.missing
}

///|
pub fn ReplayReport::invalid(self : ReplayReport) -> Int {
  self.invalid
}

///|
pub fn ReplayReport::final_timestamp(self : ReplayReport) -> Int {
  self.final_timestamp
}

///|
pub fn ReplayReport::final_state(self : ReplayReport) -> Array[Double] {
  self.final_state.copy()
}

///|
pub fn ReplayReport::final_covariance(self : ReplayReport) -> Matrix {
  self.final_covariance.copy()
}

///|
/// An append-only, bounded replay ledger.  The bounded mode is useful for
/// embedded applications where a diagnostic trail must not grow forever.
pub struct ReplayTrace {
  mut records : Array[ReplayRecord]
  capacity : Int
}

///|
pub fn ReplayTrace::new(capacity : Int) -> ReplayTrace {
  { records: [], capacity: if capacity < 0 { 0 } else { capacity } }
}

///|
pub fn ReplayTrace::length(self : ReplayTrace) -> Int {
  self.records.length()
}

///|
pub fn ReplayTrace::capacity(self : ReplayTrace) -> Int {
  self.capacity
}

///|
pub fn ReplayTrace::records(self : ReplayTrace) -> Array[ReplayRecord] {
  self.records.copy()
}

///|
fn ReplayTrace::append(self : ReplayTrace, record : ReplayRecord) -> Unit {
  if self.capacity == 0 {
    return
  }
  if self.records.length() < self.capacity {
    self.records.push(record)
    return
  }
  for i in 1.. ReplayRecord? {
  if self.records.length() == 0 {
    None
  } else {
    Some(self.records[0])
  }
}

///|
pub fn ReplayTrace::last(self : ReplayTrace) -> ReplayRecord? {
  if self.records.length() == 0 {
    None
  } else {
    Some(self.records[self.records.length() - 1])
  }
}

///|
pub fn ReplayTrace::clear(self : ReplayTrace) -> Unit {
  self.records = []
}

///|
/// Stateful replay runner for `KalmanND`.  A runner owns its filter so callers
/// can replay a log without mutating the production filter used elsewhere.
pub struct ReplaySession {
  filter : KalmanND
  trace : ReplayTrace
  mut timestamp : Int
  mut steps : Int
  mut accepted : Int
  mut rejected : Int
  mut missing : Int
  mut invalid : Int
}

///|
pub fn ReplaySession::new(
  filter : KalmanND,
  trace_capacity : Int,
) -> ReplaySession {
  {
    filter,
    trace: ReplayTrace::new(trace_capacity),
    timestamp: 0,
    steps: 0,
    accepted: 0,
    rejected: 0,
    missing: 0,
    invalid: 0,
  }
}

///|
pub fn ReplaySession::step(
  self : ReplaySession,
  event : ReplayEvent,
) -> UpdateResult {
  let mut timestamp = self.timestamp
  let result = match event {
    Predict(time) => {
      timestamp = time
      self.filter.predict()
      Accepted
    }
    Measure(time, values) => {
      timestamp = time
      self.filter.update(values)
    }
    Missing(time) => {
      timestamp = time
      self.filter.update_missing()
    }
    Restore(checkpoint) => {
      timestamp = checkpoint.timestamp()
      if self.filter.restore_checkpoint(checkpoint) {
        Accepted
      } else {
        InvalidMeasurement
      }
    }
  }
  self.timestamp = timestamp
  self.steps = self.steps + 1
  match result {
    Accepted => self.accepted = self.accepted + 1
    RejectedByGate => self.rejected = self.rejected + 1
    MissingMeasurement => self.missing = self.missing + 1
    InvalidMeasurement | SingularInnovation => self.invalid = self.invalid + 1
  }
  let record = ReplayRecord::new(
    self.timestamp,
    result,
    self.filter.state(),
    self.filter.covariance(),
    self.filter.normalized_innovation_squared(),
  )
  self.trace.append(record)
  result
}

///|
pub fn ReplaySession::run(
  self : ReplaySession,
  events : Array[ReplayEvent],
) -> Array[UpdateResult] {
  let results : Array[UpdateResult] = []
  for event in events {
    results.push(self.step(event))
  }
  results
}

///|
pub fn ReplaySession::filter(self : ReplaySession) -> KalmanND {
  self.filter
}

///|
pub fn ReplaySession::trace(self : ReplaySession) -> ReplayTrace {
  self.trace
}

///|
pub fn ReplaySession::timestamp(self : ReplaySession) -> Int {
  self.timestamp
}

///|
pub fn ReplaySession::steps(self : ReplaySession) -> Int {
  self.steps
}

///|
pub fn ReplaySession::accepted(self : ReplaySession) -> Int {
  self.accepted
}

///|
pub fn ReplaySession::rejected(self : ReplaySession) -> Int {
  self.rejected
}

///|
pub fn ReplaySession::missing(self : ReplaySession) -> Int {
  self.missing
}

///|
pub fn ReplaySession::invalid(self : ReplaySession) -> Int {
  self.invalid
}

///|
pub fn ReplaySession::reset(self : ReplaySession) -> Unit {
  self.filter.reset()
  self.trace.clear()
  self.timestamp = 0
  self.steps = 0
  self.accepted = 0
  self.rejected = 0
  self.missing = 0
  self.invalid = 0
}

///|
pub fn ReplaySession::report(self : ReplaySession) -> ReplayReport {
  {
    steps: self.steps,
    accepted: self.accepted,
    rejected: self.rejected,
    missing: self.missing,
    invalid: self.invalid,
    final_timestamp: self.timestamp,
    final_state: self.filter.state(),
    final_covariance: self.filter.covariance(),
  }
}

///|
/// Make a reproducible sequence of scalar observations with periodic drops.
/// This is intentionally small and deterministic so it is useful in examples,
/// acceptance tests, and regression tests without a random dependency.
pub fn make_replay_events(
  start_timestamp : Int,
  count : Int,
  first_value : Double,
  velocity : Double,
  missing_period : Int,
) -> Array[ReplayEvent] {
  let events : Array[ReplayEvent] = []
  let safe_count = if count < 0 { 0 } else { count }
  let safe_period = if missing_period < 0 { 0 } else { missing_period }
  for index in 0.. Bool {
  result is Accepted
}

///|
pub fn replay_result_is_data_loss(result : UpdateResult) -> Bool {
  match result {
    MissingMeasurement | InvalidMeasurement => true
    Accepted | RejectedByGate | SingularInnovation => false
  }
}