///|
/// A stable aggregate for a batch of diagnostics.
pub(all) struct DiagnosticReport {
  diagnostics : Array[Diagnostic]
  errors : Int
  warnings : Int
  infos : Int
} derive(Debug)

///|
pub fn DiagnosticReport::new(
  diagnostics : Array[Diagnostic],
) -> DiagnosticReport {
  let mut errors = 0
  let mut warnings = 0
  let mut infos = 0
  for diagnostic in diagnostics {
    match diagnostic.severity {
      Error => errors += 1
      Warning => warnings += 1
      Info => infos += 1
    }
  }
  { diagnostics, errors, warnings, infos }
}

///|
pub fn DiagnosticReport::has_errors(self : DiagnosticReport) -> Bool {
  self.errors > 0
}

///|
pub fn DiagnosticReport::total(self : DiagnosticReport) -> Int {
  self.diagnostics.length()
}

///|
pub fn DiagnosticReport::for_code(
  self : DiagnosticReport,
  code : String,
) -> Array[Diagnostic] {
  let result : Array[Diagnostic] = []
  for diagnostic in self.diagnostics {
    if diagnostic.code == code {
      result.push(diagnostic)
    }
  }
  result
}

///|
pub fn DiagnosticReport::codes(self : DiagnosticReport) -> Array[String] {
  let result : Array[String] = []
  for diagnostic in self.diagnostics {
    if !result.contains(diagnostic.code) {
      result.push(diagnostic.code)
    }
  }
  result
}

///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
  "errors=" +
  self.errors.to_string() +
  ", warnings=" +
  self.warnings.to_string() +
  ", infos=" +
  self.infos.to_string() +
  ", total=" +
  self.total().to_string()
}

///|
/// Add a note while preserving the original diagnostic value.
pub fn Diagnostic::with_note(self : Diagnostic, note : String) -> Diagnostic {
  let notes = self.notes.copy()
  notes.push(note)
  { ..self, notes, }
}

///|
fn sarif_level(severity : Severity) -> String {
  match severity {
    Error => "error"
    Warning => "warning"
    Info => "note"
  }
}

///|
/// Render diagnostics in the SARIF 2.1.0 shape used by code scanning tools.
pub fn render_sarif(
  diagnostics : Array[Diagnostic],
  source : @source.SourceFile,
) -> String {
  let builder = StringBuilder()
  builder.write_string(
    "{\"version\":\"2.1.0\",\"$schema\":\"https://json.schemastore.org/sarif-2.1.0.json\",\"runs\":[{\"tool\":{\"driver\":{\"name\":\"moonbit-rtl\"}},\"results\":[",
  )
  for index, diagnostic in diagnostics {
    if index > 0 {
      builder.write_char(',')
    }
    let (line, column) = diagnostic.at_line_col(source)
    builder.write_string("{\"ruleId\":\"")
    builder.write_string(structured_escape(diagnostic.code))
    builder.write_string("\",\"level\":\"")
    builder.write_string(sarif_level(diagnostic.severity))
    builder.write_string("\",\"message\":{\"text\":\"")
    builder.write_string(structured_escape(diagnostic.message))
    builder.write_string(
      "\"},\"locations\":[{\"physicalLocation\":{\"artifactLocation\":{\"uri\":\"",
    )
    builder.write_string(
      structured_escape(
        if diagnostic.file.is_empty() {
          source.name()
        } else {
          diagnostic.file
        },
      ),
    )
    builder.write_string("\"},\"region\":{\"startLine\":")
    builder.write_object(line + 1)
    builder.write_string(",\"startColumn\":")
    builder.write_object(column + 1)
    builder.write_string("}}}]}")
  }
  builder.write_string("]}]}\n")
  builder.to_string()
}