///|
/// A named analysis checkpoint at a simulation timestamp.
pub struct TraceCheckpoint {
  name : String
  time : Int64
  digest : Int64
  samples : Array[Sample]
  change_count : Int
  signal_count : Int
} derive(Debug)

///|
/// A checkpoint set for replay and incremental analysis.
pub struct CheckpointSet {
  trace_digest : Int64
  checkpoints : Array[TraceCheckpoint]
  ordered : Bool
} derive(Debug)

///|
/// Create checkpoints at explicit times.
pub fn make_checkpoints(
  trace : Trace,
  times : Array[(String, Int64)],
) -> CheckpointSet {
  let checkpoints : Array[TraceCheckpoint] = []
  for item in times {
    let snapshot = trace
      .snapshot_at(item.1)
      .map(fn(sample) { { signal_id: sample.0, time: item.1, value: sample.1 } })
    let changes = trace.changes
      .filter(fn(change) { change.time <= item.1 })
      .length()
    checkpoints.push({
      name: item.0,
      time: item.1,
      digest: checkpoint_digest(snapshot),
      samples: snapshot,
      change_count: changes,
      signal_count: snapshot.length(),
    })
  }
  {
    trace_digest: trace.shape_digest(),
    checkpoints,
    ordered: checkpoint_times_ordered(checkpoints),
  }
}

///|
/// Create regularly spaced checkpoints.
pub fn regular_checkpoints(trace : Trace, step : Int64) -> CheckpointSet {
  let safe_step = Int64::max(1L, step)
  let times : Array[(String, Int64)] = []
  let mut time = 0L
  let mut index = 0
  while time <= trace.end_time {
    times.push(("checkpoint-" + index.to_string(), time))
    time += safe_step
    index += 1
  }
  make_checkpoints(trace, times)
}

///|
/// Find a checkpoint by name.
pub fn CheckpointSet::find(
  self : CheckpointSet,
  name : String,
) -> TraceCheckpoint? {
  for checkpoint in self.checkpoints {
    if checkpoint.name == name {
      return Some(checkpoint)
    }
  }
  None
}

///|
/// Find the latest checkpoint at or before a timestamp.
pub fn CheckpointSet::at_or_before(
  self : CheckpointSet,
  time : Int64,
) -> TraceCheckpoint? {
  self.checkpoints.fold(init=None, fn(current, item) {
    if item.time > time {
      current
    } else {
      Some(item)
    }
  })
}

///|
/// Return whether two checkpoints have equivalent sampled state.
pub fn checkpoints_equal(
  left : TraceCheckpoint,
  right : TraceCheckpoint,
) -> Bool {
  left.time == right.time &&
  left.samples == right.samples &&
  left.change_count == right.change_count
}

///|
/// Export checkpoints as CSV.
pub fn checkpoints_csv(checkpoints : Array[TraceCheckpoint]) -> String {
  let out = StringBuilder()
  out.write_string("name,time,digest,changes,signals\n")
  for item in checkpoints {
    out.write_string(
      escape_text(item.name) +
      "," +
      item.time.to_string() +
      "," +
      item.digest.to_string() +
      "," +
      item.change_count.to_string() +
      "," +
      item.signal_count.to_string() +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render a checkpoint set as JSON.
pub fn CheckpointSet::to_json(self : CheckpointSet) -> String {
  let items = self.checkpoints.map(checkpoint_json).join(",")
  "{\"trace_digest\":" +
  self.trace_digest.to_string() +
  ",\"ordered\":" +
  self.ordered.to_string() +
  ",\"checkpoints\":[" +
  items +
  "]}"
}

///|
/// Render a checkpoint set for a terminal.
pub fn CheckpointSet::to_text(self : CheckpointSet) -> String {
  "trace_digest=" +
  self.trace_digest.to_string() +
  " checkpoints=" +
  self.checkpoints.length().to_string() +
  " ordered=" +
  self.ordered.to_string()
}

///|
/// Build a small digest from sampled values.
fn checkpoint_digest(samples : Array[Sample]) -> Int64 {
  samples.fold(init=17L, fn(hash, item) {
    let value_hash = match item.value {
      Some(value) => value.display().hash()
      None => 0
    }
    hash * 31L + item.time + value_hash.to_int64()
  })
}

///|
/// Check monotonic checkpoint order.
fn checkpoint_times_ordered(checkpoints : Array[TraceCheckpoint]) -> Bool {
  let mut previous : Int64? = None
  for item in checkpoints {
    match previous {
      Some(time) if item.time < time => return false
      _ => ()
    }
    previous = Some(item.time)
  }
  true
}

///|
/// Encode one checkpoint as JSON.
fn checkpoint_json(item : TraceCheckpoint) -> String {
  "{\"name\":\"" +
  escape_text(item.name) +
  "\",\"time\":" +
  item.time.to_string() +
  ",\"digest\":" +
  item.digest.to_string() +
  ",\"changes\":" +
  item.change_count.to_string() +
  ",\"signals\":" +
  item.signal_count.to_string() +
  "}"
}