///|
/// Generates a list of translation/rotation matrices for cyclic symmetry Cn
pub fn cyclic_symmetries(n : Int, cx : Double, cy : Double) -> Array[Matrix2D] {
  let list = []
  let step = 2.0 * @math.PI / n.to_double()
  for i in 0.. Array[Matrix2D] {
  let list = []
  let step = 2.0 * @math.PI / n.to_double()
  for i in 0.. Array[Matrix2D] {
  let list = []
  let cx = w / 2.0
  let cy = h / 2.0

  let ident = Matrix2D::identity()
  let refl_x = Matrix2D::reflect(0.0) // y' = -y
  // Translate to cell center, reflect, translate back
  let cell_refl_x = Matrix2D::translate(cx, cy)
    .multiply(refl_x)
    .multiply(Matrix2D::translate(-cx, -cy))
  let cell_refl_y = Matrix2D::translate(cx, cy)
    .multiply(Matrix2D::reflect(@math.PI / 2.0)) // x' = -x
    .multiply(Matrix2D::translate(-cx, -cy))

  match group_name {
    "p1" => list.push(ident)
    "p2" => {
      list.push(ident)
      list.push(
        Matrix2D::translate(cx, cy)
        .multiply(Matrix2D::rotate(@math.PI))
        .multiply(Matrix2D::translate(-cx, -cy)),
      )
    }
    "pm" => {
      list.push(ident)
      list.push(cell_refl_x)
    }
    "pmm" => {
      list.push(ident)
      list.push(cell_refl_x)
      list.push(cell_refl_y)
      list.push(cell_refl_x.multiply(cell_refl_y))
    }
    "p4" =>
      for i in 0..<=3 {
        let angle = i.to_double() * (@math.PI / 2.0)
        let r = Matrix2D::translate(cx, cy)
          .multiply(Matrix2D::rotate(angle))
          .multiply(Matrix2D::translate(-cx, -cy))
        list.push(r)
      }
    "p4m" =>
      for i in 0..<=3 {
        let angle = i.to_double() * (@math.PI / 2.0)
        let r = Matrix2D::translate(cx, cy)
          .multiply(Matrix2D::rotate(angle))
          .multiply(Matrix2D::translate(-cx, -cy))
        list.push(r)
        list.push(r.multiply(cell_refl_x))
      }
    "p6" =>
      for i in 0..<=5 {
        let angle = i.to_double() * (@math.PI / 3.0)
        let r = Matrix2D::translate(cx, cy)
          .multiply(Matrix2D::rotate(angle))
          .multiply(Matrix2D::translate(-cx, -cy))
        list.push(r)
      }
    "p6m" =>
      for i in 0..<=5 {
        let angle = i.to_double() * (@math.PI / 3.0)
        let r = Matrix2D::translate(cx, cy)
          .multiply(Matrix2D::rotate(angle))
          .multiply(Matrix2D::translate(-cx, -cy))
        list.push(r)
        list.push(r.multiply(cell_refl_x))
      }
    _ => list.push(ident) // fallback to p1
  }
  list
}

///|
/// Generates a full tiled layout of matrices for the given wallpaper group
pub fn generate_wallpaper_transforms(
  group_name : String,
  width : Double,
  height : Double,
  cell_w : Double,
  cell_h : Double,
) -> Array[Matrix2D] {
  let list = []
  let cell_syms = wallpaper_cell_symmetries(group_name, cell_w, cell_h)

  let cols = (width / cell_w).ceil().to_int() + 1
  let rows = (height / cell_h).ceil().to_int() + 1

  let start_r = -1
  let start_c = -1
  for r in start_r..