///|
/// Build a signal-local position index without copying waveform values.
pub fn build_index(trace : Trace) -> TraceIndex {
  let ids = trace.signals.map(fn(signal) { signal.id })
  let offsets : Array[Int] = []
  let counts : Array[Int] = []
  let positions : Array[Int] = []
  for id in ids {
    offsets.push(positions.length())
    let before = positions.length()
    for i in 0.. Int {
  self.signal_ids.length()
}

///|
/// Return the number of changes indexed for a signal.
pub fn TraceIndex::change_count(self : TraceIndex, signal_id : String) -> Int {
  match self.signal_ids.search(signal_id) {
    Some(index) => self.counts[index]
    None => 0
  }
}

///|
/// Return the original change positions for a signal.
pub fn TraceIndex::positions(
  self : TraceIndex,
  signal_id : String,
) -> Array[Int] {
  match self.signal_ids.search(signal_id) {
    Some(index) => {
      let start = self.offsets[index]
      let end = start + self.counts[index]
      self.change_positions[start:end].to_owned()
    }
    None => []
  }
}

///|
/// Return indexed changes in source order.
pub fn TraceIndex::changes(
  self : TraceIndex,
  trace : Trace,
  signal_id : String,
) -> Array[Change] {
  self.positions(signal_id).map(fn(position) { trace.changes[position] })
}

///|
/// Return the first indexed timestamp for a signal.
pub fn TraceIndex::first_time(
  self : TraceIndex,
  trace : Trace,
  signal_id : String,
) -> Int64? {
  match self.positions(signal_id).get(0) {
    Some(position) => Some(trace.changes[position].time)
    None => None
  }
}

///|
/// Return the last indexed timestamp for a signal.
pub fn TraceIndex::last_time(
  self : TraceIndex,
  trace : Trace,
  signal_id : String,
) -> Int64? {
  match self.positions(signal_id).last() {
    Some(position) => Some(trace.changes[position].time)
    None => None
  }
}

///|
/// Return the indexed value at or before a timestamp.
pub fn TraceIndex::value_at(
  self : TraceIndex,
  trace : Trace,
  signal_id : String,
  timestamp : Int64,
) -> Value? {
  let mut result : Value? = None
  for position in self.positions(signal_id) {
    let change = trace.changes[position]
    if change.time > timestamp {
      break
    }
    result = Some(change.value)
  }
  result
}

///|
/// Return indexed changes in an inclusive time window.
pub fn TraceIndex::between(
  self : TraceIndex,
  trace : Trace,
  signal_id : String,
  start~ : Int64,
  end~ : Int64,
) -> Array[Change] {
  self
  .changes(trace, signal_id)
  .filter(fn(change) { change.time >= start && change.time <= end })
}

///|
/// Return the index position for a signal identifier, if present.
pub fn TraceIndex::lookup(self : TraceIndex, signal_id : String) -> Int? {
  self.signal_ids.search(signal_id)
}

///|
/// Verify that every indexed position points to the requested signal.
pub fn TraceIndex::is_consistent(self : TraceIndex, trace : Trace) -> Bool {
  for index in 0..= trace.changes.length() {
        return false
      }
      if trace.changes[position].id != id {
        return false
      }
    }
  }
  true
}

///|
/// Return an index-backed snapshot of every signal's changes.
pub fn indexed_changes(trace : Trace) -> Array[(String, Array[Change])] {
  let index = build_index(trace)
  trace.signals.map(fn(signal) { (signal.id, index.changes(trace, signal.id)) })
}