///|
fn absolute(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
fn average(values : Array[Int]) -> Int {
  if values.length() == 0 {
    return 0
  }
  let mut sum = 0
  for value in values {
    sum = sum + value
  }
  sum / values.length()
}

///|
fn deviation_cost(values : Array[Int]) -> Int {
  let center = average(values)
  let mut cost = 0
  for value in values {
    cost = cost + absolute(value - center)
  }
  cost
}

///|
fn workload_values(
  problem : PlanningProblem,
  schedule : Schedule,
) -> Array[Int] {
  let values : Array[Int] = []
  for worker in problem.workers {
    values.push(worker_assigned_minutes(problem, schedule, worker.id))
  }
  values
}

///|
fn night_values(problem : PlanningProblem, schedule : Schedule) -> Array[Int] {
  let values : Array[Int] = []
  for worker in problem.workers {
    values.push(worker_night_count(problem, schedule, worker.id))
  }
  values
}

///|
fn weekend_values(problem : PlanningProblem, schedule : Schedule) -> Array[Int] {
  let values : Array[Int] = []
  for worker in problem.workers {
    values.push(worker_weekend_count(problem, schedule, worker.id))
  }
  values
}

///|
fn preference_cost(problem : PlanningProblem, schedule : Schedule) -> Int {
  let mut cost = 0
  for assignment in schedule.assignments {
    match problem.worker(assignment.worker_id) {
      Some(worker) =>
        match worker.preference_for(assignment.shift_id) {
          Some(preference) =>
            match preference.kind {
              Prefer => cost = cost - preference.weight
              Avoid => cost = cost + preference.weight
              Require => cost = cost - preference.weight * 2
              Unavailable => cost = cost + preference.weight * 100
            }
          None => ()
        }
      None => ()
    }
  }
  for worker in problem.workers {
    for preference in worker.preferences {
      if preference.kind == Require &&
        !schedule.contains(worker.id, preference.shift_id) {
        cost = cost + preference.weight * 100
      }
    }
  }
  cost
}

///|
fn overtime_cost(problem : PlanningProblem, schedule : Schedule) -> Int {
  let mut cost = 0
  for worker in problem.workers {
    let minutes = worker_assigned_minutes(problem, schedule, worker.id)
    if minutes > worker.target_minutes {
      cost = cost + minutes - worker.target_minutes
    }
    if minutes < worker.min_minutes {
      cost = cost + worker.min_minutes - minutes
    }
  }
  cost
}

///|
fn coverage_cost(report : ConstraintReport, kind : ViolationKind) -> Int {
  let mut cost = 0
  for violation in report.violations {
    if violation.kind == kind {
      cost = cost + violation.magnitude
    }
  }
  cost
}

///|
pub fn score_schedule(
  problem : PlanningProblem,
  schedule : Schedule,
) -> ScoreBreakdown {
  let report = evaluate_constraints(problem, schedule)
  let understaffing = coverage_cost(report, Understaffed) *
    problem.weights.understaffing
  let skill_shortage = coverage_cost(report, SkillShortage) *
    problem.weights.understaffing
  let workload_imbalance = deviation_cost(workload_values(problem, schedule)) *
    problem.weights.workload
  let night_imbalance = deviation_cost(night_values(problem, schedule)) *
    problem.weights.night
  let weekend_imbalance = deviation_cost(weekend_values(problem, schedule)) *
    problem.weights.weekend
  let preference_cost = preference_cost(problem, schedule) *
    problem.weights.preference
  let overtime_cost = overtime_cost(problem, schedule) *
    problem.weights.overtime
  let total = understaffing +
    skill_shortage +
    workload_imbalance +
    night_imbalance +
    weekend_imbalance +
    preference_cost +
    overtime_cost
  {
    total,
    understaffing,
    skill_shortage,
    workload_imbalance,
    night_imbalance,
    weekend_imbalance,
    preference_cost,
    overtime_cost,
  }
}

///|
pub fn assignment_delta(
  problem : PlanningProblem,
  schedule : Schedule,
  worker_id : String,
  shift_id : String,
) -> Int {
  let before = score_schedule(problem, schedule).total
  let after = score_schedule(
      problem,
      schedule.add(Assignment::new(worker_id, shift_id)),
    ).total
  after - before
}

///|
pub struct WorkerSummary {
  worker_id : String
  assigned_minutes : Int
  target_minutes : Int
  night_shifts : Int
  weekend_shifts : Int
  preference_cost : Int
} derive(Eq, Show, ToJson)

///|
pub fn summarize_worker(
  problem : PlanningProblem,
  schedule : Schedule,
  worker_id : String,
) -> WorkerSummary? {
  match problem.worker(worker_id) {
    None => None
    Some(worker) => {
      let single = Schedule::empty()
      for assignment in schedule.assignments {
        if assignment.worker_id == worker_id {
          single.assignments.push(assignment)
        }
      }
      Some({
        worker_id,
        assigned_minutes: worker_assigned_minutes(problem, schedule, worker_id),
        target_minutes: worker.target_minutes,
        night_shifts: worker_night_count(problem, schedule, worker_id),
        weekend_shifts: worker_weekend_count(problem, schedule, worker_id),
        preference_cost: preference_cost(problem, single),
      })
    }
  }
}

///|
pub fn workload_range(problem : PlanningProblem, schedule : Schedule) -> Int {
  if problem.workers.length() == 0 {
    return 0
  }
  let values = workload_values(problem, schedule)
  let mut minimum = values[0]
  let mut maximum = values[0]
  for value in values {
    if value < minimum {
      minimum = value
    }
    if value > maximum {
      maximum = value
    }
  }
  maximum - minimum
}

///|
pub fn coverage_ratio_per_mille(
  problem : PlanningProblem,
  schedule : Schedule,
) -> Int {
  let mut required = 0
  let mut covered = 0
  for shift in problem.shifts {
    required = required + shift.required_workers
    let count = assignment_count_for_shift(schedule, shift.id)
    if count < shift.required_workers {
      covered = covered + count
    } else {
      covered = covered + shift.required_workers
    }
  }
  if required == 0 {
    1000
  } else {
    covered * 1000 / required
  }
}