///|
fn unique_indices(indices : Array[Int], size : Int) -> Unit raise CodaError {
  let seen = Array::make(size, false)
  for i in indices {
    if i < 0 || i >= size {
      raise Shape("feature index outside composition")
    }
    if seen[i] {
      raise Invalid("repeated feature index")
    }
    seen[i] = true
  }
}

///|
pub fn subcomposition(
  x : Array[Double],
  indices : Array[Int],
) -> Array[Double] raise CodaError {
  let c = closure(x)
  vector_size(indices.length())
  unique_indices(indices, c.length())
  closure(indices.map(i => c[i]))
}

///|
/// Groups must form a disjoint, exhaustive partition. This is NOT an isometry.
pub fn amalgamate(
  x : Array[Double],
  groups : Array[Array[Int]],
) -> Array[Double] raise CodaError {
  let c = closure(x)
  vector_size(groups.length())
  let seen = Array::make(c.length(), false)
  let result = []
  for group in groups {
    if group.length() == 0 {
      raise Shape("empty amalgamation group")
    }
    let mut total = 0.0
    for i in group {
      if i < 0 || i >= c.length() {
        raise Shape("amalgamation index out of bounds")
      }
      if seen[i] {
        raise Invalid("amalgamation groups overlap")
      }
      seen[i] = true
      total += c[i]
    }
    result.push(total)
  }
  for found in seen {
    if !found {
      raise Invalid("amalgamation omits a feature")
    }
  }
  closure(result)
}

///|
/// Restore feature order after a permutation (not after feature removal).
pub fn permute(
  x : Array[Double],
  order : Array[Int],
) -> Array[Double] raise CodaError {
  if x.length() != order.length() {
    raise Shape("permutation size mismatch")
  }
  unique_indices(order, x.length())
  let c = closure(x)
  order.map(i => c[i])
}