///|
fn observation_weights(
  weights : Array[Double],
  n : Int,
) -> Array[Double] raise CodaError {
  if weights.length() != n {
    raise Shape("observation weight count mismatch")
  }
  real_vector(weights)
  let mut largest = 0.0
  for w in weights {
    if w < 0.0 {
      raise Invalid("negative observation weight")
    }
    if w > largest {
      largest = w
    }
  }
  if largest == 0.0 {
    raise Invalid("all-zero observation weights")
  }
  let mut total = 0.0
  for w in weights {
    total += w / largest
  }
  weights.map(w => w / largest / total)
}

///|
pub fn weighted_center(
  data : Dataset,
  weights : Array[Double],
) -> Array[Double] raise CodaError {
  let w = observation_weights(weights, data.row_count())
  let logs = data.clr_rows()
  let center = Array::make(data.part_count(), 0.0)
  for r, row in logs {
    for j, v in row {
      center[j] += w[r] * v
    }
  }
  clr_inverse(center)
}

///|
pub fn center(data : Dataset) -> Array[Double] raise CodaError {
  weighted_center(data, Array::make(data.row_count(), 1.0))
}

///|
/// Remove the geometric center by inverse perturbation; labels are retained.
pub fn centralize(data : Dataset) -> Dataset raise CodaError {
  let c = center(data)
  dataset(
    data.features,
    data.samples,
    data.cells.map(r => perturb_inverse(r, c)),
  )
}

///|
pub fn weighted_dispersion(
  data : Dataset,
  weights : Array[Double],
) -> Double raise CodaError {
  let w = observation_weights(weights, data.row_count())
  let c = weighted_center(data, weights)
  let mut sum = 0.0
  for i, row in data.cells {
    let d = distance(row, c)
    sum += w[i] * d * d
  }
  sum
}

///|
/// Between-center distance is descriptive, not a hypothesis test.
pub fn center_distance(
  left : Dataset,
  right : Dataset,
) -> Double raise CodaError {
  if left.features != right.features {
    raise Shape("feature labels/order mismatch")
  }
  distance(center(left), center(right))
}