///|
/// Tolerance-aware comparison settings for independently captured traces.
pub struct CompareOptions {
  time_tolerance : Int64
  include_equal : Bool
  ignore_unknown_case : Bool
} derive(Debug, Eq)

///|
/// Aggregate result for a pairwise comparison.
pub struct ComparisonSummary {
  signals_left : Int
  signals_right : Int
  compared_points : Int
  equal_points : Int
  different_points : Int
  missing_left : Int
  missing_right : Int
  first_difference : Int64?
} derive(Debug, Eq)

///|
/// Create strict comparison defaults.
pub fn default_compare_options() -> CompareOptions {
  { time_tolerance: 0L, include_equal: false, ignore_unknown_case: true }
}

///|
/// Compare two optional values while treating X and x as equivalent.
pub fn comparable_values(
  left : Value?,
  right : Value?,
  ignore_unknown_case : Bool,
) -> Bool {
  match (left, right) {
    (None, None) => true
    (Some(a), Some(b)) =>
      if ignore_unknown_case {
        values_equal(a.normalized(), b.normalized(), false)
      } else {
        values_equal(a, b, false)
      }
    _ => false
  }
}

///|
/// Return the nearest sample at or around a timestamp.
pub fn nearest_value(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
  tolerance : Int64,
) -> Value? {
  let safe = Int64::max(0L, tolerance)
  let mut result : Value? = None
  let mut best_distance : Int64? = None
  for change in signal_changes(trace, signal_id) {
    let distance = (change.time - timestamp).abs()
    if distance <= safe {
      match best_distance {
        Some(current) if distance >= current => ()
        _ => {
          best_distance = Some(distance)
          result = Some(change.value)
        }
      }
    }
  }
  result
}

///|
/// Compare all observed timestamps after nearest-sample alignment.
pub fn compare_traces(
  left : Trace,
  right : Trace,
  options : CompareOptions,
) -> ComparisonSummary {
  let ids = union_signal_ids(left, right)
  let mut equal = 0
  let mut different = 0
  let mut missing_left = 0
  let mut missing_right = 0
  let mut first : Int64? = None
  for signal_id in ids {
    let points = diff_times(left, right, signal_id)
    let left_declared = left.has_signal(signal_id)
    let right_declared = right.has_signal(signal_id)
    if !left_declared {
      missing_left += points.length()
      continue
    }
    if !right_declared {
      missing_right += points.length()
      continue
    }
    for timestamp in points {
      let left_value = nearest_value(
        left,
        signal_id,
        timestamp,
        options.time_tolerance,
      )
      let right_value = nearest_value(
        right,
        signal_id,
        timestamp,
        options.time_tolerance,
      )
      if comparable_values(left_value, right_value, options.ignore_unknown_case) {
        equal += 1
      } else {
        different += 1
        if first is None {
          first = Some(timestamp)
        }
      }
    }
  }
  {
    signals_left: left.signals.length(),
    signals_right: right.signals.length(),
    compared_points: equal + different,
    equal_points: equal,
    different_points: different,
    missing_left,
    missing_right,
    first_difference: first,
  }
}

///|
/// Return the equality ratio of two traces.
pub fn comparison_ratio(summary : ComparisonSummary) -> Double {
  if summary.compared_points == 0 {
    1.0
  } else {
    summary.equal_points.to_double() / summary.compared_points.to_double()
  }
}

///|
/// Return whether two traces meet the requested comparison policy.
pub fn comparison_passes(summary : ComparisonSummary) -> Bool {
  summary.different_points == 0 &&
  summary.missing_left == 0 &&
  summary.missing_right == 0
}

///|
/// Render a comparison summary as a stable line.
pub fn comparison_summary_line(summary : ComparisonSummary) -> String {
  "left_signals=" +
  summary.signals_left.to_string() +
  " right_signals=" +
  summary.signals_right.to_string() +
  " compared=" +
  summary.compared_points.to_string() +
  " equal=" +
  summary.equal_points.to_string() +
  " different=" +
  summary.different_points.to_string() +
  " missing_left=" +
  summary.missing_left.to_string() +
  " missing_right=" +
  summary.missing_right.to_string() +
  " ratio=" +
  comparison_ratio(summary).to_string()
}

///|
/// Render a comparison summary as JSON.
pub fn comparison_summary_json(summary : ComparisonSummary) -> String {
  let first = match summary.first_difference {
    Some(value) => value.to_string()
    None => "null"
  }
  "{\"signals_left\":" +
  summary.signals_left.to_string() +
  ",\"signals_right\":" +
  summary.signals_right.to_string() +
  ",\"compared\":" +
  summary.compared_points.to_string() +
  ",\"equal\":" +
  summary.equal_points.to_string() +
  ",\"different\":" +
  summary.different_points.to_string() +
  ",\"missing_left\":" +
  summary.missing_left.to_string() +
  ",\"missing_right\":" +
  summary.missing_right.to_string() +
  ",\"first_difference\":" +
  first +
  "}"
}

///|
/// Compare only declarations and signal paths.
pub fn declaration_compatible(left : Trace, right : Trace) -> Bool {
  if left.signals.length() != right.signals.length() {
    return false
  }
  for signal in left.signals {
    match find_signal(right, signal.id) {
      Some(other) =>
        if signal.width != other.width ||
          signal_path(signal) != signal_path(other) {
          return false
        }
      None => return false
    }
  }
  true
}