///|
/// A row in a signal timeline view.
pub struct TimelinePoint {
  time : Int64
  values : Array[(String, Value?)]
} derive(Debug, Eq)

///|
/// Build snapshots at a fixed cadence across a bounded range.
pub fn timeline(
  trace : Trace,
  start : Int64,
  end : Int64,
  step : Int64,
) -> Array[TimelinePoint] {
  let safe_step = Int64::max(1L, step)
  let left = Int64::min(start, end)
  let right = Int64::max(start, end)
  let result : Array[TimelinePoint] = []
  let mut timestamp = left
  while timestamp <= right {
    result.push({ time: timestamp, values: trace.snapshot_at(timestamp) })
    if timestamp > right - safe_step {
      break
    }
    timestamp += safe_step
  }
  result
}

///|
/// Return only one signal's samples from a timeline.
pub fn timeline_signal(
  points : Array[TimelinePoint],
  signal_id : String,
) -> Array[(Int64, Value?)] {
  points.map(fn(point) {
    let value = point.values.fold(init=None, fn(found, item) {
      if item.0 == signal_id {
        Some(item.1)
      } else {
        found
      }
    })
    (
      point.time,
      match value {
        Some(item) => item
        None => None
      },
    )
  })
}

///|
/// Render a timeline as CSV with one row per signal value.
pub fn timeline_csv(points : Array[TimelinePoint]) -> String {
  let out : Array[String] = ["time,id,value"]
  for point in points {
    for item in point.values {
      let value = match item.1 {
        Some(value) => value.display()
        None => ""
      }
      out.push(
        point.time.to_string() +
        "," +
        escape_text(item.0) +
        "," +
        escape_text(value),
      )
    }
  }
  out.join("\n")
}

///|
/// Return the first timestamp at which a signal differs from a baseline.
pub fn first_difference_from(
  trace : Trace,
  signal_id : String,
  baseline : Value,
) -> Int64? {
  for change in signal_changes(trace, signal_id) {
    if !values_equal(change.value, baseline, true) {
      return Some(change.time)
    }
  }
  None
}

///|
/// Count signal changes in equally sized windows.
pub fn change_counts_by_window(
  trace : Trace,
  signal_id : String,
  width : Int64,
) -> Array[(Int64, Int)] {
  let safe = Int64::max(1L, width)
  let result : Array[(Int64, Int)] = []
  let mut start = 0L
  while start <= trace.end_time {
    let end = Int64::min(trace.end_time, start + safe)
    let count = signal_changes(trace, signal_id).count_if(fn(change) {
      change.time >= start && change.time <= end
    })
    result.push((start, count))
    if end == trace.end_time {
      break
    }
    start += safe
  }
  result
}

///|
/// Return the maximum number of changes in any window.
pub fn maximum_window_count(
  trace : Trace,
  signal_id : String,
  width : Int64,
) -> Int {
  change_counts_by_window(trace, signal_id, width).fold(init=0, fn(best, item) {
    Int::max(best, item.1)
  })
}

///|
/// Return all timestamps with a value matching a predicate-like value.
pub fn times_with_value(
  trace : Trace,
  signal_id : String,
  expected : Value,
) -> Array[Int64] {
  signal_changes(trace, signal_id)
  .filter(fn(change) { values_equal(change.value, expected, true) })
  .map(fn(change) { change.time })
}

///|
/// Return the duration between the first and last matching value.
pub fn matching_value_span(
  trace : Trace,
  signal_id : String,
  expected : Value,
) -> Int64 {
  let times = times_with_value(trace, signal_id, expected)
  if times.length() < 2 {
    0L
  } else {
    times[times.length() - 1] - times[0]
  }
}

///|
/// Return a stable snapshot manifest at one timestamp.
pub fn snapshot_manifest(trace : Trace, timestamp : Int64) -> String {
  let out : Array[String] = []
  for item in trace.snapshot_at(timestamp) {
    let value = match item.1 {
      Some(value) => value.display()
      None => ""
    }
    out.push(item.0 + "=" + value)
  }
  out.join(" ")
}

///|
/// Return the number of signals that have a known value at a timestamp.
pub fn known_snapshot_count(trace : Trace, timestamp : Int64) -> Int {
  trace
  .snapshot_at(timestamp)
  .count_if(fn(item) {
    match item.1 {
      Some(value) => !value.is_unknown()
      None => false
    }
  })
}

///|
/// Return whether all requested timeline timestamps are monotonic.
pub fn timeline_is_monotonic(points : Array[TimelinePoint]) -> Bool {
  let mut previous : Int64? = None
  for point in points {
    match previous {
      Some(value) if point.time < value => return false
      _ => ()
    }
    previous = Some(point.time)
  }
  true
}