///|
/// Escape a string for the compact JSON helpers in this package.
pub fn escape_text(text : String) -> String {
  text
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n")
  .replace_all(old="\r", new="\\r")
}

///|
/// Encode one value as a stable JSON object fragment.
pub fn value_json(value : Value) -> String {
  match value {
    Scalar(ch) =>
      "{\"kind\":\"scalar\",\"value\":\"" + escape_text(ch.to_string()) + "\"}"
    Vector(bits) =>
      "{\"kind\":\"vector\",\"value\":\"" + escape_text(bits) + "\"}"
    Real(number) => "{\"kind\":\"real\",\"value\":" + number.to_string() + "}"
  }
}

///|
/// Encode one change as a compact JSON object.
pub fn change_json(change : Change) -> String {
  "{\"time\":" +
  change.time.to_string() +
  ",\"id\":\"" +
  escape_text(change.id) +
  "\",\"value\":" +
  value_json(change.value) +
  "}"
}

///|
/// Encode signal metadata as a compact JSON object.
pub fn signal_json(signal : Signal) -> String {
  "{\"id\":\"" +
  escape_text(signal.id) +
  "\",\"name\":\"" +
  escape_text(signal.name) +
  "\",\"width\":" +
  signal.width.to_string() +
  ",\"kind\":\"" +
  escape_text(signal.kind) +
  "\",\"path\":\"" +
  escape_text(signal_path(signal)) +
  "\"}"
}

///|
/// Export the complete trace as deterministic JSON.
pub fn trace_json(trace : Trace) -> String {
  let signals = trace.signals.map(signal_json).join(",")
  let changes = trace.changes.map(change_json).join(",")
  "{\"end_time\":" +
  trace.end_time.to_string() +
  ",\"signals\":[" +
  signals +
  "],\"changes\":[" +
  changes +
  "]}"
}

///|
/// Export signal changes as CSV for spreadsheet and plotting tools.
pub fn changes_csv(trace : Trace, signal_id? : String) -> String {
  let out = StringBuilder()
  out.write_string("time,id,value\n")
  for change in trace.changes {
    match signal_id {
      Some(id) if id != change.id => continue
      _ => ()
    }
    out.write_string(change.time.to_string())
    out.write_char(',')
    out.write_string(escape_text(change.id))
    out.write_char(',')
    out.write_string(escape_text(change.value.display()))
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// Render a trace back into a portable VCD document.
pub fn trace_to_vcd(trace : Trace) -> String {
  let out = StringBuilder()
  write_header(out, trace)
  for signal in trace.signals {
    write_signal_scope(out, signal)
  }
  out.write_string("$enddefinitions $end\n")
  let mut current_time : Int64? = None
  for change in trace.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')
  }
  out.to_string()
}

///|
/// Write one declaration under its original scope path.
fn write_signal_scope(out : StringBuilder, signal : Signal) -> Unit {
  for scope in signal.scope {
    out.write_string("$scope module " + scope + " $end\n")
  }
  if signal.scope.length() == 0 {
    out.write_string("$scope module top $end\n")
  }
  out.write_string("$var ")
  out.write_string(signal.kind)
  out.write_char(' ')
  out.write_string(signal.width.to_string())
  out.write_char(' ')
  out.write_string(signal.id)
  out.write_char(' ')
  out.write_string(signal.name)
  out.write_string(" $end\n")
  let mut index = signal.scope.length()
  while index > 0 {
    out.write_string("$upscope $end\n")
    index -= 1
  }
  if signal.scope.length() == 0 {
    out.write_string("$upscope $end\n")
  }
}

///|
fn write_header(out : StringBuilder, trace : Trace) -> Unit {
  match trace.header.date {
    Some(value) => out.write_string("$date " + value + " $end\n")
    None => ()
  }
  match trace.header.version {
    Some(value) => out.write_string("$version " + value + " $end\n")
    None => ()
  }
  match trace.header.timescale {
    Some(value) => out.write_string("$timescale " + value + " $end\n")
    None => ()
  }
  match trace.header.comment {
    Some(value) => out.write_string("$comment " + value + " $end\n")
    None => ()
  }
}

///|
fn vcd_assignment(change : Change) -> String {
  match change.value {
    Scalar(value) => value.to_string() + change.id
    Vector(bits) => "b" + bits + " " + change.id
    Real(number) => "r" + number.to_string() + " " + change.id
  }
}