///|
fn string_array_contains(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
fn int64_array_contains(values : Array[Int64], target : Int64) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
fn push_unique_string(values : Array[String], value : String) -> Unit {
  if !string_array_contains(values, value) {
    values.push(value)
  }
}

///|
fn push_unique_int64(values : Array[Int64], value : Int64) -> Unit {
  if !int64_array_contains(values, value) {
    values.push(value)
  }
}

///|
fn option_string_equals(left : String?, right : String?) -> Bool {
  match (left, right) {
    (Some(l), Some(r)) => l == r
    (None, None) => true
    _ => false
  }
}

///|
fn option_string_text(value : String?) -> String {
  match value {
    Some(value) => value
    None => "-"
  }
}

///|
fn find_signal_or_none(vcd : VcdFile, full_name : String) -> Signal? {
  find_signal(vcd, full_name)
}

///|
fn same_width(left : Signal, right : Signal) -> Bool {
  left.width == right.width
}

///|
fn same_type(left : Signal, right : Signal) -> Bool {
  left.signal_type == right.signal_type
}

///|
fn names_from_signals(signals : Array[Signal]) -> Array[String] {
  let result : Array[String] = []
  for signal in signals {
    result.push(signal.full_name)
  }
  result
}

///|
fn merged_signal_names(left : VcdFile, right : VcdFile) -> Array[String] {
  let result : Array[String] = []
  for name in names_from_signals(left.signals) {
    push_unique_string(result, name)
  }
  for name in names_from_signals(right.signals) {
    push_unique_string(result, name)
  }
  result
}

///|
fn merged_timestamps(left : VcdFile, right : VcdFile) -> Array[Int64] {
  let result : Array[Int64] = []
  for time in timestamps(left) {
    push_unique_int64(result, time)
  }
  for time in timestamps(right) {
    push_unique_int64(result, time)
  }
  result
}

///|
fn append_signal_list(
  builder : StringBuilder,
  title : String,
  values : Array[String],
) -> Unit {
  builder.write_string(title)
  builder.write_string(": ")
  if values.is_empty() {
    builder.write_string("none\n")
  } else {
    for i = 0; i < values.length(); i = i + 1 {
      if i > 0 {
        builder.write_string(", ")
      }
      builder.write_string(values[i])
    }
    builder.write_char('\n')
  }
}

///|
fn append_time_list(
  builder : StringBuilder,
  title : String,
  values : Array[Int64],
) -> Unit {
  builder.write_string(title)
  builder.write_string(": ")
  if values.is_empty() {
    builder.write_string("none\n")
  } else {
    for i = 0; i < values.length(); i = i + 1 {
      if i > 0 {
        builder.write_string(", ")
      }
      builder.write_string(values[i].to_string())
    }
    builder.write_char('\n')
  }
}

///|
fn markdown_string_list(
  builder : StringBuilder,
  title : String,
  values : Array[String],
) -> Unit {
  builder.write_string("- ")
  builder.write_string(title)
  builder.write_string(": ")
  if values.is_empty() {
    builder.write_string("none\n")
  } else {
    for i = 0; i < values.length(); i = i + 1 {
      if i > 0 {
        builder.write_string(", ")
      }
      builder.write_string(values[i])
    }
    builder.write_char('\n')
  }
}

///|
fn markdown_time_list(
  builder : StringBuilder,
  title : String,
  values : Array[Int64],
) -> Unit {
  builder.write_string("- ")
  builder.write_string(title)
  builder.write_string(": ")
  if values.is_empty() {
    builder.write_string("none\n")
  } else {
    for i = 0; i < values.length(); i = i + 1 {
      if i > 0 {
        builder.write_string(", ")
      }
      builder.write_string(values[i].to_string())
    }
    builder.write_char('\n')
  }
}

///|
pub fn compare_signal_sets(left : VcdFile, right : VcdFile) -> SignalSetDiff {
  let common : Array[String] = []
  let left_only : Array[String] = []
  let right_only : Array[String] = []
  let width_mismatches : Array[String] = []
  let type_mismatches : Array[String] = []
  for left_signal in left.signals {
    match find_signal_or_none(right, left_signal.full_name) {
      Some(right_signal) => {
        common.push(left_signal.full_name)
        if !same_width(left_signal, right_signal) {
          width_mismatches.push(left_signal.full_name)
        }
        if !same_type(left_signal, right_signal) {
          type_mismatches.push(left_signal.full_name)
        }
      }
      None => left_only.push(left_signal.full_name)
    }
  }
  for right_signal in right.signals {
    if find_signal_or_none(left, right_signal.full_name) is None {
      right_only.push(right_signal.full_name)
    }
  }
  { common, left_only, right_only, width_mismatches, type_mismatches }
}

///|
pub fn compare_timestamps(left : VcdFile, right : VcdFile) -> TimestampDiff {
  let common : Array[Int64] = []
  let left_only : Array[Int64] = []
  let right_only : Array[Int64] = []
  let left_times = timestamps(left)
  let right_times = timestamps(right)
  for time in left_times {
    if int64_array_contains(right_times, time) {
      common.push(time)
    } else {
      left_only.push(time)
    }
  }
  for time in right_times {
    if !int64_array_contains(left_times, time) {
      right_only.push(time)
    }
  }
  { common, left_only, right_only }
}

///|
pub fn compare_values_at(
  left : VcdFile,
  right : VcdFile,
  timestamp : Int64,
) -> Array[ValueDiff] raise VcdError {
  if timestamp < 0L {
    raise VcdError("timestamp must be non-negative")
  }
  let result : Array[ValueDiff] = []
  for name in merged_signal_names(left, right) {
    let left_value = if find_signal(left, name) is Some(_) {
      value_at(left, name, timestamp)
    } else {
      None
    }
    let right_value = if find_signal(right, name) is Some(_) {
      value_at(right, name, timestamp)
    } else {
      None
    }
    if !option_string_equals(left_value, right_value) {
      result.push({
        timestamp,
        full_name: name,
        left: left_value,
        right: right_value,
      })
    }
  }
  result
}

///|
pub fn compare_values_on_union_timeline(
  left : VcdFile,
  right : VcdFile,
) -> Array[ValueDiff] raise VcdError {
  let result : Array[ValueDiff] = []
  for time in merged_timestamps(left, right) {
    for diff in compare_values_at(left, right, time) {
      result.push(diff)
    }
  }
  result
}

///|
pub fn compare_vcd(left : VcdFile, right : VcdFile) -> VcdDiff raise VcdError {
  {
    signal_diff: compare_signal_sets(left, right),
    timestamp_diff: compare_timestamps(left, right),
    value_diffs: compare_values_on_union_timeline(left, right),
  }
}

///|
pub fn vcds_have_same_signal_set(left : VcdFile, right : VcdFile) -> Bool {
  let diff = compare_signal_sets(left, right)
  diff.left_only.is_empty() &&
  diff.right_only.is_empty() &&
  diff.width_mismatches.is_empty() &&
  diff.type_mismatches.is_empty()
}

///|
pub fn vcds_have_same_timestamps(left : VcdFile, right : VcdFile) -> Bool {
  let diff = compare_timestamps(left, right)
  diff.left_only.is_empty() && diff.right_only.is_empty()
}

///|
pub fn vcds_have_same_values(
  left : VcdFile,
  right : VcdFile,
) -> Bool raise VcdError {
  compare_values_on_union_timeline(left, right).is_empty()
}

///|
pub fn vcds_are_equivalent(
  left : VcdFile,
  right : VcdFile,
) -> Bool raise VcdError {
  vcds_have_same_signal_set(left, right) &&
  vcds_have_same_timestamps(left, right) &&
  vcds_have_same_values(left, right)
}

///|
pub fn count_value_diffs_at(
  left : VcdFile,
  right : VcdFile,
  timestamp : Int64,
) -> Int raise VcdError {
  compare_values_at(left, right, timestamp).length()
}

///|
pub fn first_value_diff(
  left : VcdFile,
  right : VcdFile,
) -> ValueDiff? raise VcdError {
  let diffs = compare_values_on_union_timeline(left, right)
  if diffs.is_empty() {
    None
  } else {
    Some(diffs[0])
  }
}

///|
pub fn value_diff_count(left : VcdFile, right : VcdFile) -> Int raise VcdError {
  compare_values_on_union_timeline(left, right).length()
}

///|
pub fn signal_value_diff_count(
  left : VcdFile,
  right : VcdFile,
  full_name : String,
) -> Int raise VcdError {
  let mut count = 0
  for diff in compare_values_on_union_timeline(left, right) {
    if diff.full_name == full_name {
      count = count + 1
    }
  }
  count
}

///|
pub fn render_signal_set_diff(diff : SignalSetDiff) -> String {
  let builder = StringBuilder()
  append_signal_list(builder, "Common signals", diff.common)
  append_signal_list(builder, "Left-only signals", diff.left_only)
  append_signal_list(builder, "Right-only signals", diff.right_only)
  append_signal_list(builder, "Width mismatches", diff.width_mismatches)
  append_signal_list(builder, "Type mismatches", diff.type_mismatches)
  builder.to_string()
}

///|
pub fn render_timestamp_diff(diff : TimestampDiff) -> String {
  let builder = StringBuilder()
  append_time_list(builder, "Common timestamps", diff.common)
  append_time_list(builder, "Left-only timestamps", diff.left_only)
  append_time_list(builder, "Right-only timestamps", diff.right_only)
  builder.to_string()
}

///|
pub fn render_value_diff(diff : ValueDiff) -> String {
  "\{diff.timestamp}\t\{diff.full_name}\t\{option_string_text(diff.left)}\t\{option_string_text(diff.right)}"
}

///|
pub fn render_value_diffs(diffs : Array[ValueDiff]) -> String {
  let builder = StringBuilder()
  builder.write_string("timestamp\tsignal\tleft\tright\n")
  for diff in diffs {
    builder.write_string(render_value_diff(diff))
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
pub fn render_vcd_diff(diff : VcdDiff) -> String {
  let builder = StringBuilder()
  builder.write_string("Signal differences\n")
  builder.write_string(render_signal_set_diff(diff.signal_diff))
  builder.write_string("\nTimestamp differences\n")
  builder.write_string(render_timestamp_diff(diff.timestamp_diff))
  builder.write_string("\nValue differences\n")
  builder.write_string(render_value_diffs(diff.value_diffs))
  builder.to_string()
}

///|
pub fn markdown_signal_set_diff(diff : SignalSetDiff) -> String {
  let builder = StringBuilder()
  builder.write_string("## Signal Differences\n\n")
  markdown_string_list(builder, "Common signals", diff.common)
  markdown_string_list(builder, "Left-only signals", diff.left_only)
  markdown_string_list(builder, "Right-only signals", diff.right_only)
  markdown_string_list(builder, "Width mismatches", diff.width_mismatches)
  markdown_string_list(builder, "Type mismatches", diff.type_mismatches)
  builder.to_string()
}

///|
pub fn markdown_timestamp_diff(diff : TimestampDiff) -> String {
  let builder = StringBuilder()
  builder.write_string("## Timestamp Differences\n\n")
  markdown_time_list(builder, "Common timestamps", diff.common)
  markdown_time_list(builder, "Left-only timestamps", diff.left_only)
  markdown_time_list(builder, "Right-only timestamps", diff.right_only)
  builder.to_string()
}

///|
pub fn markdown_value_diffs(diffs : Array[ValueDiff]) -> String {
  let builder = StringBuilder()
  builder.write_string("## Value Differences\n\n")
  builder.write_string("| Timestamp | Signal | Left | Right |\n")
  builder.write_string("| --- | --- | --- | --- |\n")
  for diff in diffs {
    builder.write_string("| ")
    builder.write_string(diff.timestamp.to_string())
    builder.write_string(" | ")
    builder.write_string(diff.full_name)
    builder.write_string(" | ")
    builder.write_string(option_string_text(diff.left))
    builder.write_string(" | ")
    builder.write_string(option_string_text(diff.right))
    builder.write_string(" |\n")
  }
  builder.to_string()
}

///|
pub fn markdown_vcd_diff(diff : VcdDiff) -> String {
  let builder = StringBuilder()
  builder.write_string("# VCD Comparison\n\n")
  builder.write_string(markdown_signal_set_diff(diff.signal_diff))
  builder.write_char('\n')
  builder.write_string(markdown_timestamp_diff(diff.timestamp_diff))
  builder.write_char('\n')
  builder.write_string(markdown_value_diffs(diff.value_diffs))
  builder.to_string()
}

///|
pub fn comparison_manifest(diff : VcdDiff) -> String {
  "common_signals=\{diff.signal_diff.common.length()} left_only_signals=\{diff.signal_diff.left_only.length()} right_only_signals=\{diff.signal_diff.right_only.length()} common_timestamps=\{diff.timestamp_diff.common.length()} value_diffs=\{diff.value_diffs.length()}"
}

///|
pub fn compare_and_render(
  left : VcdFile,
  right : VcdFile,
) -> String raise VcdError {
  render_vcd_diff(compare_vcd(left, right))
}

///|
pub fn compare_and_render_markdown(
  left : VcdFile,
  right : VcdFile,
) -> String raise VcdError {
  markdown_vcd_diff(compare_vcd(left, right))
}