///|
/// Weighted least-squares projection onto a chain. Input order is significant.
pub fn fit_l2(
  y : Array[Double],
  weights : Array[Double],
  direction? : Direction = Increasing,
) -> Result[Fit, String] {
  match validate_series(y, weights) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let blocks : Array[Block] = []
  for i = 0; i < y.length(); i = i + 1 {
    blocks.push({ start: i, end: i + 1, weight: weights[i], value: y[i], })
    while blocks.length() >= 2 {
      let n = blocks.length()
      let a = blocks[n - 2]
      let b = blocks[n - 1]
      if before(a.value, b.value, direction) && a.value != b.value {
        break
      }
      let weight = a.weight + b.weight
      let value = a.value * (a.weight / weight) + b.value * (b.weight / weight)
      if !finite(weight) || !finite(value) {
        return Err("pooled mean overflow")
      }
      ignore(blocks.pop())
      blocks[n - 2] = { start: a.start, end: b.end, weight, value, }
    }
  }
  let values = Array::make(y.length(), 0.0)
  for block in blocks {
    for i = block.start; i < block.end; i = i + 1 {
      values[i] = block.value
    }
  }
  match squared_loss(y, values, weights) {
    Err(e) => Err(e)
    Ok(objective) => Ok({ values, blocks, objective, direction, })
  }
}

///|
/// Equal-weight convenience API. Does not sort observations.
pub fn isotonic(
  y : Array[Double],
  direction? : Direction = Increasing,
) -> Result[Fit, String] {
  fit_l2(y, Array::make(y.length(), 1.0), direction~)
}

///|
/// Expand a partition into fitted observations, rejecting inconsistent coverage.
pub fn expand_blocks(
  blocks : Array[Block],
  size : Int,
) -> Result[Array[Double], String] {
  if size <= 0 {
    return Err("nonpositive size")
  }
  let values = Array::make(size, 0.0)
  let mut next = 0
  for block in blocks {
    if block.start != next || block.end <= block.start || block.end > size {
      return Err("blocks must partition the requested size")
    }
    if !finite(block.value) || !finite(block.weight) || block.weight <= 0.0 {
      return Err("invalid block statistics")
    }
    for i = block.start; i < block.end; i = i + 1 {
      values[i] = block.value
    }
    next = block.end
  }
  if next != size {
    return Err("incomplete partition")
  }
  Ok(values)
}