///|
/// Cubic Bézier geometry in scene space. The timing Curve type is kept
/// separate so geometry can be reused by cameras, particles, and masks.
pub struct CubicPath3D {
  start : Point3
  control_start : Point3
  control_end : Point3
  end : Point3
} derive(Debug)

///|
pub fn cubic_path3d(
  start : Point3,
  control_start : Point3,
  control_end : Point3,
  end : Point3,
) -> CubicPath3D {
  { start, control_start, control_end, end }
}

///|
pub fn CubicPath3D::start(self : CubicPath3D) -> Point3 {
  self.start
}

///|
pub fn CubicPath3D::control_start(self : CubicPath3D) -> Point3 {
  self.control_start
}

///|
pub fn CubicPath3D::control_end(self : CubicPath3D) -> Point3 {
  self.control_end
}

///|
pub fn CubicPath3D::end(self : CubicPath3D) -> Point3 {
  self.end
}

///|
pub fn CubicPath3D::sample(self : CubicPath3D, ratio : Double) -> Point3 {
  let t = clamp01(ratio)
  let a = self.start.lerp(self.control_start, t)
  let b = self.control_start.lerp(self.control_end, t)
  let c = self.control_end.lerp(self.end, t)
  a.lerp(b, t).lerp(b.lerp(c, t), t)
}

///|
pub fn CubicPath3D::tangent(self : CubicPath3D, ratio : Double) -> Point3 {
  let t = clamp01(ratio)
  let one = 1.0 - t
  let x = 3.0 *
    (
      one * one * (self.control_start.x() - self.start.x()) +
      2.0 * one * t * (self.control_end.x() - self.control_start.x()) +
      t * t * (self.end.x() - self.control_end.x())
    )
  let y = 3.0 *
    (
      one * one * (self.control_start.y() - self.start.y()) +
      2.0 * one * t * (self.control_end.y() - self.control_start.y()) +
      t * t * (self.end.y() - self.control_end.y())
    )
  let z = 3.0 *
    (
      one * one * (self.control_start.z() - self.start.z()) +
      2.0 * one * t * (self.control_end.z() - self.control_start.z()) +
      t * t * (self.end.z() - self.control_end.z())
    )
  point3(x~, y~, z~)
}

///|
pub fn CubicPath3D::curvature(self : CubicPath3D, ratio : Double) -> Double {
  let t = clamp01(ratio)
  let tangent = self.tangent(t)
  let second_x = 6.0 *
    (
      (1.0 - t) *
      (self.control_end.x() - 2.0 * self.control_start.x() + self.start.x()) +
      t * (self.end.x() - 2.0 * self.control_end.x() + self.control_start.x())
    )
  let second_y = 6.0 *
    (
      (1.0 - t) *
      (self.control_end.y() - 2.0 * self.control_start.y() + self.start.y()) +
      t * (self.end.y() - 2.0 * self.control_end.y() + self.control_start.y())
    )
  let second_z = 6.0 *
    (
      (1.0 - t) *
      (self.control_end.z() - 2.0 * self.control_start.z() + self.start.z()) +
      t * (self.end.z() - 2.0 * self.control_end.z() + self.control_start.z())
    )
  let second = point3(x=second_x, y=second_y, z=second_z)
  let cross = point3_cross(tangent, second)
  let speed = point3_length(tangent)
  if speed <= 0.000000000001 {
    0.0
  } else {
    point3_length(cross) / (speed * speed * speed)
  }
}

///|
pub fn CubicPath3D::length(self : CubicPath3D, samples : Int) -> Double {
  let count = samples.max(1)
  let mut total = 0.0
  let mut previous = self.sample(0.0)
  for index in 1..<=count {
    let current = self.sample(index.to_double() / count.to_double())
    total = total + point3_distance(previous, current)
    previous = current
  }
  total
}

///|
pub fn CubicPath3D::sample_by_distance(
  self : CubicPath3D,
  distance : Double,
  samples? : Int = 64,
) -> Point3 {
  let count = samples.max(1)
  let total = self.length(count)
  if total <= 0.0 {
    return self.start()
  }
  let target = clamp(distance, 0.0, total)
  let mut travelled = 0.0
  let mut previous = self.start()
  for index in 1..<=count {
    let current = self.sample(index.to_double() / count.to_double())
    let span = point3_distance(previous, 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.end
}

///|
pub fn CubicPath3D::polyline(
  self : CubicPath3D,
  samples : Int,
) -> Array[Point3] {
  let count = samples.max(1)
  let values : Array[Point3] = []
  for index in 0..<=count {
    values.push(self.sample(index.to_double() / count.to_double()))
  }
  values
}

///|
pub enum PathSegment3D {
  Line3D(Point3, Point3)
  Cubic3D(CubicPath3D)
} derive(Debug)

///|
pub fn path_line3d(start : Point3, end : Point3) -> PathSegment3D {
  Line3D(start, end)
}

///|
pub fn path_cubic3d(
  start : Point3,
  control_start : Point3,
  control_end : Point3,
  end : Point3,
) -> PathSegment3D {
  Cubic3D(cubic_path3d(start, control_start, control_end, end))
}

///|
pub fn PathSegment3D::start(self : PathSegment3D) -> Point3 {
  match self {
    Line3D(start, _) => start
    Cubic3D(path) => path.start
  }
}

///|
pub fn PathSegment3D::end(self : PathSegment3D) -> Point3 {
  match self {
    Line3D(_, end) => end
    Cubic3D(path) => path.end
  }
}

///|
pub fn PathSegment3D::sample(self : PathSegment3D, ratio : Double) -> Point3 {
  match self {
    Line3D(start, end) => start.lerp(end, clamp01(ratio))
    Cubic3D(path) => path.sample(ratio)
  }
}

///|
pub fn PathSegment3D::tangent(self : PathSegment3D, ratio : Double) -> Point3 {
  match self {
    Line3D(start, end) =>
      point3(
        x=end.x() - start.x(),
        y=end.y() - start.y(),
        z=end.z() - start.z(),
      )
    Cubic3D(path) => path.tangent(ratio)
  }
}

///|
pub fn PathSegment3D::length(self : PathSegment3D, samples : Int) -> Double {
  match self {
    Line3D(start, end) => point3_distance(start, end)
    Cubic3D(path) => path.length(samples)
  }
}

///|
pub fn PathSegment3D::curvature(self : PathSegment3D, ratio : Double) -> Double {
  match self {
    Line3D(_, _) => 0.0
    Cubic3D(path) => path.curvature(ratio)
  }
}

///|
pub fn PathSegment3D::polyline(
  self : PathSegment3D,
  samples : Int,
) -> Array[Point3] {
  let count = samples.max(1)
  let values : Array[Point3] = []
  for index in 0..<=count {
    values.push(self.sample(index.to_double() / count.to_double()))
  }
  values
}

///|
pub struct Bounds3D {
  minimum : Point3
  maximum : Point3
} derive(Debug)

///|
pub fn bounds3d(minimum : Point3, maximum : Point3) -> Bounds3D {
  { minimum, maximum }
}

///|
pub fn Bounds3D::minimum(self : Bounds3D) -> Point3 {
  self.minimum
}

///|
pub fn Bounds3D::maximum(self : Bounds3D) -> Point3 {
  self.maximum
}

///|
pub fn Bounds3D::size(self : Bounds3D) -> Point3 {
  point3(
    x=self.maximum.x() - self.minimum.x(),
    y=self.maximum.y() - self.minimum.y(),
    z=self.maximum.z() - self.minimum.z(),
  )
}

///|
pub fn Bounds3D::center(self : Bounds3D) -> Point3 {
  self.minimum.lerp(self.maximum, 0.5)
}

///|
pub fn Bounds3D::contains(self : Bounds3D, value : Point3) -> Bool {
  (value.x() >= self.minimum.x() && value.x() <= self.maximum.x()) &&
  (value.y() >= self.minimum.y() && value.y() <= self.maximum.y()) &&
  value.z() >= self.minimum.z() &&
  value.z() <= self.maximum.z()
}

///|
pub fn Bounds3D::expanded(self : Bounds3D, amount : Double) -> Bounds3D {
  bounds3d(
    point3(
      x=self.minimum.x() - amount,
      y=self.minimum.y() - amount,
      z=self.minimum.z() - amount,
    ),
    point3(
      x=self.maximum.x() + amount,
      y=self.maximum.y() + amount,
      z=self.maximum.z() + amount,
    ),
  )
}

///|
pub fn Bounds3D::union(self : Bounds3D, other : Bounds3D) -> Bounds3D {
  bounds3d(
    point3(
      x=self.minimum.x().min(other.minimum.x()),
      y=self.minimum.y().min(other.minimum.y()),
      z=self.minimum.z().min(other.minimum.z()),
    ),
    point3(
      x=self.maximum.x().max(other.maximum.x()),
      y=self.maximum.y().max(other.maximum.y()),
      z=self.maximum.z().max(other.maximum.z()),
    ),
  )
}

///|
pub struct Path3D {
  segments : Array[PathSegment3D]
  lengths : Array[Double]
  total_length : Double
} derive(Debug)

///|
pub fn Path3D::new(
  segments : Array[PathSegment3D],
  samples_per_curve? : Int = 32,
) -> Path3D raise MotionError {
  if segments.length() == 0 {
    raise MotionError::EmptyTrack
  }
  let lengths : Array[Double] = []
  let mut total = 0.0
  for segment in segments {
    let length = segment.length(samples_per_curve.max(1))
    lengths.push(length)
    total = total + length
  }
  { segments: segments.copy(), lengths, total_length: total }
}

///|
pub fn Path3D::segments(self : Path3D) -> Array[PathSegment3D] {
  self.segments.copy()
}

///|
pub fn Path3D::length(self : Path3D) -> Int {
  self.segments.length()
}

///|
pub fn Path3D::total_length(self : Path3D) -> Double {
  self.total_length
}

///|
pub fn Path3D::start(self : Path3D) -> Point3 {
  self.segments[0].start()
}

///|
pub fn Path3D::end(self : Path3D) -> Point3 {
  self.segments[self.segments.length() - 1].end()
}

///|
fn Path3D::locate(self : Path3D, distance : Double) -> (Int, Double) {
  if self.total_length <= 0.0 {
    return (0, 0.0)
  }
  let target = clamp(distance, 0.0, self.total_length)
  let mut offset = 0.0
  for index in 0.. Point3 {
  let (index, ratio) = self.locate(clamp01(parameter) * self.total_length)
  self.segments[index].sample(ratio)
}

///|
pub fn Path3D::sample_by_distance(self : Path3D, distance : Double) -> Point3 {
  let (index, ratio) = self.locate(distance)
  self.segments[index].sample(ratio)
}

///|
pub fn Path3D::tangent(self : Path3D, parameter : Double) -> Point3 {
  let (index, ratio) = self.locate(clamp01(parameter) * self.total_length)
  self.segments[index].tangent(ratio)
}

///|
pub fn Path3D::curvature(self : Path3D, parameter : Double) -> Double {
  let (index, ratio) = self.locate(clamp01(parameter) * self.total_length)
  self.segments[index].curvature(ratio)
}

///|
pub fn Path3D::polyline(
  self : Path3D,
  samples_per_segment : Int,
) -> Array[Point3] {
  let values : Array[Point3] = []
  for segment_index in 0.. 0 {
        values.push(points[point_index])
      }
    }
  }
  values
}

///|
pub fn Path3D::bounds(
  self : Path3D,
  samples_per_segment? : Int = 32,
) -> Bounds3D {
  let first = self.start()
  let mut minimum = first
  let mut maximum = first
  for segment in self.segments {
    let points = segment.polyline(samples_per_segment.max(1))
    for point in points {
      minimum = point3(
        x=minimum.x().min(point.x()),
        y=minimum.y().min(point.y()),
        z=minimum.z().min(point.z()),
      )
      maximum = point3(
        x=maximum.x().max(point.x()),
        y=maximum.y().max(point.y()),
        z=maximum.z().max(point.z()),
      )
    }
  }
  bounds3d(minimum, maximum)
}

///|
pub struct PathProjection3D {
  parameter : Double
  distance : Double
  value : Point3
  error : Double
} derive(Debug)

///|
pub fn PathProjection3D::parameter(self : PathProjection3D) -> Double {
  self.parameter
}

///|
pub fn PathProjection3D::distance(self : PathProjection3D) -> Double {
  self.distance
}

///|
pub fn PathProjection3D::value(self : PathProjection3D) -> Point3 {
  self.value
}

///|
pub fn PathProjection3D::error(self : PathProjection3D) -> Double {
  self.error
}

///|
pub fn Path3D::project(
  self : Path3D,
  target : Point3,
  samples_per_segment? : Int = 32,
) -> PathProjection3D {
  let steps = samples_per_segment.max(1)
  let mut best_parameter = 0.0
  let mut best_value = self.start()
  let mut best_distance = point3_distance(best_value, target)
  let mut offset = 0.0
  for segment_index in 0.. PathBuilder3D {
  { current: start, segments: [] }
}

///|
pub fn PathBuilder3D::current(self : PathBuilder3D) -> Point3 {
  self.current
}

///|
pub fn PathBuilder3D::line_to(self : PathBuilder3D, end : Point3) -> Unit {
  self.segments.push(Line3D(self.current, end))
  self.current = end
}

///|
pub fn PathBuilder3D::cubic_to(
  self : PathBuilder3D,
  control_start : Point3,
  control_end : Point3,
  end : Point3,
) -> Unit {
  self.segments.push(
    Cubic3D(cubic_path3d(self.current, control_start, control_end, end)),
  )
  self.current = end
}

///|
pub fn PathBuilder3D::close(self : PathBuilder3D) -> Unit {
  self.segments.push(Line3D(self.current, self.segments[0].start()))
  self.current = self.segments[0].start()
}

///|
pub fn PathBuilder3D::build(
  self : PathBuilder3D,
  samples_per_curve? : Int = 32,
) -> Path3D raise MotionError {
  Path3D::new(self.segments, samples_per_curve~)
}

///|
pub fn Path3D::reversed(self : Path3D) -> Path3D raise MotionError {
  let reversed : Array[PathSegment3D] = []
  let mut index = self.segments.length() - 1
  while index >= 0 {
    match self.segments[index] {
      Line3D(start, end) => reversed.push(Line3D(end, start))
      Cubic3D(path) =>
        reversed.push(
          Cubic3D(
            cubic_path3d(
              path.end,
              path.control_end,
              path.control_start,
              path.start,
            ),
          ),
        )
    }
    index = index - 1
  }
  Path3D::new(reversed)
}

///|
pub fn Path3D::append(
  self : Path3D,
  segment : PathSegment3D,
) -> Path3D raise MotionError {
  let values = self.segments.copy()
  values.push(segment)
  Path3D::new(values)
}

///|
pub fn Path3D::sample_many(
  self : Path3D,
  count : Int,
) -> Array[Point3] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let values : Array[Point3] = []
  for index in 0..