///|
pub(all) struct LinearConstraint {
  name : String
  coefficients : Array[Double]
  upper : Double
} derive(Debug, Eq)

///|
pub extend LinearConstraint with @debug.Debug::{to_repr}

///|
pub extend LinearConstraint with Eq::{equal, not_equal}

///|
pub fn filter_candidates(
  p : Plan,
  constraints : Array[LinearConstraint],
  tol~ : Double = 1.0e-10,
) -> Plan raise DesignError {
  tolerance(tol)
  validate_plan(p)
  if constraints.length() > 64 {
    raise Budget("constraint count exceeds 64")
  }
  for c in constraints {
    if c.name.length() == 0 || c.coefficients.length() != p.factors.length() {
      raise Invalid("constraint name/dimension mismatch")
    }
    require_number(c.upper)
    for x in c.coefficients {
      require_number(x)
    }
  }
  let runs = []
  for r in p.runs {
    let mut passes = true
    for c in constraints {
      let mut value = 0.0
      for j, x in r.point {
        value += c.coefficients[j] * x
      }
      require_number(value)
      if value > c.upper + tol {
        passes = false
        break
      }
    }
    if passes {
      runs.push(r)
    }
  }
  if runs.length() == 0 {
    raise Invalid("no feasible candidate runs")
  }
  plan(p.factors, runs)
}

///|
pub fn filter_points(
  p : Plan,
  predicate : (Array[Double]) -> Bool,
) -> Plan raise DesignError {
  validate_plan(p)
  let runs = []
  for r in p.runs {
    if predicate(r.point.copy()) {
      runs.push(r)
    }
  }
  if runs.length() == 0 {
    raise Invalid("predicate removed all candidates")
  }
  plan(p.factors, runs)
}

///|
pub fn unique_points(p : Plan) -> Plan raise DesignError {
  validate_plan(p)
  let runs : Array[Run] = []
  for r in p.runs {
    if !runs.any(fn(other) { other.point == r.point }) {
      runs.push(r)
    }
  }
  plan(p.factors, runs)
}