///|
/// A predicate for selecting frames from a capture trace.
pub enum TraceQueryPredicate {
  TraceQueryAny
  TraceQueryIdentifier(UInt)
  TraceQueryExtended(Bool)
  TraceQueryProtocol(Protocol)
  TraceQueryFrameClass(FrameClass)
  TraceQueryPayloadLength(Int)
  TraceQueryPayloadAtLeast(Int)
  TraceQueryPayloadByte(Int, Byte)
  TraceQueryIdentifierRange(UInt, UInt)
}

///|
pub fn trace_query_predicate_variants() -> Array[TraceQueryPredicate] {
  [
    TraceQueryAny,
    TraceQueryIdentifier(0),
    TraceQueryExtended(false),
    TraceQueryProtocol(Can20),
    TraceQueryFrameClass(ClassicData),
    TraceQueryPayloadLength(0),
    TraceQueryPayloadAtLeast(0),
    TraceQueryPayloadByte(0, 0),
    TraceQueryIdentifierRange(0, 1),
  ]
}

///|
/// A time and predicate specification for trace analysis.
pub struct TraceQuerySpec {
  mut start_us : UInt64?
  mut end_us : UInt64?
  predicates : Array[TraceQueryPredicate]
  mut inspected : Int
  mut matched : Int
}

///|
pub fn new_trace_query() -> TraceQuerySpec {
  { start_us: None, end_us: None, predicates: [], inspected: 0, matched: 0 }
}

///|
pub fn TraceQuerySpec::between(
  self : TraceQuerySpec,
  start_us : UInt64,
  end_us : UInt64,
) -> Unit {
  self.start_us = Some(start_us)
  self.end_us = Some(end_us)
}

///|
pub fn TraceQuerySpec::after(self : TraceQuerySpec, start_us : UInt64) -> Unit {
  self.start_us = Some(start_us)
}

///|
pub fn TraceQuerySpec::before(self : TraceQuerySpec, end_us : UInt64) -> Unit {
  self.end_us = Some(end_us)
}

///|
pub fn TraceQuerySpec::where_is(
  self : TraceQuerySpec,
  predicate : TraceQueryPredicate,
) -> Unit {
  self.predicates.push(predicate)
}

///|
pub fn TraceQuerySpec::predicates(
  self : TraceQuerySpec,
) -> Array[TraceQueryPredicate] {
  self.predicates.copy()
}

///|
pub fn TraceQuerySpec::inspected(self : TraceQuerySpec) -> Int {
  self.inspected
}

///|
pub fn TraceQuerySpec::matched(self : TraceQuerySpec) -> Int {
  self.matched
}

///|
pub fn TraceQuerySpec::reset_counters(self : TraceQuerySpec) -> Unit {
  self.inspected = 0
  self.matched = 0
}

///|
/// A selected trace entry with its original position.
pub struct TraceQueryMatch {
  index : Int
  timestamp_us : UInt64
  frame : Frame
}

///|
pub fn TraceQueryMatch::index(self : TraceQueryMatch) -> Int {
  self.index
}

///|
pub fn TraceQueryMatch::timestamp_us(self : TraceQueryMatch) -> UInt64 {
  self.timestamp_us
}

///|
pub fn TraceQueryMatch::frame(self : TraceQueryMatch) -> Frame {
  self.frame
}

///|
pub fn TraceQueryMatch::latency_from(
  self : TraceQueryMatch,
  timestamp_us : UInt64,
) -> UInt64 {
  if self.timestamp_us >= timestamp_us {
    self.timestamp_us - timestamp_us
  } else {
    0
  }
}

///|
/// Return whether a frame matches one predicate.
pub fn trace_query_matches_predicate(
  frame : Frame,
  predicate : TraceQueryPredicate,
) -> Bool {
  match predicate {
    TraceQueryAny => true
    TraceQueryIdentifier(id) => frame.id() == id
    TraceQueryExtended(extended) => frame.is_extended() == extended
    TraceQueryProtocol(protocol) =>
      trace_query_protocol_equal(frame.protocol(), protocol)
    TraceQueryFrameClass(class) =>
      trace_query_class_equal(classify_frame(frame), class)
    TraceQueryPayloadLength(length) => frame.data().length() == length
    TraceQueryPayloadAtLeast(length) => frame.data().length() >= length
    TraceQueryPayloadByte(index, value) =>
      match frame.data().get(index) {
        Some(actual) => actual == value
        None => false
      }
    TraceQueryIdentifierRange(start, end) =>
      frame.id() >= start && frame.id() <= end
  }
}

///|
pub fn trace_query_matches(
  frame : Frame,
  predicates : Array[TraceQueryPredicate],
) -> Bool {
  for predicate in predicates {
    if !trace_query_matches_predicate(frame, predicate) {
      return false
    }
  }
  true
}

///|
fn trace_query_protocol_equal(left : Protocol, right : Protocol) -> Bool {
  match left {
    Can20 =>
      match right {
        Can20 => true
        _ => false
      }
    CanFd =>
      match right {
        CanFd => true
        _ => false
      }
  }
}

///|
fn trace_query_class_equal(left : FrameClass, right : FrameClass) -> Bool {
  match left {
    ClassicData =>
      match right {
        ClassicData => true
        _ => false
      }
    ClassicRemote =>
      match right {
        ClassicRemote => true
        _ => false
      }
    CanFdData =>
      match right {
        CanFdData => true
        _ => false
      }
    SimulationError =>
      match right {
        SimulationError => true
        _ => false
      }
  }
}

///|
/// Select entries from a trace without changing capture order.
pub fn trace_query(
  trace : Trace,
  query : TraceQuerySpec,
) -> Array[TraceQueryMatch] {
  query.reset_counters()
  let result : Array[TraceQueryMatch] = []
  for index, entry in trace.entries() {
    query.inspected += 1
    let after_start = match query.start_us {
      Some(start) => entry.timestamp() >= start
      None => true
    }
    let before_end = match query.end_us {
      Some(end) => entry.timestamp() <= end
      None => true
    }
    if after_start &&
      before_end &&
      trace_query_matches(entry.frame(), query.predicates) {
      query.matched += 1
      result.push({
        index,
        timestamp_us: entry.timestamp(),
        frame: entry.frame(),
      })
    }
  }
  result
}

///|
/// Return the first query match, if any.
pub fn trace_query_first(
  trace : Trace,
  query : TraceQuerySpec,
) -> TraceQueryMatch? {
  let result = trace_query(trace, query)
  result.get(0)
}

///|
/// Return the last query match, if any.
pub fn trace_query_last(
  trace : Trace,
  query : TraceQuerySpec,
) -> TraceQueryMatch? {
  let result = trace_query(trace, query)
  if result.is_empty() {
    None
  } else {
    Some(result[result.length() - 1])
  }
}

///|
/// Count frames by identifier in a query result.
pub fn trace_query_identifier_counts(
  matches : Array[TraceQueryMatch],
) -> Array[(UInt, Int)] {
  let result : Array[(UInt, Int)] = []
  for item in matches {
    let mut found = false
    for index, pair in result {
      if pair.0 == item.frame().id() {
        result[index] = (pair.0, pair.1 + 1)
        found = true
        break
      }
    }
    if !found {
      result.push((item.frame().id(), 1))
    }
  }
  result.sort_by((left, right) => {
    if left.0 < right.0 {
      -1
    } else if left.0 > right.0 {
      1
    } else {
      0
    }
  })
  result
}

///|
/// A fixed time bucket for frame-rate and load dashboards.
pub struct TraceQueryBucket {
  start_us : UInt64
  end_us : UInt64
  mut frames : Int
  mut payload_bytes : Int
  mut wire_bits : Int
}

///|
pub fn TraceQueryBucket::start_us(self : TraceQueryBucket) -> UInt64 {
  self.start_us
}

///|
pub fn TraceQueryBucket::end_us(self : TraceQueryBucket) -> UInt64 {
  self.end_us
}

///|
pub fn TraceQueryBucket::frames(self : TraceQueryBucket) -> Int {
  self.frames
}

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

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

///|
pub fn TraceQueryBucket::utilization(
  self : TraceQueryBucket,
  bitrate_kbps : UInt,
) -> Double {
  let duration = self.end_us - self.start_us
  if duration == 0 || bitrate_kbps == 0 {
    0.0
  } else {
    self.wire_bits.to_double() *
    1000.0 /
    (duration.to_double() * bitrate_kbps.to_double())
  }
}

///|
/// Partition a trace into fixed-width buckets.
pub fn trace_query_buckets(
  trace : Trace,
  start_us : UInt64,
  end_us : UInt64,
  bucket_us : UInt64,
) -> Array[TraceQueryBucket] {
  if bucket_us == 0 || end_us <= start_us {
    []
  } else {
    let count = ((end_us - start_us + bucket_us - 1) / bucket_us).to_int()
    let buckets : Array[TraceQueryBucket] = []
    for index in 0.. end_us {
          end_us
        } else {
          start_us + (index.to_uint64() + 1) * bucket_us
        },
        frames: 0,
        payload_bytes: 0,
        wire_bits: 0,
      })
    }
    for entry in trace.entries() {
      if entry.timestamp() >= start_us && entry.timestamp() < end_us {
        let index = ((entry.timestamp() - start_us) / bucket_us).to_int()
        if index < buckets.length() {
          buckets[index].frames += 1
          buckets[index].payload_bytes += entry.frame().data().length()
          buckets[index].wire_bits += frame_wire_bits(entry.frame())
        }
      }
    }
    buckets
  }
}

///|
/// A normalized replay event with a relative timestamp.
pub struct TraceReplayEvent {
  offset_us : UInt64
  frame : Frame
  original_index : Int
}

///|
pub fn TraceReplayEvent::offset_us(self : TraceReplayEvent) -> UInt64 {
  self.offset_us
}

///|
pub fn TraceReplayEvent::frame(self : TraceReplayEvent) -> Frame {
  self.frame
}

///|
pub fn TraceReplayEvent::original_index(self : TraceReplayEvent) -> Int {
  self.original_index
}

///|
/// A replay plan that supports deterministic time scaling and filtering.
pub struct TraceReplayPlan {
  events : Array[TraceReplayEvent]
  scale_num : UInt64
  scale_den : UInt64
  mut cursor : Int
}

///|
pub fn trace_replay_plan(
  trace : Trace,
  scale_num? : UInt64 = 1,
  scale_den? : UInt64 = 1,
) -> TraceReplayPlan {
  let entries = trace.entries()
  let base : UInt64 = if entries.is_empty() {
    0
  } else {
    entries[0].timestamp()
  }
  let numerator : UInt64 = if scale_num == 0 { 1 } else { scale_num }
  let denominator : UInt64 = if scale_den == 0 { 1 } else { scale_den }
  let events : Array[TraceReplayEvent] = []
  for index, entry in entries {
    let delta = if entry.timestamp() >= base {
      entry.timestamp() - base
    } else {
      0
    }
    events.push({
      offset_us: delta * denominator / numerator,
      frame: entry.frame(),
      original_index: index,
    })
  }
  { events, scale_num: numerator, scale_den: denominator, cursor: 0 }
}

///|
pub fn TraceReplayPlan::events(
  self : TraceReplayPlan,
) -> Array[TraceReplayEvent] {
  self.events.copy()
}

///|
pub fn TraceReplayPlan::scale_num(self : TraceReplayPlan) -> UInt64 {
  self.scale_num
}

///|
pub fn TraceReplayPlan::scale_den(self : TraceReplayPlan) -> UInt64 {
  self.scale_den
}

///|
pub fn TraceReplayPlan::cursor(self : TraceReplayPlan) -> Int {
  self.cursor
}

///|
pub fn TraceReplayPlan::reset(self : TraceReplayPlan) -> Unit {
  self.cursor = 0
}

///|
pub fn TraceReplayPlan::next(self : TraceReplayPlan) -> TraceReplayEvent? {
  if self.cursor >= self.events.length() {
    None
  } else {
    let result = self.events[self.cursor]
    self.cursor += 1
    Some(result)
  }
}

///|
pub fn TraceReplayPlan::at(
  self : TraceReplayPlan,
  offset_us : UInt64,
) -> Array[TraceReplayEvent] {
  let result : Array[TraceReplayEvent] = []
  for event in self.events {
    if event.offset_us() <= offset_us {
      result.push(event)
    } else {
      break
    }
  }
  result
}

///|
pub fn TraceReplayPlan::duration_us(self : TraceReplayPlan) -> UInt64 {
  if self.events.is_empty() {
    0
  } else {
    self.events[self.events.length() - 1].offset_us()
  }
}

///|
/// A difference between two captures at a matching ordinal.
pub enum TraceDiffKind {
  TraceDiffMissingLeft
  TraceDiffMissingRight
  TraceDiffIdentifier
  TraceDiffPayload
  TraceDiffTimestamp
}

///|
pub fn trace_diff_kind_variants() -> Array[TraceDiffKind] {
  [
    TraceDiffMissingLeft,
    TraceDiffMissingRight,
    TraceDiffIdentifier,
    TraceDiffPayload,
    TraceDiffTimestamp,
  ]
}

///|
pub struct TraceDiff {
  ordinal : Int
  kind : TraceDiffKind
  left : TraceQueryMatch?
  right : TraceQueryMatch?
}

///|
pub fn TraceDiff::ordinal(self : TraceDiff) -> Int {
  self.ordinal
}

///|
pub fn TraceDiff::kind(self : TraceDiff) -> TraceDiffKind {
  self.kind
}

///|
pub fn TraceDiff::left(self : TraceDiff) -> TraceQueryMatch? {
  self.left
}

///|
pub fn TraceDiff::right(self : TraceDiff) -> TraceQueryMatch? {
  self.right
}

///|
/// Compare two captures in order, reporting only observable differences.
pub fn trace_diff(
  left : Trace,
  right : Trace,
  compare_timestamp? : Bool = true,
) -> Array[TraceDiff] {
  let left_entries = left.entries()
  let right_entries = right.entries()
  let total = if left_entries.length() > right_entries.length() {
    left_entries.length()
  } else {
    right_entries.length()
  }
  let result : Array[TraceDiff] = []
  for ordinal in 0..
        match right_match {
          None => ()
          Some(right_item) =>
            result.push({
              ordinal,
              kind: TraceDiffMissingLeft,
              left: None,
              right: Some(right_item),
            })
        }
      Some(left_item) =>
        match right_match {
          None =>
            result.push({
              ordinal,
              kind: TraceDiffMissingRight,
              left: Some(left_item),
              right: None,
            })
          Some(right_item) =>
            if left_item.frame().id() != right_item.frame().id() {
              result.push({
                ordinal,
                kind: TraceDiffIdentifier,
                left: Some(left_item),
                right: Some(right_item),
              })
            } else if left_item.frame().data() != right_item.frame().data() {
              result.push({
                ordinal,
                kind: TraceDiffPayload,
                left: Some(left_item),
                right: Some(right_item),
              })
            } else if compare_timestamp &&
              left_item.timestamp_us() != right_item.timestamp_us() {
              result.push({
                ordinal,
                kind: TraceDiffTimestamp,
                left: Some(left_item),
                right: Some(right_item),
              })
            }
        }
    }
  }
  result
}

///|
/// Return a stable textual summary of a trace query.
pub fn trace_query_summary(matches : Array[TraceQueryMatch]) -> String {
  let counts = trace_query_identifier_counts(matches)
  let parts : Array[String] = []
  for item in counts {
    parts.push(item.0.to_string() + ":" + item.1.to_string())
  }
  "matches=" + matches.length().to_string() + " ids=" + parts.join(",")
}