///|
fn validate_lint_policy(policy : RomanLintPolicy) -> Result[Unit, ConfigError] {
  if policy.max_candidate_length < 1 {
    Err(InvalidCandidateLength(policy.max_candidate_length))
  } else {
    Ok(())
  }
}

///|
fn has_ascii_lowercase_roman(input : String) -> Bool {
  for character in input.to_array() {
    if is_lowercase_roman_letter(character) {
      return true
    }
  }
  false
}

///|
fn has_ascii_uppercase_roman(input : String) -> Bool {
  for character in input.to_array() {
    if is_ascii_roman_letter(character) && !is_lowercase_roman_letter(character) {
      return true
    }
  }
  false
}

///|
fn case_style_diagnostic(
  input : String,
  requirement : RomanCaseRequirement,
) -> RomanDiagnostic? {
  let code_and_replacement : (DiagnosticCode, String)? = match requirement {
    AnyRomanCase => None
    RequireRomanUppercase =>
      if has_ascii_lowercase_roman(input) {
        Some((UppercaseRequiredCode, input.to_upper()))
      } else {
        None
      }
    RequireRomanLowercase =>
      if has_ascii_uppercase_roman(input) {
        Some((LowercaseRequiredCode, input.to_lower()))
      } else {
        None
      }
  }
  match code_and_replacement {
    None => None
    Some(item) =>
      Some({
        code: item.0,
        severity: DiagnosticWarning,
        message: diagnostic_message(item.0),
        span: { start: 0, end: input.to_array().length() },
        replacement: Some(item.1),
      })
  }
}

///|
fn unicode_style_diagnostic(input : String) -> RomanDiagnostic? {
  if !contains_supported_unicode_roman(input) {
    return None
  }
  let chars = input.to_array()
  let mut start = 0
  while start < chars.length() &&
        !(chars[start] >= '\u{2160}' && chars[start] <= '\u{217f}') {
    start = start + 1
  }
  let mut end = chars.length()
  while end > start &&
        !(chars[end - 1] >= '\u{2160}' && chars[end - 1] <= '\u{217f}') {
    end = end - 1
  }
  Some({
    code: UnicodeSourceNotAllowedCode,
    severity: DiagnosticWarning,
    message: diagnostic_message(UnicodeSourceNotAllowedCode),
    span: { start, end },
    replacement: None,
  })
}

///|
fn whitespace_style_diagnostic(input : String) -> RomanDiagnostic? {
  if !has_outer_whitespace(input) {
    return None
  }
  Some({
    code: OuterWhitespaceNotAllowedCode,
    severity: DiagnosticWarning,
    message: diagnostic_message(OuterWhitespaceNotAllowedCode),
    span: { start: 0, end: input.to_array().length() },
    replacement: None,
  })
}

///|
fn append_optional_diagnostic(
  diagnostics : Array[RomanDiagnostic],
  diagnostic : RomanDiagnostic?,
) -> Unit {
  match diagnostic {
    Some(value) => diagnostics.push(value)
    None => ()
  }
}

///|
fn lint_style_diagnostics(
  input : String,
  policy : RomanLintPolicy,
) -> Array[RomanDiagnostic] {
  let diagnostics : Array[RomanDiagnostic] = []
  if !policy.allow_unicode {
    append_optional_diagnostic(diagnostics, unicode_style_diagnostic(input))
  }
  if !policy.allow_outer_whitespace {
    append_optional_diagnostic(diagnostics, whitespace_style_diagnostic(input))
  }
  append_optional_diagnostic(
    diagnostics,
    case_style_diagnostic(input, policy.case_requirement),
  )
  diagnostics
}

///|
fn lint_analysis_diagnostics(
  analysis : RomanAnalysisReport,
  require_canonical : Bool,
) -> Array[RomanDiagnostic] {
  let diagnostics : Array[RomanDiagnostic] = []
  for diagnostic in analysis.diagnostics {
    if diagnostic.code == NonCanonicalCode && !require_canonical {
      diagnostics.push({ ..diagnostic, severity: DiagnosticWarning })
    } else {
      diagnostics.push(diagnostic)
    }
  }
  diagnostics
}

///|
fn count_diagnostic_severity(
  diagnostics : Array[RomanDiagnostic],
) -> (Int, Int) {
  let mut errors = 0
  let mut warnings = 0
  for diagnostic in diagnostics {
    match diagnostic.severity {
      DiagnosticError => errors = errors + 1
      DiagnosticWarning => warnings = warnings + 1
    }
  }
  (errors, warnings)
}

///|
fn lint_output_case(requirement : RomanCaseRequirement) -> LetterCase {
  match requirement {
    RequireRomanLowercase => Lowercase
    AnyRomanCase | RequireRomanUppercase => Uppercase
  }
}

///|
fn lint_replacement(
  input : String,
  analysis : RomanAnalysisReport,
  policy : RomanLintPolicy,
  has_diagnostics : Bool,
) -> String? {
  if !has_diagnostics {
    return None
  }
  let base : String? = match analysis.status {
    AnalysisAccepted(report) =>
      match
        format_with_config(report.value, {
          mode: policy.parse_config.mode,
          letter_case: lint_output_case(policy.case_requirement),
        }) {
        Ok(text) => Some(text)
        Err(_) => None
      }
    AnalysisRejected(NonCanonicalRoman(expected)) => Some(expected)
    _ => None
  }
  match base {
    None => None
    Some(text) => {
      let target = match policy.case_requirement {
        RequireRomanLowercase => text.to_lower()
        RequireRomanUppercase => text.to_upper()
        AnyRomanCase => text
      }
      if target == input {
        None
      } else {
        Some(target)
      }
    }
  }
}

///|
/// Lint one configured Roman input and retain parser and style diagnostics.
pub fn lint_roman(
  input : String,
  policy : RomanLintPolicy,
) -> Result[RomanLintReport, ConfigError] {
  match validate_lint_policy(policy) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let analysis = analyze_roman(input, policy.parse_config)
  let style_diagnostics = lint_style_diagnostics(input, policy)
  let diagnostics = lint_analysis_diagnostics(
    analysis,
    policy.require_canonical,
  )
  for diagnostic in style_diagnostics {
    diagnostics.push(diagnostic)
  }
  let counts = count_diagnostic_severity(diagnostics)
  let replacement = lint_replacement(
    input,
    analysis,
    policy,
    diagnostics.length() > 0,
  )
  Ok({
    original: input,
    policy,
    analysis,
    style_diagnostics,
    diagnostics,
    replacement,
    error_count: counts.0,
    warning_count: counts.1,
    conforms: diagnostics.length() == 0,
  })
}