///|
/// Text-level inventory collected before full VCD parsing.
pub struct SourceStatistics {
  lines : Int
  nonempty_lines : Int
  bytes : Int
  directives : Int
  timestamps : Int
  assignments : Int
  comments : Int
} derive(Debug, Eq)

///|
/// Count physical VCD constructs without allocating a Trace.
pub fn source_statistics(input : String) -> SourceStatistics {
  let mut lines = 0
  let mut nonempty_lines = 0
  let mut directives = 0
  let mut timestamps = 0
  let mut assignments = 0
  let mut comments = 0
  for raw in input.split("\n") {
    lines += 1
    let line = raw.trim().to_owned()
    if line == "" {
      continue
    }
    nonempty_lines += 1
    if line.has_prefix("$") {
      directives += 1
      if line.has_prefix("$comment") {
        comments += 1
      }
    } else if line.has_prefix("#") {
      timestamps += 1
    } else {
      assignments += 1
    }
  }
  {
    lines,
    nonempty_lines,
    bytes: input.length(),
    directives,
    timestamps,
    assignments,
    comments,
  }
}

///|
/// Normalize CRLF, CR, trailing whitespace, and repeated blank lines.
pub fn normalize_vcd_text(input : String) -> String {
  let normalized = input
    .replace_all(old="\r\n", new="\n")
    .replace_all(old="\r", new="\n")
  let out = StringBuilder()
  let mut previous_blank = false
  for raw in normalized.split("\n") {
    let line = raw.trim_end().to_owned()
    let blank = line.trim().to_owned() == ""
    if blank && previous_blank {
      continue
    }
    out.write_string(line)
    out.write_char('\n')
    previous_blank = blank
  }
  out.to_string()
}

///|
/// Add a final newline when a generated VCD is embedded in a stream.
pub fn ensure_final_newline(input : String) -> String {
  if input.has_suffix("\n") {
    input
  } else {
    input + "\n"
  }
}

///|
/// Return a compact source inventory for diagnostics and benchmark labels.
pub fn source_statistics_line(stats : SourceStatistics) -> String {
  "lines=" +
  stats.lines.to_string() +
  " nonempty=" +
  stats.nonempty_lines.to_string() +
  " bytes=" +
  stats.bytes.to_string() +
  " directives=" +
  stats.directives.to_string() +
  " timestamps=" +
  stats.timestamps.to_string() +
  " assignments=" +
  stats.assignments.to_string() +
  " comments=" +
  stats.comments.to_string()
}

///|
/// Return whether the text contains a complete definition block.
pub fn has_complete_definition_block(input : String) -> Bool {
  input.contains("$enddefinitions") &&
  input.contains("$scope") &&
  input.contains("$var")
}

///|
/// Return whether a line is a VCD control directive.
pub fn is_vcd_directive(line : String) -> Bool {
  line.trim().to_owned().has_prefix("$")
}

///|
/// Return whether a line is a timestamp marker.
pub fn is_vcd_timestamp(line : String) -> Bool {
  line.trim().to_owned().has_prefix("#")
}

///|
/// Return all directive names in source order.
pub fn directive_names(input : String) -> Array[String] {
  let result : Array[String] = []
  for raw in input.split("\n") {
    let line = raw.trim().to_owned()
    if !line.has_prefix("$") {
      continue
    }
    let fields = words(line)
    if fields.length() > 0 {
      let name = fields[0]
      if !result.contains(name) {
        result.push(name)
      }
    }
  }
  result
}

///|
/// Return a stable directive inventory line.
pub fn directive_inventory(input : String) -> String {
  directive_names(input).join(",")
}

///|
/// Parse normalized text after repairing common line-ending issues.
pub fn parse_normalized(input : String) -> Result[Trace, ParseError] {
  parse(normalize_vcd_text(ensure_final_newline(input)))
}

///|
/// Parse a batch and retain one diagnostic per source document.
pub fn parse_text_batch(inputs : Array[String]) -> Array[ParseSummary] {
  inputs.map(fn(input) { summarize_parse(input) })
}

///|
/// Return the number of parseable documents in a batch.
pub fn parseable_count(summaries : Array[ParseSummary]) -> Int {
  summaries.count_if(fn(summary) { summary.ok })
}

///|
/// Return whether two source texts normalize to the same bytes.
pub fn normalized_text_equal(left : String, right : String) -> Bool {
  normalize_vcd_text(left) == normalize_vcd_text(right)
}

///|
/// Produce an explicit source/trace consistency line.
pub fn parser_consistency_report(source : String, trace : Trace) -> String {
  let source_info = source_statistics(source)
  "source=" +
  source_statistics_line(source_info) +
  " trace_signals=" +
  trace.signals.length().to_string() +
  " trace_changes=" +
  trace.changes.length().to_string() +
  " declarations=" +
  trace.signals.all(fn(_) { source.contains("$var") }).to_string()
}