///|
/// Member order and transformation identifiers are stable within this relation version.
pub fn equivalent_relations(ty : Ty) -> Array[(String, String)] {
  let relations = [("original", "identity/v1")]
  match ty {
    Int => relations.push(("equivalent-1", "int-add-zero/v1"))
    Bool => relations.push(("equivalent-1", "bool-double-not/v1"))
    _ => ()
  }
  relations.push(("equivalent-\{relations.length()}", "true-branch/v1"))
  relations
}

///|
pub struct EquivalentGroup {
  priv original : CompleteProgram
  priv members : Array[CompleteProgram]
}

///|
pub fn CompleteProgram::equivalent_group(
  self : CompleteProgram,
) -> EquivalentGroup raise StructureError {
  let members = [self]
  members.append(self.equivalent_variants())
  { original: self, members, }
}

///|
pub fn EquivalentGroup::members(
  self : EquivalentGroup,
) -> Array[(String, String, CompleteProgram)] {
  let relations = equivalent_relations(self.original.result_type())
  self.members.mapi((index, program) => {
    let (id, transformation) = relations[index]
    (id, transformation, program)
  })
}

///|
pub fn EquivalentGroup::relations(
  self : EquivalentGroup,
) -> Array[(String, String)] {
  equivalent_relations(self.original.result_type())
}

///|
pub fn EquivalentGroup::result_type(self : EquivalentGroup) -> Ty {
  self.original.result_type()
}

///|
pub fn group_cost_model() -> String {
  "moonbit-group-sum-nodes-int-magnitude-utf8-v1"
}

///|
pub fn EquivalentGroup::cost(
  self : EquivalentGroup,
) -> @contracts.ReductionCost {
  self.members.fold(
    init={ nodes: 0, integer_magnitude: 0L, source_bytes: 0, },
    (sum, program) => {
      let cost = program.cost()
      {
        nodes: sum.nodes + cost.nodes,
        integer_magnitude: sum.integer_magnitude + cost.integer_magnitude,
        source_bytes: sum.source_bytes + cost.source_bytes,
      }
    },
  )
}

///|
pub fn EquivalentGroup::is_smaller_than(
  self : EquivalentGroup,
  other : EquivalentGroup,
) -> Bool {
  cost_is_smaller(self.cost(), other.cost())
}

///|
fn regroup(candidates : Array[CompleteProgram]) -> Array[EquivalentGroup] {
  candidates.filter_map(program => {
    Some(program.equivalent_group()) catch {
      _ => None
    }
  })
}

///|
pub fn EquivalentGroup::structural_candidates(
  self : EquivalentGroup,
) -> Array[EquivalentGroup] {
  regroup(self.original.structural_candidates())
}

///|
pub fn EquivalentGroup::integer_candidates(
  self : EquivalentGroup,
) -> Array[EquivalentGroup] {
  regroup(self.original.integer_candidates())
}