///|
/// Configuration for clock-quality analysis.
pub struct ClockConfig {
  signal_id : String
  expected_period : Int64?
  tolerance : Int64
  active_rising : Bool
  minimum_edges : Int
} derive(Debug, Eq)

///|
/// One measured clock cycle.
pub struct ClockCycle {
  index : Int
  rising_time : Int64
  falling_time : Int64?
  next_rising_time : Int64?
  period : Int64?
  high_time : Int64?
  low_time : Int64?
  duty_cycle : Double?
} derive(Debug, Eq)

///|
/// Aggregate clock-quality results.
pub struct ClockReport {
  signal_id : String
  edge_count : Int
  cycle_count : Int
  missing_falling_edges : Int
  missing_next_edges : Int
  periods : Array[Int64]
  minimum_period : Int64?
  maximum_period : Int64?
  mean_period : Double?
  period_jitter : Double?
  mean_duty_cycle : Double?
  out_of_tolerance : Int
} derive(Debug)

///|
/// A cross-clock-domain sampling relationship.
pub struct CdcObservation {
  source_id : String
  destination_id : String
  destination_time : Int64
  nearest_source_edge : Int64?
  distance : Int64?
  inside_guard_band : Bool
  source_value : Value?
  destination_value : Value?
} derive(Debug)

///|
/// Return a basic clock configuration.
pub fn clock_config(signal_id : String) -> ClockConfig {
  {
    signal_id,
    expected_period: None,
    tolerance: 0L,
    active_rising: true,
    minimum_edges: 2,
  }
}

///|
/// Attach an expected period and absolute tolerance.
pub fn ClockConfig::with_period(
  self : ClockConfig,
  period : Int64,
  tolerance : Int64,
) -> ClockConfig {
  {
    ..self,
    expected_period: Some(Int64::max(1L, period)),
    tolerance: Int64::max(0L, tolerance),
  }
}

///|
/// Require a minimum number of measured edges.
pub fn ClockConfig::with_minimum_edges(
  self : ClockConfig,
  edges : Int,
) -> ClockConfig {
  { ..self, minimum_edges: Int::max(2, edges) }
}

///|
/// Extract rising-edge timestamps for a configured clock.
pub fn clock_edge_times(trace : Trace, config : ClockConfig) -> Array[Int64] {
  let kind = if config.active_rising { Rising } else { Falling }
  edges(trace, config.signal_id)
  .filter(fn(item) { item.kind == kind })
  .map(fn(item) { item.time })
}

///|
/// Measure cycles by matching each active edge to the next edge.
pub fn clock_cycles(trace : Trace, config : ClockConfig) -> Array[ClockCycle] {
  let active = clock_edge_times(trace, config)
  let opposite_kind = if config.active_rising { Falling } else { Rising }
  let opposite = edges(trace, config.signal_id).filter(fn(item) {
    item.kind == opposite_kind
  })
  let cycles : Array[ClockCycle] = []
  for index, time in active {
    let next = if index + 1 < active.length() {
      Some(active[index + 1])
    } else {
      None
    }
    let fall = closest_edge_after(opposite, time, next)
    let period = next.map(fn(value) { value - time })
    let high = interval_to_edge(time, fall)
    let low = match (period, high) {
      (Some(total), Some(high_time)) => Some(Int64::max(0L, total - high_time))
      _ => None
    }
    cycles.push({
      index,
      rising_time: time,
      falling_time: fall,
      next_rising_time: next,
      period,
      high_time: high,
      low_time: low,
      duty_cycle: match (high, period) {
        (Some(a), Some(b)) if b > 0L => Some(a.to_double() / b.to_double())
        _ => None
      },
    })
  }
  cycles
}

///|
/// Produce an aggregate clock report.
pub fn clock_report(trace : Trace, config : ClockConfig) -> ClockReport {
  let cycles = clock_cycles(trace, config)
  let periods = cycles.filter_map(fn(item) { item.period })
  let duties = cycles.filter_map(fn(item) { item.duty_cycle })
  let missing_falling = cycles.count_if(fn(item) { item.falling_time is None })
  let missing_next = cycles.count_if(fn(item) { item.next_rising_time is None })
  let out_of_tolerance = match config.expected_period {
    Some(expected) =>
      periods.count_if(fn(period) {
        Int64::abs(period - expected) > config.tolerance
      })
    None => 0
  }
  let mean_period = mean_i64(periods)
  {
    signal_id: config.signal_id,
    edge_count: clock_edge_times(trace, config).length(),
    cycle_count: cycles.length(),
    missing_falling_edges: missing_falling,
    missing_next_edges: missing_next,
    periods,
    minimum_period: optional_minimum_i64(
      cycles.filter_map(fn(item) { item.period }),
    ),
    maximum_period: optional_maximum_i64(
      cycles.filter_map(fn(item) { item.period }),
    ),
    mean_period,
    period_jitter: period_jitter(cycles),
    mean_duty_cycle: mean_double(duties),
    out_of_tolerance,
  }
}

///|
/// Compare two clocks by nearest active-edge distance.
pub fn clock_phase_offsets(
  trace : Trace,
  left_id : String,
  right_id : String,
  guard_band : Int64,
) -> Array[Int64] {
  let left = edges(trace, left_id)
    .filter(fn(item) { item.kind == Rising })
    .map(fn(item) { item.time })
  let right = edges(trace, right_id)
    .filter(fn(item) { item.kind == Rising })
    .map(fn(item) { item.time })
  let offsets : Array[Int64] = []
  for time in left {
    match nearest_time(right, time) {
      Some(value) => offsets.push(value - time)
      None => ()
    }
  }
  offsets.filter(fn(value) {
    Int64::abs(value) <= Int64::max(0L, guard_band) || guard_band < 0L
  })
}

///|
/// Detect destination samples that occur inside a source guard band.
pub fn cdc_observations(
  trace : Trace,
  source_id : String,
  destination_id : String,
  guard_band : Int64,
) -> Array[CdcObservation] {
  let source_edges = edges(trace, source_id)
    .filter(fn(item) { item.kind == Rising })
    .map(fn(item) { item.time })
  let destination_edges = edges(trace, destination_id)
    .filter(fn(item) { item.kind == Rising })
    .map(fn(item) { item.time })
  let observations : Array[CdcObservation] = []
  for destination_time in destination_edges {
    let nearest = nearest_time(source_edges, destination_time)
    let distance = nearest.map(fn(value) {
      Int64::abs(value - destination_time)
    })
    observations.push({
      source_id,
      destination_id,
      destination_time,
      nearest_source_edge: nearest,
      distance,
      inside_guard_band: match distance {
        Some(value) => value <= Int64::max(0L, guard_band)
        None => false
      },
      source_value: value_at(trace, source_id, destination_time),
      destination_value: value_at(trace, destination_id, destination_time),
    })
  }
  observations
}

///|
/// Render a clock report as text.
pub fn ClockReport::to_text(self : ClockReport) -> String {
  "clock=" +
  self.signal_id +
  " edges=" +
  self.edge_count.to_string() +
  " cycles=" +
  self.cycle_count.to_string() +
  " mean_period=" +
  optional_double_text(self.mean_period) +
  " jitter=" +
  optional_double_text(self.period_jitter) +
  " duty=" +
  optional_double_text(self.mean_duty_cycle) +
  " out_of_tolerance=" +
  self.out_of_tolerance.to_string() +
  " missing_falling=" +
  self.missing_falling_edges.to_string()
}

///|
/// Render cycle samples as CSV.
pub fn clock_cycles_csv(cycles : Array[ClockCycle]) -> String {
  let rows : Array[String] = [
    "index,rising,falling,next,period,high,low,duty_cycle",
  ]
  for cycle in cycles {
    rows.push(
      cycle.index.to_string() +
      "," +
      cycle.rising_time.to_string() +
      "," +
      optional_int64_text(cycle.falling_time) +
      "," +
      optional_int64_text(cycle.next_rising_time) +
      "," +
      optional_int64_text(cycle.period) +
      "," +
      optional_int64_text(cycle.high_time) +
      "," +
      optional_int64_text(cycle.low_time) +
      "," +
      optional_double_text(cycle.duty_cycle),
    )
  }
  rows.join("\n")
}

///|
/// Render a clock report as JSON.
pub fn ClockReport::to_json(self : ClockReport) -> String {
  "{\"signal\":\"" +
  csv_cell(self.signal_id) +
  "\",\"edge_count\":" +
  self.edge_count.to_string() +
  ",\"cycle_count\":" +
  self.cycle_count.to_string() +
  ",\"missing_falling_edges\":" +
  self.missing_falling_edges.to_string() +
  ",\"missing_next_edges\":" +
  self.missing_next_edges.to_string() +
  ",\"minimum_period\":" +
  optional_int64_json(self.minimum_period) +
  ",\"maximum_period\":" +
  optional_int64_json(self.maximum_period) +
  ",\"mean_period\":" +
  optional_double_json(self.mean_period) +
  ",\"period_jitter\":" +
  optional_double_json(self.period_jitter) +
  ",\"mean_duty_cycle\":" +
  optional_double_json(self.mean_duty_cycle) +
  ",\"out_of_tolerance\":" +
  self.out_of_tolerance.to_string() +
  "}"
}

///|
/// Find the first opposite edge after an active edge and before the next cycle.
fn closest_edge_after(
  edges : Array[EdgeEvent],
  start : Int64,
  next : Int64?,
) -> Int64? {
  let before_next = fn(time : Int64) {
    match next {
      Some(value) => time < value
      None => true
    }
  }
  for edge in edges {
    if edge.time >= start && before_next(edge.time) {
      return Some(edge.time)
    }
  }
  None
}

///|
/// Return the distance to an edge or None.
fn interval_to_edge(start : Int64, edge : Int64?) -> Int64? {
  edge.map(fn(value) { Int64::max(0L, value - start) })
}

///|
/// Return the closest timestamp in a sorted array.
fn nearest_time(values : Array[Int64], target : Int64) -> Int64? {
  if values.length() == 0 {
    return None
  }
  let mut best = values[0]
  let mut distance = Int64::abs(best - target)
  for value in values[1:] {
    let current = Int64::abs(value - target)
    if current < distance {
      best = value
      distance = current
    }
  }
  Some(best)
}

///|
/// Calculate the mean of integer measurements.
fn mean_i64(values : Array[Int64]) -> Double? {
  if values.length() == 0 {
    None
  } else {
    Some(
      values.fold(init=0L, fn(total, item) { total + item }).to_double() /
      values.length().to_double(),
    )
  }
}

///|
/// Calculate the mean of floating-point measurements.
fn mean_double(values : Array[Double]) -> Double? {
  if values.length() == 0 {
    None
  } else {
    Some(
      values.fold(init=0.0, fn(total, item) { total + item }) /
      values.length().to_double(),
    )
  }
}

///|
/// Estimate period jitter as mean absolute deviation from the mean.
fn period_jitter(cycles : Array[ClockCycle]) -> Double? {
  let periods = cycles.filter_map(fn(item) { item.period })
  match mean_i64(periods) {
    Some(mean) =>
      Some(
        periods.fold(init=0.0, fn(total, item) {
          let delta = item.to_double() - mean
          let deviation = if delta < 0.0 { 0.0 - delta } else { delta }
          total + deviation
        }) /
        periods.length().to_double(),
      )
    None => None
  }
}