///|
priv struct NormalizedRomanUnit {
  span : SourceSpan
  source_text : String
  normalized_text : String
}

///|
priv struct NormalizedRomanInput {
  original : String
  normalized : String
  units : Array[NormalizedRomanUnit]
  content_span : SourceSpan
  used_unicode_compatibility : Bool
  trimmed_outer_whitespace : Bool
}

///|
fn roman_character_string(character : Char) -> String {
  String::from_array([character])
}

///|
fn normalized_content_bounds(
  chars : Array[Char],
  trim : Bool,
) -> (Int, Int, Bool) {
  if !trim {
    return (0, chars.length(), false)
  }
  let mut start = 0
  while start < chars.length() && is_outer_whitespace(chars[start]) {
    start = start + 1
  }
  let mut end = chars.length()
  while end > start && is_outer_whitespace(chars[end - 1]) {
    end = end - 1
  }
  (start, end, start > 0 || end < chars.length())
}

///|
fn normalize_ascii_roman_unit(
  character : Char,
  index : Int,
  config : ParseConfig,
) -> Result[NormalizedRomanUnit, RomanReportError] {
  let span : SourceSpan = { start: index, end: index + 1 }
  if is_lowercase_roman_letter(character) && !config.accept_lowercase {
    return Err(LowercaseRomanNotAllowed(span, character))
  }
  Ok({
    span,
    source_text: roman_character_string(character),
    normalized_text: roman_character_string(character.to_ascii_uppercase()),
  })
}

///|
fn normalize_report_unit(
  character : Char,
  index : Int,
  config : ParseConfig,
) -> Result[(NormalizedRomanUnit, Bool), RomanReportError] {
  if is_ascii_roman_letter(character) {
    return match normalize_ascii_roman_unit(character, index, config) {
      Ok(unit) => Ok((unit, false))
      Err(error) => Err(error)
    }
  }
  if config.mode == ParenthesizedThousands &&
    (character == '(' || character == ')') {
    let text = roman_character_string(character)
    return Ok(
      (
        {
          span: { start: index, end: index + 1 },
          source_text: text,
          normalized_text: text,
        },
        false,
      ),
    )
  }
  if config.accept_unicode {
    match unicode_roman_expansion(character) {
      Some(expansion) =>
        return Ok(
          (
            {
              span: { start: index, end: index + 1 },
              source_text: roman_character_string(character),
              normalized_text: expansion,
            },
            true,
          ),
        )
      None => ()
    }
  }
  Err(UnsupportedRomanCharacter({ start: index, end: index + 1 }, character))
}

///|
fn normalize_for_report(
  input : String,
  config : ParseConfig,
) -> Result[NormalizedRomanInput, RomanReportError] {
  let chars = input.to_array()
  let bounds = normalized_content_bounds(chars, config.trim_outer_whitespace)
  let start = bounds.0
  let end = bounds.1
  if start >= end {
    return Err(EmptyRomanInput)
  }
  let units : Array[NormalizedRomanUnit] = []
  let mut normalized = ""
  let mut used_unicode = false
  for index = start; index < end; index = index + 1 {
    let result = match normalize_report_unit(chars[index], index, config) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    units.push(result.0)
    normalized = normalized + result.0.normalized_text
    if result.1 {
      used_unicode = true
    }
  }
  Ok({
    original: input,
    normalized,
    units,
    content_span: { start, end },
    used_unicode_compatibility: used_unicode,
    trimmed_outer_whitespace: bounds.2,
  })
}

///|
/// Normalize configured Roman input to deterministic uppercase ASCII text.
pub fn normalize_roman_input(
  input : String,
  config : ParseConfig,
) -> Result[String, RomanReportError] {
  match normalize_for_report(input, config) {
    Ok(value) => Ok(value.normalized)
    Err(error) => Err(error)
  }
}