///|
/// Precomputed searchable representations for one caller-labelled value.
pub struct MatchCandidate {
  label_text : String
  full_key_value : IndexKey
  initials_key_value : IndexKey
} derive(Eq, Debug)

///|
/// Score components and fuzzy-distance evidence for one candidate.
pub struct MatchExplanation {
  query_text : String
  full_key_text : String
  initials_key_text : String
  exact_value : Double
  prefix_value : Double
  initials_value : Double
  fuzzy_value : Double
  final_value : Double
  distance_value : DistanceResult
} derive(Eq, Debug)

///|
/// One ranked result retaining the caller's original position.
pub struct RankedCandidate {
  candidate_value : MatchCandidate
  explanation_value : MatchExplanation
  caller_position : Int
} derive(Eq, Debug)

///|
fn string_starts_with(value : String, prefix : String) -> Bool {
  if prefix.length() > value.length() {
    return false
  }
  for index = 0; index < prefix.length(); index = index + 1 {
    if value[index] != prefix[index] {
      return false
    }
  }
  true
}

///|
fn compact_query(value : String) -> String {
  normalize_literal(value)
}

///|
fn maximum_int(left : Int, right : Int) -> Int {
  if left > right {
    left
  } else {
    right
  }
}

///|
fn bounded_unit(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
/// Builds search representations once for repeated queries.
pub fn match_candidate(
  label : String,
  converted : ConversionResult,
) -> MatchCandidate {
  {
    label_text: label,
    full_key_value: generate_index_key(converted, Compact),
    initials_key_value: generate_index_key(converted, Initials),
  }
}

///|
/// Scores one candidate and returns independently inspectable components.
pub fn score_match(
  query_input : String,
  candidate : MatchCandidate,
  profile : FuzzyProfile,
) -> MatchExplanation {
  let query = compact_query(query_input)
  let full = compact_query(candidate.full_key_value.key())
  let initials = compact_query(candidate.initials_key_value.key())
  let distance = weighted_pinyin_distance(query, full, profile)
  let exact = if query.length() > 0 && query == full { 0.55 } else { 0.0 }
  let prefix = if query.length() > 0 && string_starts_with(full, query) {
    0.20
  } else {
    0.0
  }
  let initials_score = if query.length() > 0 && query == initials {
    0.20
  } else {
    0.0
  }
  let length_scale = maximum_int(query.length(), full.length())
  let fuzzy_ratio = if length_scale == 0 {
    1.0
  } else {
    bounded_unit(1.0 - distance.cost() / length_scale.to_double())
  }
  let fuzzy_score = 0.25 * fuzzy_ratio
  let final_score = bounded_unit(exact + prefix + initials_score + fuzzy_score)
  {
    query_text: query,
    full_key_text: full,
    initials_key_text: initials,
    exact_value: exact,
    prefix_value: prefix,
    initials_value: initials_score,
    fuzzy_value: fuzzy_score,
    final_value: final_score,
    distance_value: distance,
  }
}

///|
/// Ranks candidates by descending score and preserves caller order for ties.
pub fn rank_matches(
  query : String,
  candidates : Array[MatchCandidate],
  profile : FuzzyProfile,
) -> Array[RankedCandidate] {
  let ranked : Array[RankedCandidate] = []
  for index = 0; index < candidates.length(); index = index + 1 {
    ranked.push({
      candidate_value: candidates[index],
      explanation_value: score_match(query, candidates[index], profile),
      caller_position: index,
    })
  }
  ranked.sort_by((left, right) => {
    let score_order = right.explanation_value.final_value.compare(
      left.explanation_value.final_value,
    )
    if score_order != 0 {
      score_order
    } else {
      left.caller_position - right.caller_position
    }
  })
  ranked
}

///|
pub fn MatchCandidate::label(self : MatchCandidate) -> String {
  self.label_text
}

///|
pub fn MatchCandidate::full_key(self : MatchCandidate) -> IndexKey {
  self.full_key_value
}

///|
pub fn MatchCandidate::initials_key(self : MatchCandidate) -> IndexKey {
  self.initials_key_value
}

///|
pub fn MatchExplanation::query(self : MatchExplanation) -> String {
  self.query_text
}

///|
pub fn MatchExplanation::full_key(self : MatchExplanation) -> String {
  self.full_key_text
}

///|
pub fn MatchExplanation::initials_key(self : MatchExplanation) -> String {
  self.initials_key_text
}

///|
pub fn MatchExplanation::exact_component(self : MatchExplanation) -> Double {
  self.exact_value
}

///|
pub fn MatchExplanation::prefix_component(self : MatchExplanation) -> Double {
  self.prefix_value
}

///|
pub fn MatchExplanation::initials_component(self : MatchExplanation) -> Double {
  self.initials_value
}

///|
pub fn MatchExplanation::fuzzy_component(self : MatchExplanation) -> Double {
  self.fuzzy_value
}

///|
pub fn MatchExplanation::score(self : MatchExplanation) -> Double {
  self.final_value
}

///|
pub fn MatchExplanation::distance(self : MatchExplanation) -> DistanceResult {
  self.distance_value
}

///|
pub fn RankedCandidate::label(self : RankedCandidate) -> String {
  self.candidate_value.label()
}

///|
pub fn RankedCandidate::score(self : RankedCandidate) -> Double {
  self.explanation_value.score()
}

///|
pub fn RankedCandidate::explanation(self : RankedCandidate) -> MatchExplanation {
  self.explanation_value
}

///|
pub fn RankedCandidate::original_position(self : RankedCandidate) -> Int {
  self.caller_position
}