///|
/// A short driven pulse detected by a width threshold.
pub struct GlitchEvent {
  signal_id : String
  start : Int64
  end : Int64
  width : Int64
  value : Char
} derive(Debug, Eq)

///|
/// A deterministic clock-quality summary.
pub struct JitterReport {
  signal_id : String
  edges : Int
  periods : Int
  mean_period : Double?
  minimum_period : Int64?
  maximum_period : Int64?
  mean_absolute_deviation : Double?
} derive(Debug, Eq)

///|
/// Find short known pulses, including both high and low pulses.
pub fn detect_glitches(
  trace : Trace,
  signal_id : String,
  maximum_width : Int64,
) -> Array[GlitchEvent] {
  let limit = Int64::max(0L, maximum_width)
  driven_intervals(trace, signal_id).filter_map(fn(interval) {
    let width = interval.end - interval.start
    match interval.value {
      Scalar(value) if width > 0L && width <= limit =>
        Some({
          signal_id,
          start: interval.start,
          end: interval.end,
          width,
          value,
        })
      _ => None
    }
  })
}

///|
/// Return the shortest known pulse for a signal.
pub fn shortest_pulse(trace : Trace, signal_id : String) -> (Char, Int64)? {
  let mut result : (Char, Int64)? = None
  for pulse in pulse_widths(trace, signal_id) {
    match result {
      Some((_, width)) if width <= pulse.1 => ()
      _ => result = Some(pulse)
    }
  }
  result
}

///|
/// Return the longest known pulse for a signal.
pub fn longest_pulse(trace : Trace, signal_id : String) -> (Char, Int64)? {
  let mut result : (Char, Int64)? = None
  for pulse in pulse_widths(trace, signal_id) {
    match result {
      Some((_, width)) if width >= pulse.1 => ()
      _ => result = Some(pulse)
    }
  }
  result
}

///|
/// Compute period jitter from consecutive rising edges.
pub fn jitter_report(trace : Trace, signal_id : String) -> JitterReport {
  let estimate = frequency_estimate(trace, signal_id)
  let times = edges(trace, signal_id).filter_map(fn(event) {
    if event.kind is Rising {
      Some(event.time)
    } else {
      None
    }
  })
  let periods : Array[Int64] = []
  if times.length() > 1 {
    for i in 1.. 0 =>
      Some(
        periods.fold(init=0.0, fn(total, period) {
          total + (period.to_double() - mean).abs()
        }) /
        periods.length().to_double(),
      )
    _ => None
  }
  {
    signal_id,
    edges: times.length(),
    periods: periods.length(),
    mean_period: estimate.mean_period,
    minimum_period: estimate.minimum_period,
    maximum_period: estimate.maximum_period,
    mean_absolute_deviation: deviation,
  }
}

///|
/// Return whether all periods lie inside an inclusive tolerance.
pub fn periods_within_tolerance(
  trace : Trace,
  signal_id : String,
  tolerance : Int64,
) -> Bool {
  let report = jitter_report(trace, signal_id)
  match report.mean_period {
    Some(mean) => {
      for period in period_values(trace, signal_id) {
        if (period.to_double() - mean).abs() > tolerance.to_double() {
          return false
        }
      }
      true
    }
    None => true
  }
}

///|
/// Return scalar pulse durations in source order.
pub fn period_values(trace : Trace, signal_id : String) -> Array[Int64] {
  let times = edges(trace, signal_id).filter_map(fn(event) {
    if event.kind is Rising {
      Some(event.time)
    } else {
      None
    }
  })
  let result : Array[Int64] = []
  if times.length() > 1 {
    for i in 1.. String {
  let out : Array[String] = ["signal,start,end,width,value"]
  for glitch in glitches {
    out.push(
      glitch.signal_id +
      "," +
      glitch.start.to_string() +
      "," +
      glitch.end.to_string() +
      "," +
      glitch.width.to_string() +
      "," +
      glitch.value.to_string(),
    )
  }
  out.join("\n")
}

///|
/// Estimate the fraction of driven intervals that are short pulses.
pub fn glitch_ratio(
  trace : Trace,
  signal_id : String,
  maximum_width : Int64,
) -> Double {
  let all = driven_intervals(trace, signal_id).length()
  if all == 0 {
    0.0
  } else {
    detect_glitches(trace, signal_id, maximum_width).length().to_double() /
    all.to_double()
  }
}