///|
/// Switches for common Pinyin input confusions.
pub struct FuzzyRules {
  zh_z_enabled : Bool
  ch_c_enabled : Bool
  sh_s_enabled : Bool
  n_l_enabled : Bool
} derive(Eq, Debug)

///|
/// Validated edit costs and equivalence rules.
pub struct FuzzyProfile {
  insertion_value : Double
  deletion_value : Double
  substitution_value : Double
  equivalence_value : Double
  rule_values : FuzzyRules
} derive(Eq, Debug)

///|
priv struct DistanceCell {
  total : Double
  insertions : Int
  deletions : Int
  substitutions : Int
  equivalences : Int
}

///|
/// Weighted distance and operation counts for auditability.
pub struct DistanceResult {
  total_value : Double
  insertion_count : Int
  deletion_count : Int
  substitution_count : Int
  equivalence_count : Int
} derive(Eq, Debug)

///|
pub fn fuzzy_rules(
  zh_z : Bool,
  ch_c : Bool,
  sh_s : Bool,
  n_l : Bool,
) -> FuzzyRules {
  {
    zh_z_enabled: zh_z,
    ch_c_enabled: ch_c,
    sh_s_enabled: sh_s,
    n_l_enabled: n_l,
  }
}

///|
pub fn default_fuzzy_rules() -> FuzzyRules {
  fuzzy_rules(true, true, true, true)
}

///|
fn validate_edit_cost(
  name : String,
  value : Double,
) -> Result[Unit, PinyinError] {
  if value.is_nan() || value.is_inf() || value <= 0.0 || value > 10.0 {
    Err(InvalidWeight(name, value))
  } else {
    Ok(())
  }
}

///|
/// Builds a profile with the standard Pinyin equivalence groups enabled.
pub fn fuzzy_profile(
  insertion : Double,
  deletion : Double,
  substitution : Double,
  equivalence : Double,
) -> Result[FuzzyProfile, PinyinError] {
  fuzzy_profile_with_rules(
    insertion,
    deletion,
    substitution,
    equivalence,
    default_fuzzy_rules(),
  )
}

///|
/// Builds a profile with caller-selected Pinyin equivalence groups.
pub fn fuzzy_profile_with_rules(
  insertion : Double,
  deletion : Double,
  substitution : Double,
  equivalence : Double,
  rules : FuzzyRules,
) -> Result[FuzzyProfile, PinyinError] {
  match validate_edit_cost("insertion", insertion) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_edit_cost("deletion", deletion) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_edit_cost("substitution", substitution) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if equivalence.is_nan() ||
    equivalence.is_inf() ||
    equivalence < 0.0 ||
    equivalence > 1.0 {
    return Err(InvalidWeight("equivalence", equivalence))
  }
  Ok({
    insertion_value: insertion,
    deletion_value: deletion,
    substitution_value: substitution,
    equivalence_value: equivalence,
    rule_values: rules,
  })
}

///|
pub fn default_fuzzy_profile() -> FuzzyProfile {
  fuzzy_profile(1.0, 1.0, 1.0, 0.25).unwrap()
}

///|
pub fn FuzzyProfile::insertion_cost(self : FuzzyProfile) -> Double {
  self.insertion_value
}

///|
pub fn FuzzyProfile::deletion_cost(self : FuzzyProfile) -> Double {
  self.deletion_value
}

///|
pub fn FuzzyProfile::substitution_cost(self : FuzzyProfile) -> Double {
  self.substitution_value
}

///|
pub fn FuzzyProfile::equivalence_cost(self : FuzzyProfile) -> Double {
  self.equivalence_value
}

///|
pub fn FuzzyProfile::rules(self : FuzzyProfile) -> FuzzyRules {
  self.rule_values
}

///|
pub fn FuzzyRules::zh_z_enabled(self : FuzzyRules) -> Bool {
  self.zh_z_enabled
}

///|
pub fn FuzzyRules::ch_c_enabled(self : FuzzyRules) -> Bool {
  self.ch_c_enabled
}

///|
pub fn FuzzyRules::sh_s_enabled(self : FuzzyRules) -> Bool {
  self.sh_s_enabled
}

///|
pub fn FuzzyRules::n_l_enabled(self : FuzzyRules) -> Bool {
  self.n_l_enabled
}

///|
fn empty_distance_cell(total : Double) -> DistanceCell {
  { total, insertions: 0, deletions: 0, substitutions: 0, equivalences: 0 }
}

///|
fn distance_operation_count(cell : DistanceCell) -> Int {
  cell.insertions + cell.deletions + cell.substitutions + cell.equivalences
}

///|
fn distance_cell_better(
  candidate : DistanceCell,
  current : DistanceCell,
) -> Bool {
  if candidate.total < current.total {
    true
  } else if candidate.total > current.total {
    false
  } else {
    distance_operation_count(candidate) < distance_operation_count(current)
  }
}

///|
fn relax_distance(
  cells : Array[DistanceCell],
  index : Int,
  candidate : DistanceCell,
) -> Unit {
  if distance_cell_better(candidate, cells[index]) {
    cells[index] = candidate
  }
}

///|
fn add_insertion(cell : DistanceCell, cost : Double) -> DistanceCell {
  {
    total: cell.total + cost,
    insertions: cell.insertions + 1,
    deletions: cell.deletions,
    substitutions: cell.substitutions,
    equivalences: cell.equivalences,
  }
}

///|
fn add_deletion(cell : DistanceCell, cost : Double) -> DistanceCell {
  {
    total: cell.total + cost,
    insertions: cell.insertions,
    deletions: cell.deletions + 1,
    substitutions: cell.substitutions,
    equivalences: cell.equivalences,
  }
}

///|
fn add_substitution(cell : DistanceCell, cost : Double) -> DistanceCell {
  {
    total: cell.total + cost,
    insertions: cell.insertions,
    deletions: cell.deletions,
    substitutions: cell.substitutions + 1,
    equivalences: cell.equivalences,
  }
}

///|
fn add_equivalence(cell : DistanceCell, cost : Double) -> DistanceCell {
  {
    total: cell.total + cost,
    insertions: cell.insertions,
    deletions: cell.deletions,
    substitutions: cell.substitutions,
    equivalences: cell.equivalences + 1,
  }
}

///|
fn is_initial_pair(single : Int, pair_first : Int, pair_second : Int) -> Int {
  if single == 122 && pair_first == 122 && pair_second == 104 {
    1
  } else if single == 99 && pair_first == 99 && pair_second == 104 {
    2
  } else if single == 115 && pair_first == 115 && pair_second == 104 {
    3
  } else {
    0
  }
}

///|
fn pair_rule_enabled(kind : Int, rules : FuzzyRules) -> Bool {
  match kind {
    1 => rules.zh_z_enabled
    2 => rules.ch_c_enabled
    3 => rules.sh_s_enabled
    _ => false
  }
}

///|
fn can_single_pair(
  single_text : String,
  single_index : Int,
  pair_text : String,
  pair_index : Int,
  rules : FuzzyRules,
) -> Bool {
  if single_index >= single_text.length() ||
    pair_index + 1 >= pair_text.length() {
    return false
  }
  pair_rule_enabled(
    is_initial_pair(
      single_text[single_index].to_int(),
      pair_text[pair_index].to_int(),
      pair_text[pair_index + 1].to_int(),
    ),
    rules,
  )
}

///|
fn can_n_l(
  left : String,
  left_index : Int,
  right : String,
  right_index : Int,
  rules : FuzzyRules,
) -> Bool {
  if !rules.n_l_enabled {
    return false
  }
  let left_code = left[left_index].to_int()
  let right_code = right[right_index].to_int()
  (left_code == 110 && right_code == 108) ||
  (left_code == 108 && right_code == 110)
}

///|
/// Computes weighted Levenshtein distance with optional Pinyin initial rules.
pub fn weighted_pinyin_distance(
  left_input : String,
  right_input : String,
  profile : FuzzyProfile,
) -> DistanceResult {
  let left = normalize_literal(left_input)
  let right = normalize_literal(right_input)
  let rows = left.length() + 1
  let columns = right.length() + 1
  let cells = Array::make(rows * columns, empty_distance_cell(1000000000.0))
  cells[0] = empty_distance_cell(0.0)
  for left_index = 0; left_index < rows; left_index = left_index + 1 {
    for right_index = 0; right_index < columns; right_index = right_index + 1 {
      let cell_index = left_index * columns + right_index
      let current = cells[cell_index]
      if left_index < left.length() {
        relax_distance(
          cells,
          (left_index + 1) * columns + right_index,
          add_deletion(current, profile.deletion_value),
        )
      }
      if right_index < right.length() {
        relax_distance(
          cells,
          left_index * columns + right_index + 1,
          add_insertion(current, profile.insertion_value),
        )
      }
      if left_index < left.length() && right_index < right.length() {
        let diagonal = if left[left_index] == right[right_index] {
          current
        } else if can_n_l(
            left,
            left_index,
            right,
            right_index,
            profile.rule_values,
          ) {
          add_equivalence(current, profile.equivalence_value)
        } else {
          add_substitution(current, profile.substitution_value)
        }
        relax_distance(
          cells,
          (left_index + 1) * columns + right_index + 1,
          diagonal,
        )
      }
      if can_single_pair(
          left,
          left_index,
          right,
          right_index,
          profile.rule_values,
        ) {
        relax_distance(
          cells,
          (left_index + 1) * columns + right_index + 2,
          add_equivalence(current, profile.equivalence_value),
        )
      }
      if can_single_pair(
          right,
          right_index,
          left,
          left_index,
          profile.rule_values,
        ) {
        relax_distance(
          cells,
          (left_index + 2) * columns + right_index + 1,
          add_equivalence(current, profile.equivalence_value),
        )
      }
    }
  }
  let result = cells[rows * columns - 1]
  {
    total_value: result.total,
    insertion_count: result.insertions,
    deletion_count: result.deletions,
    substitution_count: result.substitutions,
    equivalence_count: result.equivalences,
  }
}

///|
pub fn DistanceResult::cost(self : DistanceResult) -> Double {
  self.total_value
}

///|
pub fn DistanceResult::insertions(self : DistanceResult) -> Int {
  self.insertion_count
}

///|
pub fn DistanceResult::deletions(self : DistanceResult) -> Int {
  self.deletion_count
}

///|
pub fn DistanceResult::substitutions(self : DistanceResult) -> Int {
  self.substitution_count
}

///|
pub fn DistanceResult::equivalences(self : DistanceResult) -> Int {
  self.equivalence_count
}

///|
pub fn DistanceResult::operations(self : DistanceResult) -> Int {
  self.insertion_count +
  self.deletion_count +
  self.substitution_count +
  self.equivalence_count
}