///|
/// Return the semantic kind of a value for table-oriented consumers.
pub fn Value::kind_name(self : Value) -> String {
  match self {
    Scalar(_) => "scalar"
    Vector(_) => "vector"
    Real(_) => "real"
  }
}

///|
/// Return a stable textual value suitable for logs and CSV.
pub fn Value::text(self : Value) -> String {
  self.display()
}

///|
/// Return whether a change carries a scalar logical state.
pub fn Change::is_scalar(self : Change) -> Bool {
  self.value is Scalar(_)
}

///|
/// Return whether a change carries a vector value.
pub fn Change::is_vector(self : Change) -> Bool {
  self.value is Vector(_)
}

///|
/// Return whether a change carries a real-valued sample.
pub fn Change::is_real(self : Change) -> Bool {
  self.value is Real(_)
}

///|
/// Return a concise change representation for trace logs.
pub fn Change::text(self : Change) -> String {
  self.time.to_string() + " " + self.id + " " + self.value.display()
}

///|
/// Return a copy of a change at a different timestamp.
pub fn Change::at(self : Change, timestamp : Int64) -> Change {
  { time: timestamp, id: self.id, value: self.value }
}

///|
/// Return a fully-qualified path for a declaration.
pub fn Signal::full_path(self : Signal) -> String {
  signal_path(self)
}

///|
/// Return whether the declaration represents a vector signal.
pub fn Signal::is_vector(self : Signal) -> Bool {
  self.width > 1
}

///|
/// Return the innermost scope or an explicit top-level label.
pub fn Signal::scope_name(self : Signal) -> String {
  match self.scope.last() {
    Some(scope) => scope
    None => "top"
  }
}

///|
/// Return a compact header description.
pub fn Header::summary(self : Header) -> String {
  let parts : Array[String] = []
  match self.date {
    Some(value) => parts.push("date=" + value)
    None => ()
  }
  match self.version {
    Some(value) => parts.push("version=" + value)
    None => ()
  }
  match self.timescale {
    Some(value) => parts.push("timescale=" + value)
    None => ()
  }
  match self.comment {
    Some(value) => parts.push("comment=" + value)
    None => ()
  }
  if parts.length() == 0 {
    "empty"
  } else {
    parts.join(" ")
  }
}

///|
/// Number of declarations in a trace.
pub fn Trace::signal_count(self : Trace) -> Int {
  self.signals.length()
}

///|
/// Number of value changes in a trace.
pub fn Trace::change_count(self : Trace) -> Int {
  self.changes.length()
}

///|
/// Return the observed duration in trace ticks.
pub fn Trace::duration(self : Trace) -> Int64 {
  self.end_time
}

///|
/// Return whether an identifier is declared by this trace.
pub fn Trace::has_signal(self : Trace, signal_id : String) -> Bool {
  self.signals.any(fn(signal) { signal.id == signal_id })
}

///|
/// Return all fully-qualified signal paths in declaration order.
pub fn Trace::signal_paths(self : Trace) -> Array[String] {
  self.signals.map(signal_path)
}

///|
/// Return values for all changes at a timestamp.
pub fn Trace::changes_at(self : Trace, timestamp : Int64) -> Array[Change] {
  self.changes.filter(fn(change) { change.time == timestamp })
}

///|
/// Return the latest snapshot for every declared signal.
pub fn Trace::snapshot_at(
  self : Trace,
  timestamp : Int64,
) -> Array[(String, Value?)] {
  self.signals.map(fn(signal) {
    (signal.id, value_at(self, signal.id, timestamp))
  })
}

///|
/// Return a stable summary line useful in CLI smoke tests.
pub fn Trace::summary(self : Trace) -> String {
  "signals=" +
  self.signal_count().to_string() +
  " changes=" +
  self.change_count().to_string() +
  " duration=" +
  self.duration().to_string() +
  " header=" +
  self.header.summary()
}

///|
/// Return the number of changes in each declared scope.
pub fn Trace::scope_change_counts(self : Trace) -> Array[(String, Int)] {
  self.signals.map(fn(signal) {
    (
      signal_path(signal),
      self.changes.count_if(fn(change) { change.id == signal.id }),
    )
  })
}

///|
/// Return a bounded copy of changes in source order.
pub fn Trace::take_changes(self : Trace, limit : Int) -> Array[Change] {
  let safe = Int::max(0, limit)
  let result : Array[Change] = []
  let bound = Int::min(safe, self.changes.length())
  for index in 0.. Int64 {
  let mut digest : Int64 = self.signals.length().to_int64() * 31L +
    self.changes.length().to_int64()
  for signal in self.signals {
    digest = digest * 31L + signal.id.length().to_int64()
    digest = digest * 31L + signal.width.to_int64()
  }
  for change in self.changes {
    digest = digest * 31L + change.time
    digest = digest * 31L + change.id.length().to_int64()
  }
  digest
}