///|
/// Integer-only geometric effects for deterministic sprite and GIF output.
pub(all) enum TileMode {
  Repeat
  Mirror
  Clamp
} derive(Eq, Debug)

///|
pub(all) struct VignetteOptions {
  radius_per_mille : Int
  strength : Int
  color : Rgba
} derive(Debug)

///|
pub fn VignetteOptions::default() -> VignetteOptions {
  { radius_per_mille: 1000, strength: 600, color: rgba(0, 0, 0, 255) }
}

///|
pub fn VignetteOptions::radius(
  self : VignetteOptions,
  value : Int,
) -> VignetteOptions {
  { ..self, radius_per_mille: if value < 1 { 1 } else { value } }
}

///|
pub fn VignetteOptions::strength(
  self : VignetteOptions,
  value : Int,
) -> VignetteOptions {
  {
    ..self,
    strength: if value < 0 {
      0
    } else if value > 1000 {
      1000
    } else {
      value
    },
  }
}

///|
pub fn VignetteOptions::color(
  self : VignetteOptions,
  value : Rgba,
) -> VignetteOptions {
  { ..self, color: value }
}

///|
fn wrapped_coordinate(value : Int, size : Int, mode : TileMode) -> Int {
  if size <= 0 {
    0
  } else {
    match mode {
      Repeat => {
        let result = value % size
        if result < 0 {
          result + size
        } else {
          result
        }
      }
      Mirror => {
        let period = size * 2
        let offset = value % period
        let normalized = if offset < 0 { offset + period } else { offset }
        if normalized < size {
          normalized
        } else {
          period - normalized - 1
        }
      }
      Clamp =>
        if value < 0 {
          0
        } else if value >= size {
          size - 1
        } else {
          value
        }
    }
  }
}

///|
/// Translate a canvas with explicit wrapping semantics.
pub fn Canvas::translate(
  self : Canvas,
  offset_x : Int,
  offset_y : Int,
  mode? : TileMode = Clamp,
) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for y in 0.. Canvas {
  if width < 0 || height < 0 {
    panic()
  }
  let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
  for y in 0.. 0 && self.height > 0 {
        result.pixels[x + y * width] = self.pixels[sx + sy * self.width]
      }
    }
  }
  result
}

///|
/// Mirror an image around its vertical and horizontal axes.
pub fn Canvas::mirror(
  self : Canvas,
  horizontal : Bool,
  vertical : Bool,
) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for y in 0.. Canvas {
  let safe_sectors = if sectors < 2 {
    2
  } else if sectors > 8 {
    8
  } else {
    sectors
  }
  let result = self.copy()
  let center_x = self.width / 2
  let center_y = self.height / 2
  for y in 0..= dy.abs() { 0 } else { 1 }
      let sx = if quadrant == 0 {
        center_x + dx.abs()
      } else {
        center_x + dy.abs()
      }
      let sy = if quadrant == 0 {
        center_y + dy.abs()
      } else {
        center_y + dx.abs()
      }
      let clamped_x = if sx < 0 {
        0
      } else if sx >= self.width {
        self.width - 1
      } else {
        sx
      }
      let clamped_y = if sy < 0 {
        0
      } else if sy >= self.height {
        self.height - 1
      } else {
        sy
      }
      let base = self.pixels[clamped_x + clamped_y * self.width]
      let angle_shift = (x + y) % safe_sectors
      result.pixels[x + y * self.width] = if angle_shift == 0 {
        base
      } else {
        rgba(base.blue(), base.red(), base.green(), base.alpha())
      }
    }
  }
  result
}

///|
/// Apply a radial falloff over a canvas.
pub fn Canvas::vignette(
  self : Canvas,
  options? : VignetteOptions = VignetteOptions::default(),
) -> Canvas {
  let result = self.copy()
  let center_x = self.width / 2
  let center_y = self.height / 2
  let max_distance = center_x * center_x + center_y * center_y
  let radius = if options.radius_per_mille < 1 {
    1
  } else {
    options.radius_per_mille
  }
  for y in 0.. 1000 {
          1000
        } else {
          value
        }
      }
      let source = result.pixels[x + y * self.width]
      let keep = 1000 - falloff
      let red = (
          source.red().to_int() * keep + options.color.red().to_int() * falloff
        ) /
        1000
      let green = (
          source.green().to_int() * keep +
          options.color.green().to_int() * falloff
        ) /
        1000
      let blue = (
          source.blue().to_int() * keep +
          options.color.blue().to_int() * falloff
        ) /
        1000
      result.pixels[x + y * self.width] = rgba(
        red.to_byte(),
        green.to_byte(),
        blue.to_byte(),
        source.alpha(),
      )
    }
  }
  result
}

///|
/// Draw a soft rectangular border around the visible image.
pub fn Canvas::border(self : Canvas, thickness : Int, color : Rgba) -> Canvas {
  let result = self.copy()
  let safe = if thickness < 0 { 0 } else { thickness }
  for y in 0..= self.width - safe ||
        y >= self.height - safe {
        result.pixels[x + y * self.width] = blend_rgba_mode(
          color,
          result.pixels[x + y * self.width],
          Normal,
        )
      }
    }
  }
  result
}

///|
/// Add a regular grid useful for pixel-art previews.
pub fn Canvas::grid(self : Canvas, spacing : Int, color : Rgba) -> Canvas {
  let result = self.copy()
  let safe = if spacing < 1 { 1 } else { spacing }
  for y in 0.. Canvas {
  let rotated = self.rotate(rotation)
  let result = Canvas::new(width=rotated.width, height=rotated.height, fill~)
  ignore(result.blit(rotated, destination_x=0, destination_y=0))
  result
}

///|
/// Extract evenly spaced stripes for a glitch-style animated effect.
pub fn Canvas::stripe_shift(
  self : Canvas,
  stripe_height : Int,
  offset : Int,
) -> Canvas {
  let result = self.copy()
  let safe_height = if stripe_height < 1 { 1 } else { stripe_height }
  for y in 0..