///|
/// Spatial interpolation helpers for camera rigs and scene objects.
pub struct Transform3Delta {
  translation : Point3
  rotation : Quaternion
  scale : Point3
} derive(Debug)

///|
pub fn Transform3Delta::translation(self : Transform3Delta) -> Point3 {
  self.translation
}

///|
pub fn Transform3Delta::rotation(self : Transform3Delta) -> Quaternion {
  self.rotation
}

///|
pub fn Transform3Delta::scale(self : Transform3Delta) -> Point3 {
  self.scale
}

///|
pub fn transform3_delta(
  first : Transform3D,
  second : Transform3D,
) -> Transform3Delta {
  let translation = point3(
    x=second.position().x() - first.position().x(),
    y=second.position().y() - first.position().y(),
    z=second.position().z() - first.position().z(),
  )
  let rotation = first
    .rotation()
    .conjugate()
    .multiply(second.rotation())
    .normalized()
  let scale = point3(
    x=if first.scale().x() == 0.0 {
      0.0
    } else {
      second.scale().x() / first.scale().x()
    },
    y=if first.scale().y() == 0.0 {
      0.0
    } else {
      second.scale().y() / first.scale().y()
    },
    z=if first.scale().z() == 0.0 {
      0.0
    } else {
      second.scale().z() / first.scale().z()
    },
  )
  { translation, rotation, scale }
}

///|
pub fn apply_transform3_delta(
  value : Transform3D,
  delta : Transform3Delta,
  weight : Double,
) -> Transform3D {
  let ratio = clamp01(weight)
  value.combine(
    transform3d(
      position=delta.translation.lerp(point3(x=0.0, y=0.0, z=0.0), 1.0 - ratio),
      rotation=Quaternion::identity().slerp(delta.rotation, ratio),
      scale=point3(
        x=1.0 + (delta.scale.x() - 1.0) * ratio,
        y=1.0 + (delta.scale.y() - 1.0) * ratio,
        z=1.0 + (delta.scale.z() - 1.0) * ratio,
      ),
    ),
  )
}

///|
pub fn transform3_distance(first : Transform3D, second : Transform3D) -> Double {
  let position = point3_distance(first.position(), second.position())
  let scale = point3_distance(first.scale(), second.scale())
  let rotation = 1.0 - first.rotation().dot(second.rotation()).abs()
  position + scale + rotation
}

///|
pub fn transform3_clamp_scale(
  value : Transform3D,
  minimum : Point3,
  maximum : Point3,
) -> Transform3D {
  transform3d(
    position=value.position(),
    rotation=value.rotation(),
    scale=point3(
      x=clamp(value.scale().x(), minimum.x(), maximum.x()),
      y=clamp(value.scale().y(), minimum.y(), maximum.y()),
      z=clamp(value.scale().z(), minimum.z(), maximum.z()),
    ),
  )
}

///|
pub fn transform3_look_at(
  position : Point3,
  target : Point3,
  up : Point3,
) -> Transform3D {
  let forward = point3_normalized(
    point3(
      x=target.x() - position.x(),
      y=target.y() - position.y(),
      z=target.z() - position.z(),
    ),
  )
  let right = point3_normalized(point3_cross(up, forward))
  let corrected_up = point3_cross(forward, right)
  let trace = right.x() + corrected_up.y() + forward.z()
  let rotation = if trace > 0.0 {
    let scale = @math.pow(trace + 1.0, 0.5) * 2.0
    quaternion(
      x=(corrected_up.z() - forward.y()) / scale,
      y=(forward.x() - right.z()) / scale,
      z=(right.y() - corrected_up.x()) / scale,
      w=0.25 * scale,
    )
  } else {
    Quaternion::identity()
  }
  transform3d(
    position~,
    rotation=rotation.normalized(),
    scale=point3(x=1.0, y=1.0, z=1.0),
  )
}

///|
pub fn transform3_axes(value : Transform3D) -> (Point3, Point3, Point3) {
  (value.rotation().right(), value.rotation().up(), value.rotation().forward())
}

///|
pub fn transform3_local_point(value : Transform3D, point : Point3) -> Point3 {
  value.apply(point)
}

///|
pub fn transform3_world_point(value : Transform3D, point : Point3) -> Point3 {
  value.inverse().apply(point)
}

///|
pub fn transform3_track_error(
  track : Transform3Track,
  samples : Int,
) -> Double raise MotionError {
  let values = track.sample_many(samples.max(2))
  let mut maximum = 0.0
  for index in 1..