///|
/// Errors raised by Bezier segment construction and sampling.
pub(all) suberror BezierError {
  InvalidControlPoints(message~ : String)
  InvalidParameter(message~ : String)
}

///|
/// A quadratic or cubic Bezier segment.
pub struct BezierSegment {
  control_points : Array[@core.Point]
}

///|
fn validate_controls(points : Array[@core.Point]) -> Unit raise BezierError {
  if points.length() != 3 && points.length() != 4 {
    raise BezierError::InvalidControlPoints(
      message="a Bezier segment needs three or four control points",
    )
  }
  for point in points {
    if !point.is_finite() {
      raise BezierError::InvalidControlPoints(
        message="Bezier control points must be finite",
      )
    }
  }
}

///|
/// Construct a quadratic Bezier segment.
pub fn quadratic_bezier(
  start : @core.Point,
  control : @core.Point,
  end : @core.Point,
) -> BezierSegment raise BezierError {
  let points = [start, control, end]
  validate_controls(points)
  { control_points: points, }
}

///|
/// Construct a cubic Bezier segment.
pub fn cubic_bezier(
  start : @core.Point,
  control1 : @core.Point,
  control2 : @core.Point,
  end : @core.Point,
) -> BezierSegment raise BezierError {
  let points = [start, control1, control2, end]
  validate_controls(points)
  { control_points: points, }
}

///|
pub fn BezierSegment::control_points(
  self : BezierSegment,
) -> Array[@core.Point] {
  self.control_points.copy()
}

///|
pub fn BezierSegment::degree(self : BezierSegment) -> Int32 {
  self.control_points.length() - 1
}

///|
fn lerp_point(
  first : @core.Point,
  second : @core.Point,
  t : Double,
) -> @core.Point {
  @core.point(
    first.x() + (second.x() - first.x()) * t,
    first.y() + (second.y() - first.y()) * t,
  )
}

///|
fn validate_parameter(value : Double) -> Unit raise BezierError {
  if value.is_nan() || value.is_inf() {
    raise BezierError::InvalidParameter(
      message="Bezier parameter must be finite",
    )
  }
}

///|
/// Evaluate the segment at parameter `t` using de Casteljau interpolation.
pub fn BezierSegment::evaluate(
  self : BezierSegment,
  t : Double,
) -> @core.Point raise BezierError {
  validate_parameter(t)
  let mut level = self.control_points.copy()
  while level.length() > 1 {
    let next = Array(capacity=level.length() - 1)
    for index in 0..<(level.length() - 1) {
      next.push(lerp_point(level[index], level[index + 1], t))
    }
    level = next
  }
  level[0]
}

///|
/// Split the segment into two segments at parameter `t`.
pub fn BezierSegment::split(
  self : BezierSegment,
  t : Double,
) -> (BezierSegment, BezierSegment) raise BezierError {
  validate_parameter(t)
  if t < 0.0 || t > 1.0 {
    raise BezierError::InvalidParameter(
      message="split parameter must be in [0, 1]",
    )
  }
  let p = self.control_points
  let first = lerp_point(p[0], p[1], t)
  if p.length() == 3 {
    let second = lerp_point(p[1], p[2], t)
    let middle = lerp_point(first, second, t)
    (
      quadratic_bezier(p[0], first, middle),
      quadratic_bezier(middle, second, p[2]),
    )
  } else {
    let second = lerp_point(p[1], p[2], t)
    let third = lerp_point(p[2], p[3], t)
    let first_middle = lerp_point(first, second, t)
    let second_middle = lerp_point(second, third, t)
    let middle = lerp_point(first_middle, second_middle, t)
    (
      cubic_bezier(p[0], first, first_middle, middle),
      cubic_bezier(middle, second_middle, third, p[3]),
    )
  }
}

///|
/// Sample a polyline approximation with `steps` equal intervals.
pub fn BezierSegment::sample(
  self : BezierSegment,
  steps : Int32,
) -> Array[@core.Point] raise BezierError {
  if steps <= 0 {
    raise BezierError::InvalidParameter(message="sample steps must be positive")
  }
  let result = Array(capacity=steps + 1)
  for index in 0..<=steps {
    result.push(
      self.evaluate(Double::from_int(index) / Double::from_int(steps)),
    )
  }
  result
}

///|
/// Approximate arc length by summing a sampled polyline.
pub fn BezierSegment::length(
  self : BezierSegment,
  steps~ : Int32 = 32,
) -> Double raise BezierError {
  let samples = self.sample(steps)
  let mut total = 0.0
  for index in 1..