///|
/// Aggregate counters for a collection of CAN frames.
pub struct FrameMetrics {
  total : Int
  classic_data : Int
  classic_remote : Int
  can_fd : Int
  errors : Int
  extended : Int
  payload_bytes : Int
  wire_bits : Int
  minimum_id : UInt?
  maximum_id : UInt?
  identifiers : Array[UInt]
  payload_histogram : Array[Int]
}

///|
/// Calculate deterministic metrics without changing the input order.
pub fn frame_metrics(frames : Array[Frame]) -> FrameMetrics {
  let histogram = Array::make(65, 0)
  let identifiers : Array[UInt] = []
  let mut classic_data = 0
  let mut classic_remote = 0
  let mut can_fd = 0
  let mut errors = 0
  let mut extended = 0
  let mut payload_bytes = 0
  let mut wire_bits = 0
  let mut minimum_id : UInt? = None
  let mut maximum_id : UInt? = None
  for frame in frames {
    match classify_frame(frame) {
      ClassicData => classic_data += 1
      ClassicRemote => classic_remote += 1
      CanFdData => can_fd += 1
      SimulationError => errors += 1
    }
    if frame.is_extended() {
      extended += 1
    }
    let length = frame.data().length()
    histogram[length] += 1
    payload_bytes += length
    wire_bits += frame_wire_bits(frame)
    identifiers.push(frame.id())
    match minimum_id {
      Some(value) => if frame.id() < value { minimum_id = Some(frame.id()) }
      None => minimum_id = Some(frame.id())
    }
    match maximum_id {
      Some(value) => if frame.id() > value { maximum_id = Some(frame.id()) }
      None => maximum_id = Some(frame.id())
    }
  }
  identifiers.sort()
  identifiers.dedup()
  {
    total: frames.length(),
    classic_data,
    classic_remote,
    can_fd,
    errors,
    extended,
    payload_bytes,
    wire_bits,
    minimum_id,
    maximum_id,
    identifiers,
    payload_histogram: histogram,
  }
}

///|
pub fn FrameMetrics::total(self : FrameMetrics) -> Int {
  self.total
}

///|
pub fn FrameMetrics::classic_data(self : FrameMetrics) -> Int {
  self.classic_data
}

///|
pub fn FrameMetrics::classic_remote(self : FrameMetrics) -> Int {
  self.classic_remote
}

///|
pub fn FrameMetrics::can_fd(self : FrameMetrics) -> Int {
  self.can_fd
}

///|
pub fn FrameMetrics::errors(self : FrameMetrics) -> Int {
  self.errors
}

///|
pub fn FrameMetrics::extended(self : FrameMetrics) -> Int {
  self.extended
}

///|
pub fn FrameMetrics::payload_bytes(self : FrameMetrics) -> Int {
  self.payload_bytes
}

///|
pub fn FrameMetrics::wire_bits(self : FrameMetrics) -> Int {
  self.wire_bits
}

///|
pub fn FrameMetrics::minimum_id(self : FrameMetrics) -> UInt? {
  self.minimum_id
}

///|
pub fn FrameMetrics::maximum_id(self : FrameMetrics) -> UInt? {
  self.maximum_id
}

///|
pub fn FrameMetrics::identifiers(self : FrameMetrics) -> Array[UInt] {
  self.identifiers.copy()
}

///|
pub fn FrameMetrics::payload_count(self : FrameMetrics, length : Int) -> Int {
  match self.payload_histogram.get(length) {
    Some(value) => value
    None => 0
  }
}

///|
/// Calculate frames per second over an observation interval.
pub fn frames_per_second(
  metrics : FrameMetrics,
  duration_us : UInt64,
) -> Double {
  if duration_us == 0 {
    0.0
  } else {
    metrics.total.to_double() * 1_000_000.0 / duration_us.to_double()
  }
}

///|
/// Calculate approximate utilization for a bitrate in kbit/s.
pub fn bus_utilization(
  metrics : FrameMetrics,
  duration_us : UInt64,
  bitrate_kbps : UInt,
) -> Double {
  if duration_us == 0 || bitrate_kbps == 0 {
    0.0
  } else {
    metrics.wire_bits.to_double() *
    1000.0 /
    (duration_us.to_double() * bitrate_kbps.to_double())
  }
}

///|
/// Render a stable metrics report for a CLI or CI artifact.
pub fn FrameMetrics::to_text(self : FrameMetrics) -> String {
  "frames=\{self.total} classic_data=\{self.classic_data} classic_remote=\{self.classic_remote} can_fd=\{self.can_fd} errors=\{self.errors} payload_bytes=\{self.payload_bytes} wire_bits=\{self.wire_bits} unique_ids=\{self.identifiers.length()}"
}

///|
/// Compute metrics directly from a trace.
pub fn trace_metrics(trace : Trace) -> FrameMetrics {
  frame_metrics(trace.frames())
}