///|
/// Returns a user-facing description of a problem variant.
pub fn ProblemVariant::description(self : ProblemVariant) -> String {
  match self {
    ProblemVariant::CVRP => "capacity-constrained vehicle routing"
    ProblemVariant::VRPTW => "vehicle routing with time windows"
  }
}

///|
pub fn DistanceMetric::description(self : DistanceMetric) -> String {
  match self {
    DistanceMetric::Euclidean => "continuous Euclidean distance"
    DistanceMetric::RoundedEuclidean => "rounded Euclidean distance (EUC_2D)"
    DistanceMetric::Manhattan => "rectilinear Manhattan distance"
    DistanceMetric::Chebyshev => "maximum-coordinate Chebyshev distance"
  }
}

///|
pub fn VrpInstance::validate(self : VrpInstance) -> ValidationReport {
  validate_instance(self)
}

///|
pub fn Solution::validate(
  self : Solution,
  instance : VrpInstance,
) -> ValidationReport {
  validate_solution(instance, self)
}

///|
pub fn Solution::is_complete(self : Solution, instance : VrpInstance) -> Bool {
  self.unassigned.is_empty() &&
  self.assigned_customers().length() == instance.customer_count() &&
  solution_duplicate_count(self) == 0
}

///|
pub fn Solution::is_valid_for(
  self : Solution,
  instance : VrpInstance,
  policy : FeasibilityPolicy,
) -> Bool {
  assess_solution(instance, self, policy).is_feasible()
}

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

///|
pub fn RoutePlan::load(self : RoutePlan) -> Double {
  self.metrics.load
}

///|
pub fn RoutePlan::duration(self : RoutePlan) -> Double {
  self.metrics.duration
}

///|
pub fn RoutePlan::is_feasible(self : RoutePlan) -> Bool {
  self.metrics.feasible
}

///|
pub fn RoutePlan::customer_indices(self : RoutePlan) -> Array[Int] {
  self.stops.copy()
}

///|
pub fn VrpInstance::has_time_windows(self : VrpInstance) -> Bool {
  if self.variant is ProblemVariant::VRPTW {
    true
  } else {
    self.customers.any(fn(customer) {
      customer.ready_time != 0.0 || customer.due_time != 1_000_000_000.0
    })
  }
}

///|
pub fn VrpInstance::is_capacity_feasible(self : VrpInstance) -> Bool {
  self.total_demand() <= self.fleet_capacity()
}

///|
pub fn VrpInstance::customer_labels(self : VrpInstance) -> Array[Int] {
  self.customers.map(fn(customer) { customer.external_id })
}

///|
pub fn SolveConfig::iteration_budget(self : SolveConfig) -> Int {
  self.iterations.max(0)
}

///|
pub fn SolveConfig::destroy_range(self : SolveConfig) -> (Int, Int) {
  (self.min_destroy.max(0), self.max_destroy.max(self.min_destroy))
}

///|
pub fn SolveConfig::penalties(
  self : SolveConfig,
) -> (Double, Double, Double, Double) {
  (
    self.capacity_penalty,
    self.time_penalty,
    self.duration_penalty,
    self.unassigned_penalty,
  )
}

///|
pub fn SolveResult::best(self : SolveResult) -> Solution {
  self.best_solution.copy()
}

///|
pub fn SolveResult::seed(self : SolveResult) -> Solution {
  self.seed_solution.copy()
}

///|
pub fn SolveResult::problem(self : SolveResult) -> VrpInstance {
  self.instance
}

///|
pub fn SolveResult::is_improved(self : SolveResult) -> Bool {
  self.stats.best_objective < self.stats.seed_objective
}

///|
pub fn SolveResult::improvement(self : SolveResult) -> Double {
  self.stats.seed_objective - self.stats.best_objective
}

///|
pub fn SolveResult::completion_ratio(self : SolveResult) -> Double {
  self.best_solution.completion_ratio(self.instance)
}

///|
pub fn Customer::window_width(self : Customer) -> Double {
  self.due_time - self.ready_time
}

///|
pub fn VehicleProfile::capacity_per_route(self : VehicleProfile) -> Double {
  self.capacity
}

///|
pub fn RouteMetrics::total_violation(self : RouteMetrics) -> Double {
  self.capacity_violation + self.time_violation + self.duration_violation
}

///|
pub fn ConstraintSummary::unserved_ratio(
  self : ConstraintSummary,
  instance : VrpInstance,
) -> Double {
  if instance.customer_count() == 0 {
    0.0
  } else {
    Double::from_int(self.unassigned_count) /
    Double::from_int(instance.customer_count())
  }
}

///|
pub fn CostBreakdown::penalty_total(self : CostBreakdown) -> Double {
  self.capacity_penalty +
  self.time_penalty +
  self.duration_penalty +
  self.unassigned_penalty
}