///|
pub(all) struct Hypothesis {
  id : String
  tokens : Array[String]
  acoustic_score : Double
} derive(Debug, Eq)

///|
pub(all) struct RescoreOptions {
  lm_weight : Double
  acoustic_weight : Double
  length_weight : Double
  scoring : ScoreOptions
} derive(Debug, Eq)

///|
pub fn RescoreOptions::default() -> RescoreOptions {
  {
    lm_weight: 1.0,
    acoustic_weight: 1.0,
    length_weight: 0.0,
    scoring: ScoreOptions::default(),
  }
}

///|
pub(all) struct RankedHypothesis {
  id : String
  input_index : Int32
  score : Double
  language : SentenceScore
} derive(Debug, Eq)

///|
fn finite_number(value : Double) -> Bool {
  valid_log(value) && value != @double.neg_infinity
}

///|
/// Acoustic scores must already use compatible units (typically log10).
/// Zero LM weight disables the LM term even for impossible sentences.
pub fn Model::rescore(
  self : Model,
  hypotheses : Array[Hypothesis],
  options~ : RescoreOptions = RescoreOptions::default(),
) -> Array[RankedHypothesis] raise ArpaError {
  if !finite_number(options.lm_weight) ||
    options.lm_weight < 0.0 ||
    !finite_number(options.acoustic_weight) ||
    options.acoustic_weight < 0.0 ||
    !finite_number(options.length_weight) {
    raise Invalid("invalid rescoring weights")
  }
  if hypotheses.length() > self.limits.max_records {
    raise Budget("hypothesis limit")
  }
  let ids : Map[String, Bool] = Map([])
  let ranked = []
  let mut work = 0
  for i = 0; i < hypotheses.length(); i = i + 1 {
    let h = hypotheses[i]
    if h.id.is_empty() || ids.contains(h.id) {
      raise Invalid("hypothesis IDs must be nonempty and unique")
    }
    if !finite_number(h.acoustic_score) {
      raise Invalid("nonfinite acoustic score")
    }
    if h.tokens.length() + 1 > self.limits.max_work - work {
      raise Budget("rescoring work limit")
    }
    work += h.tokens.length() + 1
    ids[h.id] = true
    let language = self.sentence(h.tokens, options=options.scoring)
    let lm_term = if options.lm_weight == 0.0 {
      0.0
    } else {
      options.lm_weight * language.log_probability
    }
    let score = lm_term +
      options.acoustic_weight * h.acoustic_score +
      options.length_weight * h.tokens.length().to_double()
    if !valid_log(score) {
      raise Invalid("rescoring arithmetic overflow")
    }
    ranked.push({ id: h.id, input_index: i, score, language, })
  }
  ranked.sort_by(fn(a, b) {
    if a.score > b.score {
      -1
    } else if a.score < b.score {
      1
    } else {
      a.input_index - b.input_index
    }
  })
  ranked
}

///|
pub extend Hypothesis with @debug.Debug::{to_repr}

///|
pub extend Hypothesis with Eq::{equal, not_equal}

///|
pub extend RescoreOptions with @debug.Debug::{to_repr}

///|
pub extend RescoreOptions with Eq::{equal, not_equal}

///|
pub extend RankedHypothesis with @debug.Debug::{to_repr}

///|
pub extend RankedHypothesis with Eq::{equal, not_equal}