///|
pub fn reciprocal_rank_at(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  cutoff : Int,
  threshold : Int,
) -> Double {
  let relevance = build_relevance_map(qrels)
  let ranked = sorted_run_items(run)
  let limit = Int::min(Int::max(cutoff, 0), ranked.length())
  for index in 0..= threshold {
      return reciprocal_rank(index)
    }
  }
  0.0
}

///|
pub fn average_precision_at(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  cutoff : Int,
  threshold : Int,
) -> Double {
  let relevance = build_relevance_map(qrels)
  let ranked = sorted_run_items(run)
  let limit = Int::min(Int::max(cutoff, 0), ranked.length())
  let mut relevant_total = 0
  for _, value in relevance {
    if value >= threshold {
      relevant_total += 1
    }
  }
  if relevant_total == 0 {
    return 0.0
  }
  let mut hits = 0
  let mut total = 0.0
  for index in 0..= threshold {
      hits += 1
      total += to_ratio(hits, index + 1)
    }
  }
  total / Double::from_int(relevant_total)
}

///|
pub fn ndcg_at(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
  cutoff : Int,
  threshold : Int,
  gain_scheme : GainScheme,
) -> Double {
  ignore(threshold)
  let relevance = build_relevance_map(qrels)
  let ranked = sorted_run_items(run)
  let limit = Int::min(Int::max(cutoff, 0), ranked.length())
  let values : Array[Int] = []
  for _, value in relevance {
    values.push(value)
  }
  let best = ideal_dcg(values, cutoff, gain_scheme)
  if best == 0.0 {
    return 0.0
  }
  let mut actual = 0.0
  for index in 0.. Double {
  judged_recall_at(qrels, run, cutoff)
}

///|
pub fn score_monotonicity(run : Array[RetrievedDoc]) -> Double {
  if run.length() < 2 {
    return if run.is_empty() { 0.0 } else { 1.0 }
  }
  let mut ordered = 0
  for index in 1..= run[index].score {
      ordered += 1
    }
  }
  to_ratio(ordered, run.length() - 1)
}

///|
pub fn run_quality(
  qrels : Array[JudgedDoc],
  run : Array[RetrievedDoc],
) -> RunQuality {
  let profile = profile_dataset(qrels, run)
  let unique = retrieved_document_count(run)
  let mut finite = 0
  for item in run {
    if !item.score.is_nan() && !item.score.is_inf() {
      finite += 1
    }
  }
  {
    query_count: query_id_set_from_run(run).length(),
    row_count: run.length(),
    unique_row_count: unique,
    duplicate_rate: if run.is_empty() {
      0.0
    } else {
      1.0 - to_ratio(unique, run.length())
    },
    unjudged_rate: if run.is_empty() {
      0.0
    } else {
      to_ratio(profile.unjudged_retrievals, run.length())
    },
    score_monotonicity: score_monotonicity(run),
    finite_score_rate: if run.is_empty() {
      0.0
    } else {
      to_ratio(finite, run.length())
    },
  }
}