///|
fn algorithm_label(algorithm : Algorithm) -> String {
  match algorithm {
    Soundex => "Soundex"
    RefinedSoundex => "RefinedSoundex"
    Nysiis => "Nysiis"
    Metaphone => "Metaphone"
    DoubleMetaphone => "DoubleMetaphone"
    Caverphone1 => "Caverphone1"
    Caverphone2 => "Caverphone2"
    MatchRating => "MatchRating"
  }
}

///|
fn metric_label(metric : SimilarityMetric) -> String {
  match metric {
    Levenshtein => "Levenshtein"
    Jaro => "Jaro"
    JaroWinkler(_) => "JaroWinkler"
    Dice(_) => "Dice"
  }
}

///|
fn encoded_keys_intersect(left : EncodedKeys, right : EncodedKeys) -> Bool {
  let left_values = left.all_non_empty_keys()
  let right_values = right.all_non_empty_keys()
  for left_value in left_values {
    for right_value in right_values {
      if left_value == right_value {
        return true
      }
    }
  }
  false
}

///|
fn append_normalization_notices(
  output : Array[String],
  side : String,
  notices : Array[NormalizationNotice],
) -> Unit {
  for notice in notices {
    match notice {
      CharactersDropped(count) =>
        output.push(side + ": characters dropped=" + "\{count}")
      DiacriticsFolded(count) =>
        output.push(side + ": diacritics folded=" + "\{count}")
      InputTruncated(count) =>
        output.push(side + ": input truncated=" + "\{count}")
    }
  }
}

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

///|
/// Compares two names and returns every score used by the final decision.
pub fn match_names(
  left : String,
  right : String,
  config : MatchConfig,
) -> Result[MatchEvidence, MatchError] {
  match validate_match_config(config) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let left_result = match normalize_name(left, config.normalization) {
    Err(error) => return Err(NormalizationFailed(error))
    Ok(result) => result
  }
  let right_result = match normalize_name(right, config.normalization) {
    Err(error) => return Err(NormalizationFailed(error))
    Ok(result) => result
  }
  let left_keys : Array[EncodedKeys] = []
  let right_keys : Array[EncodedKeys] = []
  let components : Array[ComponentScore] = []
  let mut contribution_sum = 0.0
  let mut weight_sum = 0.0
  for encoder in config.encoders {
    let left_encoded = encode_normalized_keys(
      left_result.normalized,
      encoder.algorithm,
    )
    let right_encoded = encode_normalized_keys(
      right_result.normalized,
      encoder.algorithm,
    )
    left_keys.push(left_encoded)
    right_keys.push(right_encoded)
    let raw_score = if encoded_keys_intersect(left_encoded, right_encoded) {
      1.0
    } else {
      0.0
    }
    let contribution = raw_score * encoder.weight
    components.push({
      name: algorithm_label(encoder.algorithm),
      raw_score,
      weight: encoder.weight,
      contribution,
    })
    contribution_sum = contribution_sum + contribution
    weight_sum = weight_sum + encoder.weight
  }
  let string_result = match
    matching_string_score(left_result, right_result, config) {
    Err(error) => return Err(SimilarityFailed(error))
    Ok(value) => value
  }
  let string_score = string_result.score
  let string_contribution = string_score * config.string_weight
  components.push({
    name: metric_label(config.metric),
    raw_score: string_score,
    weight: config.string_weight,
    contribution: string_contribution,
  })
  contribution_sum = contribution_sum + string_contribution
  weight_sum = weight_sum + config.string_weight
  let score = bounded_match_score(contribution_sum / weight_sum)
  let notices : Array[String] = []
  append_normalization_notices(notices, "left", left_result.notices)
  append_normalization_notices(notices, "right", right_result.notices)
  for notice in string_result.notices {
    notices.push(notice)
  }
  Ok({
    left_normalized: left_result.normalized,
    right_normalized: right_result.normalized,
    left_keys,
    right_keys,
    components,
    score,
    threshold: config.threshold,
    matched: score >= config.threshold,
    notices,
  })
}