///|
/// A composable two-dimensional transform tree.
///
/// Every constructor in this first tree is affine in the plane, but keeping
/// composition explicit mirrors Matplotlib's transform graph and lets layout
/// code carry one value through point, path, and BBox operations.
pub(all) enum Transform2D {
  Affine(Affine2D)
  Composite(Transform2D, Transform2D)
  BboxMap(BBox, BBox)
  BboxTo(BBox)
  BboxFrom(BBox)
  Blended(Transform2D, Transform2D)
  ScaledTranslation(Double, Double, Affine2D)
}

///|
/// Wrap an affine transform in the transform tree.
pub fn affine_transform(value : Affine2D) -> Transform2D {
  Transform2D::Affine(value)
}

///|
/// Return the identity transform as a tree node.
pub fn identity_transform() -> Transform2D {
  Transform2D::Affine(identity())
}

///|
/// Compose transforms so that `second` is applied first, matching
/// `Affine2D::then`.
pub fn composite_transform(
  first : Transform2D,
  second : Transform2D,
) -> Transform2D {
  Transform2D::Composite(first, second)
}

///|
/// Map coordinates linearly from one bounding box to another.
pub fn bbox_transform(input : BBox, output : BBox) -> Transform2D {
  Transform2D::BboxMap(input, output)
}

///|
/// Map the unit box to `output`.
pub fn bbox_transform_to(output : BBox) -> Transform2D {
  Transform2D::BboxTo(output)
}

///|
/// Map `input` to the unit box.
pub fn bbox_transform_from(input : BBox) -> Transform2D {
  Transform2D::BboxFrom(input)
}

///|
/// Use one transform for the x coordinate and another for y.
pub fn blended_transform(
  x_transform : Transform2D,
  y_transform : Transform2D,
) -> Transform2D {
  Transform2D::Blended(x_transform, y_transform)
}

///|
/// Translate by `(x, y)` after expressing that offset in `scale_transform`'s
/// coordinate system. This is the transform-tree equivalent of
/// Matplotlib's `ScaledTranslation`.
pub fn scaled_translation(
  x : Double,
  y : Double,
  scale_transform : Affine2D,
) -> Transform2D {
  Transform2D::ScaledTranslation(x, y, scale_transform)
}

///|
fn bbox_map_affine(input : BBox, output : BBox) -> Affine2D {
  let input_width = input.width()
  let input_height = input.height()
  let x_scale = output.width() / input_width
  let y_scale = output.height() / input_height
  affine_from_values(
    x_scale,
    0.0,
    0.0,
    y_scale,
    output.x0() - input.x0() * x_scale,
    output.y0() - input.y0() * y_scale,
  )
}

///|
fn Transform2D::to_affine(self : Transform2D) -> Affine2D {
  match self {
    Transform2D::Affine(value) => value
    Transform2D::Composite(first, second) =>
      first.to_affine().then(second.to_affine())
    Transform2D::BboxMap(input, output) => bbox_map_affine(input, output)
    Transform2D::BboxTo(output) => bbox_map_affine(unit_bbox(), output)
    Transform2D::BboxFrom(input) => bbox_map_affine(input, unit_bbox())
    Transform2D::Blended(x_transform, y_transform) => {
      let x_values = x_transform.to_affine().to_values()
      let y_values = y_transform.to_affine().to_values()
      let (x_a, _, x_c, _, x_e, _) = x_values
      let (_, y_b, _, y_d, _, y_f) = y_values
      affine_from_values(x_a, y_b, x_c, y_d, x_e, y_f)
    }
    Transform2D::ScaledTranslation(x, y, scale_transform) => {
      let offset = scale_transform.apply(@core.point(x, y))
      affine_from_values(1.0, 0.0, 0.0, 1.0, offset.x(), offset.y())
    }
  }
}

///|
/// Materialize the current tree as one affine transform.
pub fn Transform2D::affine(self : Transform2D) -> Affine2D {
  self.to_affine()
}

///|
/// Apply the transform to one point.
pub fn Transform2D::apply(
  self : Transform2D,
  value : @core.Point,
) -> @core.Point {
  self.to_affine().apply(value)
}

///|
/// Apply the transform to an array without mutating the input.
pub fn Transform2D::apply_all(
  self : Transform2D,
  values : Array[@core.Point],
) -> Array[@core.Point] {
  self.to_affine().apply_all(values)
}

///|
/// Apply the transform to a displacement vector.
pub fn Transform2D::apply_delta(
  self : Transform2D,
  value : @core.Point,
) -> @core.Point {
  self.to_affine().apply_delta(value)
}

///|
/// Compose a tree transform using the same order as `Affine2D::then`.
pub fn Transform2D::then(
  self : Transform2D,
  other : Transform2D,
) -> Transform2D {
  composite_transform(self, other)
}

///|
/// Return whether the materialized transform has no x/y cross terms.
pub fn Transform2D::is_separable(self : Transform2D) -> Bool {
  self.to_affine().is_separable()
}

///|
/// Invert the transform tree.
pub fn Transform2D::inverse(
  self : Transform2D,
) -> Transform2D raise TransformError {
  match self {
    Transform2D::Affine(value) => Transform2D::Affine(value.inverse())
    Transform2D::Composite(first, second) =>
      Transform2D::Composite(second.inverse(), first.inverse())
    Transform2D::BboxMap(input, output) => Transform2D::BboxMap(output, input)
    Transform2D::BboxTo(output) => Transform2D::BboxFrom(output)
    Transform2D::BboxFrom(input) => Transform2D::BboxTo(input)
    Transform2D::Blended(x_transform, y_transform) => {
      if !x_transform.is_separable() || !y_transform.is_separable() {
        raise TransformError::Singular
      }
      Transform2D::Blended(x_transform.inverse(), y_transform.inverse())
    }
    Transform2D::ScaledTranslation(x, y, scale_transform) =>
      Transform2D::Affine(
        scaled_translation(x, y, scale_transform).affine().inverse(),
      )
  }
}

///|
/// Transform all four corners and return the enclosing axis-aligned box.
pub fn Transform2D::transform_bbox(self : Transform2D, value : BBox) -> BBox {
  value.transformed(self.to_affine())
}

///|
/// Transform every vertex and code in a path.
pub fn Transform2D::transform_path(
  self : Transform2D,
  value : Path,
) -> Path raise PathError {
  value.transformed(self.to_affine())
}