///|
/// Header and declaration facts used for compatibility reports.
pub struct CompatibilitySummary {
  has_date : Bool
  has_version : Bool
  has_timescale : Bool
  has_comment : Bool
  variable_kinds : Array[(String, Int)]
  supports_dumpvars : Bool
  supports_real : Bool
} derive(Debug, Eq)

///|
/// Summarize syntax features found in a parsed trace and its source text.
pub fn compatibility_summary(
  source : String,
  trace : Trace,
) -> CompatibilitySummary {
  let kinds : Array[(String, Int)] = []
  let supports_real = trace.signals.any(fn(signal) {
      signal.kind.to_lower() == "real"
    }) ||
    trace.changes.any(fn(change) { change.value is Real(_) })
  for signal in trace.signals {
    match kinds.search_by(fn(item) { item.0 == signal.kind }) {
      Some(index) => kinds[index] = (signal.kind, kinds[index].1 + 1)
      None => kinds.push((signal.kind, 1))
    }
  }
  {
    has_date: trace.header.date is Some(_),
    has_version: trace.header.version is Some(_),
    has_timescale: trace.header.timescale is Some(_),
    has_comment: trace.header.comment is Some(_),
    variable_kinds: kinds,
    supports_dumpvars: source.contains("$dumpvars"),
    supports_real,
  }
}

///|
/// Return whether a source uses the standard `$dumpvars` block.
pub fn has_dumpvars(source : String) -> Bool {
  source.contains("$dumpvars")
}

///|
/// Return whether all values fit their signal declarations.
pub fn values_fit_declarations(trace : Trace) -> Bool {
  trace.changes.all(fn(change) {
    match find_signal(trace, change.id) {
      Some(signal) =>
        change.value.width() == 0 || change.value.width() <= signal.width
      None => false
    }
  })
}

///|
/// Return whether serialization preserves declarations and observable changes.
pub fn round_trip_equal(trace : Trace) -> Bool {
  match parse(trace_to_vcd_grouped(trace)) {
    Ok(decoded) =>
      same_signal_shape(trace, decoded) && traces_equal(trace, decoded)
    Err(_) => false
  }
}

///|
/// Serialize, parse, and return the canonical trace when possible.
pub fn canonical_round_trip(trace : Trace) -> Trace? {
  match parse(trace_to_vcd_grouped(trace)) {
    Ok(decoded) => Some(compact_trace(decoded))
    Err(_) => None
  }
}

///|
/// Return counts of declared variable kinds.
pub fn variable_kind_counts(trace : Trace) -> Array[(String, Int)] {
  compatibility_summary("", trace).variable_kinds
}

///|
/// Return a concise compatibility label.
pub fn compatibility_label(source : String, trace : Trace) -> String {
  let summary = compatibility_summary(source, trace)
  let features : Array[String] = []
  if summary.has_date {
    features.push("date")
  }
  if summary.has_version {
    features.push("version")
  }
  if summary.has_timescale {
    features.push("timescale")
  }
  if summary.supports_dumpvars {
    features.push("dumpvars")
  }
  if summary.supports_real {
    features.push("real")
  }
  if features.length() == 0 {
    "basic"
  } else {
    features.join(",")
  }
}

///|
/// Return whether all declarations have supported variable kinds.
pub fn supported_variable_kinds(trace : Trace) -> Bool {
  trace.signals.all(fn(signal) {
    match signal.kind.to_lower() {
      "wire" | "reg" | "logic" | "real" | "integer" | "parameter" => true
      _ => false
    }
  })
}

///|
/// Produce a stable compatibility report.
pub fn compatibility_report(source : String, trace : Trace) -> String {
  let summary = compatibility_summary(source, trace)
  let out : Array[String] = [
    "compatibility: " + compatibility_label(source, trace),
    "declarations: " + trace.signals.length().to_string(),
    "changes: " + trace.changes.length().to_string(),
    "widths: " + values_fit_declarations(trace).to_string(),
    "variable-kinds: " + supported_variable_kinds(trace).to_string(),
  ]
  for item in summary.variable_kinds {
    out.push("kind " + item.0 + "=" + item.1.to_string())
  }
  out.join("\n")
}