///|
/// Severity for lightweight trace quality checks.
pub(all) enum TraceLintSeverity {
  Info
  Warning
  Error
} derive(Debug, Eq, ToJson)

///|
/// A non-throwing quality observation for a trace document.
pub struct TraceLintIssue {
  severity : TraceLintSeverity
  code : String
  message : String
  step : Int
} derive(Debug, Eq, ToJson)

///|
pub fn TraceLintIssue::new(
  severity~ : TraceLintSeverity,
  code~ : String,
  message~ : String,
  step? : Int = -1,
) -> TraceLintIssue {
  { severity, code, message, step }
}

///|
pub fn TraceLintIssue::line(self : TraceLintIssue) -> String {
  let where_text = if self.step < 0 { "trace" } else { "step \{self.step}" }
  "[\{lint_severity_name(self.severity)}] \{self.code} at \{where_text}: \{self.message}"
}

///|
pub fn TraceLintIssue::markdown_row(self : TraceLintIssue) -> String {
  let where_text = if self.step < 0 { "trace" } else { "step \{self.step}" }
  "| \{lint_severity_name(self.severity)} | \{escape_quality_cell(self.code)} | \{escape_quality_cell(where_text)} | \{escape_quality_cell(self.message)} |"
}

///|
pub fn TraceLintIssue::is_error(self : TraceLintIssue) -> Bool {
  self.severity is Error
}

///|
pub fn TraceLintIssue::is_warning(self : TraceLintIssue) -> Bool {
  self.severity is Warning
}

///|
pub fn TraceLintIssue::is_info(self : TraceLintIssue) -> Bool {
  self.severity is Info
}

///|
pub fn TraceLintIssue::severity_name(self : TraceLintIssue) -> String {
  lint_severity_name(self.severity)
}

///|
/// A compact row describing one renderable point in a trace timeline.
pub struct TraceTimelineEntry {
  step : Int
  event_name : String
  target_count : Int
  object_count : Int
  entity_count : Int
  highlight_count : Int
  annotation_title : String
  completed : Bool
} derive(Debug, Eq, ToJson)

///|
pub fn TraceTimelineEntry::new(
  step~ : Int,
  event_name~ : String,
  target_count~ : Int,
  object_count~ : Int,
  entity_count~ : Int,
  highlight_count~ : Int,
  annotation_title? : String = "",
  completed? : Bool = false,
) -> TraceTimelineEntry {
  {
    step,
    event_name,
    target_count,
    object_count,
    entity_count,
    highlight_count,
    annotation_title,
    completed,
  }
}

///|
pub fn TraceTimelineEntry::label(self : TraceTimelineEntry) -> String {
  let prefix = if self.step < 0 { "initial" } else { "step \{self.step}" }
  if self.annotation_title == "" {
    "\{prefix}: \{self.event_name}"
  } else {
    "\{prefix}: \{self.event_name} - \{self.annotation_title}"
  }
}

///|
pub fn TraceTimelineEntry::markdown_row(self : TraceTimelineEntry) -> String {
  "| \{quality_step_label(self.step)} | \{escape_quality_cell(self.event_name)} | \{self.target_count} | \{self.object_count} | \{self.entity_count} | \{self.highlight_count} | \{escape_quality_cell(self.annotation_title)} | \{self.completed} |"
}

///|
pub fn AlgorithmTrace::timeline(
  self : AlgorithmTrace,
) -> Array[TraceTimelineEntry] {
  let entries : Array[TraceTimelineEntry] = [
    TraceTimelineEntry::new(
      step=-1,
      event_name="initial",
      target_count=0,
      object_count=self.initial_scene.objects.length(),
      entity_count=self.initial_scene.entity_count(),
      highlight_count=self.initial_scene.highlights.length(),
    ),
  ]
  for step in self.steps {
    entries.push(
      TraceTimelineEntry::new(
        step=step.index,
        event_name=timeline_event_name(step.event),
        target_count=timeline_event_target_count(step.event),
        object_count=step.scene.objects.length(),
        entity_count=step.scene.entity_count(),
        highlight_count=step.scene.highlights.length(),
        annotation_title=match step.annotation {
          Some(note) => note.title
          None => ""
        },
        completed=step.event is Complete,
      ),
    )
  }
  entries
}

///|
/// Run non-throwing quality checks that complement `AlgorithmTrace::validate`.
pub fn AlgorithmTrace::lint(self : AlgorithmTrace) -> Array[TraceLintIssue] {
  let issues : Array[TraceLintIssue] = []
  if self.title.trim() == "" {
    issues.push(
      TraceLintIssue::new(
        severity=Error,
        code="empty-title",
        message="Trace title is empty.",
      ),
    )
  }
  if self.algorithm.trim() == "" {
    issues.push(
      TraceLintIssue::new(
        severity=Error,
        code="empty-algorithm",
        message="Trace algorithm name is empty.",
      ),
    )
  }
  if self.initial_scene.objects.is_empty() {
    issues.push(
      TraceLintIssue::new(
        severity=Warning,
        code="empty-initial-scene",
        message="Initial scene has no objects to render.",
      ),
    )
  }
  if self.steps.is_empty() {
    issues.push(
      TraceLintIssue::new(
        severity=Warning,
        code="empty-timeline",
        message="Trace has no recorded steps.",
      ),
    )
  } else if !(self.steps.last().unwrap().event is Complete) {
    issues.push(
      TraceLintIssue::new(
        severity=Warning,
        code="missing-complete",
        message="The last recorded step is not a Complete event.",
        step=self.steps.last().unwrap().index,
      ),
    )
  }
  if self.summary.is_empty() {
    issues.push(
      TraceLintIssue::new(
        severity=Info,
        code="empty-summary",
        message="Trace has no summary attributes for CLI or report output.",
      ),
    )
  }
  for step in self.steps {
    lint_step_shape(self, step, issues)
  }
  issues
}

///|
pub fn AlgorithmTrace::lint_report(self : AlgorithmTrace) -> String {
  let issues = self.lint()
  let out = StringBuilder()
  if issues.is_empty() {
    out.write_string("No trace lint issues.\n")
  } else {
    for issue in issues {
      out.write_string(issue.line())
      out.write_string("\n")
    }
  }
  out.to_string()
}

///|
pub fn AlgorithmTrace::lint_markdown(self : AlgorithmTrace) -> String {
  let out = StringBuilder()
  out.write_string("| Severity | Code | Location | Message |\n")
  out.write_string("|---|---|---|---|\n")
  let issues = self.lint()
  if issues.is_empty() {
    out.write_string("| info | clean | trace | No trace lint issues. |\n")
  } else {
    for issue in issues {
      out.write_string(issue.markdown_row())
      out.write_string("\n")
    }
  }
  out.to_string()
}

///|
pub fn AlgorithmTrace::lint_count(
  self : AlgorithmTrace,
  severity : TraceLintSeverity,
) -> Int {
  self
  .lint()
  .fold(init=0, fn(total, issue) {
    let increment = if issue.severity == severity { 1 } else { 0 }
    total + increment
  })
}

///|
pub fn AlgorithmTrace::has_lint_errors(self : AlgorithmTrace) -> Bool {
  self.lint().any(fn(issue) { issue.is_error() })
}

///|
pub fn AlgorithmTrace::has_lint_warnings(self : AlgorithmTrace) -> Bool {
  self.lint().any(fn(issue) { issue.is_warning() })
}

///|
pub fn AlgorithmTrace::timeline_report(self : AlgorithmTrace) -> String {
  let out = StringBuilder()
  for entry in self.timeline() {
    out.write_string(entry.label())
    out.write_string(
      " | targets=\{entry.target_count} objects=\{entry.object_count} entities=\{entry.entity_count} highlights=\{entry.highlight_count}\n",
    )
  }
  out.to_string()
}

///|
pub fn AlgorithmTrace::timeline_table(self : AlgorithmTrace) -> String {
  let out = StringBuilder()
  out.write_string(
    "| Step | Event | Targets | Objects | Entities | Highlights | Annotation | Complete |\n",
  )
  out.write_string("|---|---|---:|---:|---:|---:|---|---|\n")
  for entry in self.timeline() {
    out.write_string(entry.markdown_row())
    out.write_string("\n")
  }
  out.to_string()
}

///|
fn lint_step_shape(
  trace : AlgorithmTrace,
  step : AlgorithmTraceStep,
  issues : Array[TraceLintIssue],
) -> Unit {
  if step.scene.objects.is_empty() {
    issues.push(
      TraceLintIssue::new(
        severity=Warning,
        code="empty-step-scene",
        message="Step scene has no objects to render.",
        step=step.index,
      ),
    )
  }
  if timeline_event_target_count(step.event) == 0 &&
    !(step.event is Initialize ||
    step.event is Complete ||
    step.event is Custom(_, _)) {
    issues.push(
      TraceLintIssue::new(
        severity=Warning,
        code="event-without-target",
        message="Semantic event has no target references.",
        step=step.index,
      ),
    )
  }
  if step.annotation is None && !(step.event is Complete) {
    issues.push(
      TraceLintIssue::new(
        severity=Info,
        code="missing-annotation",
        message="Step has no teaching annotation.",
        step=step.index,
      ),
    )
  }
  if step.scene.entity_count() > trace.initial_scene.entity_count() * 4 &&
    trace.initial_scene.entity_count() > 0 {
    issues.push(
      TraceLintIssue::new(
        severity=Info,
        code="large-scene-growth",
        message="Step scene has grown more than four times beyond the initial scene.",
        step=step.index,
      ),
    )
  }
}

///|
fn timeline_event_name(event : TraceEvent) -> String {
  match event {
    Initialize => "initialize"
    Compare(_) => "compare"
    Swap(_, _) => "swap"
    Visit(_) => "visit"
    Update(_, _) => "update"
    Union(_, _) => "union"
    Relax(_, _, _) => "relax"
    Complete => "complete"
    Custom(kind, _) => kind
  }
}

///|
fn timeline_event_target_count(event : TraceEvent) -> Int {
  match event {
    Compare(targets) => targets.length()
    Swap(_, _) => 2
    Visit(_) => 1
    Update(_, _) => 1
    Union(_, _) => 2
    Relax(_, _, _) => 2
    _ => 0
  }
}

///|
fn lint_severity_name(severity : TraceLintSeverity) -> String {
  match severity {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
fn quality_step_label(step : Int) -> String {
  if step < 0 {
    "initial"
  } else {
    step.to_string()
  }
}

///|
fn escape_quality_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new="
") }