///|
/// A detailed diagnostic emitted by deep waveform validation.
pub struct TraceDiagnostic {
  severity : String
  code : String
  signal_id : String?
  time : Int64?
  message : String
} derive(Debug, Eq)

///|
/// Validate declarations, change ordering, value widths, and metadata.
pub fn deep_validate(trace : Trace) -> Array[TraceDiagnostic] {
  let result : Array[TraceDiagnostic] = []
  if trace.signals.length() == 0 {
    result.push({
      severity: "error",
      code: "no-signals",
      signal_id: None,
      time: None,
      message: "trace declares no signals",
    })
  }
  let ids : Array[String] = []
  for signal in trace.signals {
    if ids.contains(signal.id) {
      result.push({
        severity: "error",
        code: "duplicate-id",
        signal_id: Some(signal.id),
        time: None,
        message: "signal identifier is declared more than once",
      })
    } else {
      ids.push(signal.id)
    }
    if signal.width <= 0 {
      result.push({
        severity: "error",
        code: "invalid-width",
        signal_id: Some(signal.id),
        time: None,
        message: "signal width must be positive",
      })
    }
    if signal.name == "" {
      result.push({
        severity: "warning",
        code: "empty-name",
        signal_id: Some(signal.id),
        time: None,
        message: "signal has an empty leaf name",
      })
    }
  }
  let mut previous_time : Int64? = None
  for change in trace.changes {
    match previous_time {
      Some(time) if change.time < time =>
        result.push({
          severity: "error",
          code: "time-order",
          signal_id: Some(change.id),
          time: Some(change.time),
          message: "changes are not monotonic by timestamp",
        })
      _ => ()
    }
    previous_time = Some(change.time)
    if !ids.contains(change.id) {
      result.push({
        severity: "error",
        code: "unknown-id",
        signal_id: Some(change.id),
        time: Some(change.time),
        message: "change references an undeclared identifier",
      })
      continue
    }
    match find_signal(trace, change.id) {
      Some(signal) if change.value.width() > 0 &&
        change.value.width() > signal.width =>
        result.push({
          severity: "warning",
          code: "width-overflow",
          signal_id: Some(change.id),
          time: Some(change.time),
          message: "value is wider than the declaration",
        })
      _ => ()
    }
    if change.time < 0L || change.time > trace.end_time {
      result.push({
        severity: "warning",
        code: "time-range",
        signal_id: Some(change.id),
        time: Some(change.time),
        message: "change is outside the trace time range",
      })
    }
  }
  match trace.header.timescale {
    Some(text) =>
      match parse_timescale(text) {
        Ok(_) => ()
        Err(_) =>
          result.push({
            severity: "warning",
            code: "timescale",
            signal_id: None,
            time: None,
            message: "header timescale is not recognized",
          })
      }
    None =>
      result.push({
        severity: "warning",
        code: "missing-timescale",
        signal_id: None,
        time: None,
        message: "trace has no timescale header",
      })
  }
  result
}

///|
/// Return whether deep validation reports no errors.
pub fn deep_is_valid(trace : Trace) -> Bool {
  !deep_validate(trace).any(fn(item) { item.severity == "error" })
}

///|
/// Return a count of diagnostics grouped by severity.
pub fn diagnostic_counts(items : Array[TraceDiagnostic]) -> (Int, Int) {
  let mut errors = 0
  let mut warnings = 0
  for item in items {
    if item.severity == "error" {
      errors += 1
    } else {
      warnings += 1
    }
  }
  (errors, warnings)
}

///|
/// Render detailed validation diagnostics as text.
pub fn diagnostics_report(items : Array[TraceDiagnostic]) -> String {
  let out : Array[String] = []
  for item in items {
    let signal = match item.signal_id {
      Some(id) => " signal=" + id
      None => ""
    }
    let time = match item.time {
      Some(value) => " time=" + value.to_string()
      None => ""
    }
    out.push(
      item.severity + " " + item.code + signal + time + ": " + item.message,
    )
  }
  if out.length() == 0 {
    "ok"
  } else {
    out.join("\n")
  }
}

///|
/// Return a trace after dropping changes with unknown identifiers.
pub fn drop_unknown_changes(trace : Trace) -> Trace {
  let ids = trace_signal_ids(trace)
  let changes = trace.changes.filter(fn(change) { ids.contains(change.id) })
  {
    header: trace.header,
    signals: trace.signals,
    changes,
    end_time: trace.end_time,
  }
}

///|
/// Return a trace after sorting changes and removing redundant writes.
pub fn repair_trace(trace : Trace) -> Trace {
  compact_trace(sort_trace_changes(drop_unknown_changes(trace)))
}