///|
/// Online digest for large traces that should not retain every change.
pub struct StreamingDigest {
  mut changes : Int
  signals : Array[(String, Int)]
  mut first_time : Int64?
  mut last_time : Int64?
  mut unknown : Int
  mut checksum : Int64
} derive(Debug, Eq)

///|
/// Immutable digest snapshot suitable for persistence.
pub struct DigestSummary {
  changes : Int
  signals : Int
  first_time : Int64?
  last_time : Int64?
  unknown : Int
  checksum : Int64
} derive(Debug, Eq)

///|
/// Create an empty online digest.
pub fn new_streaming_digest() -> StreamingDigest {
  {
    changes: 0,
    signals: [],
    first_time: None,
    last_time: None,
    unknown: 0,
    checksum: 0L,
  }
}

///|
/// Add one change to the digest in source order.
pub fn StreamingDigest::push_change(
  self : StreamingDigest,
  change : Change,
) -> Unit {
  self.changes += 1
  match self.first_time {
    None => self.first_time = Some(change.time)
    Some(_) => ()
  }
  self.last_time = Some(change.time)
  if change.value.is_unknown() {
    self.unknown += 1
  }
  self.checksum = self.checksum * 31L +
    change.time +
    change.id.length().to_int64()
  self.checksum = self.checksum * 31L + change.value.width().to_int64()
  match self.signals.search_by(fn(item) { item.0 == change.id }) {
    Some(index) => self.signals[index] = (change.id, self.signals[index].1 + 1)
    None => self.signals.push((change.id, 1))
  }
}

///|
/// Consume all changes from a trace into an online digest.
pub fn digest_trace(trace : Trace) -> DigestSummary {
  let digest = new_streaming_digest()
  for change in trace.changes {
    digest.push_change(change)
  }
  digest.snapshot()
}

///|
/// Freeze the current online counters.
pub fn StreamingDigest::snapshot(self : StreamingDigest) -> DigestSummary {
  {
    changes: self.changes,
    signals: self.signals.length(),
    first_time: self.first_time,
    last_time: self.last_time,
    unknown: self.unknown,
    checksum: self.checksum,
  }
}

///|
/// Return the count for one signal from a mutable digest.
pub fn StreamingDigest::signal_change_count(
  self : StreamingDigest,
  signal_id : String,
) -> Int {
  match self.signals.search_by(fn(item) { item.0 == signal_id }) {
    Some(index) => self.signals[index].1
    None => 0
  }
}

///|
/// Return a stable text representation of an immutable digest.
pub fn DigestSummary::to_text(self : DigestSummary) -> String {
  "changes=" +
  self.changes.to_string() +
  " signals=" +
  self.signals.to_string() +
  " first=" +
  optional_time_text(self.first_time) +
  " last=" +
  optional_time_text(self.last_time) +
  " unknown=" +
  self.unknown.to_string() +
  " checksum=" +
  self.checksum.to_string()
}

///|
/// Return a stable JSON representation of an immutable digest.
pub fn DigestSummary::to_json(self : DigestSummary) -> String {
  "{\"changes\":" +
  self.changes.to_string() +
  ",\"signals\":" +
  self.signals.to_string() +
  ",\"first\":" +
  optional_time_json(self.first_time) +
  ",\"last\":" +
  optional_time_json(self.last_time) +
  ",\"unknown\":" +
  self.unknown.to_string() +
  ",\"checksum\":" +
  self.checksum.to_string() +
  "}"
}

///|
fn optional_time_text(value : Int64?) -> String {
  match value {
    Some(time) => time.to_string()
    None => "-"
  }
}

///|
fn optional_time_json(value : Int64?) -> String {
  match value {
    Some(time) => time.to_string()
    None => "null"
  }
}

///|
/// Return whether a digest describes an empty input.
pub fn DigestSummary::is_empty(self : DigestSummary) -> Bool {
  self.changes == 0
}

///|
/// Return the duration covered by an immutable digest.
pub fn DigestSummary::duration(self : DigestSummary) -> Int64 {
  match (self.first_time, self.last_time) {
    (Some(first), Some(last)) => last - first
    _ => 0L
  }
}