///|
/// A parse attempt summary that keeps errors machine-readable.
pub struct ParseSummary {
  ok : Bool
  signal_count : Int
  change_count : Int
  end_time : Int64
  error : String?
} derive(Debug, Eq)

///|
/// Classify a parse error without exposing implementation details.
pub fn parse_error_code(error : ParseError) -> String {
  match error {
    InvalidDirective(_) => "invalid-directive"
    InvalidVariable(_) => "invalid-variable"
    InvalidValue(_) => "invalid-value"
    InvalidTime(_) => "invalid-time"
    EmptyInput => "empty-input"
    MissingEnddefinitions => "missing-enddefinitions"
    ScopeUnderflow => "scope-underflow"
    DuplicateIdentifier(_) => "duplicate-identifier"
    UnknownIdentifier(_) => "unknown-identifier"
    UnsupportedValue(_) => "unsupported-value"
  }
}

///|
/// Render a parse error with its original payload.
pub fn parse_error_message(error : ParseError) -> String {
  match error {
    InvalidDirective(value) => "invalid directive: " + value
    InvalidVariable(value) => "invalid variable: " + value
    InvalidValue(value) => "invalid value: " + value
    InvalidTime(value) => "invalid time: " + value
    EmptyInput => "input is empty"
    MissingEnddefinitions => "missing $enddefinitions"
    ScopeUnderflow => "$upscope has no matching $scope"
    DuplicateIdentifier(value) => "duplicate identifier: " + value
    UnknownIdentifier(value) => "unknown identifier: " + value
    UnsupportedValue(value) => "unsupported value: " + value
  }
}

///|
/// Parse text while returning a summary instead of raising or branching at call sites.
pub fn summarize_parse(input : String) -> ParseSummary {
  match parse(input) {
    Ok(trace) =>
      {
        ok: true,
        signal_count: trace.signals.length(),
        change_count: trace.changes.length(),
        end_time: trace.end_time,
        error: None,
      }
    Err(error) =>
      {
        ok: false,
        signal_count: 0,
        change_count: 0,
        end_time: 0L,
        error: Some(parse_error_message(error)),
      }
  }
}

///|
/// Parse every non-empty document in a line-separated batch.
pub fn parse_batch_safely(input : String) -> Array[ParseSummary] {
  input
  .split("\n\n")
  .filter(fn(item) { item.trim().to_owned() != "" })
  .map(fn(item) { summarize_parse(item.to_owned()) })
  .to_array()
}

///|
/// Return whether a string has the minimum VCD structure.
pub fn looks_like_vcd(input : String) -> Bool {
  input.contains("$enddefinitions") && input.contains("$var")
}

///|
/// Return the number of non-empty physical lines in an input.
pub fn physical_line_count(input : String) -> Int {
  input.split("\n").count_if(fn(line) { line.trim().to_owned() != "" })
}

///|
/// Validate a text document and return a diagnostic suitable for a CLI.
pub fn text_diagnostic(input : String) -> String {
  let summary = summarize_parse(input)
  match summary.error {
    Some(error) => "error: " + error
    None =>
      "ok: signals=" +
      summary.signal_count.to_string() +
      " changes=" +
      summary.change_count.to_string() +
      " end=" +
      summary.end_time.to_string()
  }
}