///|
/// A matched pair of source and target rising edges.
pub struct LatencySample {
  source_signal : String
  target_signal : String
  source_time : Int64
  target_time : Int64?
  latency : Int64?
} derive(Debug, Eq)

///|
/// Summary of matched edge latency.
pub struct LatencyReport {
  source_signal : String
  target_signal : String
  samples : Int
  matched : Int
  minimum : Int64?
  maximum : Int64?
  mean : Double?
} derive(Debug, Eq)

///|
/// Match every source rising edge with the first target rising edge at or after it.
pub fn correlate_rising_edges(
  trace : Trace,
  source_signal : String,
  target_signal : String,
  maximum_latency : Int64,
) -> Array[LatencySample] {
  let target_times = edges(trace, target_signal).filter_map(fn(event) {
    if event.kind is Rising {
      Some(event.time)
    } else {
      None
    }
  })
  let result : Array[LatencySample] = []
  for source in edges(trace, source_signal) {
    if source.kind is Rising {
      let match_time = target_times
        .filter(fn(time) {
          time >= source.time && time - source.time <= maximum_latency
        })
        .get(0)
      result.push({
        source_signal,
        target_signal,
        source_time: source.time,
        target_time: match_time,
        latency: match_time.map(fn(time) { time - source.time }),
      })
    }
  }
  result
}

///|
/// Summarize rising-edge latency pairs.
pub fn latency_report(
  trace : Trace,
  source_signal : String,
  target_signal : String,
  maximum_latency : Int64,
) -> LatencyReport {
  let samples = correlate_rising_edges(
    trace, source_signal, target_signal, maximum_latency,
  )
  let latencies = samples.filter_map(fn(sample) { sample.latency })
  let total : Int64 = latencies.fold(init=0L, fn(sum, value) { sum + value })
  let minimum = latencies.fold(init=None, fn(current, value) {
    match current {
      Some(item) => Some(Int64::min(item, value))
      None => Some(value)
    }
  })
  let maximum = latencies.fold(init=None, fn(current, value) {
    match current {
      Some(item) => Some(Int64::max(item, value))
      None => Some(value)
    }
  })
  {
    source_signal,
    target_signal,
    samples: samples.length(),
    matched: latencies.length(),
    minimum,
    maximum,
    mean: if latencies.length() == 0 {
      None
    } else {
      Some(total.to_double() / latencies.length().to_double())
    },
  }
}

///|
/// Return source edges that have no target edge within the allowed latency.
pub fn unmatched_rising_edges(
  trace : Trace,
  source_signal : String,
  target_signal : String,
  maximum_latency : Int64,
) -> Array[LatencySample] {
  correlate_rising_edges(trace, source_signal, target_signal, maximum_latency).filter(fn(
      sample,
    ) {
      sample.latency is None
    },
  )
}

///|
/// Check a setup/hold window around each reference rising edge.
pub fn setup_hold_violations(
  trace : Trace,
  reference_signal : String,
  data_signal : String,
  setup : Int64,
  hold : Int64,
) -> Array[Int64] {
  let result : Array[Int64] = []
  let safe_setup = Int64::max(0L, setup)
  let safe_hold = Int64::max(0L, hold)
  for edge in edges(trace, reference_signal) {
    if edge.kind is Rising {
      let lower = edge.time - safe_setup
      let upper = edge.time + safe_hold
      if signal_changes(trace, data_signal).any(fn(change) {
          change.time >= lower && change.time <= upper
        }) {
        result.push(edge.time)
      }
    }
  }
  result
}