///|
pub fn Diagnostic::with_rule(self : Diagnostic, rule : String) -> Diagnostic {
  { ..self, rule, }
}

///|
pub fn Diagnostic::with_notes(
  self : Diagnostic,
  notes : Array[String],
) -> Diagnostic {
  { ..self, notes, }
}

///|
pub fn Diagnostic::location_key(self : Diagnostic) -> String {
  self.file + ":" + self.span.start.to_string() + ":" + self.code
}

///|
pub fn Diagnostic::is_at_least(self : Diagnostic, minimum : Severity) -> Bool {
  match (minimum, self.severity) {
    (Error, Error) => true
    (Warning, Error | Warning) => true
    (Info, Error | Warning | Info) => true
    _ => false
  }
}

///|
pub fn stable_diagnostic_order(
  diagnostics : Array[Diagnostic],
) -> Array[Diagnostic] {
  let result : Array[Diagnostic] = []
  for diagnostic in diagnostics {
    let mut inserted = false
    let key = diagnostic.location_key()
    for index, existing in result {
      if !inserted && key < existing.location_key() {
        result.insert(index, diagnostic)
        inserted = true
      }
    }
    if !inserted {
      result.push(diagnostic)
    }
  }
  result
}

///|
pub fn group_diagnostics_by_code(
  diagnostics : Array[Diagnostic],
) -> Array[(String, Int)] {
  let result : Array[(String, Int)] = []
  for diagnostic in diagnostics {
    let mut found = false
    for index, entry in result {
      let (code, count) = entry
      if code == diagnostic.code {
        result[index] = (code, count + 1)
        found = true
      }
    }
    if !found {
      result.push((diagnostic.code, 1))
    }
  }
  result
}

///|
pub fn diagnostics_for_file(
  diagnostics : Array[Diagnostic],
  file : String,
) -> Array[Diagnostic] {
  diagnostics.filter(fn(item) { item.file == file })
}

///|
pub fn diagnostics_have_code(
  diagnostics : Array[Diagnostic],
  code : String,
) -> Bool {
  diagnostics.any(fn(item) { item.code == code })
}

///|
pub fn diagnostic_summary(diagnostics : Array[Diagnostic]) -> String {
  let (errors, warnings, infos) = severity_counts(diagnostics)
  "errors=" +
  errors.to_string() +
  ", warnings=" +
  warnings.to_string() +
  ", infos=" +
  infos.to_string() +
  ", total=" +
  diagnostics.length().to_string()
}

///|
pub fn render_diagnostic_table(diagnostics : Array[Diagnostic]) -> String {
  let builder = StringBuilder()
  builder.write_string("CODE\tSEVERITY\tFILE\tSTART\tMESSAGE\n")
  for diagnostic in stable_diagnostic_order(diagnostics) {
    builder.write_string(diagnostic.code)
    builder.write_char('\t')
    builder.write_string(diagnostic.severity.label())
    builder.write_char('\t')
    builder.write_string(diagnostic.file)
    builder.write_char('\t')
    builder.write_object(diagnostic.span.start)
    builder.write_char('\t')
    builder.write_string(diagnostic.message)
    builder.write_char('\n')
  }
  builder.to_string()
}