///|
pub struct Candidate {
  name : String
  variables : Array[Float]
  score : Float
} derive(Debug, Eq)

///|
pub fn candidate(
  name : String,
  variables : Array[Float],
  score : Float,
) -> Candidate {
  { name, variables, score }
}

///|
pub fn Candidate::variable(self : Candidate, index : Int) -> Float? {
  self.variables.get(index)
}

///|
pub fn Candidate::dimension(self : Candidate) -> Int {
  self.variables.length()
}

///|
pub fn Candidate::is_finite(self : Candidate) -> Bool {
  self.score == self.score &&
  self.variables.filter(fn(value) { value != value }).length() == 0
}

///|

///|
pub struct Constraint {
  index : Int
  lower : Float
  upper : Float
} derive(Debug, Eq)

///|
pub fn constraint(index : Int, lower : Float, upper : Float) -> Constraint {
  { index, lower, upper }
}

///|
pub fn Constraint::contains(self : Constraint, candidate : Candidate) -> Bool {
  match candidate.variable(self.index) {
    Some(value) => value >= self.lower && value <= self.upper
    None => false
  }
}

///|
pub fn feasible(candidate : Candidate, constraints : Array[Constraint]) -> Bool {
  if !candidate.is_finite() {
    false
  } else {
    for constraint in constraints {
      if !constraint.contains(candidate) {
        return false
      }
    }
    true
  }
}

///|
pub fn optimize(
  candidates : Array[Candidate],
  constraints : Array[Constraint],
) -> Candidate? {
  let mut best : Candidate? = None
  for candidate in candidates {
    if feasible(candidate, constraints) {
      match best {
        Some(current) =>
          if candidate.score > current.score {
            best = Some(candidate)
          }
        None => best = Some(candidate)
      }
    }
  }
  best
}

///|
pub fn evaluate_candidates(
  candidates : Array[Candidate],
  constraints : Array[Constraint],
) -> Array[Candidate] {
  let result : Array[Candidate] = []
  for candidate in candidates {
    if feasible(candidate, constraints) {
      result.push(candidate)
    }
  }
  result
}

///|
pub fn best_candidate(candidates : Array[Candidate]) -> Candidate? {
  optimize(candidates, [])
}

///|
pub fn worst_candidate(candidates : Array[Candidate]) -> Candidate? {
  if candidates.length() == 0 {
    None
  } else {
    let mut result = candidates[0]
    for candidate in candidates[1:] {
      if candidate.score < result.score {
        result = candidate
      }
    }
    Some(result)
  }
}

///|
pub fn pareto_front(candidates : Array[Candidate]) -> Array[Candidate] {
  let result : Array[Candidate] = []
  for candidate in candidates {
    let mut dominated = false
    for other in candidates {
      if other.score > candidate.score &&
        other.dimension() == candidate.dimension() {
        dominated = true
      }
    }
    if !dominated {
      result.push(candidate)
    }
  }
  result
}

///|
pub fn score_range(candidates : Array[Candidate]) -> Float {
  if candidates.length() == 0 {
    0.0
  } else {
    let mut minimum = candidates[0].score
    let mut maximum = candidates[0].score
    for candidate in candidates {
      if candidate.score < minimum {
        minimum = candidate.score
      }
      if candidate.score > maximum {
        maximum = candidate.score
      }
    }
    maximum - minimum
  }
}

///|
pub fn normalize_scores(candidates : Array[Candidate]) -> Array[Candidate] {
  let result : Array[Candidate] = []
  let stats = summarize(candidates.map(fn(item) { item.score }))
  for candidate in candidates {
    let normalized : Float = if stats.maximum == stats.minimum {
      0.0
    } else {
      (candidate.score - stats.minimum) / (stats.maximum - stats.minimum)
    }
    result.push({ ..candidate, score: normalized })
  }
  result
}

///|
pub fn candidate_table(candidates : Array[Candidate]) -> ReportTable {
  let rows : Array[Array[String]] = []
  for candidate in candidates {
    rows.push([
      candidate.name,
      "{candidate.score}",
      "{candidate.dimension()}",
      "{candidate.is_finite()}",
    ])
  }
  table(["candidate", "score", "dimensions", "finite"], rows)
}

///|
pub struct WeightedObjective {
  weights : Array[Float]
  offset : Float
} derive(Debug, Eq)

///|
pub fn weighted_objective(
  weights : Array[Float],
  offset : Float,
) -> WeightedObjective {
  { weights, offset }
}

///|
pub fn WeightedObjective::evaluate(
  self : WeightedObjective,
  variables : Array[Float],
) -> Float {
  let mut score : Float = self.offset
  for index, weight in self.weights {
    if index < variables.length() {
      score = score + weight * variables[index]
    }
  }
  score
}

///|
pub fn WeightedObjective::rank(
  self : WeightedObjective,
  candidates : Array[Candidate],
) -> Array[Candidate] {
  let result = candidates.map(fn(candidate) {
    { ..candidate, score: self.evaluate(candidate.variables) }
  })
  result.sort_by(fn(left, right) {
    if left.score > right.score {
      -1
    } else if left.score < right.score {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn constraint_violation(
  candidate : Candidate,
  constraints : Array[Constraint],
) -> Float {
  let mut total : Float = 0.0
  for item in constraints {
    match candidate.variable(item.index) {
      Some(value) =>
        if value < item.lower {
          total = total + item.lower - value
        } else if value > item.upper {
          total = total + value - item.upper
        }
      None => total = total + item.upper - item.lower
    }
  }
  total
}

///|
pub fn penalty_score(
  candidate : Candidate,
  constraints : Array[Constraint],
  penalty : Float,
) -> Float {
  candidate.score - penalty * constraint_violation(candidate, constraints)
}

///|
pub fn penalized_best(
  candidates : Array[Candidate],
  constraints : Array[Constraint],
  penalty : Float,
) -> Candidate? {
  let scored = candidates.map(fn(candidate) {
    { ..candidate, score: penalty_score(candidate, constraints, penalty) }
  })
  best_candidate(scored)
}

///|
pub fn sweep_candidates(
  config : SweepConfig,
  objective : WeightedObjective,
) -> Array[Candidate] {
  let result : Array[Candidate] = []
  for value in config.values() {
    let variables = [value]
    result.push(
      candidate("{config.label}", variables, objective.evaluate(variables)),
    )
  }
  result
}

///|
pub fn optimization_report(
  candidates : Array[Candidate],
  constraints : Array[Constraint],
) -> ReportSection {
  let feasible_candidates = evaluate_candidates(candidates, constraints)
  {
    title: "Optimization",
    kind: Method,
    body: candidate_table(feasible_candidates).to_markdown(),
  }
}