///|
/// Placement policies for repeating a motif across a rectangular surface.
pub(all) enum TileMode {
  Repeat
  MirrorX
  MirrorY
  MirrorBoth
  RotateQuarter
} derive(Eq, Debug, ToJson)

///|
/// A bounded tile surface with deterministic cell geometry.
pub(all) struct TileGrid {
  bounds : Rect
  tile_width : Double
  tile_height : Double
  columns : Int
  rows : Int
  mode : TileMode
} derive(Eq, Debug, ToJson)

///|
/// A single tile cell and its local-to-surface transform.
pub(all) struct TileCell {
  row : Int
  column : Int
  bounds : Rect
  transform : Matrix2D
} derive(Eq, Debug, ToJson)

///|
/// Reports how well a point set joins at the edges of a tile.
pub(all) struct SeamReport {
  horizontal_error : Double
  vertical_error : Double
  sample_count : Int
  periodic : Bool
} derive(Eq, Debug, ToJson)

///|
/// Creates a tile grid. Invalid dimensions collapse to a safe empty surface.
pub fn TileGrid::new(
  bounds : Rect,
  tile_width : Double,
  tile_height : Double,
  mode? : TileMode = Repeat,
) -> TileGrid {
  let width = tile_width.max(0.0)
  let height = tile_height.max(0.0)
  let columns = if width <= 0.0 {
    0
  } else {
    (bounds.width / width + 0.999999).to_int()
  }
  let rows = if height <= 0.0 {
    0
  } else {
    (bounds.height / height + 0.999999).to_int()
  }
  { bounds, tile_width: width, tile_height: height, columns, rows, mode }
}

///|
pub fn TileGrid::with_counts(
  self : TileGrid,
  columns : Int,
  rows : Int,
) -> TileGrid {
  { ..self, columns: columns.max(0), rows: rows.max(0) }
}

///|
pub fn TileGrid::cell_count(self : TileGrid) -> Int {
  self.columns * self.rows
}

///|
pub fn TileGrid::surface_size(self : TileGrid) -> Vec2 {
  Vec2::new(self.bounds.width, self.bounds.height)
}

///|
fn tile_origin(grid : TileGrid, row : Int, column : Int) -> Vec2 {
  Vec2::new(
    grid.bounds.x + column.to_double() * grid.tile_width,
    grid.bounds.y + row.to_double() * grid.tile_height,
  )
}

///|
fn tile_center(grid : TileGrid, row : Int, column : Int) -> Vec2 {
  let origin = tile_origin(grid, row, column)
  Vec2::new(origin.x + grid.tile_width / 2.0, origin.y + grid.tile_height / 2.0)
}

///|
fn mirror_transform(grid : TileGrid, row : Int, column : Int) -> Matrix2D {
  let origin = tile_origin(grid, row, column)
  let x = origin.x
  let y = origin.y
  let mirror_x = match grid.mode {
    MirrorX => column % 2 == 1
    MirrorBoth => column % 2 == 1
    _ => false
  }
  let mirror_y = match grid.mode {
    MirrorY => row % 2 == 1
    MirrorBoth => row % 2 == 1
    _ => false
  }
  let sx = if mirror_x { -1.0 } else { 1.0 }
  let sy = if mirror_y { -1.0 } else { 1.0 }
  Matrix2D::translate(
    if mirror_x {
      x + grid.tile_width
    } else {
      x
    },
    if mirror_y {
      y + grid.tile_height
    } else {
      y
    },
  ).multiply(Matrix2D::scale(sx, sy))
}

///|
fn cell_transform(grid : TileGrid, row : Int, column : Int) -> Matrix2D {
  match grid.mode {
    RotateQuarter => {
      let center = tile_center(grid, row, column)
      Matrix2D::translate(center.x, center.y)
      .multiply(
        Matrix2D::rotate(
          if (row + column) % 2 == 0 {
            0.0
          } else {
            @math.PI / 2.0
          },
        ),
      )
      .multiply(Matrix2D::translate(-center.x, -center.y))
      .multiply(
        Matrix2D::translate(
          tile_origin(grid, row, column).x,
          tile_origin(grid, row, column).y,
        ),
      )
    }
    _ => mirror_transform(grid, row, column)
  }
}

///|
/// Enumerates tile cells in row-major order.
pub fn TileGrid::cells(self : TileGrid) -> Array[TileCell] {
  let result = []
  for row in 0.. Array[Matrix2D] {
  let result = []
  for cell in self.cells() {
    result.push(cell.transform)
  }
  result
}

///|
/// Transforms one path into every tile cell.
pub fn tile_path(path : Path, grid : TileGrid) -> Array[Path] {
  let result = []
  for transform in grid.transforms() {
    result.push(path.transform(transform))
  }
  result
}

///|
/// Stamps a motif into every tile cell while preserving its style.
pub fn tile_motif(motif : Motif, grid : TileGrid) -> Array[Motif] {
  let result = []
  for cell in grid.cells() {
    let (x, y) = cell.transform.transform_point(motif.anchor.x, motif.anchor.y)
    let quarter_turn = match grid.mode {
      RotateQuarter =>
        if (cell.row + cell.column) % 2 == 0 {
          0.0
        } else {
          @math.PI / 2.0
        }
      _ => 0.0
    }
    result.push(motif.placed(x, y, motif.scale, motif.rotation + quarter_turn))
  }
  result
}

///|
/// Renders a repeated path as an SVG document using the supplied style.
pub fn tile_document(
  path : Path,
  style : SvgStyle,
  grid : TileGrid,
) -> SvgDocument {
  let document = SvgDocument::new(grid.bounds.width, grid.bounds.height)
  for placed in tile_path(path, grid) {
    document.add(SvgElement::SvgPath(placed, style))
  }
  document
}

///|
/// Wraps a point into a tile-local coordinate system.
pub fn wrap_point(point : Vec2, width : Double, height : Double) -> Vec2 {
  if width <= 0.0 || height <= 0.0 {
    point
  } else {
    let wx = point.x % width
    let wy = point.y % height
    Vec2::new(
      if wx < 0.0 {
        wx + width
      } else {
        wx
      },
      if wy < 0.0 {
        wy + height
      } else {
        wy
      },
    )
  }
}

///|
/// Wraps all points into a tile, useful before serializing a motif template.
pub fn wrap_points(
  points : Array[Vec2],
  width : Double,
  height : Double,
) -> Array[Vec2] {
  let result = []
  for point in points {
    result.push(wrap_point(point, width, height))
  }
  result
}

///|
/// Returns the shortest distance between two points on a periodic surface.
pub fn torus_distance(
  left : Vec2,
  right : Vec2,
  width : Double,
  height : Double,
) -> Double {
  if width <= 0.0 || height <= 0.0 {
    left.distance(right)
  } else {
    let dx = (left.x - right.x).abs()
    let dy = (left.y - right.y).abs()
    let wrapped_x = dx.min(width - dx)
    let wrapped_y = dy.min(height - dy)
    Vec2::new(wrapped_x, wrapped_y).length()
  }
}

///|
/// Measures opposite-edge point pairs for a periodic motif template.
pub fn seam_report(
  points : Array[Vec2],
  width : Double,
  height : Double,
  tolerance? : Double = 0.001,
) -> SeamReport {
  if points.length() < 2 || width <= 0.0 || height <= 0.0 {
    {
      horizontal_error: 0.0,
      vertical_error: 0.0,
      sample_count: points.length(),
      periodic: true,
    }
  } else {
    let mut horizontal = 0.0
    let mut vertical = 0.0
    for point in points {
      let wrapped = wrap_point(point, width, height)
      horizontal = horizontal.max((point.x - wrapped.x).abs())
      vertical = vertical.max((point.y - wrapped.y).abs())
    }
    let threshold = tolerance.max(0.0)
    {
      horizontal_error: horizontal,
      vertical_error: vertical,
      sample_count: points.length(),
      periodic: horizontal <= threshold && vertical <= threshold,
    }
  }
}

///|
/// Produces points repeated in a rectangular tile neighborhood.
pub fn repeat_points(
  points : Array[Vec2],
  width : Double,
  height : Double,
  radius : Int,
) -> Array[Vec2] {
  let result = []
  if width > 0.0 && height > 0.0 && radius >= 0 {
    for row in -radius..<=radius {
      for column in -radius..<=radius {
        let offset = Vec2::new(
          column.to_double() * width,
          row.to_double() * height,
        )
        for point in points {
          result.push(point.add(offset))
        }
      }
    }
  }
  result
}

///|
/// Produces a path with translated copies around a tile boundary.
pub fn repeat_path(
  path : Path,
  width : Double,
  height : Double,
  radius : Int,
) -> Array[Path] {
  let result = []
  if width > 0.0 && height > 0.0 && radius >= 0 {
    for row in -radius..<=radius {
      for column in -radius..<=radius {
        let transform = Matrix2D::translate(
          column.to_double() * width,
          row.to_double() * height,
        )
        result.push(path.transform(transform))
      }
    }
  }
  result
}

///|
/// Builds a seam-safe diagonal stripe tile.
pub fn diagonal_stripe_tile(
  width : Double,
  height : Double,
  spacing : Double,
  stroke : Double,
) -> Array[Path] {
  let result = []
  ignore(stroke)
  if width > 0.0 && height > 0.0 && spacing > 0.0 {
    let count = ((width + height) / spacing).to_int() + 2
    for i in 0.. Array[Path] {
  let result = []
  if width > 0.0 && height > 0.0 && brick_width > 0.0 && brick_height > 0.0 {
    let columns = (width / brick_width).to_int() + 1
    let rows = (height / brick_height).to_int() + 1
    for row in 0.. Array[Path] {
  let result = []
  if bands > 0 && width > 0.0 && height > 0.0 {
    let step_x = width / bands.to_double()
    let step_y = height / bands.to_double()
    for i in 0.. Array[Vec2] {
  let result = []
  if columns > 0 && rows > 0 {
    let sx = width / columns.to_double()
    let sy = height / rows.to_double()
    for row in 0.. Int {
  let mut value = grid.columns * 31 + grid.rows
  value = value * 31 + grid.tile_width.to_int()
  value = value * 31 + grid.tile_height.to_int()
  match grid.mode {
    Repeat => value
    MirrorX => value + 1
    MirrorY => value + 2
    MirrorBoth => value + 3
    RotateQuarter => value + 4
  }
}