///|
fn interpret_normalized_value(text : String) -> Int? {
  let chars = text.to_array()
  if chars.length() == 0 {
    return None
  }
  let mut total = 0
  let mut index = 0
  while index < chars.length() {
    let current = match symbol_value(chars[index]) {
      Some(value) => value
      None => return None
    }
    if index + 1 < chars.length() {
      let next = match symbol_value(chars[index + 1]) {
        Some(value) => value
        None => return None
      }
      if current < next {
        total = total + next - current
        index = index + 2
        continue
      }
    }
    total = total + current
    index = index + 1
  }
  Some(total)
}

///|
fn normalized_index_span(
  input : NormalizedRomanInput,
  normalized_index : Int,
  width : Int,
) -> SourceSpan {
  let mut offset = 0
  let mut start = input.content_span.start
  let mut end = input.content_span.end
  let target_end = normalized_index + width
  let mut found_start = false
  for unit in input.units {
    let unit_width = unit.normalized_text.to_array().length()
    let unit_end = offset + unit_width
    if !found_start && normalized_index < unit_end {
      start = unit.span.start
      found_start = true
    }
    if found_start && target_end <= unit_end {
      end = unit.span.end
      return { start, end }
    }
    offset = unit_end
  }
  { start, end }
}

///|
fn unit_token_role(text : String) -> RomanTokenRole {
  let chars = text.to_array()
  if chars.length() == 2 {
    match (symbol_value(chars[0]), symbol_value(chars[1])) {
      (Some(left), Some(right)) =>
        if left < right {
          SubtractiveToken
        } else {
          SymbolToken
        }
      _ => SymbolToken
    }
  } else {
    SymbolToken
  }
}

///|
fn token_from_unit(unit : NormalizedRomanUnit) -> RomanToken {
  let value = match interpret_normalized_value(unit.normalized_text) {
    Some(number) => number
    None => 0
  }
  {
    span: unit.span,
    source_text: unit.source_text,
    normalized_text: unit.normalized_text,
    value,
    role: unit_token_role(unit.normalized_text),
  }
}

///|
fn build_report_tokens(input : NormalizedRomanInput) -> Array[RomanToken] {
  let tokens : Array[RomanToken] = []
  let mut index = 0
  while index < input.units.length() {
    let current = input.units[index]
    if current.normalized_text == "(" {
      tokens.push({
        span: current.span,
        source_text: current.source_text,
        normalized_text: "(",
        value: 0,
        role: GroupOpenToken,
      })
      index = index + 1
      continue
    }
    if current.normalized_text == ")" {
      tokens.push({
        span: current.span,
        source_text: current.source_text,
        normalized_text: ")",
        value: 0,
        role: GroupCloseToken,
      })
      index = index + 1
      continue
    }
    if current.normalized_text.to_array().length() == 1 &&
      index + 1 < input.units.length() &&
      input.units[index + 1].normalized_text.to_array().length() == 1 {
      let next = input.units[index + 1]
      let left_chars = current.normalized_text.to_array()
      let right_chars = next.normalized_text.to_array()
      if is_subtractive_pair(left_chars[0], right_chars[0]) {
        let left_value = symbol_value(left_chars[0]).unwrap()
        let right_value = symbol_value(right_chars[0]).unwrap()
        tokens.push({
          span: { start: current.span.start, end: next.span.end },
          source_text: current.source_text + next.source_text,
          normalized_text: current.normalized_text + next.normalized_text,
          value: right_value - left_value,
          role: SubtractiveToken,
        })
        index = index + 2
        continue
      }
    }
    tokens.push(token_from_unit(current))
    index = index + 1
  }
  tokens
}

///|
fn parse_error_span(
  input : NormalizedRomanInput,
  index : Int,
  width : Int,
) -> SourceSpan {
  normalized_index_span(input, index, width)
}

///|
fn map_legacy_parse_error(
  input : NormalizedRomanInput,
  error : ParseError,
) -> RomanReportError {
  match error {
    EmptyInput => EmptyRomanInput
    InvalidCharacter(index, character) =>
      UnsupportedRomanCharacter(parse_error_span(input, index, 1), character)
    InvalidRepetition(index, _) =>
      InvalidRomanGrammar(
        parse_error_span(input, index, 1),
        InvalidRepetitionCode,
      )
    InvalidSubtractivePair(index, _, _) =>
      InvalidRomanGrammar(
        parse_error_span(input, index, 2),
        InvalidSubtractionCode,
      )
    InvalidOrder(index) =>
      InvalidRomanGrammar(parse_error_span(input, index, 1), InvalidOrderCode)
    NonCanonical(expected) => NonCanonicalRoman(expected)
    ParseOutOfRange(value) => RomanReportOutOfRange(value)
  }
}

///|
fn parse_profile_candidate(
  input : NormalizedRomanInput,
  config : ParseConfig,
) -> Result[Int, RomanReportError] {
  match config.mode {
    ModernCanonical =>
      match parse(input.normalized) {
        Ok(value) => Ok(value)
        Err(error) => Err(map_legacy_parse_error(input, error))
      }
    AdditiveHistorical | ClockFace =>
      match interpret_normalized_value(input.normalized) {
        None =>
          Err(InvalidRomanGrammar(input.content_span, UnsupportedCharacterCode))
        Some(value) => Ok(value)
      }
    ParenthesizedThousands => parse_extended_candidate(input)
  }
}

///|
fn report_format_config(config : ParseConfig) -> FormatConfig {
  { mode: config.mode, letter_case: Uppercase }
}

///|
fn report_notices(input : NormalizedRomanInput) -> Array[String] {
  let notices : Array[String] = []
  if input.used_unicode_compatibility {
    notices.push("Unicode Roman compatibility characters expanded to ASCII")
  }
  if input.trimmed_outer_whitespace {
    notices.push("outer whitespace removed by configured policy")
  }
  notices
}

///|
/// Parse configured Roman input and return source-located evidence.
pub fn parse_with_config(
  input : String,
  config : ParseConfig,
) -> Result[ParseReport, RomanReportError] {
  let normalized = match normalize_for_report(input, config) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let value = match parse_profile_candidate(normalized, config) {
    Ok(number) => number
    Err(error) => return Err(error)
  }
  let canonical = match
    format_with_config(value, report_format_config(config)) {
    Ok(text) => text
    Err(_) => return Err(RomanReportOutOfRange(value))
  }
  if normalized.normalized != canonical {
    return Err(NonCanonicalRoman(canonical))
  }
  Ok({
    original: normalized.original,
    normalized: normalized.normalized,
    value,
    canonical,
    tokens: build_report_tokens(normalized),
    diagnostics: [],
    notices: report_notices(normalized),
    used_unicode_compatibility: normalized.used_unicode_compatibility,
    trimmed_outer_whitespace: normalized.trimmed_outer_whitespace,
  })
}