///|
/// A bounded variable in a process design space.
pub(all) struct DesignVariable {
  name : String
  lower : Double
  upper : Double
  samples : Int
} derive(Debug, Eq, ToJson)

///|
pub fn DesignVariable::new(
  name~ : String,
  lower~ : Double,
  upper~ : Double,
  samples~ : Int,
) -> DesignVariable raise @domain.ExtractionError {
  if name.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="design variable name cannot be blank",
    )
  }
  @domain.require_finite("design lower", lower)
  @domain.require_finite("design upper", upper)
  if upper < lower || samples < 1 {
    raise @domain.ExtractionError::InvalidArgument(
      message="design variable bounds or samples are invalid",
    )
  }
  { name, lower, upper, samples }
}

///|
pub fn DesignVariable::values(self : DesignVariable) -> Array[Double] {
  if self.samples == 1 {
    [self.lower]
  } else {
    [
      for index in 0.. {
        self.lower +
        (self.upper - self.lower) *
        index.to_double() /
        (self.samples - 1).to_double()
      }
    ]
  }
}

///|
pub fn DesignVariable::midpoint(self : DesignVariable) -> Double {
  (self.lower + self.upper) / 2.0
}

///|
pub fn DesignVariable::contains(self : DesignVariable, value : Double) -> Bool {
  value >= self.lower && value <= self.upper
}

///|
pub(all) enum ConstraintKind {
  AtLeast(target~ : Double)
  AtMost(target~ : Double)
  Equal(target~ : Double, tolerance~ : Double)
} derive(Debug, Eq, ToJson)

///|
pub(all) struct DesignConstraint {
  name : String
  kind : ConstraintKind
} derive(Debug, Eq, ToJson)

///|
pub fn DesignConstraint::new(
  name~ : String,
  kind~ : ConstraintKind,
) -> DesignConstraint raise @domain.ExtractionError {
  if name.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="constraint name cannot be blank",
    )
  }
  match kind {
    AtLeast(target~) => @domain.require_finite("constraint target", target)
    AtMost(target~) => @domain.require_finite("constraint target", target)
    Equal(target~, tolerance~) => {
      @domain.require_finite("constraint target", target)
      @domain.require_positive("constraint tolerance", tolerance)
    }
  }
  { name, kind }
}

///|
pub fn DesignConstraint::violation(
  self : DesignConstraint,
  value : Double,
) -> Double {
  match self.kind {
    AtLeast(target~) => (target - value).max(0.0)
    AtMost(target~) => (value - target).max(0.0)
    Equal(target~, tolerance~) =>
      (value - target).abs().max(tolerance) - tolerance
  }
}

///|
pub fn DesignConstraint::passes(
  self : DesignConstraint,
  value : Double,
) -> Bool {
  self.violation(value) <= 0.0
}

///|
pub(all) struct DesignCandidate {
  values : Array[Double]
  objective : Double
  constraint_values : Array[Double]
  total_violation : Double
  feasible : Bool
  rank : Int
} derive(Debug, Eq, ToJson)

///|
pub fn DesignCandidate::new(
  values~ : Array[Double],
  objective~ : Double,
  constraint_values~ : Array[Double],
  constraints~ : ArrayView[DesignConstraint],
) -> DesignCandidate raise @domain.ExtractionError {
  @domain.require_finite("design objective", objective)
  if constraint_values.length() != constraints.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="constraint value and definition dimensions differ",
    )
  }
  let mut violation = 0.0
  for index, constraint in constraints {
    @domain.require_finite("constraint value", constraint_values[index])
    violation += constraint.violation(constraint_values[index])
  }
  {
    values,
    objective,
    constraint_values,
    total_violation: violation,
    feasible: violation <= 0.0,
    rank: 0,
  }
}

///|
pub fn DesignCandidate::penalized_objective(
  self : DesignCandidate,
  penalty~ : Double,
) -> Double raise @domain.ExtractionError {
  @domain.require_non_negative("constraint penalty", penalty)
  self.objective + penalty * self.total_violation * self.total_violation
}

///|
pub fn DesignCandidate::dominates(
  self : DesignCandidate,
  other : DesignCandidate,
) -> Bool {
  let self_feasible = self.feasible
  let other_feasible = other.feasible
  if self_feasible && !other_feasible {
    true
  } else if !self_feasible && other_feasible {
    false
  } else if !self_feasible && !other_feasible {
    self.total_violation < other.total_violation
  } else {
    self.objective <= other.objective &&
    (self.objective < other.objective || self.values != other.values)
  }
}

///|
pub(all) struct DesignSpace {
  variables : Array[DesignVariable]
  constraints : Array[DesignConstraint]
} derive(Debug, Eq, ToJson)

///|
pub fn DesignSpace::new(
  variables~ : ArrayView[DesignVariable],
  constraints? : ArrayView[DesignConstraint] = [],
) -> DesignSpace raise @domain.ExtractionError {
  if variables.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  { variables: variables.to_owned(), constraints: constraints.to_owned() }
}

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

///|
pub fn DesignSpace::variables(self : DesignSpace) -> Array[DesignVariable] {
  self.variables.copy()
}

///|
pub fn DesignSpace::constraints(self : DesignSpace) -> Array[DesignConstraint] {
  self.constraints.copy()
}

///|
fn enumerate_values(
  variables : ArrayView[DesignVariable],
  dimension : Int,
  prefix : Array[Double],
  output : Array[Array[Double]],
) -> Unit {
  if dimension == variables.length() {
    output.push(prefix)
  } else {
    for value in variables[dimension].values() {
      let next = prefix.copy()
      next.push(value)
      enumerate_values(variables, dimension + 1, next, output)
    }
  }
}

///|
pub fn DesignSpace::grid(self : DesignSpace) -> Array[Array[Double]] {
  let output : Array[Array[Double]] = []
  enumerate_values(self.variables, 0, [], output)
  output
}

///|
pub(all) struct OptimizationSummary {
  candidates : Array[DesignCandidate]
  feasible_count : Int
  best : DesignCandidate?
  pareto_front : Array[DesignCandidate]
  evaluated : Int
} derive(Debug, Eq, ToJson)

///|
fn sorted_by_objective(
  candidates : ArrayView[DesignCandidate],
) -> Array[DesignCandidate] {
  let result : Array[DesignCandidate] = []
  for candidate in candidates {
    let mut position = result.length()
    for index, other in result {
      if candidate.objective < other.objective {
        position = index
        break
      }
    }
    result.push(candidate)
    let mut index = result.length() - 1
    while index > position {
      let value = result[index]
      result[index] = result[index - 1]
      result[index - 1] = value
      index -= 1
    }
  }
  result
}

///|
pub fn pareto_candidates(
  candidates : ArrayView[DesignCandidate],
) -> Array[DesignCandidate] {
  let result : Array[DesignCandidate] = []
  for index, candidate in candidates {
    let mut dominated = false
    for other_index, other in candidates {
      if index != other_index && other.dominates(candidate) {
        dominated = true
      }
    }
    if !dominated {
      result.push(candidate)
    }
  }
  result
}

///|
pub fn evaluate_grid(
  space : DesignSpace,
  evaluate : (ArrayView[Double]) -> (Double, Array[Double]) raise @domain.ExtractionError,
) -> OptimizationSummary raise @domain.ExtractionError {
  let candidates : Array[DesignCandidate] = []
  for values in space.grid() {
    let (objective, constraint_values) = evaluate(values)
    candidates.push(
      DesignCandidate::new(
        values~,
        objective~,
        constraint_values~,
        constraints=space.constraints,
      ),
    )
  }
  let sorted = sorted_by_objective(candidates)
  let feasible = [ for candidate in sorted if candidate.feasible => candidate ]
  let best = feasible.get(0)
  {
    candidates: sorted,
    feasible_count: feasible.length(),
    best,
    pareto_front: pareto_candidates(sorted),
    evaluated: sorted.length(),
  }
}

///|
pub(all) struct CoordinateSearchResult {
  values : Array[Double]
  objective : Double
  iterations : Int
  evaluations : Int
  converged : Bool
} derive(Debug, Eq, ToJson)

///|
/// Coordinate search for a bounded, derivative-free design objective.
pub fn coordinate_search(
  variables : ArrayView[DesignVariable],
  objective : (ArrayView[Double]) -> Double raise @domain.ExtractionError,
  step_fraction? : Double = 0.25,
  maximum_iterations? : Int = 80,
) -> CoordinateSearchResult raise @domain.ExtractionError {
  if variables.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  @domain.require_positive("coordinate step fraction", step_fraction)
  if step_fraction >= 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="coordinate step fraction must be below one",
    )
  }
  let mut values = [ for variable in variables => variable.midpoint() ]
  let mut objective_value = objective(values)
  let mut step = step_fraction
  let mut evaluations = 1
  let mut converged = false
  let mut iterations = 0
  for iteration in 0.. DesignCandidate raise @domain.ExtractionError {
  if candidates.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let sorted = sorted_by_objective(candidates)
  for candidate in sorted {
    if candidate.feasible {
      return candidate
    }
  }
  let mut best = candidates[0]
  for candidate in candidates {
    if candidate.total_violation < best.total_violation {
      best = candidate
    }
  }
  best
}

///|
pub fn constraint_margin(
  _candidate : DesignCandidate,
  constraint : DesignConstraint,
  value : Double,
) -> Double {
  constraint.violation(value) * -1.0
}

///|
pub fn design_distance(
  first : ArrayView[Double],
  second : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
  if first.length() != second.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="design vectors have different dimensions",
    )
  }
  let mut sum = 0.0
  for index in 0.. Array[ConstraintAudit] raise @domain.ExtractionError {
  if constraints.length() != candidate.constraint_values.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="audit dimensions differ",
    )
  }
  [
    for index, constraint in constraints => {
      let violation = constraint.violation(candidate.constraint_values[index])
      {
        name: constraint.name,
        value: candidate.constraint_values[index],
        violation,
        margin: -violation,
        passed: violation <= 0.0,
      }
    }
  ]
}