///|
/// Errors raised by transform operations.
pub(all) suberror TransformError {
  Singular
}

///|
/// A two-dimensional affine transform represented as
/// x' = a*x + c*y + e and y' = b*x + d*y + f.
pub struct Affine2D {
  a : Double
  b : Double
  c : Double
  d : Double
  e : Double
  f : Double
}

///|
/// Construct the identity transform.
pub fn identity() -> Affine2D {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0, f: 0.0, }
}

///|
/// Construct a translation transform.
pub fn translation(x : Double, y : Double) -> Affine2D {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: x, f: y, }
}

///|
/// Construct a scaling transform.
pub fn scaling(x : Double, y : Double) -> Affine2D {
  { a: x, b: 0.0, c: 0.0, d: y, e: 0.0, f: 0.0, }
}

///|
/// Construct an affine transform directly from the six non-trivial matrix
/// values. The matrix is
/// `[[a, c, e], [b, d, f], [0, 0, 1]]`.
pub fn affine_from_values(
  a : Double,
  b : Double,
  c : Double,
  d : Double,
  e : Double,
  f : Double,
) -> Affine2D {
  { a, b, c, d, e, f, }
}

///|
/// Construct a counter-clockwise rotation in radians around the origin.
pub fn rotation(radians : Double) -> Affine2D {
  let cosine = @math.cos(radians)
  let sine = @math.sin(radians)
  { a: cosine, b: sine, c: -sine, d: cosine, e: 0.0, f: 0.0, }
}

///|
/// Construct a rotation in degrees around the origin.
pub fn rotation_degrees(degrees : Double) -> Affine2D {
  rotation(degrees * @math.PI / 180.0)
}

///|
/// Construct an x/y shear transform. The arguments are shear angles in
/// radians, matching Matplotlib's `Affine2D.skew` convention.
pub fn skew(x_radians : Double, y_radians : Double) -> Affine2D {
  {
    a: 1.0,
    b: @math.tan(y_radians),
    c: @math.tan(x_radians),
    d: 1.0,
    e: 0.0,
    f: 0.0,
  }
}

///|
/// Construct an x/y shear transform from degree angles.
pub fn skew_degrees(x_degrees : Double, y_degrees : Double) -> Affine2D {
  skew(x_degrees * @math.PI / 180.0, y_degrees * @math.PI / 180.0)
}

///|
/// Compose transforms so the returned transform applies `other` first.
pub fn Affine2D::then(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,
    e: self.a * other.e + self.c * other.f + self.e,
    f: self.b * other.e + self.d * other.f + self.f,
  }
}

///|
/// Compose two transforms using the same order as `then`.
pub fn Affine2D::compose(self : Affine2D, other : Affine2D) -> Affine2D {
  self.then(other)
}

///|
/// Return the six affine matrix values in `(a, b, c, d, e, f)` order.
pub fn Affine2D::to_values(
  self : Affine2D,
) -> (Double, Double, Double, Double, Double, Double) {
  (self.a, self.b, self.c, self.d, self.e, self.f)
}

///|
/// Return the affine matrix as three row-major rows.
pub fn Affine2D::matrix(self : Affine2D) -> Array[Array[Double]] {
  [[self.a, self.c, self.e], [self.b, self.d, self.f], [0.0, 0.0, 1.0]]
}

///|
/// Whether x and y are transformed independently.
pub fn Affine2D::is_separable(self : Affine2D) -> Bool {
  self.b == 0.0 && self.c == 0.0
}

///|
/// Apply a transform to one point.
pub fn Affine2D::apply(self : Affine2D, value : @core.Point) -> @core.Point {
  @core.point(
    self.a * value.x() + self.c * value.y() + self.e,
    self.b * value.x() + self.d * value.y() + self.f,
  )
}

///|
/// Apply only the linear part of a transform to a displacement vector.
pub fn Affine2D::apply_delta(
  self : Affine2D,
  value : @core.Point,
) -> @core.Point {
  @core.point(
    self.a * value.x() + self.c * value.y(),
    self.b * value.x() + self.d * value.y(),
  )
}

///|
/// Return a transform with an additional translation applied after `self`.
pub fn Affine2D::translated(
  self : Affine2D,
  x : Double,
  y : Double,
) -> Affine2D {
  translation(x, y).then(self)
}

///|
/// Return a transform with an additional scale applied after `self`.
pub fn Affine2D::scaled(self : Affine2D, x : Double, y : Double) -> Affine2D {
  scaling(x, y).then(self)
}

///|
/// Return a transform with an additional rotation applied after `self`.
pub fn Affine2D::rotated(self : Affine2D, radians : Double) -> Affine2D {
  rotation(radians).then(self)
}

///|
/// Return a transform with an additional shear applied after `self`.
pub fn Affine2D::skewed(
  self : Affine2D,
  x_radians : Double,
  y_radians : Double,
) -> Affine2D {
  skew(x_radians, y_radians).then(self)
}

///|
/// Apply a transform to an array without mutating the input.
pub fn Affine2D::apply_all(
  self : Affine2D,
  values : Array[@core.Point],
) -> Array[@core.Point] {
  let result = Array(capacity=values.length())
  for value in values {
    result.push(self.apply(value))
  }
  result
}

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

///|
/// Compute the inverse transform.
pub fn Affine2D::inverse(self : Affine2D) -> Affine2D raise TransformError {
  let determinant = self.determinant()
  if determinant == 0.0 || determinant.is_nan() || determinant.is_inf() {
    raise TransformError::Singular
  }
  let inverse_determinant = 1.0 / determinant
  let a = self.d * inverse_determinant
  let b = -self.b * inverse_determinant
  let c = -self.c * inverse_determinant
  let d = self.a * inverse_determinant
  { a, b, c, d, e: -(a * self.e + c * self.f), f: -(b * self.e + d * self.f), }
}