///|
/// Selects one deterministic search-key representation.
pub(all) enum IndexProfile {
  ExactNormalized
  FullPinyin
  Initials
  Compact
} derive(Eq, Debug)

///|
/// One search-key fragment tied to its original source span.
pub struct IndexSegment {
  value_text : String
  source_span : SourceSpan
  source_kind : TokenKind
} derive(Eq, Debug)

///|
/// A normalized key plus auditable source mapping.
pub struct IndexKey {
  key_text : String
  profile_value : IndexProfile
  segment_values : Array[IndexSegment]
} derive(Eq, Debug)

///|
fn ascii_lower(code : Int) -> Int {
  if code >= 65 && code <= 90 {
    code + 32
  } else {
    code
  }
}

///|
fn is_folded_punctuation(code : Int) -> Bool {
  (code >= 0 && code <= 47) ||
  (code >= 58 && code <= 64) ||
  (code >= 91 && code <= 96) ||
  (code >= 123 && code <= 127) ||
  code == 0x3000 ||
  code == 0x3001 ||
  code == 0x3002 ||
  (code >= 0xFF01 && code <= 0xFF0F) ||
  (code >= 0xFF1A && code <= 0xFF20) ||
  (code >= 0xFF3B && code <= 0xFF40) ||
  (code >= 0xFF5B && code <= 0xFF65)
}

///|
fn normalize_literal(value : String) -> String {
  let output_codes : Array[Int] = []
  for code in text_code_points(value) {
    if !is_folded_punctuation(code) {
      output_codes.push(ascii_lower(code))
    }
  }
  code_points_text(output_codes)
}

///|
fn token_plain_pinyin(token : ConvertedToken) -> String {
  let candidates = token.candidates()
  if candidates.length() > 0 {
    match parse_syllable(candidates[0]) {
      Ok(syllable) => syllable.format(PlainPinyin, Lowercase)
      Err(_) => normalize_literal(token.rendered())
    }
  } else {
    normalize_literal(token.rendered())
  }
}

///|
fn token_initial(token : ConvertedToken) -> String {
  let candidates = token.candidates()
  if candidates.length() > 0 {
    match parse_syllable(candidates[0]) {
      Ok(syllable) => {
        let plain = syllable.format(PlainPinyin, Lowercase)
        if plain.length() == 0 {
          ""
        } else {
          plain[0].unsafe_to_char().to_string()
        }
      }
      Err(_) => normalize_literal(token.rendered())
    }
  } else {
    normalize_literal(token.rendered())
  }
}

///|
fn index_segment_value(
  token : ConvertedToken,
  profile : IndexProfile,
) -> String {
  match profile {
    ExactNormalized => normalize_literal(token.original())
    FullPinyin | Compact =>
      if token.kind() == PinyinToken {
        token_plain_pinyin(token)
      } else {
        normalize_literal(token.original())
      }
    Initials =>
      if token.kind() == PinyinToken {
        token_initial(token)
      } else {
        normalize_literal(token.original())
      }
  }
}

///|
fn needs_key_separator(
  profile : IndexProfile,
  previous : TokenKind?,
  current : TokenKind,
) -> Bool {
  match profile {
    Compact | ExactNormalized => false
    FullPinyin =>
      match previous {
        Some(previous_kind) =>
          previous_kind == PinyinToken || current == PinyinToken
        None => false
      }
    Initials =>
      match previous {
        Some(previous_kind) => previous_kind != current
        None => false
      }
  }
}

///|
/// Generates a deterministic search key from an existing conversion result.
pub fn generate_index_key(
  converted : ConversionResult,
  profile : IndexProfile,
) -> IndexKey {
  let segments : Array[IndexSegment] = []
  let mut key = ""
  let mut previous_kind : TokenKind? = None
  for token in converted.tokens() {
    let value = index_segment_value(token, profile)
    if value.length() > 0 {
      if needs_key_separator(profile, previous_kind, token.kind()) {
        key = key + " "
      }
      key = key + value
      segments.push({
        value_text: value,
        source_span: token.span(),
        source_kind: token.kind(),
      })
      previous_kind = Some(token.kind())
    }
  }
  { key_text: key, profile_value: profile, segment_values: segments }
}

///|
pub fn IndexSegment::value(self : IndexSegment) -> String {
  self.value_text
}

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

///|
pub fn IndexSegment::kind(self : IndexSegment) -> TokenKind {
  self.source_kind
}

///|
pub fn IndexKey::key(self : IndexKey) -> String {
  self.key_text
}

///|
pub fn IndexKey::profile(self : IndexKey) -> IndexProfile {
  self.profile_value
}

///|
pub fn IndexKey::segments(self : IndexKey) -> Array[IndexSegment] {
  self.segment_values.copy()
}