///|
pub struct NestedScopeGroup[A, B, S, U] {
  priv nested : Array[NestedScope[A, B, S, U]]
  priv child : ScopeGroup[Product[A, B], Product[S, U]]
}

///|
pub fn[A : Lattice, B : Lattice, S : Compare, U : Compare] ScopeGroup::nested(
  self : ScopeGroup[A, S],
  minimum : B,
  domain : SummaryDomain[B, U],
) -> NestedScopeGroup[A, B, S, U] {
  let nested = self.scopes.map(fn(outer) {
    let inner = outer.worker.make_scope(
      Product(outer.minimum, minimum),
      SummaryDomain::product(outer.domain, domain),
      false,
      family=outer.progress.family,
    )
    { outer, inner, minimum, }
  })
  let child = coordinate_scopes(nested.map(fn(nested) { nested.inner }))
  { nested, child, }
}

///|
pub fn[A, B, S, U] NestedScopeGroup::scope(
  self : NestedScopeGroup[A, B, S, U],
) -> ScopeGroup[Product[A, B], Product[S, U]] {
  self.child
}

///|
pub fn[D, A : Lattice, B : Lattice, R, S, U] NestedScopeGroup::enter(
  self : NestedScopeGroup[A, B, S, U],
  sources : Array[Collection[D, A, R]],
) -> Array[Collection[D, Product[A, B], R]] {
  guard sources.length() == self.nested.length() else {
    abort("enter needs one collection per peer")
  }
  Array::makei(sources.length(), fn(index) {
    self.nested[index].enter(sources[index])
  })
}

///|
pub fn[D, A : Lattice, B : Lattice, R, S, U] NestedScopeGroup::leave(
  self : NestedScopeGroup[A, B, S, U],
  sources : Array[Collection[D, Product[A, B], R]],
) -> Array[Collection[D, A, R]] {
  guard sources.length() == self.nested.length() else {
    abort("leave needs one collection per peer")
  }
  Array::makei(sources.length(), fn(index) {
    self.nested[index].leave(sources[index])
  })
}

///|
/// Build a fixed point over all peers. Use the child group's exchange method to
/// colocate equal keys before keyed operators inside the iterative computation.
pub fn[D, T : Lattice, R : Abelian, S : Compare] ScopeGroup::iterate(
  self : ScopeGroup[T, S],
  sources : Array[Collection[D, T, R]],
  logic : (
    ScopeGroup[Product[T, Int64], Product[S, Int64]],
    Array[Collection[D, Product[T, Int64], R]],
  ) -> Array[Collection[D, Product[T, Int64], R]],
) -> Array[Collection[D, T, R]] {
  let nested = self.nested(0L, SummaryDomain::int64())
  let child = nested.scope()
  let entered = nested.enter(sources)
  let variables = Array::makei(entered.length(), fn(index) {
    child
    .scope(index)
    .variable_from(
      entered[index],
      Product(self.scopes[index].domain.identity, 1L),
    )
  })
  let result = logic(child, variables.map(fn(variable) { variable.1 }))
  guard result.length() == variables.length() else {
    abort("iteration needs one result per peer")
  }
  for index, variable in variables {
    variable.0.set(result[index])
  }
  nested.leave(result)
}