///|
/// A reusable batch of constraints that can be assembled before posting.
pub struct ConstraintSet {
  constraints : Array[Constraint]
}

///|
/// Create an empty constraint set.
pub fn constraint_set() -> ConstraintSet {
  { constraints: [] }
}

///|
/// Append one constraint.
pub fn ConstraintSet::add(
  self : ConstraintSet,
  constraint : Constraint,
) -> ConstraintSet {
  self.constraints.push(constraint)
  self
}

///|
/// Append every constraint from another set.
pub fn ConstraintSet::append(
  self : ConstraintSet,
  other : ConstraintSet,
) -> ConstraintSet {
  for constraint in other.constraints {
    self.constraints.push(constraint)
  }
  self
}

///|
/// Number of constraints in the set.
pub fn ConstraintSet::length(self : ConstraintSet) -> Int {
  self.constraints.length()
}

///|
/// Return a defensive copy.
pub fn ConstraintSet::constraints(self : ConstraintSet) -> Array[Constraint] {
  self.constraints.copy()
}

///|
/// Post every constraint and return the posted count.
pub fn ConstraintSet::post(self : ConstraintSet, solver : Solver) -> Int {
  for constraint in self.constraints {
    solver.add_constraint(constraint)
  }
  self.constraints.length()
}

///|
/// Render labels for a constraint batch.
pub fn ConstraintSet::labels(self : ConstraintSet) -> Array[String] {
  self.constraints.map(constraint => constraint_label(constraint))
}

///|
/// Generate pairwise inequality constraints for a variable group.
pub fn all_different_pairs(variables : Array[Int]) -> ConstraintSet {
  let result = constraint_set()
  for left in 0.. ConstraintSet {
  constraint_set()
  .add(all_different(variables))
  .append(all_different_pairs(variables))
}

///|
/// Generate a chain of strict inequalities.
pub fn increasing_chain(variables : Array[Int]) -> ConstraintSet {
  let result = constraint_set()
  if variables.length() < 2 {
    return result
  }
  for index in 0..<(variables.length() - 1) {
    ignore(result.add(less_than(variables[index], variables[index + 1])))
  }
  result
}

///|
/// Generate a chain of non-strict inequalities.
pub fn nondecreasing_chain(variables : Array[Int]) -> ConstraintSet {
  let result = constraint_set()
  if variables.length() < 2 {
    return result
  }
  for index in 0..<(variables.length() - 1) {
    ignore(result.add(less_equal(variables[index], variables[index + 1])))
  }
  result
}

///|
/// Generate a cyclic adjacency rule for a sequence.
pub fn cyclic_different(variables : Array[Int]) -> ConstraintSet {
  if variables.length() < 2 {
    return constraint_set()
  }
  let result = all_different_pairs(variables)
  ignore(result.add(not_equal(variables[0], variables[variables.length() - 1])))
  result
}

///|
/// Generate lower and upper bound constraints for one variable group.
pub fn group_bounds(
  variables : Array[Int],
  lower : Int,
  upper : Int,
) -> ConstraintSet {
  let result = constraint_set()
  for variable in variables {
    ignore(result.add(between(variable, lower, upper)))
  }
  result
}

///|
/// Generate an exact count and its useful at-most/at-least decomposition.
pub fn count_bundle(
  variables : Array[Int],
  value : Int,
  count : Int,
) -> ConstraintSet {
  constraint_set()
  .add(count_value(variables, value, count))
  .add(at_most_value(variables, value, count))
  .add(at_least_value(variables, value, count))
}

///|
/// Generate a one-hot Boolean encoding.
pub fn one_hot(indicators : Array[Int]) -> ConstraintSet {
  constraint_set()
  .add(sum(indicators, 1))
  .add(at_least_value(indicators, 1, 1))
  .add(at_most_value(indicators, 1, 1))
}

///|
/// Generate an exact-k Boolean cardinality encoding.
pub fn exactly_k(indicators : Array[Int], count : Int) -> ConstraintSet {
  constraint_set().add(sum(indicators, count))
}

///|
/// Generate a table relation for an array of variables.
pub fn allowed_rows(
  variables : Array[Int],
  rows : Array[Array[Int]],
) -> ConstraintSet {
  constraint_set().add(table(variables, rows))
}

///|
/// Generate row and column constraints for a rectangular matrix.
pub fn matrix_all_different(
  matrix : Array[Array[Int]],
  rows : Int,
  columns : Int,
) -> ConstraintSet? {
  if rows < 1 || columns < 1 || matrix.length() != rows {
    return None
  }
  for row in matrix {
    if row.length() != columns {
      return None
    }
  }
  let result = constraint_set()
  for row in matrix {
    ignore(result.add(all_different(row)))
  }
  for column in 0.. ConstraintSet? {
  if rows < 1 || columns < 1 || matrix.length() != rows {
    return None
  }
  for row in matrix {
    if row.length() != columns {
      return None
    }
  }
  let result = constraint_set()
  for row in matrix {
    ignore(result.add(sum(row, row_target)))
  }
  for column in 0.. ConstraintSet {
  let result = constraint_set()
  for left in 0.. ConstraintSet {
  constraint_set().add(no_overlap(intervals))
}

///|
/// Generate a cumulative capacity constraint.
pub fn capacity_constraint(
  tasks : Array[(Int, Int, Int)],
  capacity : Int,
) -> ConstraintSet {
  constraint_set().add(cumulative(tasks, capacity))
}

///|
/// Generate a weighted equality and both safe bound relaxations.
pub fn weighted_sum_bundle(
  terms : Array[(Int, Int)],
  target : Int,
) -> ConstraintSet {
  constraint_set()
  .add(linear(terms, target))
  .add(linear_less_equal(terms, target))
  .add(linear_greater_equal(terms, target))
}

///|
/// A small report about a posted constraint batch.
pub struct ConstraintSetReport {
  count : Int
  labels : Array[String]
  fingerprint : String
}

///|
/// Summarize a constraint set before posting.
pub fn ConstraintSet::report(self : ConstraintSet) -> ConstraintSetReport {
  let builder = StringBuilder()
  for constraint in self.constraints {
    builder.write_string("\{constraint_label(constraint)};")
  }
  {
    count: self.constraints.length(),
    labels: self.labels(),
    fingerprint: builder.to_string(),
  }
}

///|
/// Return the report's count.
pub fn ConstraintSetReport::count(self : ConstraintSetReport) -> Int {
  self.count
}

///|
/// Return report labels.
pub fn ConstraintSetReport::labels(self : ConstraintSetReport) -> Array[String] {
  self.labels.copy()
}

///|
/// Return a report fingerprint.
pub fn ConstraintSetReport::fingerprint(self : ConstraintSetReport) -> String {
  self.fingerprint
}

///|
/// Render a batch report.
pub fn ConstraintSetReport::describe(self : ConstraintSetReport) -> String {
  "count=\{self.count}, labels=\{Repr(self.labels)}, fingerprint=\{self.fingerprint}"
}