///|
pub fn VrpInstance::customer_count(self : VrpInstance) -> Int {
  self.customers.length()
}

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

///|
pub fn VrpInstance::customer(
  self : VrpInstance,
  customer_index : Int,
) -> Customer {
  self.customers[customer_index]
}

///|
pub fn VrpInstance::label_of(self : VrpInstance, customer_index : Int) -> Int {
  self.customer(customer_index).external_id
}

///|
pub fn ProblemVariant::name(self : ProblemVariant) -> String {
  match self {
    ProblemVariant::CVRP => "CVRP"
    ProblemVariant::VRPTW => "VRPTW"
  }
}

///|
pub fn VrpInstance::distance(
  self : VrpInstance,
  from_index : Int,
  to_index : Int,
) -> Double {
  self.distance_matrix[from_index][to_index]
}

///|
pub fn evaluate_route(
  instance : VrpInstance,
  stops : Array[Int],
) -> RouteMetrics {
  let depot_index = instance.depot_index()
  let arrivals : Array[Double] = []
  let mut previous = depot_index
  let mut load = 0.0
  let mut duration = 0.0
  let mut distance = 0.0
  let mut waiting_time = 0.0
  let mut time_violation = 0.0
  for stop in stops {
    let customer = instance.customer(stop)
    let leg = instance.distance(previous, stop)
    duration += leg
    distance += leg
    let arrival = duration
    let service_start = if arrival < customer.ready_time {
      customer.ready_time
    } else {
      arrival
    }
    waiting_time += service_start - arrival
    if service_start > customer.due_time {
      time_violation += service_start - customer.due_time
    }
    arrivals.push(service_start)
    duration = service_start + customer.service_time
    load += customer.demand
    previous = stop
  }
  if !stops.is_empty() {
    let back = instance.distance(previous, depot_index)
    duration += back
    distance += back
  }
  let capacity_violation = if load > instance.vehicle.capacity {
    load - instance.vehicle.capacity
  } else {
    0.0
  }
  let duration_violation = if duration > instance.vehicle.max_route_duration {
    duration - instance.vehicle.max_route_duration
  } else {
    0.0
  }
  {
    distance,
    duration,
    waiting_time,
    load,
    capacity_violation,
    time_violation,
    duration_violation,
    feasible: capacity_violation == 0.0 &&
    time_violation == 0.0 &&
    duration_violation == 0.0,
    arrivals,
  }
}

///|
pub fn build_route(instance : VrpInstance, stops : Array[Int]) -> RoutePlan {
  { stops, metrics: evaluate_route(instance, stops) }
}

///|
fn compute_solution_objective(
  total_distance : Double,
  total_capacity_violation : Double,
  total_time_violation : Double,
  total_duration_violation : Double,
  unassigned_count : Int,
  config : SolveConfig,
) -> Double {
  total_distance +
  total_capacity_violation * config.capacity_penalty +
  total_time_violation * config.time_penalty +
  total_duration_violation * config.duration_penalty +
  Double::from_int(unassigned_count) * config.unassigned_penalty
}

///|
pub fn build_solution(
  instance : VrpInstance,
  route_stops : Array[Array[Int]],
  unassigned : Array[Int],
  config : SolveConfig,
) -> Solution {
  let routes : Array[RoutePlan] = []
  let mut total_distance = 0.0
  let mut total_capacity_violation = 0.0
  let mut total_time_violation = 0.0
  let mut total_duration_violation = 0.0
  let mut feasible = unassigned.is_empty()
  for stops in route_stops {
    if stops.is_empty() {
      continue
    }
    let route = build_route(instance, stops)
    routes.push(route)
    total_distance += route.metrics.distance
    total_capacity_violation += route.metrics.capacity_violation
    total_time_violation += route.metrics.time_violation
    total_duration_violation += route.metrics.duration_violation
    if !route.metrics.feasible {
      feasible = false
    }
  }
  if routes.length() > instance.vehicle.fleet_size {
    feasible = false
  }
  {
    routes,
    unassigned,
    total_distance,
    objective: compute_solution_objective(
      total_distance,
      total_capacity_violation,
      total_time_violation,
      total_duration_violation,
      unassigned.length(),
      config,
    ),
    feasible: feasible &&
    total_capacity_violation == 0.0 &&
    total_time_violation == 0.0 &&
    total_duration_violation == 0.0,
    total_capacity_violation,
    total_time_violation,
    total_duration_violation,
  }
}

///|
pub fn Solution::copy(self : Solution) -> Solution {
  let routes : Array[RoutePlan] = []
  for route in self.routes {
    routes.push(copy_route_plan(route))
  }
  {
    routes,
    unassigned: self.unassigned.copy(),
    total_distance: self.total_distance,
    objective: self.objective,
    feasible: self.feasible,
    total_capacity_violation: self.total_capacity_violation,
    total_time_violation: self.total_time_violation,
    total_duration_violation: self.total_duration_violation,
  }
}

///|
fn copy_route_plan(route : RoutePlan) -> RoutePlan {
  {
    stops: route.stops.copy(),
    metrics: {
      distance: route.metrics.distance,
      duration: route.metrics.duration,
      waiting_time: route.metrics.waiting_time,
      load: route.metrics.load,
      capacity_violation: route.metrics.capacity_violation,
      time_violation: route.metrics.time_violation,
      duration_violation: route.metrics.duration_violation,
      feasible: route.metrics.feasible,
      arrivals: route.metrics.arrivals.copy(),
    },
  }
}

///|
pub fn Solution::route_stops(self : Solution) -> Array[Array[Int]] {
  self.routes.map(fn(route) { route.stops.copy() })
}

///|
pub fn Solution::assigned_customers(self : Solution) -> Array[Int] {
  let assigned : Array[Int] = []
  for route in self.routes {
    assigned.append(route.stops)
  }
  assigned
}

///|
pub fn Solution::customer_labels(
  self : Solution,
  instance : VrpInstance,
) -> Array[Array[Int]] {
  self.routes.map(fn(route) {
    route.stops.map(fn(stop) { instance.label_of(stop) })
  })
}

///|
pub fn all_customer_indices(instance : VrpInstance) -> Array[Int] {
  Array::makei(instance.customer_count(), fn(index) { index })
}

///|
pub fn empty_solution(instance : VrpInstance, config : SolveConfig) -> Solution {
  build_solution(instance, [], all_customer_indices(instance), config)
}

///|
pub fn singleton_solution(
  instance : VrpInstance,
  config : SolveConfig,
) -> Solution {
  let routes = all_customer_indices(instance).map(fn(index) { [index] })
  build_solution(instance, routes, [], config)
}