///|
/// A reusable signal/time query window.
pub struct QueryWindow {
  signal_id : String?
  start : Int64
  end : Int64
} derive(Debug, Eq)

///|
/// Create an inclusive query window.
pub fn query_window(
  signal_id? : String,
  start~ : Int64,
  end~ : Int64,
) -> QueryWindow {
  { signal_id, start, end: Int64::max(start, end) }
}

///|
/// Return all declared identifiers in declaration order.
pub fn trace_signal_ids(trace : Trace) -> Array[String] {
  trace.signals.map(fn(signal) { signal.id })
}

///|
/// Return changes for one signal in source order.
pub fn signal_changes(trace : Trace, signal_id : String) -> Array[Change] {
  trace.changes.filter(fn(change) { change.id == signal_id })
}

///|
/// Return timestamps for one signal.
pub fn change_times(trace : Trace, signal_id : String) -> Array[Int64] {
  signal_changes(trace, signal_id).map(fn(change) { change.time })
}

///|
/// Return distinct values in first-seen order.
pub fn distinct_values(trace : Trace, signal_id : String) -> Array[Value] {
  let result : Array[Value] = []
  for change in signal_changes(trace, signal_id) {
    if !result.contains(change.value) {
      result.push(change.value)
    }
  }
  result
}

///|
/// Return the first observed value for a signal.
pub fn first_value(trace : Trace, signal_id : String) -> Value? {
  match signal_changes(trace, signal_id).get(0) {
    Some(change) => Some(change.value)
    None => None
  }
}

///|
/// Return the last observed value for a signal.
pub fn last_value(trace : Trace, signal_id : String) -> Value? {
  match signal_changes(trace, signal_id).last() {
    Some(change) => Some(change.value)
    None => None
  }
}

///|
/// Return the value strictly before a timestamp.
pub fn value_before(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Value? {
  let mut result : Value? = None
  for change in signal_changes(trace, signal_id) {
    if change.time >= timestamp {
      break
    }
    result = Some(change.value)
  }
  result
}

///|
/// Return the first value strictly after a timestamp.
pub fn value_after(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Value? {
  for change in signal_changes(trace, signal_id) {
    if change.time > timestamp {
      return Some(change.value)
    }
  }
  None
}

///|
/// Return the next change after a timestamp.
pub fn next_change(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Change? {
  for change in signal_changes(trace, signal_id) {
    if change.time > timestamp {
      return Some(change)
    }
  }
  None
}

///|
/// Return the previous change at or before a timestamp.
pub fn previous_change(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Change? {
  let mut result : Change? = None
  for change in signal_changes(trace, signal_id) {
    if change.time > timestamp {
      break
    }
    result = Some(change)
  }
  result
}

///|
/// Return changes after an exclusive timestamp.
pub fn changes_after(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Array[Change] {
  signal_changes(trace, signal_id).filter(fn(change) { change.time > timestamp })
}

///|
/// Return changes before an exclusive timestamp.
pub fn changes_before(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Array[Change] {
  signal_changes(trace, signal_id).filter(fn(change) { change.time < timestamp })
}

///|
/// Return whether a signal changed exactly at a timestamp.
pub fn has_change_at(
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Bool {
  signal_changes(trace, signal_id).any(fn(change) { change.time == timestamp })
}

///|
/// Return a signal's changes filtered by an inclusive query window.
pub fn query_window_changes(
  trace : Trace,
  window : QueryWindow,
) -> Array[Change] {
  trace.changes.filter(fn(change) {
    let signal_ok = match window.signal_id {
      Some(id) => id == change.id
      None => true
    }
    signal_ok && change.time >= window.start && change.time <= window.end
  })
}

///|
/// Return the latest value for every declared signal at a timestamp.
pub fn snapshot(trace : Trace, timestamp : Int64) -> Array[Sample] {
  trace.signals.map(fn(signal) {
    {
      signal_id: signal.id,
      time: timestamp,
      value: value_at(trace, signal.id, timestamp),
    }
  })
}

///|
/// Return a snapshot for only signals matching a glob.
pub fn snapshot_glob(
  trace : Trace,
  pattern : String,
  timestamp : Int64,
) -> Array[Sample] {
  find_signals_glob(trace, pattern).map(fn(signal) {
    {
      signal_id: signal.id,
      time: timestamp,
      value: value_at(trace, signal.id, timestamp),
    }
  })
}

///|
/// Return a bit at a zero-based position from the most significant side.
pub fn value_bit(value : Value, index : Int) -> LogicBit? {
  let bits = value.bits()
  if index < 0 || index >= bits.length() {
    None
  } else {
    Some(bits[index])
  }
}

///|
/// Return a vector slice with half-open bounds.
pub fn value_slice(value : Value, start : Int, end : Int) -> Value {
  let bits = value.bits()
  let left = Int::max(0, Int::min(start, bits.length()))
  let right = Int::max(left, Int::min(end, bits.length()))
  let selected = bits[left:right].to_owned()
  if selected.length() == 1 && value.width() == 1 {
    Scalar(selected[0].to_char())
  } else {
    Vector(bits_to_string(selected))
  }
}

///|
/// Pad a value on the left to a requested width.
pub fn value_zero_extend(value : Value, width : Int) -> Value {
  let bits = value.bits()
  let target = Int::max(width, bits.length())
  let result : Array[LogicBit] = []
  for _ in 0..<(target - bits.length()) {
    result.push(Zero)
  }
  result.append(bits)
  if target == 1 {
    Scalar(result[0].to_char())
  } else {
    Vector(bits_to_string(result))
  }
}

///|
/// Return the highest known integer value represented by a binary vector.
pub fn unsigned_value(value : Value) -> Int64? {
  value.to_int()
}

///|
/// Return changes whose values satisfy a caller predicate.
pub fn filter_changes(
  trace : Trace,
  signal_id : String,
  predicate : (Value) -> Bool,
) -> Array[Change] {
  signal_changes(trace, signal_id).filter(fn(change) { predicate(change.value) })
}

///|
/// Return the number of changes observed for each declared identifier.
pub fn change_count_catalog(trace : Trace) -> Array[(String, Int)] {
  trace.signals.map(fn(signal) {
    (signal.id, signal_changes(trace, signal.id).length())
  })
}

///|
/// Return the first timestamp of each declared signal.
pub fn first_time_catalog(trace : Trace) -> Array[(String, Int64?)] {
  trace.signals.map(fn(signal) {
    (signal.id, change_times(trace, signal.id).get(0))
  })
}

///|
/// Return all changes for a set of identifiers without changing trace order.
pub fn changes_for_ids(trace : Trace, ids : Array[String]) -> Array[Change] {
  trace.changes.filter(fn(change) { ids.contains(change.id) })
}

///|
/// Return a trace-level change count for a query window.
pub fn query_window_count(trace : Trace, window : QueryWindow) -> Int {
  query_window_changes(trace, window).length()
}