///|
fn push_value_count(counts : Array[ValueCount], value : String) -> Unit {
  for i = 0; i < counts.length(); i = i + 1 {
    if counts[i].value == value {
      counts[i] = { value: counts[i].value, count: counts[i].count + 1 }
      return
    }
  }
  counts.push({ value, count: 1 })
}

///|
fn signal_or_raise(vcd : VcdFile, full_name : String) -> Signal raise VcdError {
  match find_signal(vcd, full_name) {
    Some(signal) => signal
    None => raise VcdError("signal not found: \{full_name}")
  }
}

///|
fn check_window(window : TimeWindow) -> Unit raise VcdError {
  if window.start_time < 0L || window.end_time < 0L {
    raise VcdError("time window must be non-negative")
  }
  if window.end_time < window.start_time {
    raise VcdError("time window end must not be before start")
  }
}

///|
fn char_count(value : String, target : Char) -> Int {
  let mut count = 0
  for ch in value {
    if ch == target {
      count = count + 1
    }
  }
  count
}

///|
fn all_chars(value : String, target : Char) -> Bool {
  if value.is_empty() {
    return false
  }
  for ch in value {
    if ch != target {
      return false
    }
  }
  true
}

///|
fn contains_only_known_binary(value : String) -> Bool {
  if value.is_empty() {
    return false
  }
  for ch in value {
    if ch != '0' && ch != '1' {
      return false
    }
  }
  true
}

///|
fn value_kind(value : String) -> String {
  if value.is_empty() {
    "empty"
  } else if all_chars(value, '0') {
    "zero"
  } else if all_chars(value, '1') {
    "one"
  } else if contains_only_known_binary(value) {
    "known-binary"
  } else if char_count(value, 'x') > 0 && char_count(value, 'z') > 0 {
    "mixed-unknown-high-impedance"
  } else if char_count(value, 'x') > 0 {
    "unknown"
  } else if char_count(value, 'z') > 0 {
    "high-impedance"
  } else {
    "mixed"
  }
}

///|
/// Return true when the value is exactly one scalar logic character.
pub fn is_scalar_logic_value(value : String) -> Bool {
  value.length() == 1 &&
  (value == "0" || value == "1" || value == "x" || value == "z")
}

///|
/// Return true when the value has more than one valid logic character.
pub fn is_vector_logic_value(value : String) -> Bool {
  if value.length() <= 1 {
    return false
  }
  for ch in value {
    match ch {
      '0' | '1' | 'x' | 'z' => ()
      _ => return false
    }
  }
  true
}

///|
/// Return true when a value contains only known binary bits.
pub fn is_known_binary(value : String) -> Bool {
  contains_only_known_binary(value)
}

///|
/// Return true when a value has any unknown bit.
pub fn has_unknown_bit(value : String) -> Bool {
  char_count(value, 'x') > 0
}

///|
/// Return true when a value has any high-impedance bit.
pub fn has_high_impedance_bit(value : String) -> Bool {
  char_count(value, 'z') > 0
}

///|
/// Count zeros in a scalar or vector value.
pub fn zero_bit_count(value : String) -> Int {
  char_count(value, '0')
}

///|
/// Count ones in a scalar or vector value.
pub fn one_bit_count(value : String) -> Int {
  char_count(value, '1')
}

///|
/// Count unknown bits in a scalar or vector value.
pub fn unknown_bit_count(value : String) -> Int {
  char_count(value, 'x')
}

///|
/// Count high-impedance bits in a scalar or vector value.
pub fn high_impedance_bit_count(value : String) -> Int {
  char_count(value, 'z')
}

///|
/// Return per-character counts for one value.
pub fn logic_counts(value : String) -> LogicCounts {
  {
    zeros: zero_bit_count(value),
    ones: one_bit_count(value),
    unknowns: unknown_bit_count(value),
    high_impedances: high_impedance_bit_count(value),
    width: value.length(),
  }
}

///|
/// Return a compact textual classification of a parsed value.
pub fn classify_value(value : String) -> String {
  value_kind(value)
}

///|
/// Convert a known binary value to Int64 when it fits.
pub fn binary_value_to_int64(value : String) -> Int64? {
  if !contains_only_known_binary(value) {
    return None
  }
  let mut result = 0L
  for ch in value {
    let bit = if ch == '1' { 1L } else { 0L }
    if result > (9223372036854775807L - bit) / 2L {
      return None
    }
    result = result * 2L + bit
  }
  Some(result)
}

///|
/// Count bit positions that differ between two equal-width known or four-state values.
pub fn hamming_distance(left : String, right : String) -> Int? {
  if left.length() != right.length() {
    return None
  }
  let mut distance = 0
  for i = 0; i < left.length(); i = i + 1 {
    if left[i] != right[i] {
      distance = distance + 1
    }
  }
  Some(distance)
}

///|
/// Return all distinct values written to a signal.
pub fn distinct_values(
  vcd : VcdFile,
  full_name : String,
) -> Array[String] raise VcdError {
  let result : Array[String] = []
  for change in signal_changes(vcd, full_name) {
    let mut found = false
    for value in result {
      if value == change.value {
        found = true
        break
      }
    }
    if !found {
      result.push(change.value)
    }
  }
  result
}

///|
/// Count occurrences of each value in a signal change history.
pub fn value_counts(
  vcd : VcdFile,
  full_name : String,
) -> Array[ValueCount] raise VcdError {
  let counts : Array[ValueCount] = []
  for change in signal_changes(vcd, full_name) {
    push_value_count(counts, change.value)
  }
  counts
}

///|
/// Count writes of a concrete value for a signal.
pub fn count_value(
  vcd : VcdFile,
  full_name : String,
  value : String,
) -> Int raise VcdError {
  let mut count = 0
  for change in signal_changes(vcd, full_name) {
    if change.value == value {
      count = count + 1
    }
  }
  count
}

///|
/// Return the first change at or after a timestamp.
pub fn first_change_at_or_after(
  vcd : VcdFile,
  full_name : String,
  timestamp : Int64,
) -> SignalChange? raise VcdError {
  if timestamp < 0L {
    raise VcdError("timestamp must be non-negative")
  }
  for change in signal_changes(vcd, full_name) {
    if change.timestamp >= timestamp {
      return Some(change)
    }
  }
  None
}

///|
/// Return the last change at or before a timestamp.
pub fn last_change_at_or_before(
  vcd : VcdFile,
  full_name : String,
  timestamp : Int64,
) -> SignalChange? raise VcdError {
  if timestamp < 0L {
    raise VcdError("timestamp must be non-negative")
  }
  let mut result : SignalChange? = None
  for change in signal_changes(vcd, full_name) {
    if change.timestamp <= timestamp {
      result = Some(change)
    } else {
      break
    }
  }
  result
}

///|
/// Return changes inside an inclusive time window.
pub fn signal_changes_in_window(
  vcd : VcdFile,
  full_name : String,
  window : TimeWindow,
) -> Array[SignalChange] raise VcdError {
  check_window(window)
  let result : Array[SignalChange] = []
  for change in signal_changes(vcd, full_name) {
    if change.timestamp >= window.start_time &&
      change.timestamp <= window.end_time {
      result.push(change)
    }
  }
  result
}

///|
/// Return all changes in an inclusive time window.
pub fn changes_in_window(
  vcd : VcdFile,
  window : TimeWindow,
) -> Array[SignalChange] raise VcdError {
  check_window(window)
  let result : Array[SignalChange] = []
  for change in vcd.changes {
    if change.timestamp >= window.start_time &&
      change.timestamp <= window.end_time {
      result.push(change)
    }
  }
  result
}

///|
/// Count all changes inside an inclusive time window.
pub fn change_count_in_window(
  vcd : VcdFile,
  window : TimeWindow,
) -> Int raise VcdError {
  changes_in_window(vcd, window).length()
}

///|
/// Return true if a signal changes inside an inclusive time window.
pub fn signal_changes_inside_window(
  vcd : VcdFile,
  full_name : String,
  window : TimeWindow,
) -> Bool raise VcdError {
  signal_changes_in_window(vcd, full_name, window).length() > 0
}

///|
/// Return the first timestamp in a window, if the file has any matching change.
pub fn first_time_in_window(
  vcd : VcdFile,
  window : TimeWindow,
) -> Int64? raise VcdError {
  let changes = changes_in_window(vcd, window)
  if changes.is_empty() {
    None
  } else {
    Some(changes[0].timestamp)
  }
}

///|
/// Return the last timestamp in a window, if the file has any matching change.
pub fn last_time_in_window(
  vcd : VcdFile,
  window : TimeWindow,
) -> Int64? raise VcdError {
  let changes = changes_in_window(vcd, window)
  if changes.is_empty() {
    None
  } else {
    Some(changes[changes.length() - 1].timestamp)
  }
}

///|
/// Summarize how completely one signal is covered by changes.
pub fn signal_coverage(
  vcd : VcdFile,
  full_name : String,
) -> SignalCoverage raise VcdError {
  let _signal = signal_or_raise(vcd, full_name)
  let stats = signal_stats(vcd, full_name)
  {
    full_name,
    has_value: stats.change_count > 0,
    first_change_time: stats.first_change_time,
    last_change_time: stats.last_change_time,
    change_count: stats.change_count,
    transition_count: transition_count(vcd, full_name),
    unknown_change_count: unknown_change_count(vcd, full_name),
    high_impedance_change_count: high_impedance_change_count(vcd, full_name),
  }
}

///|
/// Return coverage summaries for all declared signals.
pub fn all_signal_coverage(
  vcd : VcdFile,
) -> Array[SignalCoverage] raise VcdError {
  let result : Array[SignalCoverage] = []
  for signal in vcd.signals {
    result.push(signal_coverage(vcd, signal.full_name))
  }
  result
}

///|
/// Count signals with at least one value.
pub fn covered_signal_count(vcd : VcdFile) -> Int raise VcdError {
  let mut count = 0
  for coverage in all_signal_coverage(vcd) {
    if coverage.has_value {
      count = count + 1
    }
  }
  count
}

///|
/// Count signals that never receive a value.
pub fn uncovered_signal_count(vcd : VcdFile) -> Int {
  unchanged_signals(vcd).length()
}

///|
/// Render value counts for one signal.
pub fn render_value_counts(
  vcd : VcdFile,
  full_name : String,
) -> String raise VcdError {
  let builder = StringBuilder()
  builder.write_string("value\tcount\n")
  for item in value_counts(vcd, full_name) {
    builder.write_string(item.value)
    builder.write_char('\t')
    builder.write_string(item.count.to_string())
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// Render signal coverage summaries.
pub fn render_coverage(vcd : VcdFile) -> String raise VcdError {
  let builder = StringBuilder()
  builder.write_string(
    "signal\thas_value\tchanges\ttransitions\tunknown\tz\tfirst\tlast\n",
  )
  for item in all_signal_coverage(vcd) {
    builder.write_string(item.full_name)
    builder.write_char('\t')
    builder.write_string(item.has_value.to_string())
    builder.write_char('\t')
    builder.write_string(item.change_count.to_string())
    builder.write_char('\t')
    builder.write_string(item.transition_count.to_string())
    builder.write_char('\t')
    builder.write_string(item.unknown_change_count.to_string())
    builder.write_char('\t')
    builder.write_string(item.high_impedance_change_count.to_string())
    builder.write_char('\t')
    builder.write_string(option_time_to_string(item.first_change_time))
    builder.write_char('\t')
    builder.write_string(option_time_to_string(item.last_change_time))
    builder.write_char('\n')
  }
  builder.to_string()
}