///|
pub enum DiagnosticSeverity {
  Note
  Warning
  Error
} derive(Debug, Eq)

///|
pub struct SourceLocation {
  line : Int
  column : Int
  offset : Int
} derive(Debug, Eq)

///|
pub fn SourceLocation::new(
  line : Int,
  column : Int,
  offset : Int,
) -> SourceLocation {
  { line, column, offset }
}

///|
pub struct Diagnostic {
  severity : DiagnosticSeverity
  path : String
  message : String
  location : SourceLocation?
} derive(Debug, Eq)

///|
pub struct DiagnosticBag {
  items : Array[Diagnostic]
} derive(Debug, Eq)

///|
pub fn Diagnostic::note(path : String, message : String) -> Diagnostic {
  { severity: Note, path, message, location: None }
}

///|
pub fn Diagnostic::warning(
  path : String,
  message : String,
  location : SourceLocation?,
) -> Diagnostic {
  { severity: Warning, path, message, location }
}

///|
pub fn Diagnostic::error(
  path : String,
  message : String,
  location : SourceLocation?,
) -> Diagnostic {
  { severity: Error, path, message, location }
}

///|
pub fn DiagnosticSeverity::label(self : DiagnosticSeverity) -> String {
  match self {
    Note => "note"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn DiagnosticBag::empty() -> DiagnosticBag {
  { items: [] }
}

///|
pub fn DiagnosticBag::push(
  self : DiagnosticBag,
  diagnostic : Diagnostic,
) -> DiagnosticBag {
  let items : Array[Diagnostic] = []
  for item in self.items {
    items.push(item)
  }
  items.push(diagnostic)
  { items, }
}

///|
pub fn DiagnosticBag::merge(
  self : DiagnosticBag,
  other : DiagnosticBag,
) -> DiagnosticBag {
  let result = self
  let mut merged = result
  for item in other.items {
    merged = merged.push(item)
  }
  merged
}

///|
pub fn DiagnosticBag::has_errors(self : DiagnosticBag) -> Bool {
  self.items.any(fn(item) { item.severity == Error })
}

///|
pub fn DiagnosticBag::sorted(self : DiagnosticBag) -> DiagnosticBag {
  let result : Array[Diagnostic] = []
  for item in self.items {
    let mut position = result.length()
    for i, existing in result {
      if compare_diagnostics(item, existing) < 0 {
        position = i
        break
      }
    }
    result.push(item)
    let last = result.length() - 1
    let mut j = last
    while j > position {
      let previous = result[j - 1]
      result[j] = previous
      j -= 1
    }
    result[position] = item
  }
  { items: result }
}

///|
pub fn DiagnosticBag::summary(self : DiagnosticBag) -> String {
  let mut notes = 0
  let mut warnings = 0
  let mut errors = 0
  for item in self.items {
    match item.severity {
      Note => notes += 1
      Warning => warnings += 1
      Error => errors += 1
    }
  }
  "\{errors} errors, \{warnings} warnings, \{notes} notes"
}

///|
fn compare_diagnostics(left : Diagnostic, right : Diagnostic) -> Int {
  match (left.location, right.location) {
    (None, None) => compare_stable_names(left.path, right.path)
    (None, Some(_)) => -1
    (Some(_), None) => 1
    (Some(a), Some(b)) =>
      if a.line != b.line {
        a.line - b.line
      } else if a.column != b.column {
        a.column - b.column
      } else if a.offset != b.offset {
        a.offset - b.offset
      } else {
        compare_stable_names(left.path, right.path)
      }
  }
}