///|
/// Counts of observable value classes for one signal.
pub struct ValueDistribution {
  signal_id : String
  total : Int
  scalar : Int
  vector : Int
  real : Int
  zero : Int
  one : Int
  unknown : Int
  high_impedance : Int
} derive(Debug, Eq)

///|
/// Count value classes without losing the distinction between x and z.
pub fn value_distribution(
  trace : Trace,
  signal_id : String,
) -> ValueDistribution {
  let mut total = 0
  let mut scalar = 0
  let mut vector = 0
  let mut real = 0
  let mut zero = 0
  let mut one = 0
  let mut unknown = 0
  let mut high_impedance = 0
  for change in trace.changes {
    if change.id != signal_id {
      continue
    }
    total += 1
    match change.value {
      Scalar(value) => {
        scalar += 1
        match logic_bit(value) {
          Zero => zero += 1
          One => one += 1
          Unknown => unknown += 1
          HighImpedance => high_impedance += 1
        }
      }
      Vector(bits) => {
        vector += 1
        if bits.contains("x") || bits.contains("X") {
          unknown += 1
        }
        if bits.contains("z") || bits.contains("Z") {
          high_impedance += 1
        }
      }
      Real(_) => real += 1
    }
  }
  { signal_id, total, scalar, vector, real, zero, one, unknown, high_impedance }
}

///|
/// Return every observed scalar transition and its multiplicity.
pub fn transition_matrix(
  trace : Trace,
  signal_id : String,
) -> Array[(Char, Char, Int)] {
  let result : Array[(Char, Char, Int)] = []
  let mut previous : Char? = None
  for change in trace.changes {
    if change.id != signal_id {
      continue
    }
    match change.value {
      Scalar(current) => {
        match previous {
          Some(before) => {
            let found = result.search_by(fn(item) {
              item.0 == before && item.1 == current
            })
            match found {
              Some(index) =>
                result[index] = (before, current, result[index].2 + 1)
              None => result.push((before, current, 1))
            }
          }
          None => ()
        }
        previous = Some(current)
      }
      _ => previous = None
    }
  }
  result
}

///|
/// Render a transition matrix as stable CSV.
pub fn transition_matrix_csv(trace : Trace, signal_id : String) -> String {
  let out : Array[String] = ["from,to,count"]
  for item in transition_matrix(trace, signal_id) {
    out.push(
      item.0.to_string() + "," + item.1.to_string() + "," + item.2.to_string(),
    )
  }
  out.join("\n")
}

///|
/// Fraction of changes whose value contains no x or z state.
pub fn known_value_ratio(trace : Trace, signal_id : String) -> Double {
  let distribution = value_distribution(trace, signal_id)
  if distribution.total == 0 {
    0.0
  } else {
    (distribution.total - distribution.unknown - distribution.high_impedance).to_double() /
    distribution.total.to_double()
  }
}

///|
/// Fraction of changes carrying an unknown or high-impedance state.
pub fn unknown_value_ratio(trace : Trace, signal_id : String) -> Double {
  1.0 - known_value_ratio(trace, signal_id)
}

///|
/// Count signal changes per unit of observed trace time.
pub fn change_rate(trace : Trace, signal_id : String) -> Double {
  if trace.end_time <= 0L {
    0.0
  } else {
    signal_changes(trace, signal_id).length().to_double() /
    trace.end_time.to_double()
  }
}

///|
/// Count how many declared signals changed at each timestamp.
pub fn active_signal_counts(trace : Trace) -> Array[(Int64, Int)] {
  let result : Array[(Int64, Int)] = []
  let mut index = 0
  while index < trace.changes.length() {
    let timestamp = trace.changes[index].time
    let ids : Array[String] = []
    while index < trace.changes.length() &&
          trace.changes[index].time == timestamp {
      if !ids.contains(trace.changes[index].id) {
        ids.push(trace.changes[index].id)
      }
      index += 1
    }
    result.push((timestamp, ids.length()))
  }
  result
}

///|
/// Return the timestamp at which the most signals changed together.
pub fn maximum_parallel_activity(trace : Trace) -> (Int64, Int) {
  let result = active_signal_counts(trace)
  let mut best = (0L, 0)
  for item in result {
    if item.1 > best.1 {
      best = item
    }
  }
  best
}

///|
/// Summarize all declared signals as a compact table.
pub fn statistics_catalog(
  trace : Trace,
) -> Array[(String, Int, Double, Double)] {
  trace.signals.map(fn(signal) {
    (
      signal_path(signal),
      signal_changes(trace, signal.id).length(),
      known_value_ratio(trace, signal.id),
      change_rate(trace, signal.id),
    )
  })
}

///|
/// Render a stable CSV catalog for plotting and spreadsheet review.
pub fn statistics_catalog_csv(trace : Trace) -> String {
  let out : Array[String] = ["path,changes,known_ratio,change_rate"]
  for item in statistics_catalog(trace) {
    out.push(
      escape_text(item.0) +
      "," +
      item.1.to_string() +
      "," +
      item.2.to_string() +
      "," +
      item.3.to_string(),
    )
  }
  out.join("\n")
}

///|
/// Encode one value distribution for dashboards.
pub fn value_distribution_json(item : ValueDistribution) -> String {
  "{\"signal_id\":\"" +
  escape_text(item.signal_id) +
  "\",\"total\":" +
  item.total.to_string() +
  ",\"scalar\":" +
  item.scalar.to_string() +
  ",\"vector\":" +
  item.vector.to_string() +
  ",\"real\":" +
  item.real.to_string() +
  ",\"zero\":" +
  item.zero.to_string() +
  ",\"one\":" +
  item.one.to_string() +
  ",\"unknown\":" +
  item.unknown.to_string() +
  ",\"high_impedance\":" +
  item.high_impedance.to_string() +
  "}"
}

///|
/// Produce a short human-readable signal quality line.
pub fn quality_line(trace : Trace, signal_id : String) -> String {
  let item = value_distribution(trace, signal_id)
  signal_id +
  " total=" +
  item.total.to_string() +
  " known=" +
  known_value_ratio(trace, item.signal_id).to_string() +
  " unknown=" +
  item.unknown.to_string() +
  " z=" +
  item.high_impedance.to_string()
}