///|
fn lint_issue(
  severity : LintSeverity,
  code : String,
  key : String,
  message : String,
  argument? : String,
  selector? : String,
) -> LintIssue {
  { severity, code, key, message, argument, selector }
}

///|
pub fn LintOptions::default() -> LintOptions {
  {
    report_extra_keys: true,
    report_extra_arguments: true,
    report_extra_selectors: true,
    require_locale_categories: true,
  }
}

///|
pub fn LintOptions::minimal() -> LintOptions {
  {
    report_extra_keys: false,
    report_extra_arguments: false,
    report_extra_selectors: false,
    require_locale_categories: false,
  }
}

///|
pub fn LintOptions::report_extra_keys(self : LintOptions) -> Bool {
  self.report_extra_keys
}

///|
pub fn LintOptions::report_extra_arguments(self : LintOptions) -> Bool {
  self.report_extra_arguments
}

///|
pub fn LintOptions::report_extra_selectors(self : LintOptions) -> Bool {
  self.report_extra_selectors
}

///|
pub fn LintOptions::require_locale_categories(self : LintOptions) -> Bool {
  self.require_locale_categories
}

///|
fn signature_result(
  template : String,
) -> Result[MessageSignature, MessageDiagnostic] {
  try parse_message(template) catch {
    error => Err(diagnostic_from_error(template, error))
  } noraise {
    message => Ok(message.signature())
  }
}

///|
fn role_name_set(roles : Array[ArgumentRole]) -> Map[String, Unit] {
  let output : Map[String, Unit] = Map([])
  for role in roles {
    output[role.name()] = ()
  }
  output
}

///|
fn missing_role_names(
  expected : Array[ArgumentRole],
  actual : Array[ArgumentRole],
) -> Array[String] {
  let actual_names = role_name_set(actual)
  let missing : Array[String] = []
  for role in expected {
    if !actual_names.contains(role.name()) {
      missing.push(role.name())
    }
  }
  missing.sort()
  missing
}

///|
fn extra_role_names(
  expected : Array[ArgumentRole],
  actual : Array[ArgumentRole],
) -> Array[String] {
  missing_role_names(actual, expected)
}

///|
fn argument_has_numeric_role(argument : ArgumentUse) -> Bool {
  argument.uses_role(CardinalSelector) || argument.uses_role(OrdinalSelector)
}

///|
fn argument_has_text_selector(argument : ArgumentUse) -> Bool {
  argument.uses_role(SelectSelector)
}

///|
fn lint_argument_roles(
  issues : Array[LintIssue],
  key : String,
  reference : ArgumentUse,
  translation : ArgumentUse,
) -> Unit {
  let missing = missing_role_names(reference.roles, translation.roles)
  let extra = extra_role_names(reference.roles, translation.roles)
  if argument_has_numeric_role(reference) !=
    argument_has_numeric_role(translation) {
    issues.push(
      lint_issue(
        Error,
        "argument-type-mismatch",
        key,
        "Argument '\{reference.name}' changes between numeric and text use.",
        argument=reference.name,
      ),
    )
  }
  if missing.length() > 0 {
    issues.push(
      lint_issue(
        Error,
        "missing-argument-role",
        key,
        "Argument '\{reference.name}' is missing roles: \{missing.join(", ")}.",
        argument=reference.name,
      ),
    )
  }
  if extra.length() > 0 {
    issues.push(
      lint_issue(
        Warning,
        "extra-argument-role",
        key,
        "Argument '\{reference.name}' has extra roles: \{extra.join(", ")}.",
        argument=reference.name,
      ),
    )
  }
  if argument_has_numeric_role(translation) &&
    argument_has_text_selector(translation) {
    issues.push(
      lint_issue(
        Error,
        "conflicting-argument-roles",
        key,
        "Argument '\{reference.name}' is used as both numeric and select selector.",
        argument=reference.name,
      ),
    )
  }
}

///|
fn lint_arguments(
  issues : Array[LintIssue],
  key : String,
  reference : MessageSignature,
  translation : MessageSignature,
  options : LintOptions,
) -> Unit {
  for expected in reference.arguments {
    match translation.argument(expected.name) {
      None =>
        issues.push(
          lint_issue(
            Error,
            "missing-argument",
            key,
            "Missing argument: \{expected.name}",
            argument=expected.name,
          ),
        )
      Some(actual) => lint_argument_roles(issues, key, expected, actual)
    }
  }
  if options.report_extra_arguments {
    for actual in translation.arguments {
      if !reference.has_argument(actual.name) {
        issues.push(
          lint_issue(
            Error,
            "extra-argument",
            key,
            "Unexpected argument: \{actual.name}",
            argument=actual.name,
          ),
        )
      }
    }
  }
}

///|
fn choice_occurrence_before(
  choices : Array[ChoiceUse],
  current : ChoiceUse,
) -> Int {
  let mut occurrence = 0
  for candidate in choices {
    if candidate.ordinal >= current.ordinal {
      break
    }
    if candidate.argument == current.argument && candidate.kind == current.kind {
      occurrence += 1
    }
  }
  occurrence
}

///|
fn nth_choice(
  signature : MessageSignature,
  argument : String,
  kind : ChoiceKind,
  occurrence : Int,
) -> ChoiceUse? {
  let mut current = 0
  for choice in signature.choices {
    if choice.argument == argument && choice.kind == kind {
      if current == occurrence {
        return Some(choice)
      }
      current += 1
    }
  }
  None
}

///|
fn choice_with_other_kind(
  signature : MessageSignature,
  argument : String,
  kind : ChoiceKind,
) -> ChoiceUse? {
  for choice in signature.choices {
    if choice.argument == argument && choice.kind != kind {
      return Some(choice)
    }
  }
  None
}

///|
fn selector_required_from_reference(
  kind : ChoiceKind,
  selector : String,
) -> Bool {
  match kind {
    Select => selector != "other"
    Plural | SelectOrdinal => selector.has_prefix("=")
  }
}

///|
fn lint_reference_selectors(
  issues : Array[LintIssue],
  key : String,
  expected : ChoiceUse,
  actual : ChoiceUse,
  options : LintOptions,
) -> Unit {
  for selector in expected.selectors {
    if selector_required_from_reference(expected.kind, selector) &&
      !actual.has_selector(selector) {
      issues.push(
        lint_issue(
          Error,
          "missing-selector",
          key,
          "Choice '\{expected.argument}' is missing selector '\{selector}'.",
          argument=expected.argument,
          selector~,
        ),
      )
    }
  }
  if options.report_extra_selectors && expected.kind == Select {
    for selector in actual.selectors {
      if selector != "other" && !expected.has_selector(selector) {
        issues.push(
          lint_issue(
            Warning,
            "extra-selector",
            key,
            "Choice '\{expected.argument}' has unexpected selector '\{selector}'.",
            argument=expected.argument,
            selector~,
          ),
        )
      }
    }
  }
  if options.report_extra_selectors && expected.kind != Select {
    for selector in actual.exact_selectors() {
      if !expected.has_selector(selector) {
        issues.push(
          lint_issue(
            Warning,
            "extra-exact-selector",
            key,
            "Choice '\{expected.argument}' adds exact selector '\{selector}'.",
            argument=expected.argument,
            selector~,
          ),
        )
      }
    }
  }
}

///|
fn required_categories_for_choice(
  locale : String,
  kind : ChoiceKind,
) -> Array[String] {
  match kind {
    Select => []
    Plural => required_cardinal_categories(locale)
    SelectOrdinal => required_ordinal_categories(locale)
  }
}

///|
fn lint_locale_categories(
  issues : Array[LintIssue],
  key : String,
  locale : String,
  choice : ChoiceUse,
) -> Unit {
  for category in required_categories_for_choice(locale, choice.kind) {
    if !choice.has_selector(category) {
      issues.push(
        lint_issue(
          Warning,
          "missing-plural-category",
          key,
          "Locale '\{locale}' expects '\{category}' for \{choice.kind.name()} argument '\{choice.argument}'.",
          argument=choice.argument,
          selector=category,
        ),
      )
    }
  }
}

///|
fn lint_choices(
  issues : Array[LintIssue],
  key : String,
  locale : String,
  reference : MessageSignature,
  translation : MessageSignature,
  options : LintOptions,
) -> Unit {
  for expected in reference.choices {
    let occurrence = choice_occurrence_before(reference.choices, expected)
    match
      nth_choice(translation, expected.argument, expected.kind, occurrence) {
      Some(actual) =>
        lint_reference_selectors(issues, key, expected, actual, options)
      None =>
        match
          choice_with_other_kind(translation, expected.argument, expected.kind) {
          Some(actual) =>
            issues.push(
              lint_issue(
                Error,
                "choice-kind-mismatch",
                key,
                "Argument '\{expected.argument}' changed from \{expected.kind.name()} to \{actual.kind.name()}.",
                argument=expected.argument,
              ),
            )
          None =>
            issues.push(
              lint_issue(
                Error,
                "missing-choice",
                key,
                "Missing \{expected.kind.name()} choice for argument '\{expected.argument}'.",
                argument=expected.argument,
              ),
            )
        }
    }
  }
  if options.require_locale_categories {
    for choice in translation.choices {
      lint_locale_categories(issues, key, locale, choice)
    }
  }
}

///|
fn lint_valid_templates(
  issues : Array[LintIssue],
  key : String,
  locale : String,
  reference : MessageSignature,
  translation : MessageSignature,
  options : LintOptions,
) -> Unit {
  lint_arguments(issues, key, reference, translation, options)
  lint_choices(issues, key, locale, reference, translation, options)
}

///|
fn lint_entry(
  issues : Array[LintIssue],
  key : String,
  reference_template : String,
  translated_template : String,
  locale : String,
  options : LintOptions,
) -> Unit {
  match
    (
      signature_result(reference_template),
      signature_result(translated_template),
    ) {
    (Err(diagnostic), _) =>
      issues.push(
        lint_issue(
          Error,
          "invalid-reference-template",
          key,
          diagnostic.display(),
        ),
      )
    (_, Err(diagnostic)) =>
      issues.push(
        lint_issue(Error, "invalid-template", key, diagnostic.display()),
      )
    (Ok(reference), Ok(translation)) =>
      lint_valid_templates(issues, key, locale, reference, translation, options)
  }
}

///|
/// Compare a translation against the reference catalog with explicit policy.
pub fn lint_catalogs_with_options(
  reference : Catalog,
  translation : Catalog,
  options : LintOptions,
) -> LintReport {
  let issues : Array[LintIssue] = []
  for key in reference.keys() {
    let reference_template = reference.entries[key]
    match translation.entries.get(key) {
      None =>
        issues.push(
          lint_issue(Error, "missing-key", key, "Missing translation."),
        )
      Some(translated_template) =>
        lint_entry(
          issues,
          key,
          reference_template,
          translated_template,
          translation.locale,
          options,
        )
    }
  }
  if options.report_extra_keys {
    for key in translation.keys() {
      if !reference.contains(key) {
        issues.push(
          lint_issue(
            Warning,
            "extra-key",
            key,
            "No reference message for this key.",
          ),
        )
      }
    }
  }
  { reference_locale: reference.locale, locale: translation.locale, issues }
}

///|
/// Compare a translation against the reference catalog.
pub fn lint_catalogs(reference : Catalog, translation : Catalog) -> LintReport {
  lint_catalogs_with_options(reference, translation, LintOptions::default())
}

///|
/// Lint multiple translations using one reference catalog.
pub fn lint_catalog_set(
  reference : Catalog,
  translations : Array[Catalog],
) -> Array[LintReport] {
  let reports : Array[LintReport] = []
  for translation in translations {
    reports.push(lint_catalogs(reference, translation))
  }
  reports
}

///|
pub fn LintSeverity::name(self : LintSeverity) -> String {
  match self {
    Error => "error"
    Warning => "warning"
    Information => "information"
  }
}

///|
pub fn LintSeverity::rank(self : LintSeverity) -> Int {
  match self {
    Error => 3
    Warning => 2
    Information => 1
  }
}

///|
pub fn LintIssue::severity(self : LintIssue) -> LintSeverity {
  self.severity
}

///|
pub fn LintIssue::code(self : LintIssue) -> String {
  self.code
}

///|
pub fn LintIssue::key(self : LintIssue) -> String {
  self.key
}

///|
pub fn LintIssue::message(self : LintIssue) -> String {
  self.message
}

///|
pub fn LintIssue::argument(self : LintIssue) -> String? {
  self.argument
}

///|
pub fn LintIssue::selector(self : LintIssue) -> String? {
  self.selector
}

///|
pub fn LintIssue::display(self : LintIssue) -> String {
  "\{self.severity.name()} \{self.code} \{self.key}: \{self.message}"
}

///|
pub fn LintReport::ok(self : LintReport) -> Bool {
  self.issues.length() == 0
}

///|
pub fn LintReport::has_errors(self : LintReport) -> Bool {
  for issue in self.issues {
    if issue.severity == Error {
      return true
    }
  }
  false
}

///|
pub fn LintReport::reference_locale(self : LintReport) -> String {
  self.reference_locale
}

///|
pub fn LintReport::locale(self : LintReport) -> String {
  self.locale
}

///|
pub fn LintReport::issues(self : LintReport) -> Array[LintIssue] {
  self.issues
}

///|
pub fn LintReport::issues_with_severity(
  self : LintReport,
  severity : LintSeverity,
) -> Array[LintIssue] {
  let output : Array[LintIssue] = []
  for issue in self.issues {
    if issue.severity == severity {
      output.push(issue)
    }
  }
  output
}

///|
pub fn LintReport::issues_with_code(
  self : LintReport,
  code : String,
) -> Array[LintIssue] {
  let output : Array[LintIssue] = []
  for issue in self.issues {
    if issue.code == code {
      output.push(issue)
    }
  }
  output
}

///|
pub fn LintReport::contains_code(self : LintReport, code : String) -> Bool {
  for issue in self.issues {
    if issue.code == code {
      return true
    }
  }
  false
}

///|
pub fn LintReport::summary(self : LintReport) -> LintSummary {
  let mut errors = 0
  let mut warnings = 0
  let mut information = 0
  for issue in self.issues {
    match issue.severity {
      Error => errors += 1
      Warning => warnings += 1
      Information => information += 1
    }
  }
  { errors, warnings, information, total: self.issues.length() }
}

///|
pub fn LintSummary::errors(self : LintSummary) -> Int {
  self.errors
}

///|
pub fn LintSummary::warnings(self : LintSummary) -> Int {
  self.warnings
}

///|
pub fn LintSummary::information(self : LintSummary) -> Int {
  self.information
}

///|
pub fn LintSummary::total(self : LintSummary) -> Int {
  self.total
}

///|
pub fn LintSummary::display(self : LintSummary) -> String {
  "\{self.errors} errors, \{self.warnings} warnings, \{self.information} information"
}

///|
pub fn LintReport::exit_code(self : LintReport) -> Int {
  if self.has_errors() {
    2
  } else if self.issues.length() > 0 {
    1
  } else {
    0
  }
}

///|
pub fn LintReport::to_text(self : LintReport) -> String {
  if self.issues.length() == 0 {
    return "OK"
  }
  let output = StringBuilder::new()
  for index, issue in self.issues {
    if index > 0 {
      output.write_char('\n')
    }
    output.write_string(issue.display())
  }
  output.write_char('\n')
  output.write_string(self.summary().display())
  output.to_string()
}

///|
fn write_json_optional_string(
  output : StringBuilder,
  name : String,
  value : String?,
) -> Unit {
  match value {
    Some(text) => {
      output.write_char(',')
      write_json_string(output, name)
      output.write_char(':')
      write_json_string(output, text)
    }
    None => ()
  }
}

///|
fn write_lint_issue_json(output : StringBuilder, issue : LintIssue) -> Unit {
  output.write_char('{')
  write_json_string(output, "severity")
  output.write_char(':')
  write_json_string(output, issue.severity.name())
  output.write_char(',')
  write_json_string(output, "code")
  output.write_char(':')
  write_json_string(output, issue.code)
  output.write_char(',')
  write_json_string(output, "key")
  output.write_char(':')
  write_json_string(output, issue.key)
  output.write_char(',')
  write_json_string(output, "message")
  output.write_char(':')
  write_json_string(output, issue.message)
  write_json_optional_string(output, "argument", issue.argument)
  write_json_optional_string(output, "selector", issue.selector)
  output.write_char('}')
}

///|
pub fn LintReport::to_json(self : LintReport) -> String {
  let summary = self.summary()
  let output = StringBuilder::new()
  output.write_char('{')
  write_json_string(output, "reference_locale")
  output.write_char(':')
  write_json_string(output, self.reference_locale)
  output.write_char(',')
  write_json_string(output, "locale")
  output.write_char(':')
  write_json_string(output, self.locale)
  output.write_char(',')
  write_json_string(output, "ok")
  output.write_char(':')
  output.write_string(if self.ok() { "true" } else { "false" })
  output.write_char(',')
  write_json_string(output, "errors")
  output.write_char(':')
  output.write_string(summary.errors.to_string())
  output.write_char(',')
  write_json_string(output, "warnings")
  output.write_char(':')
  output.write_string(summary.warnings.to_string())
  output.write_char(',')
  write_json_string(output, "issues")
  output.write_string(":[")
  for index, issue in self.issues {
    if index > 0 {
      output.write_char(',')
    }
    write_lint_issue_json(output, issue)
  }
  output.write_string("]}")
  output.to_string()
}