///|
pub(all) struct CriterionComparison {
  criterion : String
  left_value : Double
  right_value : Double
  oriented_difference : Double
  weighted_preference_left : Double
  weighted_preference_right : Double
} derive(@debug.Debug)

///|
pub(all) struct PairExplanation {
  left : String
  right : String
  criteria : Array[CriterionComparison]
  aggregate_left : Double
  aggregate_right : Double
  net_preference : Double
  left_dominates : Bool
  right_dominates : Bool
} derive(@debug.Debug)

///|
pub fn explain_pair(
  m : DecisionMatrix,
  weights : Array[Double],
  preferences : Array[Preference],
  left : Int32,
  right : Int32,
) -> PairExplanation raise ChoiceError {
  let w = matrix_weights(m, weights)
  preference_specs(m, preferences)
  if left < 0 ||
    left >= m.names.length() ||
    right < 0 ||
    right >= m.names.length() {
    raise Invalid("alternative index out of bounds")
  }
  let criteria : Array[CriterionComparison] = []
  for j in 0.. c.weighted_preference_left))
  let aggregate_right = sum(criteria.map(c => c.weighted_preference_right))
  {
    left: m.names[left],
    right: m.names[right],
    criteria,
    aggregate_left,
    aggregate_right,
    net_preference: aggregate_left - aggregate_right,
    left_dominates: dominates(m, left, right),
    right_dominates: dominates(m, right, left),
  }
}

///|
pub(all) struct AdditiveBreakEven {
  intercept : Double
  slope : Double
  crossing : Double?
  tied_for_all_weights : Bool
} derive(@debug.Debug)

///|
/// Exact SAW one-coordinate weight-grid crossing for a fixed normalization matrix.
pub fn additive_break_even(
  m : DecisionMatrix,
  weights : Array[Double],
  criterion : Int32,
  left : Int32,
  right : Int32,
) -> AdditiveBreakEven raise ChoiceError {
  let w = matrix_weights(m, weights)
  if w.length() < 2 {
    raise Invalid("break-even sweep requires at least two criteria")
  }
  if criterion < 0 || criterion >= w.length() {
    raise Invalid("criterion index out of range")
  }
  let _ = m.cell(left, criterion)
  let _ = m.cell(right, criterion)
  let x = m.normalize(MinMax)
  let rest = sum([ for j in 0.. w[j] ])
  let mut intercept = 0.0
  for j in 0.. 0.0 {
      w[j] / rest
    } else {
      1.0 / (w.length() - 1).to_double()
    }
    intercept += share * (x[left][j] - x[right][j])
  }
  let slope = x[left][criterion] - x[right][criterion] - intercept
  let root = if slope == 0.0 {
    None
  } else {
    let value = -intercept / slope
    if value >= 0.0 && value <= 1.0 {
      Some(value)
    } else {
      None
    }
  }
  {
    intercept,
    slope,
    crossing: root,
    tied_for_all_weights: intercept == 0.0 && slope == 0.0,
  }
}

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

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

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