///|
/// Aggregated route and assignment metrics suitable for dashboards and reports.
pub struct ConstraintSummary {
  route_count : Int
  assigned_count : Int
  unassigned_count : Int
  total_distance : Double
  total_duration : Double
  total_waiting : Double
  total_load : Double
  capacity_utilization : Double
  maximum_lateness : Double
  feasible : Bool
} derive(Debug, ToJson)

///|
pub fn ConstraintSummary::route_count(self : ConstraintSummary) -> Int {
  self.route_count
}

///|
pub fn ConstraintSummary::assigned_count(self : ConstraintSummary) -> Int {
  self.assigned_count
}

///|
pub fn ConstraintSummary::unassigned_count(self : ConstraintSummary) -> Int {
  self.unassigned_count
}

///|
pub fn ConstraintSummary::total_distance(self : ConstraintSummary) -> Double {
  self.total_distance
}

///|
pub fn ConstraintSummary::total_duration(self : ConstraintSummary) -> Double {
  self.total_duration
}

///|
pub fn ConstraintSummary::total_waiting(self : ConstraintSummary) -> Double {
  self.total_waiting
}

///|
pub fn ConstraintSummary::total_load(self : ConstraintSummary) -> Double {
  self.total_load
}

///|
pub fn ConstraintSummary::capacity_utilization(
  self : ConstraintSummary,
) -> Double {
  self.capacity_utilization
}

///|
pub fn ConstraintSummary::maximum_lateness(self : ConstraintSummary) -> Double {
  self.maximum_lateness
}

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

///|
/// A normalized view of one route's resource consumption.
pub struct RouteProfile {
  stop_count : Int
  load_ratio : Double
  distance : Double
  duration : Double
  waiting : Double
  lateness : Double
  slack : Double
  feasible : Bool
} derive(Debug, ToJson)

///|
pub fn RouteProfile::stop_count(self : RouteProfile) -> Int {
  self.stop_count
}

///|
pub fn RouteProfile::load_ratio(self : RouteProfile) -> Double {
  self.load_ratio
}

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

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

///|
pub fn RouteProfile::waiting(self : RouteProfile) -> Double {
  self.waiting
}

///|
pub fn RouteProfile::lateness(self : RouteProfile) -> Double {
  self.lateness
}

///|
pub fn RouteProfile::slack(self : RouteProfile) -> Double {
  self.slack
}

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

///|
pub fn route_profile(instance : VrpInstance, route : RoutePlan) -> RouteProfile {
  let capacity = instance.vehicle.capacity
  let load_ratio = if capacity <= 0.0 {
    0.0
  } else {
    route.metrics.load / capacity
  }
  let lateness = route.metrics.time_violation
  let duration_slack = instance.vehicle.max_route_duration -
    route.metrics.duration
  {
    stop_count: route.stops.length(),
    load_ratio,
    distance: route.metrics.distance,
    duration: route.metrics.duration,
    waiting: route.metrics.waiting_time,
    lateness,
    slack: duration_slack.max(0.0),
    feasible: route.metrics.feasible,
  }
}

///|
pub fn Solution::constraint_summary(
  self : Solution,
  instance : VrpInstance,
) -> ConstraintSummary {
  let mut total_duration = 0.0
  let mut total_waiting = 0.0
  let mut total_load = 0.0
  let mut maximum_lateness = 0.0
  for route in self.routes {
    total_duration += route.metrics.duration
    total_waiting += route.metrics.waiting_time
    total_load += route.metrics.load
    maximum_lateness = Double::max(
      maximum_lateness,
      route.metrics.time_violation,
    )
  }
  let fleet_capacity = instance.vehicle.capacity *
    Double::from_int(instance.vehicle.fleet_size)
  let utilization = if fleet_capacity <= 0.0 {
    0.0
  } else {
    total_load / fleet_capacity
  }
  {
    route_count: self.routes.length(),
    assigned_count: self.assigned_customers().length(),
    unassigned_count: self.unassigned.length(),
    total_distance: self.total_distance,
    total_duration,
    total_waiting,
    total_load,
    capacity_utilization: utilization,
    maximum_lateness,
    feasible: self.feasible,
  }
}

///|
pub fn Solution::route_profiles(
  self : Solution,
  instance : VrpInstance,
) -> Array[RouteProfile] {
  self.routes.map(fn(route) { route_profile(instance, route) })
}

///|
pub fn Solution::max_time_violation(self : Solution) -> Double {
  self.routes.fold(init=0.0, fn(current, route) {
    Double::max(current, route.metrics.time_violation)
  })
}

///|
pub fn Solution::max_capacity_violation(self : Solution) -> Double {
  self.routes.fold(init=0.0, fn(current, route) {
    Double::max(current, route.metrics.capacity_violation)
  })
}

///|
pub fn Solution::route_loads(self : Solution) -> Array[Double] {
  self.routes.map(fn(route) { route.metrics.load })
}

///|
pub fn Solution::route_distances(self : Solution) -> Array[Double] {
  self.routes.map(fn(route) { route.metrics.distance })
}

///|
pub fn Solution::route_durations(self : Solution) -> Array[Double] {
  self.routes.map(fn(route) { route.metrics.duration })
}

///|
pub fn Solution::completion_ratio(
  self : Solution,
  instance : VrpInstance,
) -> Double {
  let total = instance.customer_count()
  if total == 0 {
    1.0
  } else {
    Double::from_int(self.assigned_customers().length()) /
    Double::from_int(total)
  }
}

///|
pub fn RoutePlan::last_customer(self : RoutePlan) -> Int? {
  self.stops.last()
}

///|
pub fn RoutePlan::first_customer(self : RoutePlan) -> Int? {
  self.stops.get(0)
}

///|
pub fn VrpInstance::fleet_capacity(self : VrpInstance) -> Double {
  self.vehicle.capacity * Double::from_int(self.vehicle.fleet_size)
}

///|
pub fn VrpInstance::service_horizon(self : VrpInstance) -> Double {
  self.depot.due_time - self.depot.ready_time
}

///|
pub fn RouteMetrics::has_violation(self : RouteMetrics) -> Bool {
  self.capacity_violation > 0.0 ||
  self.time_violation > 0.0 ||
  self.duration_violation > 0.0
}