///|
/// Axis-aligned bounds for a sampled media path.
pub(all) struct Bounds2D {
  minimum : Point2
  maximum : Point2
} derive(Debug)

///|
pub fn Bounds2D::minimum(self : Bounds2D) -> Point2 {
  self.minimum
}

///|
pub fn Bounds2D::maximum(self : Bounds2D) -> Point2 {
  self.maximum
}

///|
/// A cubic geometric Bézier path. This is separate from Bezier timing curves.
pub struct CubicPath2D {
  start : Point2
  control_start : Point2
  control_end : Point2
  end : Point2
} derive(Debug)

///|
pub fn cubic_path2d(
  start : Point2,
  control_start : Point2,
  control_end : Point2,
  end : Point2,
) -> CubicPath2D {
  { start, control_start, control_end, end }
}

///|
fn point_lerp(first : Point2, second : Point2, t : Double) -> Point2 {
  first.lerp(second, t)
}

///|
pub fn CubicPath2D::sample(self : CubicPath2D, t : Double) -> Point2 {
  let x = clamp01(t)
  let first = point_lerp(self.start, self.control_start, x)
  let second = point_lerp(self.control_start, self.control_end, x)
  let third = point_lerp(self.control_end, self.end, x)
  let left = point_lerp(first, second, x)
  let right = point_lerp(second, third, x)
  point_lerp(left, right, x)
}

///|
pub fn CubicPath2D::tangent(self : CubicPath2D, t : Double) -> Point2 {
  let x = clamp01(t)
  let one_minus = 1.0 - x
  let first = self.control_start.lerp(self.start, one_minus)
  let second = self.control_end.lerp(self.control_start, one_minus)
  let third = self.end.lerp(self.control_end, one_minus)
  let left = second.lerp(first, one_minus)
  let right = third.lerp(second, one_minus)
  { x: 3.0 * (right.x - left.x), y: 3.0 * (right.y - left.y) }
}

///|
pub fn CubicPath2D::length(self : CubicPath2D, segments : Int) -> Double {
  if segments <= 0 {
    0.0
  } else {
    let mut total = 0.0
    let mut previous = self.sample(0.0)
    for i in 1..<=segments {
      let current = self.sample(i.to_double() / segments.to_double())
      total = total + previous.distance(current)
      previous = current
    }
    total
  }
}

///|
pub fn CubicPath2D::sample_by_distance(
  self : CubicPath2D,
  distance : Double,
  segments? : Int = 64,
) -> Point2 {
  let total = self.length(segments)
  if total <= 0.0 {
    return self.sample(0.0)
  }
  let target = clamp(distance, 0.0, total)
  let mut travelled = 0.0
  let mut previous = self.sample(0.0)
  for i in 1..<=segments {
    let current = self.sample(i.to_double() / segments.to_double())
    let span = previous.distance(current)
    if travelled + span >= target {
      let ratio = if span == 0.0 { 0.0 } else { (target - travelled) / span }
      return previous.lerp(current, ratio)
    }
    travelled = travelled + span
    previous = current
  }
  self.sample(1.0)
}

///|
pub fn CubicPath2D::bounds(
  self : CubicPath2D,
  segments? : Int = 32,
) -> Bounds2D {
  let first = self.sample(0.0)
  let mut min_x = first.x
  let mut min_y = first.y
  let mut max_x = first.x
  let mut max_y = first.y
  for i in 1..<=segments {
    let point = self.sample(i.to_double() / segments.to_double())
    if point.x < min_x {
      min_x = point.x
    }
    if point.y < min_y {
      min_y = point.y
    }
    if point.x > max_x {
      max_x = point.x
    }
    if point.y > max_y {
      max_y = point.y
    }
  }
  { minimum: { x: min_x, y: min_y }, maximum: { x: max_x, y: max_y } }
}

///|
pub fn CubicPath2D::polyline(
  self : CubicPath2D,
  segments : Int,
) -> Array[Point2] {
  let result : Array[Point2] = []
  if segments <= 0 {
    result.push(self.sample(0.0))
    return result
  }
  for i in 0..<=segments {
    result.push(self.sample(i.to_double() / segments.to_double()))
  }
  result
}