///|
struct WeightTable {
  destroy_weights : Array[Double]
  repair_weights : Array[Double]
} derive(Debug)

///|
fn destroy_operator_at(index : Int) -> @core.DestroyOperator {
  match index {
    0 => @core.DestroyOperator::Random
    1 => @core.DestroyOperator::Shaw
    2 => @core.DestroyOperator::Worst
    3 => @core.DestroyOperator::Route
    4 => @core.DestroyOperator::Cluster
    _ => @core.DestroyOperator::TimeCritical
  }
}

///|
fn repair_operator_at(index : Int) -> @core.RepairOperator {
  match index {
    0 => @core.RepairOperator::Greedy
    1 => @core.RepairOperator::Regret2
    2 => @core.RepairOperator::DemandFirst
    _ => @core.RepairOperator::TimeWindow
  }
}

///|
fn roulette_pick(weights : Array[Double], rng : @rng.Rng) -> Int {
  let total = weights.fold(init=0.0, fn(sum, item) { sum + item })
  if total <= 0.0 {
    return 0
  }
  let threshold = rng.next_double() * total
  let mut cumulative = 0.0
  for index in 0.. Int {
  let customer_count = instance.customer_count()
  let lower = config.min_destroy.max(1).min(customer_count)
  let upper = config.max_destroy.max(lower).min(customer_count)
  if upper == lower {
    lower
  } else {
    lower + rng.next_int(upper - lower + 1)
  }
}

///|
fn reward_for(
  candidate : @core.Solution,
  current : @core.Solution,
  best : @core.Solution,
) -> Double {
  if candidate.objective < best.objective {
    9.0
  } else if candidate.objective < current.objective {
    5.0
  } else if candidate.feasible {
    2.0
  } else {
    0.6
  }
}

///|
fn accept_candidate(
  candidate : @core.Solution,
  current : @core.Solution,
  temperature : Double,
  rng : @rng.Rng,
) -> Bool {
  if candidate.objective <= current.objective {
    true
  } else {
    let delta = candidate.objective - current.objective
    let probability = @math.exp(-(delta / temperature.max(0.001)))
    rng.next_double() < probability
  }
}

///|
pub fn solve_with_seed(
  instance : @core.VrpInstance,
  seed_solution : @core.Solution,
  config? : @core.SolveConfig = @core.SolveConfig::default(),
) -> @core.SolveResult {
  let mut current = seed_solution
  let mut best = seed_solution
  let mut temperature = config.initial_temperature
  let weights = {
    destroy_weights: [1.0, 1.0, 1.0, 0.6, 0.6, 0.6],
    repair_weights: [1.0, 1.0, 0.6, 0.6],
  }
  let rng = @rng.Rng::new(
    instance.customer_count() * 7919 +
    config.iterations * 97 +
    current.routes.length() * 31,
  )
  let mut accepted_moves = 0
  let mut improved_moves = 0
  let mut local_improvements = 0
  let mut evaluated_candidates = 0
  let mut best_iteration = 0
  for iteration in 0.. 0 {
      local_search(
        instance,
        repaired,
        config,
        LocalSearchConfig::default().tune(max_rounds=config.local_search_rounds),
      )
    } else {
      repaired
    }
    if candidate.objective < repaired.objective {
      local_improvements += 1
    }
    evaluated_candidates += 1
    if accept_candidate(candidate, current, temperature, rng) {
      current = candidate
      accepted_moves += 1
      if current.objective < best.objective {
        best = current
        improved_moves += 1
        best_iteration = iteration + 1
      }
      let reward = reward_for(current, seed_solution, best)
      weights.destroy_weights[destroy_index] = 0.75 *
        weights.destroy_weights[destroy_index] +
        0.25 * reward
      weights.repair_weights[repair_index] = 0.75 *
        weights.repair_weights[repair_index] +
        0.25 * reward
    } else {
      weights.destroy_weights[destroy_index] = 0.9 *
        weights.destroy_weights[destroy_index] +
        0.1 * 0.5
      weights.repair_weights[repair_index] = 0.9 *
        weights.repair_weights[repair_index] +
        0.1 * 0.5
    }
    temperature *= config.cooling_rate
  }
  let stats = @core.SolveStats::new(
    config.iterations,
    accepted_moves,
    improved_moves,
    best_iteration,
    seed_solution.objective,
    best.objective,
    local_improvements~,
    evaluated_candidates~,
  )
  @core.SolveResult::new(instance, seed_solution, best, stats)
}

///|
pub fn solve(
  instance : @core.VrpInstance,
  config? : @core.SolveConfig = @core.SolveConfig::default(),
) -> @core.SolveResult {
  let seed_solution = @constructive.clarke_wright_seed(instance, config~)
  solve_with_seed(instance, seed_solution, config~)
}