///|
/// Aggregate metrics for one fixed analysis window.
pub struct EventWindowMetric {
  start : Int64
  end : Int64
  changes : Int
  active_signals : Int
  rising_edges : Int
  falling_edges : Int
  unknown_changes : Int
  density : Double
} derive(Debug, Eq)

///|
/// Calculate event metrics for every fixed-width window.
pub fn event_window_metrics(
  trace : Trace,
  width : Int64,
) -> Array[EventWindowMetric] {
  let result : Array[EventWindowMetric] = []
  for window in time_windows(trace, width) {
    let summary = summarize_window(trace, window.0, window.1)
    let duration = Int64::max(1L, window.1 - window.0)
    result.push({
      start: summary.start,
      end: summary.end,
      changes: summary.changes,
      active_signals: summary.signals,
      rising_edges: summary.rising_edges,
      falling_edges: summary.falling_edges,
      unknown_changes: summary.unknown_changes,
      density: summary.changes.to_double() / duration.to_double(),
    })
  }
  result
}

///|
/// Render event metrics as CSV for charting.
pub fn event_window_metrics_csv(metrics : Array[EventWindowMetric]) -> String {
  let out : Array[String] = [
    "start,end,changes,active_signals,rising,falling,unknown,density",
  ]
  for item in metrics {
    out.push(
      item.start.to_string() +
      "," +
      item.end.to_string() +
      "," +
      item.changes.to_string() +
      "," +
      item.active_signals.to_string() +
      "," +
      item.rising_edges.to_string() +
      "," +
      item.falling_edges.to_string() +
      "," +
      item.unknown_changes.to_string() +
      "," +
      item.density.to_string(),
    )
  }
  out.join("\n")
}

///|
/// Return the busiest window by change count.
pub fn busiest_event_window(
  metrics : Array[EventWindowMetric],
) -> EventWindowMetric? {
  let mut best : EventWindowMetric? = None
  for item in metrics {
    match best {
      Some(current) if current.changes >= item.changes => ()
      _ => best = Some(item)
    }
  }
  best
}

///|
/// Return the average event density across non-empty windows.
pub fn mean_event_density(metrics : Array[EventWindowMetric]) -> Double {
  if metrics.length() == 0 {
    0.0
  } else {
    metrics.fold(init=0.0, fn(total, item) { total + item.density }) /
    metrics.length().to_double()
  }
}

///|
/// Return a bounded window quality score based on unknown samples.
pub fn event_window_quality(item : EventWindowMetric) -> Int {
  let penalty = Int::min(100, item.unknown_changes * 10)
  100 - penalty
}

///|
/// Return whether windows cover the complete trace without gaps.
pub fn windows_cover_trace(trace : Trace, width : Int64) -> Bool {
  let windows = time_windows(trace, width)
  if windows.length() == 0 {
    return trace.end_time < 0L
  }
  if windows[0].0 != 0L {
    return false
  }
  for index in 1.. String {
  let metrics = event_window_metrics(trace, width)
  let busiest = match busiest_event_window(metrics) {
    Some(item) => item.changes.to_string()
    None => "0"
  }
  "windows=" +
  metrics.length().to_string() +
  " busiest_changes=" +
  busiest +
  " mean_density=" +
  mean_event_density(metrics).to_string() +
  " cover=" +
  windows_cover_trace(trace, width).to_string()
}