///|
/// One lexicographic objective component.
pub struct ObjectiveTerm {
  variable : Int
  direction : OptimizationDirection
  weight : Int
}

///|
/// Create an objective component with a non-zero weight.
pub fn objective_term(
  variable : Int,
  direction : OptimizationDirection,
  weight : Int,
) -> ObjectiveTerm {
  { variable, direction, weight: if weight == 0 { 1 } else { weight } }
}

///|
/// Read an objective variable identifier.
pub fn ObjectiveTerm::variable(self : ObjectiveTerm) -> Int {
  self.variable
}

///|
/// Read an objective direction.
pub fn ObjectiveTerm::direction(self : ObjectiveTerm) -> OptimizationDirection {
  self.direction
}

///|
/// Read the objective weight.
pub fn ObjectiveTerm::weight(self : ObjectiveTerm) -> Int {
  self.weight
}

///|
/// A multi-component objective used by application adapters.
pub struct Objective {
  terms : Array[ObjectiveTerm]
}

///|
/// Construct an objective from ordered terms.
pub fn objective(terms : Array[ObjectiveTerm]) -> Objective {
  { terms: terms.copy() }
}

///|
/// Construct a one-variable objective.
pub fn single_objective(
  variable : Int,
  direction : OptimizationDirection,
) -> Objective {
  objective([objective_term(variable, direction, 1)])
}

///|
/// Append a lower-priority objective component.
pub fn Objective::then(
  self : Objective,
  variable : Int,
  direction : OptimizationDirection,
  weight : Int,
) -> Objective {
  self.terms.push(objective_term(variable, direction, weight))
  self
}

///|
/// Return objective terms.
pub fn Objective::terms(self : Objective) -> Array[ObjectiveTerm] {
  self.terms.copy()
}

///|
/// Return the tuple of objective values for a solution.
pub fn Objective::values(self : Objective, solution : Solution) -> Array[Int] {
  self.terms.map(term => solution.get(term.variable))
}

///|
/// Calculate a weighted scalar score.
pub fn Objective::score(self : Objective, solution : Solution) -> Int {
  let mut score = 0
  for term in self.terms {
    let value = solution.get(term.variable)
    match term.direction {
      MinimizeValue => score -= value * term.weight
      MaximizeValue => score += value * term.weight
    }
  }
  score
}

///|
/// Return whether `candidate` is preferred to `incumbent` lexicographically.
pub fn Objective::is_better(
  self : Objective,
  candidate : Solution,
  incumbent : Solution,
) -> Bool {
  for term in self.terms {
    let candidate_value = candidate.get(term.variable)
    let incumbent_value = incumbent.get(term.variable)
    if candidate_value == incumbent_value {
      continue
    }
    match term.direction {
      MinimizeValue => return candidate_value < incumbent_value
      MaximizeValue => return candidate_value > incumbent_value
    }
  }
  false
}

///|
/// Return the preferred solution from a non-empty array.
pub fn Objective::best(
  self : Objective,
  solutions : Array[Solution],
) -> Solution? {
  match solutions.get(0) {
    None => None
    Some(first) => {
      let mut best = first
      for solution in solutions {
        if self.is_better(solution, best) {
          best = solution
        }
      }
      Some(best)
    }
  }
}

///|
/// A score paired with a solution for reporting and ranking.
pub struct ScoredSolution {
  solution : Solution
  score : Int
  rank : Int
}

///|
/// Build a scored solution.
pub fn scored_solution(
  solution : Solution,
  score : Int,
  rank : Int,
) -> ScoredSolution {
  { solution, score, rank }
}

///|
/// Return the underlying solution.
pub fn ScoredSolution::solution(self : ScoredSolution) -> Solution {
  self.solution
}

///|
/// Return the scalar score.
pub fn ScoredSolution::score(self : ScoredSolution) -> Int {
  self.score
}

///|
/// Return the one-based display rank.
pub fn ScoredSolution::rank(self : ScoredSolution) -> Int {
  self.rank
}

///|
/// Return a stable score report.
pub fn ScoredSolution::describe(self : ScoredSolution) -> String {
  "rank=\{self.rank}, score=\{self.score}, values=\{Repr(self.solution.values)}"
}

///|
/// Enumerate and rank solutions by a multi-component objective.
pub fn Solver::ranked_solutions(
  self : Solver,
  objective : Objective,
  limit : Int,
) -> Array[ScoredSolution] {
  self.limit(if limit < 1 { 1 } else { limit })
  let solutions = self.solve_all()
  let ranked : Array[Solution] = solutions.copy()
  ranked.sort_by((left, right) => {
    if objective.is_better(left, right) {
      -1
    } else if objective.is_better(right, left) {
      1
    } else {
      0
    }
  })
  let result : Array[ScoredSolution] = []
  for index, solution in ranked {
    result.push(scored_solution(solution, objective.score(solution), index + 1))
  }
  result
}

///|
/// Return the best solution after bounded enumeration.
pub fn Solver::best_solution(
  self : Solver,
  objective : Objective,
  limit : Int,
) -> ScoredSolution? {
  self.ranked_solutions(objective, limit).get(0)
}

///|
/// Return whether a solution dominates another over all objective terms.
pub fn Objective::dominates(
  self : Objective,
  candidate : Solution,
  other : Solution,
) -> Bool {
  let mut strictly_better = false
  for term in self.terms {
    let left = candidate.get(term.variable)
    let right = other.get(term.variable)
    match term.direction {
      MinimizeValue => {
        if left > right {
          return false
        }
        if left < right {
          strictly_better = true
        }
      }
      MaximizeValue => {
        if left < right {
          return false
        }
        if left > right {
          strictly_better = true
        }
      }
    }
  }
  strictly_better
}

///|
/// Filter a solution set to its Pareto frontier.
pub fn Objective::pareto_front(
  self : Objective,
  solutions : Array[Solution],
) -> Array[Solution] {
  let result : Array[Solution] = []
  for candidate in solutions {
    let mut dominated = false
    for other in solutions {
      if self.dominates(other, candidate) {
        dominated = true
        break
      }
    }
    if !dominated {
      result.push(candidate)
    }
  }
  result
}

///|
/// Return a simple objective summary.
pub fn Objective::describe(self : Objective) -> String {
  let builder = StringBuilder()
  for index, term in self.terms {
    if index > 0 {
      builder.write_string(" then ")
    }
    let direction = match term.direction {
      MinimizeValue => "min"
      MaximizeValue => "max"
    }
    builder.write_string("\{direction}(v\{term.variable})*\{term.weight}")
  }
  builder.to_string()
}

///|
/// Build a lexicographic objective from parallel arrays.
pub fn objective_from_directions(
  variables : Array[Int],
  directions : Array[OptimizationDirection],
) -> Objective? {
  if variables.length() != directions.length() || variables.length() == 0 {
    return None
  }
  let terms : Array[ObjectiveTerm] = []
  for index in 0.. Int {
  if variables.length() != preferred.length() ||
    variables.length() != penalty.length() {
    return 2147483647
  }
  let mut score = 0
  for index in 0..