///|
fn normalize_input(input : String) -> Result[Array[Char], ParseError] {
  if input.is_empty() {
    return Err(EmptyInput)
  }
  let normalized : Array[Char] = []
  let chars = input.to_array()
  for index = 0; index < chars.length(); index = index + 1 {
    let original = chars[index]
    let upper = original.to_ascii_uppercase()
    match symbol_value(upper) {
      Some(_) => normalized.push(upper)
      None => return Err(InvalidCharacter(index, original))
    }
  }
  Ok(normalized)
}

///|
fn validate_repetitions(chars : Array[Char]) -> Result[Unit, ParseError] {
  let mut previous = '\u{0}'
  let mut run_length = 0
  for index = 0; index < chars.length(); index = index + 1 {
    let symbol = chars[index]
    if symbol == previous {
      run_length = run_length + 1
    } else {
      previous = symbol
      run_length = 1
    }
    let limit = if is_repeatable_symbol(symbol) { 3 } else { 1 }
    if run_length > limit {
      return Err(InvalidRepetition(index, symbol))
    }
  }
  Ok(())
}

///|
/// Parse an uppercase or lowercase Roman numeral.
pub fn parse(input : String) -> Result[Int, ParseError] {
  let chars = match normalize_input(input) {
    Ok(chars) => chars
    Err(error) => return Err(error)
  }
  match validate_repetitions(chars) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let mut value = 0
  let mut index = 0
  let mut previous_token = 4000
  while index < chars.length() {
    let current = match symbol_value(chars[index]) {
      Some(current) => current
      None => return Err(InvalidCharacter(index, chars[index]))
    }
    if index + 1 < chars.length() {
      let next = match symbol_value(chars[index + 1]) {
        Some(next) => next
        None => return Err(InvalidCharacter(index + 1, chars[index + 1]))
      }
      if current < next {
        if !is_subtractive_pair(chars[index], chars[index + 1]) {
          return Err(
            InvalidSubtractivePair(index, chars[index], chars[index + 1]),
          )
        }
        let token_value = next - current
        if token_value > previous_token {
          return Err(InvalidOrder(index))
        }
        value = value + token_value
        previous_token = token_value
        index = index + 2
        continue
      }
    }
    if current > previous_token {
      return Err(InvalidOrder(index))
    }
    value = value + current
    previous_token = current
    index = index + 1
  }
  match format(value) {
    Ok(expected) =>
      if String::from_array(chars) == expected {
        Ok(value)
      } else {
        Err(NonCanonical(expected))
      }
    Err(_) => Err(ParseOutOfRange(value))
  }
}