///|
/// Candidate consensus summary used for deterministic model ranking.
pub(all) struct ConsensusCandidate2 {
  inliers : Int
  total : Int
  ratio : Double
  score : Double
  residual_sum : Double
  threshold : Double
} derive(Debug, Eq)

///|
pub fn consensus_candidate(
  residuals : ArrayView[Double],
  threshold : Double,
) -> ConsensusCandidate2 {
  let mut inliers = 0
  let mut score = 0.0
  let mut residual_sum = 0.0
  for residual in residuals {
    let value = residual.abs()
    residual_sum += value
    if value <= threshold {
      inliers += 1
      score += threshold - value
    }
  }
  {
    inliers,
    total: residuals.length(),
    ratio: if residuals.length() == 0 {
      0.0
    } else {
      inliers.to_double() / residuals.length().to_double()
    },
    score,
    residual_sum,
    threshold,
  }
}

///|
pub fn evaluate_consensus_candidates(
  residual_sets : ArrayView[Array[Double]],
  threshold~ : Double,
) -> Array[ConsensusCandidate2] {
  let result : Array[ConsensusCandidate2] = []
  for residuals in residual_sets {
    result.push(consensus_candidate(residuals, threshold))
  }
  result
}

///|
pub fn select_best_candidate(
  candidates : ArrayView[ConsensusCandidate2],
) -> ConsensusCandidate2 {
  if candidates.length() == 0 {
    {
      inliers: 0,
      total: 0,
      ratio: 0.0,
      score: 0.0,
      residual_sum: 0.0,
      threshold: 0.0,
    }
  } else {
    let mut best = candidates[0]
    for candidate in candidates[1:] {
      if consensus_candidate_better(candidate, best) {
        best = candidate
      }
    }
    best
  }
}

///|
pub fn consensus_candidate_better(
  left : ConsensusCandidate2,
  right : ConsensusCandidate2,
) -> Bool {
  left.inliers > right.inliers ||
  (left.inliers == right.inliers && left.score > right.score) ||
  (
    left.inliers == right.inliers &&
    left.score == right.score &&
    left.residual_sum < right.residual_sum
  )
}

///|
pub fn rank_consensus_candidates(
  candidates : ArrayView[ConsensusCandidate2],
) -> Array[ConsensusCandidate2] {
  let result = candidates.to_owned()
  result.sort_by(fn(left, right) {
    if consensus_candidate_better(left, right) {
      -1
    } else if consensus_candidate_better(right, left) {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn candidate_margin(
  best : ConsensusCandidate2,
  second : ConsensusCandidate2,
) -> Double {
  best.score - second.score
}

///|
pub fn candidate_is_confident(
  best : ConsensusCandidate2,
  second : ConsensusCandidate2,
  minimum_margin : Double,
) -> Bool {
  best.inliers > 0 && candidate_margin(best, second) >= minimum_margin
}

///|
pub fn candidate_mask(
  candidate : ConsensusCandidate2,
  residuals : ArrayView[Double],
) -> Array[Bool] {
  let result : Array[Bool] = []
  for residual in residuals {
    result.push(residual.abs() <= candidate.threshold)
  }
  result
}

///|
pub fn candidate_inlier_residuals(
  candidate : ConsensusCandidate2,
  residuals : ArrayView[Double],
) -> Array[Double] {
  let result : Array[Double] = []
  for residual in residuals {
    if residual.abs() <= candidate.threshold {
      result.push(residual)
    }
  }
  result
}

///|
pub fn candidate_outlier_residuals(
  candidate : ConsensusCandidate2,
  residuals : ArrayView[Double],
) -> Array[Double] {
  let result : Array[Double] = []
  for residual in residuals {
    if residual.abs() > candidate.threshold {
      result.push(residual)
    }
  }
  result
}

///|
pub fn candidate_refined_threshold(
  candidate : ConsensusCandidate2,
  residuals : ArrayView[Double],
  multiplier : Double,
  minimum : Double,
) -> Double raise @core.GeometryError {
  let inliers = candidate_inlier_residuals(candidate, residuals)
  (multiplier * @core.median_absolute_deviation2(inliers)).max(minimum)
}

///|
pub fn candidate_score_per_point(candidate : ConsensusCandidate2) -> Double {
  if candidate.total == 0 {
    0.0
  } else {
    candidate.score / candidate.total.to_double()
  }
}

///|
pub fn candidate_support_score(
  candidate : ConsensusCandidate2,
  expected_minimum : Int,
) -> Double {
  if expected_minimum <= 0 {
    candidate.ratio
  } else {
    (candidate.inliers.to_double() / expected_minimum.to_double()).min(1.0)
  }
}