///|
/// Weights used when ranking related customers for destroy operators.
pub struct RelatednessWeights {
  distance : Double
  demand : Double
  time_window : Double
  service : Double
} derive(Debug, ToJson)

///|
pub fn RelatednessWeights::default() -> RelatednessWeights {
  { distance: 1.0, demand: 1.5, time_window: 0.1, service: 0.2 }
}

///|
pub fn RelatednessWeights::tune(
  self : RelatednessWeights,
  distance? : Double = self.distance,
  demand? : Double = self.demand,
  time_window? : Double = self.time_window,
  service? : Double = self.service,
) -> RelatednessWeights {
  { distance, demand, time_window, service }
}

///|
pub fn RelatednessWeights::distance(self : RelatednessWeights) -> Double {
  self.distance
}

///|
pub fn RelatednessWeights::demand(self : RelatednessWeights) -> Double {
  self.demand
}

///|
pub fn RelatednessWeights::time_window(self : RelatednessWeights) -> Double {
  self.time_window
}

///|
pub fn RelatednessWeights::service(self : RelatednessWeights) -> Double {
  self.service
}

///|
/// Scores two customers by spatial, demand, and temporal similarity.
pub fn relatedness_score(
  instance : @core.VrpInstance,
  left : Int,
  right : Int,
  weights : RelatednessWeights,
) -> Double {
  let left_customer = instance.customer(left)
  let right_customer = instance.customer(right)
  weights.distance() * instance.distance(left, right) +
  weights.demand() *
  Double::abs(left_customer.demand() - right_customer.demand()) +
  weights.time_window() *
  Double::abs(left_customer.ready_time() - right_customer.ready_time()) +
  weights.service() *
  Double::abs(left_customer.service_time() - right_customer.service_time())
}

///|
/// Returns customer indices ordered from most to least related to a seed.
pub fn rank_related_customers(
  instance : @core.VrpInstance,
  seed : Int,
  candidates : Array[Int],
  weights : RelatednessWeights,
) -> Array[Int] {
  let ranked = candidates.copy()
  ranked.sort_by(fn(left, right) {
    let order = Double::compare(
      relatedness_score(instance, seed, left, weights),
      relatedness_score(instance, seed, right, weights),
    )
    if order == 0 {
      left - right
    } else {
      order
    }
  })
  ranked
}

///|
/// Measures how evenly a solution uses its vehicle capacities.
pub fn route_balance_score(
  instance : @core.VrpInstance,
  solution : @core.Solution,
) -> Double {
  let loads = solution.route_loads()
  if loads.is_empty() || instance.vehicle().capacity <= 0.0 {
    0.0
  } else {
    let average = loads.fold(init=0.0, fn(total, load) { total + load }) /
      Double::from_int(loads.length())
    let deviation = loads.fold(init=0.0, fn(total, load) {
      total + Double::abs(load - average)
    })
    1.0 -
    deviation / (Double::from_int(loads.length()) * instance.vehicle().capacity)
  }
}

///|
/// Scores a candidate with a quality/constraint trade-off for dashboards.
pub fn operator_candidate_score(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  balance_weight : Double,
) -> Double {
  solution.objective_value() -
  balance_weight * route_balance_score(instance, solution)
}

///|
/// Selects a destroy operator from a score vector with deterministic tie breaks.
pub fn select_operator(scores : Array[Double]) -> Int {
  if scores.is_empty() {
    return 0
  }
  let mut selected = 0
  for index in 1.. scores[selected] {
      selected = index
    }
  }
  selected
}