///|
/// A single transformation operation.
pub(all) enum TransformOp {
  Scale((Double, Double), Double, Double)
  Rotate((Double, Double), Double)
  Translate(Double, Double)
  ShearX((Double, Double), Double)
  ShearY((Double, Double), Double)
} derive(Debug)

///|
/// A list of transformations, stored with the most recent op at the head.
pub struct Transform {
  ops : Array[TransformOp]
} derive(Debug)

///|
/// A transformation matrix (a c e / b d f / 0 0 1).
pub(all) struct TransformMatrix {
  a : Double
  b : Double
  c : Double
  d : Double
  e : Double
  f : Double
} derive(Debug)

///|
/// The identity transform.
pub fn Transform::identity() -> Transform {
  { ops: [] }
}

///|
/// Create a transform from an array of ops.
pub fn Transform::new(ops : Array[TransformOp]) -> Transform {
  { ops, }
}

///|
/// The identity matrix.
pub fn TransformMatrix::identity() -> TransformMatrix {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0, f: 0.0 }
}

///|
fn point_eq(a : (Double, Double), b : (Double, Double)) -> Bool {
  a.0 == b.0 && a.1 == b.1
}

///|
fn concat_ops(
  a : Array[TransformOp],
  b : Array[TransformOp],
) -> Array[TransformOp] {
  let out = Array::new(capacity=a.length() + b.length())
  for item in a {
    out.push(item)
  }
  for item in b {
    out.push(item)
  }
  out
}

///|
fn tail_ops(tr : Array[TransformOp]) -> Array[TransformOp] {
  let out = Array::new(
    capacity=if tr.length() > 0 { tr.length() - 1 } else { 0 },
  )
  let mut i = 1
  while i < tr.length() {
    out.push(tr[i])
    i = i + 1
  }
  out
}

///|
fn safe_float(value : Double) -> Double {
  if value == 0.0 || value.is_nan() || value.is_inf() {
    0.0
  } else {
    value
  }
}

///|
fn pi() -> Double {
  4.0 * @math.atan(1.0)
}

///|
fn string_of_op(op : TransformOp) -> String {
  match op {
    TransformOp::Scale((x, y), sx, sy) =>
      "Scale about (\{x}, \{y}) by \{sx} in x and \{sy} in y\n"
    TransformOp::Rotate((x, y), angle) =>
      "Rotate by \{angle} about (\{x}, \{y})\n"
    TransformOp::Translate(dx, dy) => "Translate by \{dx}, \{dy}\n"
    TransformOp::ShearX((x, y), factor) =>
      "Shear in X about (\{x}, \{y}), proportionality constant \{factor}\n"
    TransformOp::ShearY((x, y), factor) =>
      "Shear in Y about (\{x}, \{y}), proportionality constant \{factor}\n"
  }
}

///|
/// Make a string of a transform for debug purposes.
pub fn Transform::to_string(self : Transform) -> String {
  let sb = StringBuilder::new()
  for op in self.ops[:].rev_iter() {
    sb.write_string(string_of_op(op))
  }
  sb.to_string()
}

///|
/// String of a transformation matrix.
pub fn TransformMatrix::to_string(self : TransformMatrix) -> String {
  "\{self.a}, \{self.b}, \{self.c}, \{self.d}, \{self.e}, \{self.f}"
}

///|
/// Compose a transformation operation onto an existing transform.
pub fn Transform::compose(self : Transform, op : TransformOp) -> Transform {
  if self.ops.length() == 0 {
    return { ops: [op] }
  }
  let head = self.ops[0]
  match (head, op) {
    (TransformOp::Translate(dx, dy), TransformOp::Translate(dx2, dy2)) =>
      {
        ops: concat_ops(
          [TransformOp::Translate(dx + dx2, dy + dy2)],
          tail_ops(self.ops),
        ),
      }
    (TransformOp::Scale(p, sx, sy), TransformOp::Scale(p2, sx2, sy2)) =>
      if point_eq(p, p2) {
        {
          ops: concat_ops(
            [TransformOp::Scale(p, sx * sx2, sy * sy2)],
            tail_ops(self.ops),
          ),
        }
      } else {
        { ops: concat_ops([op], self.ops) }
      }
    (TransformOp::Rotate(p, a), TransformOp::Rotate(p2, a2)) =>
      if point_eq(p, p2) {
        {
          ops: concat_ops([TransformOp::Rotate(p, a + a2)], tail_ops(self.ops)),
        }
      } else {
        { ops: concat_ops([op], self.ops) }
      }
    (TransformOp::ShearX(p, a), TransformOp::ShearX(p2, a2)) =>
      if point_eq(p, p2) {
        {
          ops: concat_ops([TransformOp::ShearX(p, a + a2)], tail_ops(self.ops)),
        }
      } else {
        { ops: concat_ops([op], self.ops) }
      }
    (TransformOp::ShearY(p, a), TransformOp::ShearY(p2, a2)) =>
      if point_eq(p, p2) {
        {
          ops: concat_ops([TransformOp::ShearY(p, a + a2)], tail_ops(self.ops)),
        }
      } else {
        { ops: concat_ops([op], self.ops) }
      }
    _ => { ops: concat_ops([op], self.ops) }
  }
}

///|
/// Append two transforms (perform b then a).
pub fn Transform::append(self : Transform, other : Transform) -> Transform {
  { ops: concat_ops(self.ops, other.ops) }
}

///|
/// Compose two matrices. Applying the result is equivalent to applying m then m'.
pub fn TransformMatrix::compose(
  self : TransformMatrix,
  m : TransformMatrix,
) -> TransformMatrix {
  {
    a: self.a * m.a + self.c * m.b,
    c: self.a * m.c + self.c * m.d,
    e: self.a * m.e + self.c * m.f + self.e,
    b: self.b * m.a + self.d * m.b,
    d: self.b * m.c + self.d * m.d,
    f: self.b * m.e + self.d * m.f + self.f,
  }
}

///|
/// Matrix inversion. Raises NonInvertable if no inverse.
pub fn TransformMatrix::invert(
  self : TransformMatrix,
) -> TransformMatrix raise NonInvertable {
  let divisor = self.a * self.d - self.b * self.c
  if divisor == 0.0 {
    raise NonInvertable::NonInvertable
  }
  let det = 1.0 / divisor
  if det == 0.0 || det.is_nan() || det.is_inf() {
    raise NonInvertable::NonInvertable
  }
  {
    a: det * self.d,
    b: det * -self.b,
    c: det * -self.c,
    d: det * self.a,
    e: det * (self.c * self.f - self.d * self.e),
    f: det * (self.b * self.e - self.a * self.f),
  }
}

///|
/// Make a translation matrix.
pub fn TransformMatrix::translate(tx : Double, ty : Double) -> TransformMatrix {
  { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: tx, f: ty }
}

///|
/// Make a scale matrix about a center point.
pub fn TransformMatrix::scale(
  center : (Double, Double),
  sx : Double,
  sy : Double,
) -> TransformMatrix {
  let translate = TransformMatrix::translate(-center.0, -center.1)
  let translate_back = TransformMatrix::translate(center.0, center.1)
  let scale_matrix = { a: sx, b: 0.0, c: 0.0, d: sy, e: 0.0, f: 0.0 }
  translate_back.compose(scale_matrix.compose(translate))
}

///|
/// Make a rotation matrix about a center point.
pub fn TransformMatrix::rotate(
  center : (Double, Double),
  angle : Double,
) -> TransformMatrix {
  let translate = TransformMatrix::translate(-center.0, -center.1)
  let translate_back = TransformMatrix::translate(center.0, center.1)
  let rotation_matrix = {
    a: @math.cos(angle),
    b: @math.sin(angle),
    c: -@math.sin(angle),
    d: @math.cos(angle),
    e: 0.0,
    f: 0.0,
  }
  translate_back.compose(rotation_matrix.compose(translate))
}

///|
/// Matrix to shear in x about a point.
pub fn TransformMatrix::shear_x(
  center : (Double, Double),
  factor : Double,
) -> TransformMatrix {
  let translate = TransformMatrix::translate(-center.0, -center.1)
  let translate_back = TransformMatrix::translate(center.0, center.1)
  let shear_matrix = { a: 1.0, b: 0.0, c: factor, d: 1.0, e: 0.0, f: 0.0 }
  translate_back.compose(shear_matrix.compose(translate))
}

///|
/// Matrix to shear in y about a point.
pub fn TransformMatrix::shear_y(
  center : (Double, Double),
  factor : Double,
) -> TransformMatrix {
  let translate = TransformMatrix::translate(-center.0, -center.1)
  let translate_back = TransformMatrix::translate(center.0, center.1)
  let shear_matrix = { a: 1.0, b: factor, c: 0.0, d: 1.0, e: 0.0, f: 0.0 }
  translate_back.compose(shear_matrix.compose(translate))
}

///|
/// Make a matrix from a single transformation operation.
pub fn TransformOp::to_matrix(self : TransformOp) -> TransformMatrix {
  match self {
    TransformOp::Scale(center, sx, sy) => TransformMatrix::scale(center, sx, sy)
    TransformOp::Rotate(center, angle) => TransformMatrix::rotate(center, angle)
    TransformOp::Translate(dx, dy) => TransformMatrix::translate(dx, dy)
    TransformOp::ShearX(center, factor) =>
      TransformMatrix::shear_x(center, factor)
    TransformOp::ShearY(center, factor) =>
      TransformMatrix::shear_y(center, factor)
  }
}

///|
/// Make a matrix from a transform.
pub fn Transform::to_matrix(self : Transform) -> TransformMatrix {
  let mut acc = TransformMatrix::identity()
  for op in self.ops {
    acc = op.to_matrix().compose(acc)
  }
  acc
}

///|
/// Transform a coordinate by a given transformation matrix.
pub fn TransformMatrix::apply(
  self : TransformMatrix,
  point : (Double, Double),
) -> (Double, Double) {
  let x = point.0
  let y = point.1
  (x * self.a + y * self.c + self.e, x * self.b + y * self.d + self.f)
}

///|
/// Transform a coordinate by a given transform.
pub fn Transform::apply(
  self : Transform,
  point : (Double, Double),
) -> (Double, Double) {
  let mut x = point.0
  let mut y = point.1
  for op in self.ops[:].rev_iter() {
    match op {
      TransformOp::Scale((cx, cy), sx, sy) => {
        let dx = x - cx
        let dy = y - cy
        x = dx * sx + cx
        y = dy * sy + cy
      }
      TransformOp::Rotate((cx, cy), angle) => {
        let cosine = @math.cos(angle)
        let sine = @math.sin(angle)
        let dx = x - cx
        let dy = y - cy
        let nx = dx * cosine + dy * -sine
        let ny = dx * sine + dy * cosine
        x = nx + cx
        y = ny + cy
      }
      TransformOp::Translate(dx, dy) => {
        x = x + dx
        y = y + dy
      }
      TransformOp::ShearX((cx, cy), factor) => {
        let dx = x - cx
        let dy = y - cy
        x = dx + dy * factor + cx
        y = dy + cy
      }
      TransformOp::ShearY((cx, cy), factor) => {
        let dx = x - cx
        let dy = y - cy
        x = dx + cx
        y = dx * factor + dy + cy
      }
    }
  }
  (x, y)
}

///|
/// Decompose a transformation matrix into scale, aspect, rotation, shear,
/// translation in x, translation in y.
pub fn TransformMatrix::decompose(
  self : TransformMatrix,
) -> (Double, Double, Double, Double, Double, Double) {
  let axb = self.a * self.d - self.c * self.b
  let moda = (self.a * self.a + self.b * self.b).sqrt()
  let modb = (self.c * self.c + self.d * self.d).sqrt()
  let adotb = self.a * self.c + self.b * self.d
  let scale = axb / moda
  let abs_scale = scale.abs()
  let aspect = if abs_scale == 0.0 { 1.0 } else { moda / abs_scale }
  let rotation = @math.atan2(self.b, self.a)
  let shear = if moda * modb == 0.0 {
    0.0
  } else {
    pi() / 2.0 - @math.acos(adotb / (moda * modb))
  }
  (
    safe_float(scale),
    safe_float(aspect),
    safe_float(rotation),
    safe_float(shear),
    safe_float(self.e),
    safe_float(self.f),
  )
}

///|
/// Recompose a matrix from components.
pub fn TransformMatrix::recompose(
  scale : Double,
  aspect : Double,
  rotation : Double,
  shear : Double,
  tx : Double,
  ty : Double,
) -> TransformMatrix {
  let scale_aspect_shear = {
    a: scale.abs() * aspect,
    b: 0.0,
    c: scale * @math.tan(shear),
    d: scale,
    e: 0.0,
    f: 0.0,
  }
  let rotated = TransformMatrix::rotate((0.0, 0.0), rotation).compose(
    scale_aspect_shear,
  )
  TransformMatrix::translate(tx, ty).compose(rotated)
}

///|
/// Exception raised if a matrix is non-invertible.
pub suberror NonInvertable {
  NonInvertable
}