///|
/// Compact 2D affine matrix used when many sprites share one transform
/// hierarchy. The six values follow the conventional column-vector form.
pub struct Affine2D {
  a : Double
  b : Double
  c : Double
  d : Double
  tx : Double
  ty : Double
} derive(Debug)

///|
pub fn affine2d_identity() -> Affine2D {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0 }
}

///|
pub fn affine2d_translation(value : Point2) -> Affine2D {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: value.x(), ty: value.y() }
}

///|
pub fn affine2d_scale(value : Point2) -> Affine2D {
  { a: value.x(), b: 0.0, c: 0.0, d: value.y(), tx: 0.0, ty: 0.0 }
}

///|
pub fn affine2d_rotation(angle : Double) -> Affine2D {
  let cosine = @math.cos(angle)
  let sine = @math.sin(angle)
  { a: cosine, b: sine, c: -sine, d: cosine, tx: 0.0, ty: 0.0 }
}

///|
pub fn affine2d_skew(skew : Double) -> Affine2D {
  { a: 1.0, b: 0.0, c: @math.tan(skew), d: 1.0, tx: 0.0, ty: 0.0 }
}

///|
pub fn affine2d_from_transform(value : Transform2D) -> Affine2D {
  affine2d_translation(value.position()).multiply(
    affine2d_rotation(value.rotation()).multiply(
      affine2d_skew(value.skew()).multiply(affine2d_scale(value.scale())),
    ),
  )
}

///|
pub fn Affine2D::a(self : Affine2D) -> Double {
  self.a
}

///|
pub fn Affine2D::b(self : Affine2D) -> Double {
  self.b
}

///|
pub fn Affine2D::c(self : Affine2D) -> Double {
  self.c
}

///|
pub fn Affine2D::d(self : Affine2D) -> Double {
  self.d
}

///|
pub fn Affine2D::translation(self : Affine2D) -> Point2 {
  point2(x=self.tx, y=self.ty)
}

///|
pub fn Affine2D::determinant(self : Affine2D) -> Double {
  self.a * self.d - self.b * self.c
}

///|
pub fn Affine2D::multiply(self : Affine2D, other : Affine2D) -> Affine2D {
  {
    a: self.a * other.a + self.c * other.b,
    b: self.b * other.a + self.d * other.b,
    c: self.a * other.c + self.c * other.d,
    d: self.b * other.c + self.d * other.d,
    tx: self.a * other.tx + self.c * other.ty + self.tx,
    ty: self.b * other.tx + self.d * other.ty + self.ty,
  }
}

///|
pub fn Affine2D::apply(self : Affine2D, point : Point2) -> Point2 {
  point2(
    x=self.a * point.x() + self.c * point.y() + self.tx,
    y=self.b * point.x() + self.d * point.y() + self.ty,
  )
}

///|
pub fn Affine2D::inverse(self : Affine2D) -> Affine2D {
  let determinant = self.determinant()
  if determinant == 0.0 {
    return affine2d_identity()
  }
  let inverse = 1.0 / determinant
  {
    a: self.d * inverse,
    b: -self.b * inverse,
    c: -self.c * inverse,
    d: self.a * inverse,
    tx: (self.c * self.ty - self.d * self.tx) * inverse,
    ty: (self.b * self.tx - self.a * self.ty) * inverse,
  }
}

///|
pub fn Affine2D::lerp(
  self : Affine2D,
  other : Affine2D,
  ratio : Double,
) -> Affine2D {
  let t = clamp01(ratio)
  {
    a: self.a + (other.a - self.a) * t,
    b: self.b + (other.b - self.b) * t,
    c: self.c + (other.c - self.c) * t,
    d: self.d + (other.d - self.d) * t,
    tx: self.tx + (other.tx - self.tx) * t,
    ty: self.ty + (other.ty - self.ty) * t,
  }
}

///|
pub struct AffineDecomposition2D {
  position : Point2
  scale : Point2
  rotation : Double
  skew : Double
  determinant : Double
} derive(Debug)

///|
pub fn AffineDecomposition2D::position(self : AffineDecomposition2D) -> Point2 {
  self.position
}

///|
pub fn AffineDecomposition2D::scale(self : AffineDecomposition2D) -> Point2 {
  self.scale
}

///|
pub fn AffineDecomposition2D::rotation(self : AffineDecomposition2D) -> Double {
  self.rotation
}

///|
pub fn AffineDecomposition2D::skew(self : AffineDecomposition2D) -> Double {
  self.skew
}

///|
pub fn AffineDecomposition2D::determinant(
  self : AffineDecomposition2D,
) -> Double {
  self.determinant
}

///|
pub fn Affine2D::decompose(self : Affine2D) -> AffineDecomposition2D {
  let scale_x = @math.hypot(self.a, self.b)
  let rotation = if scale_x == 0.0 { 0.0 } else { @math.atan2(self.b, self.a) }
  let normalized_a = if scale_x == 0.0 { 0.0 } else { self.a / scale_x }
  let normalized_b = if scale_x == 0.0 { 0.0 } else { self.b / scale_x }
  let shear = normalized_a * self.c + normalized_b * self.d
  let orthogonal_c = self.c - normalized_a * shear
  let orthogonal_d = self.d - normalized_b * shear
  let scale_y = @math.hypot(orthogonal_c, orthogonal_d)
  {
    position: point2(x=self.tx, y=self.ty),
    scale: point2(x=scale_x, y=scale_y),
    rotation,
    skew: if scale_y == 0.0 {
      0.0
    } else {
      @math.atan(shear / scale_y)
    },
    determinant: self.determinant(),
  }
}

///|
pub fn transform2d_from_affine(value : Affine2D) -> Transform2D {
  let decomposition = value.decompose()
  transform2d(
    position=decomposition.position,
    scale=decomposition.scale,
    rotation=decomposition.rotation,
    skew=decomposition.skew,
  )
}

///|
pub fn transform2d_apply(value : Transform2D, point : Point2) -> Point2 {
  affine2d_from_transform(value).apply(point)
}

///|
pub struct Transform2Sample {
  time : Double
  value : Transform2D
  matrix : Affine2D
} derive(Debug)

///|
pub fn transform2_sample(
  time : Double,
  value : Transform2D,
) -> Transform2Sample {
  { time, value, matrix: affine2d_from_transform(value) }
}

///|
pub fn Transform2Sample::time(self : Transform2Sample) -> Double {
  self.time
}

///|
pub fn Transform2Sample::value(self : Transform2Sample) -> Transform2D {
  self.value
}

///|
pub fn Transform2Sample::matrix(self : Transform2Sample) -> Affine2D {
  self.matrix
}

///|
pub fn Transform3D::translated(
  self : Transform3D,
  offset : Point3,
) -> Transform3D {
  transform3d(
    position=point3(
      x=self.position().x() + offset.x(),
      y=self.position().y() + offset.y(),
      z=self.position().z() + offset.z(),
    ),
    rotation=self.rotation(),
    scale=self.scale(),
  )
}

///|
pub fn Transform3D::scaled(
  self : Transform3D,
  multiplier : Point3,
) -> Transform3D {
  transform3d(
    position=self.position(),
    rotation=self.rotation(),
    scale=point3(
      x=self.scale().x() * multiplier.x(),
      y=self.scale().y() * multiplier.y(),
      z=self.scale().z() * multiplier.z(),
    ),
  )
}

///|
pub fn Transform3D::around(self : Transform3D, pivot : Point3) -> Transform3D {
  let offset = point3(
    x=self.position().x() - pivot.x(),
    y=self.position().y() - pivot.y(),
    z=self.position().z() - pivot.z(),
  )
  let rotated = self.rotation().rotate(offset)
  transform3d(
    position=point3(
      x=pivot.x() + rotated.x(),
      y=pivot.y() + rotated.y(),
      z=pivot.z() + rotated.z(),
    ),
    rotation=self.rotation(),
    scale=self.scale(),
  )
}

///|
pub fn Transform3D::is_identity(self : Transform3D) -> Bool {
  self.position().x() == 0.0 &&
  self.position().y() == 0.0 &&
  self.position().z() == 0.0 &&
  self.rotation().x() == 0.0 &&
  self.rotation().y() == 0.0 &&
  self.rotation().z() == 0.0 &&
  self.rotation().w() == 1.0 &&
  self.scale().x() == 1.0 &&
  self.scale().y() == 1.0 &&
  self.scale().z() == 1.0
}

///|
pub fn transform3d_average(
  values : Array[Transform3D],
  weights : Array[Double],
) -> Transform3D {
  if values.length() == 0 {
    return Transform3D::identity()
  }
  let count = values.length().min(weights.length())
  if count == 0 {
    return Transform3D::identity()
  }
  let normalized = normalize_weights(weights)
  let mut result = values[0]
  for index in 1..