///|
/// Summary for one inclusive analysis window.
pub struct WindowSummary {
  start : Int64
  end : Int64
  changes : Int
  signals : Int
  rising_edges : Int
  falling_edges : Int
  unknown_changes : Int
} derive(Debug, Eq)

///|
/// A rolling point for plotting change density and activity.
pub struct RollingPoint {
  time : Int64
  changes : Int
  active_signals : Int
} derive(Debug, Eq)

///|
/// Return windows of fixed width over the complete trace.
pub fn time_windows(trace : Trace, width : Int64) -> Array[(Int64, Int64)] {
  let safe = Int64::max(1L, width)
  let result : Array[(Int64, Int64)] = []
  let mut start : Int64 = 0L
  while start <= trace.end_time {
    result.push((start, Int64::min(trace.end_time, start + safe)))
    start += safe
  }
  result
}

///|
/// Summarize a single inclusive time window.
pub fn summarize_window(
  trace : Trace,
  start : Int64,
  end : Int64,
) -> WindowSummary {
  let changes = trace.changes.filter(fn(change) {
    change.time >= start && change.time <= end
  })
  let ids : Array[String] = []
  let mut rising = 0
  let mut falling = 0
  let mut unknown = 0
  for change in changes {
    if !ids.contains(change.id) {
      ids.push(change.id)
    }
    if change.value.is_unknown() {
      unknown += 1
    }
  }
  for signal in ids {
    for event in edges(trace, signal) {
      if event.time >= start && event.time <= end {
        match event.kind {
          Rising => rising += 1
          Falling => falling += 1
          _ => ()
        }
      }
    }
  }
  {
    start,
    end,
    changes: changes.length(),
    signals: ids.length(),
    rising_edges: rising,
    falling_edges: falling,
    unknown_changes: unknown,
  }
}

///|
/// Summarize every fixed-width window.
pub fn window_summaries(trace : Trace, width : Int64) -> Array[WindowSummary] {
  time_windows(trace, width).map(fn(window) {
    summarize_window(trace, window.0, window.1)
  })
}

///|
/// Produce one rolling point at the beginning of every window.
pub fn rolling_points(trace : Trace, width : Int64) -> Array[RollingPoint] {
  window_summaries(trace, width).map(fn(item) {
    { time: item.start, changes: item.changes, active_signals: item.signals }
  })
}

///|
/// Return the fraction of a window covered by known scalar intervals.
pub fn window_coverage(
  trace : Trace,
  signal_id : String,
  start : Int64,
  end : Int64,
) -> Double {
  let width = Int64::max(0L, end - start)
  if width == 0L {
    return 0.0
  }
  let covered = value_intervals(trace, signal_id).fold(init=0L, fn(
    total,
    interval,
  ) {
    let left = Int64::max(start, interval.start)
    let right = Int64::min(end, interval.end)
    let known = match interval.value {
      Scalar(value) => is_known_scalar(value)
      _ => false
    }
    if known && right > left {
      total + right - left
    } else {
      total
    }
  })
  covered.to_double() / width.to_double()
}

///|
/// Return windows where a signal has at least one transition.
pub fn active_windows(
  trace : Trace,
  signal_id : String,
  width : Int64,
) -> Array[(Int64, Int64)] {
  time_windows(trace, width).filter(fn(window) {
    signal_changes(trace, signal_id).any(fn(change) {
      change.time >= window.0 && change.time <= window.1
    })
  })
}

///|
/// Return the peak transition count in any window.
pub fn peak_window_transitions(
  trace : Trace,
  signal_id : String,
  width : Int64,
) -> Int {
  time_windows(trace, width).fold(init=0, fn(current, window) {
    let count = signal_changes(trace, signal_id).count_if(fn(change) {
      change.time >= window.0 && change.time <= window.1
    })
    Int::max(current, count)
  })
}

///|
/// Render rolling points as CSV.
pub fn rolling_points_csv(points : Array[RollingPoint]) -> String {
  let out : Array[String] = ["time,changes,active_signals"]
  for point in points {
    out.push(
      point.time.to_string() +
      "," +
      point.changes.to_string() +
      "," +
      point.active_signals.to_string(),
    )
  }
  out.join("\n")
}