///|
/// Structural statistics used to select a heuristic and explain a benchmark.
pub struct InstanceStatistics {
  customer_count : Int
  total_demand : Double
  average_demand : Double
  maximum_demand : Double
  minimum_demand : Double
  demand_capacity_ratio : Double
  fleet_size : Int
  average_nearest_neighbor : Double
  average_depot_distance : Double
  time_windowed_customers : Int
} derive(Debug, ToJson)

///|
pub fn InstanceStatistics::customer_count(self : InstanceStatistics) -> Int {
  self.customer_count
}

///|
pub fn InstanceStatistics::total_demand(self : InstanceStatistics) -> Double {
  self.total_demand
}

///|
pub fn InstanceStatistics::average_demand(self : InstanceStatistics) -> Double {
  self.average_demand
}

///|
pub fn InstanceStatistics::maximum_demand(self : InstanceStatistics) -> Double {
  self.maximum_demand
}

///|
pub fn InstanceStatistics::minimum_demand(self : InstanceStatistics) -> Double {
  self.minimum_demand
}

///|
pub fn InstanceStatistics::demand_capacity_ratio(
  self : InstanceStatistics,
) -> Double {
  self.demand_capacity_ratio
}

///|
pub fn InstanceStatistics::fleet_size(self : InstanceStatistics) -> Int {
  self.fleet_size
}

///|
pub fn InstanceStatistics::average_nearest_neighbor(
  self : InstanceStatistics,
) -> Double {
  self.average_nearest_neighbor
}

///|
pub fn InstanceStatistics::average_depot_distance(
  self : InstanceStatistics,
) -> Double {
  self.average_depot_distance
}

///|
pub fn InstanceStatistics::time_windowed_customers(
  self : InstanceStatistics,
) -> Int {
  self.time_windowed_customers
}

///|
fn nearest_neighbor_distance(
  instance : VrpInstance,
  customer_index : Int,
) -> Double {
  let best = 1_000_000_000.0
  let mut selected = best
  for candidate in 0.. InstanceStatistics {
  let count = instance.customer_count()
  let mut total = 0.0
  let mut maximum = 0.0
  let mut minimum = 1_000_000_000.0
  let mut nearest_total = 0.0
  let mut depot_total = 0.0
  let mut time_windowed = 0
  let depot_index = instance.depot_index()
  for index, customer in instance.customers {
    total += customer.demand
    maximum = Double::max(maximum, customer.demand)
    minimum = Double::min(minimum, customer.demand)
    nearest_total += nearest_neighbor_distance(instance, index)
    depot_total += instance.distance(depot_index, index)
    if customer.ready_time > 0.0 || customer.due_time < instance.depot.due_time {
      time_windowed += 1
    }
  }
  let average = if count == 0 { 0.0 } else { total / Double::from_int(count) }
  let nearest_average = if count <= 1 {
    0.0
  } else {
    nearest_total / Double::from_int(count)
  }
  let depot_average = if count == 0 {
    0.0
  } else {
    depot_total / Double::from_int(count)
  }
  let capacity = instance.fleet_capacity()
  {
    customer_count: count,
    total_demand: total,
    average_demand: average,
    maximum_demand: if count == 0 {
      0.0
    } else {
      maximum
    },
    minimum_demand: if count == 0 {
      0.0
    } else {
      minimum
    },
    demand_capacity_ratio: if capacity <= 0.0 {
      0.0
    } else {
      total / capacity
    },
    fleet_size: instance.vehicle.fleet_size,
    average_nearest_neighbor: nearest_average,
    average_depot_distance: depot_average,
    time_windowed_customers: time_windowed,
  }
}

///|
pub fn route_customer_overlap(left : RoutePlan, right : RoutePlan) -> Int {
  let mut count = 0
  for customer in left.stops {
    if right.stops.contains(customer) {
      count += 1
    }
  }
  count
}

///|
pub fn solution_duplicate_count(solution : Solution) -> Int {
  let assigned = solution.assigned_customers()
  let unique = assigned.copy()
  unique.sort()
  unique.dedup()
  assigned.length() - unique.length()
}

///|
pub fn solution_route_count_histogram(solution : Solution) -> Array[Int] {
  let histogram : Array[Int] = []
  for route in solution.routes {
    let bucket = route.stops.length().min(20)
    while histogram.length() <= bucket {
      histogram.push(0)
    }
    histogram[bucket] += 1
  }
  histogram
}

///|
pub fn solution_route_length_percentiles(
  solution : Solution,
) -> (Double, Double, Double) {
  let values = solution.routes.map(fn(route) { route.metrics.distance })
  if values.is_empty() {
    (0.0, 0.0, 0.0)
  } else {
    values.sort()
    let low = values[(values.length() - 1) / 4]
    let middle = values[(values.length() - 1) / 2]
    let high = values[(values.length() - 1) * 3 / 4]
    (low, middle, high)
  }
}