///|
pub(all) struct Ranking {
  labels : Array[String]
  scores : Array[Double]
  order : Array[Int32]
  ranks : Array[Int32]
  higher_is_better : Bool
  tie_tolerance : Double
} derive(@debug.Debug)

///|
/// Stable exact sort, followed by anchor-based competition-rank tie groups.
pub fn rank_scores(
  labels : Array[String],
  scores : Array[Double],
  higher_is_better~ : Bool = true,
  tolerance~ : Double = 1.0e-10,
) -> Ranking raise ChoiceError {
  valid_labels(labels, 256)
  valid_tolerance(tolerance)
  if scores.length() != labels.length() {
    raise Invalid("score and label counts differ")
  }
  for x in scores {
    let _ = checked(x)
  }
  let order = [ for i in 0.. i ]
  for i in 1.. 0 {
      let before = order[j - 1]
      let better = if higher_is_better {
        scores[item] > scores[before]
      } else {
        scores[item] < scores[before]
      }
      if !better {
        break
      }
      order[j] = before
      j -= 1
    }
    order[j] = item
  }
  let ranks = Array::make(scores.length(), 1)
  let mut group_start = 0
  for k, index in order {
    if (scores[index] - scores[order[group_start]]).abs() > tolerance {
      group_start = k
    }
    ranks[index] = group_start + 1
  }
  {
    labels: labels.copy(),
    scores: scores.copy(),
    order,
    ranks,
    higher_is_better,
    tie_tolerance: tolerance,
  }
}

///|
fn validated_ranking(r : Ranking) -> Ranking raise ChoiceError {
  let canonical = rank_scores(
    r.labels,
    r.scores,
    higher_is_better=r.higher_is_better,
    tolerance=r.tie_tolerance,
  )
  if canonical.order != r.order || canonical.ranks != r.ranks {
    raise Invalid("ranking order/ranks do not match scores")
  }
  canonical
}

///|
pub fn Ranking::winners(self : Ranking) -> Array[String] raise ChoiceError {
  let r = validated_ranking(self)
  [
    for index in r.order if r.ranks[index] == 1 => r.labels[index]
  ]
}

///|
pub fn Ranking::top(
  self : Ranking,
  count : Int32,
) -> Array[String] raise ChoiceError {
  let r = validated_ranking(self)
  if count < 0 || count > r.order.length() {
    raise Invalid("requested top count out of range")
  }
  [
    for k in 0.. r.labels[r.order[k]]
  ]
}

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