///|
/// Differential samples for camera rails and particle emitters.
pub struct PathSample3D {
  parameter : Double
  distance : Double
  position : Point3
  tangent : Point3
  normal : Point3
  curvature : Double
} derive(Debug)

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

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

///|
pub fn PathSample3D::position(self : PathSample3D) -> Point3 {
  self.position
}

///|
pub fn PathSample3D::tangent(self : PathSample3D) -> Point3 {
  self.tangent
}

///|
pub fn PathSample3D::normal(self : PathSample3D) -> Point3 {
  self.normal
}

///|
pub fn PathSample3D::curvature(self : PathSample3D) -> Double {
  self.curvature
}

///|
fn tangent_normal(
  path : Path3D,
  parameter : Double,
) -> (Point3, Point3, Double) {
  let tangent = point3_normalized(path.tangent(parameter))
  let epsilon = 0.0001
  let left = point3_normalized(path.tangent((parameter - epsilon).max(0.0)))
  let right = point3_normalized(path.tangent((parameter + epsilon).min(1.0)))
  let normal = point3_normalized(
    point3(
      x=right.x() - left.x(),
      y=right.y() - left.y(),
      z=right.z() - left.z(),
    ),
  )
  (tangent, normal, path.curvature(parameter))
}

///|
pub fn sample_path3d(path : Path3D, parameter : Double) -> PathSample3D {
  let t = clamp01(parameter)
  let position = path.sample(t)
  let tangent = tangent_normal(path, t)
  {
    parameter: t,
    distance: t * path.total_length(),
    position,
    tangent: tangent.0,
    normal: tangent.1,
    curvature: tangent.2,
  }
}

///|
pub fn sample_path3d_by_distance(
  path : Path3D,
  distance : Double,
) -> PathSample3D {
  let parameter = if path.total_length() == 0.0 {
    0.0
  } else {
    clamp(distance, 0.0, path.total_length()) / path.total_length()
  }
  sample_path3d(path, parameter)
}

///|
pub struct PathMetricProfile3D {
  samples : Array[PathSample3D]
  total_length : Double
  maximum_curvature : Double
  average_curvature : Double
} derive(Debug)

///|
pub fn build_path_metric_profile(
  path : Path3D,
  count : Int,
) -> PathMetricProfile3D {
  let actual = count.max(2)
  let samples : Array[PathSample3D] = []
  let mut maximum = 0.0
  let mut sum = 0.0
  for index in 0.. Array[PathSample3D] {
  self.samples.copy()
}

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

///|
pub fn PathMetricProfile3D::maximum_curvature(
  self : PathMetricProfile3D,
) -> Double {
  self.maximum_curvature
}

///|
pub fn PathMetricProfile3D::average_curvature(
  self : PathMetricProfile3D,
) -> Double {
  self.average_curvature
}

///|
pub fn PathMetricProfile3D::sample(
  self : PathMetricProfile3D,
  parameter : Double,
) -> PathSample3D {
  if self.samples.length() == 0 {
    return {
      parameter: 0.0,
      distance: 0.0,
      position: point3(x=0.0, y=0.0, z=0.0),
      tangent: point3(x=0.0, y=0.0, z=1.0),
      normal: point3(x=0.0, y=1.0, z=0.0),
      curvature: 0.0,
    }
  }
  let position = clamp01(parameter) * (self.samples.length() - 1).to_double()
  let index = @math.floor(position).to_int().min(self.samples.length() - 2)
  let ratio = position - index.to_double()
  let left = self.samples[index]
  let right = self.samples[index + 1]
  {
    parameter: left.parameter + (right.parameter - left.parameter) * ratio,
    distance: left.distance + (right.distance - left.distance) * ratio,
    position: left.position.lerp(right.position, ratio),
    tangent: point3_normalized(left.tangent.lerp(right.tangent, ratio)),
    normal: point3_normalized(left.normal.lerp(right.normal, ratio)),
    curvature: left.curvature + (right.curvature - left.curvature) * ratio,
  }
}

///|
pub struct PathFrame3D {
  position : Point3
  tangent : Point3
  normal : Point3
  binormal : Point3
} derive(Debug)

///|
pub fn PathFrame3D::position(self : PathFrame3D) -> Point3 {
  self.position
}

///|
pub fn PathFrame3D::tangent(self : PathFrame3D) -> Point3 {
  self.tangent
}

///|
pub fn PathFrame3D::normal(self : PathFrame3D) -> Point3 {
  self.normal
}

///|
pub fn PathFrame3D::binormal(self : PathFrame3D) -> Point3 {
  self.binormal
}

///|
pub fn build_path_frame(sample : PathSample3D, up : Point3) -> PathFrame3D {
  let tangent = point3_normalized(sample.tangent)
  let binormal = point3_normalized(point3_cross(tangent, up))
  let normal = point3_normalized(point3_cross(binormal, tangent))
  { position: sample.position, tangent, normal, binormal }
}

///|
pub fn PathFrame3D::offset(
  self : PathFrame3D,
  horizontal : Double,
  vertical : Double,
) -> Point3 {
  point3(
    x=self.position.x() +
      self.normal.x() * horizontal +
      self.binormal.x() * vertical,
    y=self.position.y() +
      self.normal.y() * horizontal +
      self.binormal.y() * vertical,
    z=self.position.z() +
      self.normal.z() * horizontal +
      self.binormal.z() * vertical,
  )
}

///|
pub fn PathFrame3D::look_target(
  self : PathFrame3D,
  distance : Double,
) -> Point3 {
  point3(
    x=self.position.x() + self.tangent.x() * distance,
    y=self.position.y() + self.tangent.y() * distance,
    z=self.position.z() + self.tangent.z() * distance,
  )
}

///|
pub fn path_chord_error(path : Path3D, samples : Int) -> Double {
  let count = samples.max(2)
  let mut maximum = 0.0
  let mut previous = path.sample(0.0)
  for index in 1..<=count {
    let current = path.sample(index.to_double() / count.to_double())
    let midpoint = previous.lerp(current, 0.5)
    let actual = path.sample((index.to_double() - 0.5) / count.to_double())
    maximum = maximum.max(point3_distance(midpoint, actual))
    previous = current
  }
  maximum
}

///|
pub fn path_speed_at(path : Path3D, parameter : Double) -> Double {
  point3_length(path.tangent(parameter))
}

///|
pub fn path_speed_profile(path : Path3D, count : Int) -> Array[Double] {
  let values : Array[Double] = []
  let actual = count.max(1)
  for index in 0.. Bool {
  point3_distance(path.start(), path.end()) <= tolerance.abs()
}

///|
pub fn PathMetricProfile3D::max_speed(self : PathMetricProfile3D) -> Double {
  let mut maximum = 0.0
  for sample in self.samples {
    maximum = maximum.max(1.0 / (sample.parameter + 0.000001))
  }
  maximum
}