///|
/// Render a single declaration line without surrounding scope directives.
pub fn vcd_declaration(signal : Signal) -> String {
  "$var " +
  signal.kind +
  " " +
  signal.width.to_string() +
  " " +
  signal.id +
  " " +
  signal.name +
  " $end"
}

///|
/// Render a scope opening directive.
pub fn vcd_scope_open(scope : String) -> String {
  "$scope module " + scope + " $end"
}

///|
/// Render a scope closing directive.
pub fn vcd_scope_close() -> String {
  "$upscope $end"
}

///|
/// Render declarations grouped by their complete scope path.
pub fn trace_to_vcd_grouped(trace : Trace) -> String {
  let out = StringBuilder()
  write_header(out, trace)
  let opened : Array[String] = []
  for signal in trace.signals {
    let common = common_scope_length(opened, signal.scope)
    let mut close_index = opened.length()
    while close_index > common {
      out.write_string(vcd_scope_close() + "\n")
      close_index -= 1
    }
    while opened.length() > common {
      ignore(opened.pop())
    }
    for scope_index in common.. 0 {
    out.write_string(vcd_scope_close() + "\n")
    ignore(opened.pop())
  }
  out.write_string("$enddefinitions $end\n")
  write_changes(out, trace.changes)
  out.to_string()
}

///|
fn common_scope_length(left : Array[String], right : Array[String]) -> Int {
  let bound = Int::min(left.length(), right.length())
  let mut index = 0
  while index < bound && left[index] == right[index] {
    index += 1
  }
  index
}

///|
fn write_changes(out : StringBuilder, changes : Array[Change]) -> Unit {
  let mut current_time : Int64? = None
  for change in changes {
    if current_time != Some(change.time) {
      out.write_char('#')
      out.write_string(change.time.to_string())
      out.write_char('\n')
      current_time = Some(change.time)
    }
    out.write_string(vcd_assignment(change))
    out.write_char('\n')
  }
}

///|
/// Render one signal array as indented JSON.
pub fn signals_json_pretty(trace : Trace, indent : String) -> String {
  let out : Array[String] = ["["]
  for index in 0.. String {
  let out : Array[String] = ["["]
  for index in 0.. String {
  "{\n" +
  "  \"end_time\": " +
  trace.end_time.to_string() +
  ",\n  \"signals\": " +
  signals_json_pretty(trace, "  ") +
  ",\n  \"changes\": " +
  changes_json_pretty(trace, "  ") +
  "\n}"
}

///|
/// Return a small export inventory for verification reports.
pub fn export_summary(trace : Trace) -> String {
  let json = trace_json(trace)
  let csv = changes_csv(trace)
  let vcd = trace_to_vcd_grouped(trace)
  "json_chars=" +
  json.length().to_string() +
  " csv_chars=" +
  csv.length().to_string() +
  " vcd_chars=" +
  vcd.length().to_string() +
  " shape=" +
  trace.shape_digest().to_string()
}

///|
/// Return whether a serialized trace can be parsed again.
pub fn export_parseable(trace : Trace) -> Bool {
  parse(trace_to_vcd_grouped(trace)) is Ok(_)
}

///|
/// Compare grouped export with the source trace's declaration shape.
pub fn grouped_export_preserves_shape(trace : Trace) -> Bool {
  match parse(trace_to_vcd_grouped(trace)) {
    Ok(decoded) => same_signal_shape(trace, decoded)
    Err(_) => false
  }
}

///|
/// Return all declaration paths as a newline-delimited manifest.
pub fn declaration_manifest(trace : Trace) -> String {
  trace.signals
  .map(fn(signal) {
    signal.id + "\t" + signal.width.to_string() + "\t" + signal_path(signal)
  })
  .join("\n")
}

///|
/// Return the compact JSON byte count for one trace.
pub fn json_size(trace : Trace) -> Int {
  trace_json(trace).length()
}

///|
/// Return the CSV header expected by plotting tools.
pub fn csv_header() -> String {
  "time,id,value"
}