///|
/// Neighborhoods implemented by the local improvement layer.
pub(all) enum NeighborhoodKind {
  TwoOpt
  Relocate
  Swap
  Reverse
} derive(Debug, Eq, ToJson)

///|
pub struct ImprovementReport {
  solution : @core.Solution
  rounds : Int
  moves : Int
  start_objective : Double
  final_objective : Double
} derive(Debug, ToJson)

///|
pub fn ImprovementReport::new(
  solution : @core.Solution,
  rounds : Int,
  moves : Int,
  start_objective : Double,
  final_objective : Double,
) -> ImprovementReport {
  { solution, rounds, moves, start_objective, final_objective }
}

///|
pub fn ImprovementReport::solution(self : ImprovementReport) -> @core.Solution {
  self.solution.copy()
}

///|
pub fn ImprovementReport::rounds(self : ImprovementReport) -> Int {
  self.rounds
}

///|
pub fn ImprovementReport::moves(self : ImprovementReport) -> Int {
  self.moves
}

///|
pub fn ImprovementReport::start_objective(self : ImprovementReport) -> Double {
  self.start_objective
}

///|
pub fn ImprovementReport::final_objective(self : ImprovementReport) -> Double {
  self.final_objective
}

///|
fn accept_improvement(
  current : @core.Solution,
  candidate : @core.Solution,
) -> @core.Solution {
  if candidate.is_feasible() &&
    candidate.objective_value() < current.objective_value() {
    candidate
  } else {
    current
  }
}

///|
/// Searches all single-customer relocations and keeps the best strict improvement.
pub fn best_relocate(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  config : @core.SolveConfig,
) -> @core.Solution {
  let mut best = solution
  for from_route in 0.. ()
      Some(route) =>
        for from_position in 0.. ()
              Some(target) =>
                for to_position in 0..<=target.stop_count() {
                  match
                    @core.relocate_customer(
                      instance, solution, from_route, from_position, to_route, to_position,
                      config,
                    ) {
                    None => ()
                    Some(candidate) =>
                      best = accept_improvement(best, candidate)
                  }
                }
            }
          }
        }
    }
  }
  best
}

///|
/// Searches all swaps between route positions and keeps the best improvement.
pub fn best_swap(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  config : @core.SolveConfig,
) -> @core.Solution {
  let mut best = solution
  for left_route in 0.. ()
      Some(left_plan) =>
        for left_position in 0.. ()
              Some(right_plan) => {
                let start_position = if right_route == left_route {
                  left_position + 1
                } else {
                  0
                }
                for right_position in start_position.. ()
                    Some(candidate) =>
                      best = accept_improvement(best, candidate)
                  }
                }
              }
            }
          }
        }
    }
  }
  best
}

///|
/// Searches a complete route reversal neighborhood.
pub fn best_reverse(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  config : @core.SolveConfig,
) -> @core.Solution {
  let mut best = solution
  for route_index in 0.. ()
      Some(candidate) => best = accept_improvement(best, candidate)
    }
  }
  best
}

///|
/// Runs a bounded variable-neighborhood descent over the core move operators.
pub fn variable_neighborhood_descent(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  config : @core.SolveConfig,
  max_rounds : Int,
) -> @core.Solution {
  let mut current = solution
  let rounds = max_rounds.max(0)
  for _ in 0..= before {
      break
    }
  }
  current
}

///|
/// Same search as `variable_neighborhood_descent`, with useful run statistics.
pub fn variable_neighborhood_report(
  instance : @core.VrpInstance,
  solution : @core.Solution,
  config : @core.SolveConfig,
  max_rounds : Int,
) -> ImprovementReport {
  let start = solution.objective_value()
  let improved = variable_neighborhood_descent(
    instance, solution, config, max_rounds,
  )
  let finish = improved.objective_value()
  let moves = if finish < start { 1 } else { 0 }
  ImprovementReport::new(improved, max_rounds.max(0), moves, start, finish)
}