///|
fn contains_supported_unicode_roman(input : String) -> Bool {
  for character in input.to_array() {
    if character >= '\u{2160}' && character <= '\u{217f}' {
      return true
    }
  }
  false
}

///|
fn has_outer_whitespace(input : String) -> Bool {
  let chars = input.to_array()
  if chars.length() == 0 {
    return false
  }
  is_outer_whitespace(chars[0]) ||
  is_outer_whitespace(chars[chars.length() - 1])
}

///|
fn analysis_content_span(input : String, config : ParseConfig) -> SourceSpan {
  let chars = input.to_array()
  if chars.length() == 0 {
    return { start: 0, end: 0 }
  }
  let bounds = normalized_content_bounds(chars, config.trim_outer_whitespace)
  if bounds.0 >= bounds.1 {
    { start: 0, end: chars.length() }
  } else {
    { start: bounds.0, end: bounds.1 }
  }
}

///|
fn diagnostic_message(code : DiagnosticCode) -> String {
  match code {
    EmptyInputCode => "Roman numeral input is empty"
    InvalidConfigurationCode => "Roman processing configuration is invalid"
    UppercaseRequiredCode => "Roman numeral source must use uppercase letters"
    LowercaseRequiredCode => "Roman numeral source must use lowercase letters"
    UnicodeSourceNotAllowedCode =>
      "Unicode Roman compatibility source characters are disabled"
    OuterWhitespaceNotAllowedCode =>
      "outer whitespace is disabled by lint policy"
    SingleSymbolExcludedCode => "single-symbol Roman candidates are disabled"
    CandidateTooLongCode =>
      "Roman candidate exceeds the configured length limit"
    UnsupportedCharacterCode => "input contains an unsupported Roman character"
    LowercaseNotAllowedCode => "lowercase Roman letters are disabled"
    InvalidRepetitionCode => "Roman symbol repetition is invalid"
    InvalidSubtractionCode => "Roman subtractive pair is invalid"
    InvalidOrderCode => "Roman symbols are in an invalid order"
    MalformedParenthesesCode => "parenthesized-thousands structure is malformed"
    NestedParenthesesCode => "nested Roman parentheses are not supported"
    EmptyParenthesizedGroupCode => "parenthesized-thousands group is empty"
    NonCanonicalCode =>
      "Roman numeral spelling is not canonical for the selected profile"
    ValueOutOfRangeCode =>
      "Roman numeral value is outside the selected profile range"
  }
}

///|
fn diagnostic_from_report_error(
  original : String,
  content_span : SourceSpan,
  error : RomanReportError,
) -> RomanDiagnostic {
  let mapped : (DiagnosticCode, SourceSpan, String?) = match error {
    EmptyRomanInput =>
      (EmptyInputCode, { start: 0, end: original.to_array().length() }, None)
    UnsupportedRomanCharacter(error_span, _) =>
      (UnsupportedCharacterCode, error_span, None)
    LowercaseRomanNotAllowed(error_span, character) =>
      (
        LowercaseNotAllowedCode,
        error_span,
        Some(String::from_array([character.to_ascii_uppercase()])),
      )
    InvalidRomanGrammar(error_span, grammar_code) =>
      (grammar_code, error_span, None)
    NonCanonicalRoman(expected) =>
      (NonCanonicalCode, content_span, Some(expected))
    RomanReportOutOfRange(_) => (ValueOutOfRangeCode, content_span, None)
    InvalidRomanConfiguration(_) =>
      (
        InvalidConfigurationCode,
        { start: 0, end: original.to_array().length() },
        None,
      )
  }
  {
    code: mapped.0,
    severity: DiagnosticError,
    message: diagnostic_message(mapped.0),
    span: mapped.1,
    replacement: mapped.2,
  }
}

///|
/// Analyze configured input and retain diagnostics for rejected values.
pub fn analyze_roman(
  input : String,
  config : ParseConfig,
) -> RomanAnalysisReport {
  let observed_unicode_compatibility = contains_supported_unicode_roman(input)
  let observed_outer_whitespace = has_outer_whitespace(input)
  match parse_with_config(input, config) {
    Ok(report) =>
      {
        original: input,
        config,
        status: AnalysisAccepted(report),
        diagnostics: [],
        replacement: None,
        observed_unicode_compatibility,
        observed_outer_whitespace,
      }
    Err(error) => {
      let diagnostic = diagnostic_from_report_error(
        input,
        analysis_content_span(input, config),
        error,
      )
      {
        original: input,
        config,
        status: AnalysisRejected(error),
        diagnostics: [diagnostic],
        replacement: diagnostic.replacement,
        observed_unicode_compatibility,
        observed_outer_whitespace,
      }
    }
  }
}