///|
pub(all) enum TokenKind {
  PinyinToken
  LiteralToken
} derive(Eq, Debug)

///|
pub(all) enum DecisionSource {
  PhraseLexicon
  UnicodeReading
  UnknownPreserved
} derive(Eq, Debug)

///|
/// One auditable output token tied to a scalar span in the source.
pub struct ConvertedToken {
  original_value : String
  rendered_value : String
  token_kind : TokenKind
  decision_value : DecisionSource
  source_span : SourceSpan
  candidate_values : Array[String]
} derive(Eq, Debug)

///|
/// Structured conversion result with output and decision evidence.
pub struct ConversionResult {
  source_value : String
  rendered_value : String
  token_values : Array[ConvertedToken]
  diagnostic_values : Array[Diagnostic]
} derive(Eq, Debug)

///|
fn copy_tokens(values : Array[ConvertedToken]) -> Array[ConvertedToken] {
  let output : Array[ConvertedToken] = []
  for value in values {
    output.push(value)
  }
  output
}

///|
fn copy_diagnostics(values : Array[Diagnostic]) -> Array[Diagnostic] {
  let output : Array[Diagnostic] = []
  for value in values {
    output.push(value)
  }
  output
}

///|
fn render_raw_reading(
  raw : String,
  options : ConvertOptions,
) -> Result[String, PinyinError] {
  match parse_syllable(raw) {
    Ok(value) => Ok(value.format(options.output_style, options.output_case))
    Err(_) => Err(InvalidGeneratedData("invalid_unihan_reading:" + raw))
  }
}

///|
fn render_reading_set(
  set : ReadingSet,
  options : ConvertOptions,
) -> Result[String, PinyinError] {
  let readings = set.readings()
  if readings.length() == 0 {
    return Err(UnknownCharacter(set.code_point()))
  }
  if readings.length() > 1 && options.ambiguity_behavior == RejectAmbiguous {
    return Err(AmbiguousCharacter(set.code_point(), readings.length()))
  }
  let limit = if options.ambiguity_behavior == PreserveAllReadings {
    readings.length()
  } else {
    1
  }
  let mut output = ""
  for index = 0; index < limit; index = index + 1 {
    if index > 0 {
      output = output + "/"
    }
    let rendered_reading = match render_raw_reading(readings[index], options) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    output = output + rendered_reading
  }
  Ok(output)
}

///|
fn append_rendered(
  current : String,
  rendered : String,
  kind : TokenKind,
  previous : TokenKind?,
  separator : String,
) -> String {
  if previous == Some(PinyinToken) && kind == PinyinToken {
    current + separator + rendered
  } else {
    current + rendered
  }
}

///|
/// Converts mixed text using phrase overrides before Unicode character lookup.
pub fn convert_text(
  source : String,
  lexicon : Lexicon,
  options : ConvertOptions,
) -> Result[ConversionResult, PinyinError] {
  let codes = text_code_points(source)
  let tokens : Array[ConvertedToken] = []
  let diagnostics : Array[Diagnostic] = []
  let mut rendered = ""
  let mut previous_kind : TokenKind? = None
  let mut cursor = 0
  while cursor < codes.length() {
    match lexicon.longest_match(codes, cursor) {
      Some(found) => {
        let phrase_readings = found.readings()
        for offset = 0; offset < found.length(); offset = offset + 1 {
          let syllable = phrase_readings[offset]
          let output = syllable.format(
            options.output_style,
            options.output_case,
          )
          let span = source_span(cursor + offset, cursor + offset + 1).unwrap()
          let token = {
            original_value: code_points_text([codes[cursor + offset]]),
            rendered_value: output,
            token_kind: PinyinToken,
            decision_value: PhraseLexicon,
            source_span: span,
            candidate_values: [syllable.format(ToneMarks, Lowercase)],
          }
          rendered = append_rendered(
            rendered,
            output,
            PinyinToken,
            previous_kind,
            options.token_separator,
          )
          previous_kind = Some(PinyinToken)
          tokens.push(token)
        }
        cursor = cursor + found.length()
      }
      None => {
        let code = codes[cursor]
        match lookup_readings(code) {
          Some(set) => {
            let candidates = set.readings()
            let output = match render_reading_set(set, options) {
              Err(error) => return Err(error)
              Ok(value) => value
            }
            let span = source_span(cursor, cursor + 1).unwrap()
            if candidates.length() > 1 {
              diagnostics.push(
                diagnostic("ambiguous", "multiple Unicode readings", Some(span)),
              )
            }
            tokens.push({
              original_value: code_points_text([code]),
              rendered_value: output,
              token_kind: PinyinToken,
              decision_value: UnicodeReading,
              source_span: span,
              candidate_values: candidates,
            })
            rendered = append_rendered(
              rendered,
              output,
              PinyinToken,
              previous_kind,
              options.token_separator,
            )
            previous_kind = Some(PinyinToken)
          }
          None =>
            match options.unknown_behavior {
              RejectUnknown => return Err(UnknownCharacter(code))
              DropUnknown =>
                diagnostics.push(
                  diagnostic(
                    "unknown_dropped",
                    "unknown character was dropped",
                    Some(source_span(cursor, cursor + 1).unwrap()),
                  ),
                )
              PreserveUnknown => {
                let original = code_points_text([code])
                let span = source_span(cursor, cursor + 1).unwrap()
                tokens.push({
                  original_value: original,
                  rendered_value: original,
                  token_kind: LiteralToken,
                  decision_value: UnknownPreserved,
                  source_span: span,
                  candidate_values: [],
                })
                rendered = append_rendered(
                  rendered,
                  original,
                  LiteralToken,
                  previous_kind,
                  options.token_separator,
                )
                previous_kind = Some(LiteralToken)
              }
            }
        }
        cursor = cursor + 1
      }
    }
  }
  Ok({
    source_value: source,
    rendered_value: rendered,
    token_values: tokens,
    diagnostic_values: diagnostics,
  })
}

///|
pub fn ConvertedToken::original(self : ConvertedToken) -> String {
  self.original_value
}

///|
pub fn ConvertedToken::rendered(self : ConvertedToken) -> String {
  self.rendered_value
}

///|
pub fn ConvertedToken::kind(self : ConvertedToken) -> TokenKind {
  self.token_kind
}

///|
pub fn ConvertedToken::decision_source(self : ConvertedToken) -> DecisionSource {
  self.decision_value
}

///|
pub fn ConvertedToken::span(self : ConvertedToken) -> SourceSpan {
  self.source_span
}

///|
pub fn ConvertedToken::candidates(self : ConvertedToken) -> Array[String] {
  self.candidate_values.copy()
}

///|
pub fn ConversionResult::source(self : ConversionResult) -> String {
  self.source_value
}

///|
pub fn ConversionResult::rendered(self : ConversionResult) -> String {
  self.rendered_value
}

///|
pub fn ConversionResult::tokens(
  self : ConversionResult,
) -> Array[ConvertedToken] {
  copy_tokens(self.token_values)
}

///|
pub fn ConversionResult::diagnostics(
  self : ConversionResult,
) -> Array[Diagnostic] {
  copy_diagnostics(self.diagnostic_values)
}