///|
/// Policy for aligning traces from independent capture sessions.
pub enum AlignmentMode {
  FirstEdge
  RisingEdgeMedian
  MarkerChange
  StartAtZero
} derive(Debug, Eq)

///|
/// A measured time offset between two traces.
pub struct AlignmentEstimate {
  mode : AlignmentMode
  left_marker : String?
  right_marker : String?
  left_time : Int64?
  right_time : Int64?
  offset : Int64
  confidence : Double
} derive(Debug)

///|
/// A pair of aligned traces and their estimate.
pub struct AlignmentResult {
  left : Trace
  right : Trace
  estimate : AlignmentEstimate
  overlap_start : Int64
  overlap_end : Int64
  comparable : Bool
} derive(Debug)

///|
/// List alignment modes.
pub fn alignment_mode_catalog() -> Array[AlignmentMode] {
  [FirstEdge, RisingEdgeMedian, MarkerChange, StartAtZero]
}

///|
/// Estimate an offset using named marker signals.
pub fn estimate_alignment(
  left : Trace,
  right : Trace,
  left_marker : String?,
  right_marker : String?,
  mode : AlignmentMode,
) -> AlignmentEstimate {
  let left_times = alignment_marker_times(left, left_marker, mode)
  let right_times = alignment_marker_times(right, right_marker, mode)
  let left_time = alignment_first(left_times)
  let right_time = alignment_first(right_times)
  let offset = match (left_time, right_time) {
    (Some(a), Some(b)) => a - b
    _ => 0L
  }
  let confidence = if left_times.length() == 0 || right_times.length() == 0 {
    0.0
  } else {
    Int::min(left_times.length(), right_times.length()).to_double() /
    Int::max(left_times.length(), right_times.length()).to_double()
  }
  { mode, left_marker, right_marker, left_time, right_time, offset, confidence }
}

///|
/// Shift two traces into one comparable time domain.
pub fn align_traces(
  left : Trace,
  right : Trace,
  left_marker : String?,
  right_marker : String?,
  mode : AlignmentMode,
) -> AlignmentResult {
  let estimate = estimate_alignment(
    left, right, left_marker, right_marker, mode,
  )
  let shifted_right = shift_trace(right, estimate.offset)
  let start = Int64::max(
    0L,
    Int64::max(first_alignment_time(left), first_alignment_time(shifted_right)),
  )
  let end = Int64::min(left.end_time, shifted_right.end_time)
  {
    left,
    right: shifted_right,
    estimate,
    overlap_start: start,
    overlap_end: end,
    comparable: end >= start,
  }
}

///|
/// Return whether two traces have a non-empty aligned overlap.
pub fn AlignmentResult::has_overlap(self : AlignmentResult) -> Bool {
  self.comparable
}

///|
/// Return an inclusive overlap window.
pub fn AlignmentResult::overlap(self : AlignmentResult) -> (Int64, Int64) {
  (self.overlap_start, self.overlap_end)
}

///|
/// Render an alignment estimate.
pub fn AlignmentEstimate::to_text(self : AlignmentEstimate) -> String {
  "mode=" +
  alignment_mode_name(self.mode) +
  " offset=" +
  self.offset.to_string() +
  " confidence=" +
  self.confidence.to_string()
}

///|
/// Render alignment metadata as JSON.
pub fn AlignmentResult::to_json(self : AlignmentResult) -> String {
  "{\"offset\":" +
  self.estimate.offset.to_string() +
  ",\"confidence\":" +
  self.estimate.confidence.to_string() +
  ",\"overlap_start\":" +
  self.overlap_start.to_string() +
  ",\"overlap_end\":" +
  self.overlap_end.to_string() +
  ",\"comparable\":" +
  self.comparable.to_string() +
  "}"
}

///|
/// Return marker times according to an alignment mode.
fn alignment_marker_times(
  trace : Trace,
  marker : String?,
  mode : AlignmentMode,
) -> Array[Int64] {
  match mode {
    StartAtZero => [0L]
    _ =>
      match marker {
        Some(signal_id) =>
          match mode {
            FirstEdge | MarkerChange =>
              trace.changes
              .filter(fn(change) { change.id == signal_id })
              .map(fn(change) { change.time })
            RisingEdgeMedian =>
              edges(trace, signal_id)
              .filter(fn(item) { item.kind is Rising })
              .map(fn(item) { item.time })
            StartAtZero => [0L]
          }
        None => []
      }
  }
}

///|
/// Return the first change timestamp, or zero for an empty trace.
fn first_alignment_time(trace : Trace) -> Int64 {
  if trace.changes.length() == 0 {
    0L
  } else {
    trace.changes[0].time
  }
}

///|
/// Read the first marker timestamp without relying on collection extensions.
fn alignment_first(values : Array[Int64]) -> Int64? {
  if values.length() == 0 {
    None
  } else {
    Some(values[0])
  }
}

///|
/// Name an alignment mode.
pub fn alignment_mode_name(value : AlignmentMode) -> String {
  match value {
    FirstEdge => "first-edge"
    RisingEdgeMedian => "rising-edge-median"
    MarkerChange => "marker-change"
    StartAtZero => "start-at-zero"
  }
}