///|
/// Neighborhood geometry used by mask cleanup and sprite extraction.
pub(all) enum MorphologyMode {
  Square
  Diamond
  Cross
} derive(Eq, Debug)

///|
pub(all) struct ComponentInfo {
  label : Int
  pixels : Int
  left : Int
  top : Int
  right : Int
  bottom : Int
} derive(Debug)

///|
pub fn ComponentInfo::label(self : ComponentInfo) -> Int {
  self.label
}

///|
pub fn ComponentInfo::pixels(self : ComponentInfo) -> Int {
  self.pixels
}

///|
pub fn ComponentInfo::bounds(self : ComponentInfo) -> GifRect {
  GifRect::new(
    left=self.left,
    top=self.top,
    width=self.right - self.left + 1,
    height=self.bottom - self.top + 1,
  )
}

///|
fn morphology_offsets(mode : MorphologyMode, radius : Int) -> Array[(Int, Int)] {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let offsets : Array[(Int, Int)] = []
  for y in -safe_radius..<(safe_radius + 1) {
    for x in -safe_radius..<(safe_radius + 1) {
      let distance = if mode == Diamond { x.abs() + y.abs() } else { 0 }
      let selected = match mode {
        Square => true
        Diamond => distance <= safe_radius
        Cross => x == 0 || y == 0
      }
      if selected {
        offsets.push((x, y))
      }
    }
  }
  offsets
}

///|
fn alpha_at(canvas : Canvas, x : Int, y : Int) -> Int {
  match canvas.get(x~, y~) {
    None => 0
    Some(color) => color.alpha().to_int()
  }
}

///|
fn any_neighbor(
  canvas : Canvas,
  x : Int,
  y : Int,
  offsets : ArrayView[(Int, Int)],
  threshold : Int,
) -> Bool {
  for pair in offsets {
    let (dx, dy) = pair
    if alpha_at(canvas, x + dx, y + dy) >= threshold {
      return true
    }
  }
  false
}

///|
fn all_neighbor(
  canvas : Canvas,
  x : Int,
  y : Int,
  offsets : ArrayView[(Int, Int)],
  threshold : Int,
) -> Bool {
  for pair in offsets {
    let (dx, dy) = pair
    if alpha_at(canvas, x + dx, y + dy) < threshold {
      return false
    }
  }
  true
}

///|
/// Expand opaque regions using a square, diamond, or cross neighborhood.
pub fn Canvas::dilate(
  self : Canvas,
  radius : Int,
  mode? : MorphologyMode = Square,
  threshold? : Byte = 1,
) -> Canvas {
  let offsets = morphology_offsets(mode, radius)
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for y in 0..= threshold.to_int() => {
              chosen = color
              break
            }
            _ => ()
          }
        }
        ignore(result.set(x~, y~, color=chosen))
      }
    }
  }
  result
}

///|
/// Contract opaque regions while preserving the source color where possible.
pub fn Canvas::erode(
  self : Canvas,
  radius : Int,
  mode? : MorphologyMode = Square,
  threshold? : Byte = 1,
) -> Canvas {
  let offsets = morphology_offsets(mode, radius)
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for y in 0..= threshold.to_int() &&
          all_neighbor(self, x, y, offsets[:], threshold.to_int()) =>
          ignore(result.set(x~, y~, color~))
        _ => ()
      }
    }
  }
  result
}

///|
/// Open an alpha mask: erode then dilate.
pub fn Canvas::open_mask(
  self : Canvas,
  radius : Int,
  mode? : MorphologyMode = Square,
  threshold? : Byte = 1,
) -> Canvas {
  self.erode(radius, mode~, threshold~).dilate(radius, mode~, threshold~)
}

///|
/// Close small holes in an alpha mask: dilate then erode.
pub fn Canvas::close_mask(
  self : Canvas,
  radius : Int,
  mode? : MorphologyMode = Square,
  threshold? : Byte = 1,
) -> Canvas {
  self.dilate(radius, mode~, threshold~).erode(radius, mode~, threshold~)
}

///|
/// Return the newly exposed outline of an alpha shape.
pub fn Canvas::outline(
  self : Canvas,
  radius? : Int = 1,
  mode? : MorphologyMode = Square,
  threshold? : Byte = 1,
) -> Canvas {
  let expanded = self.dilate(radius, mode~, threshold~)
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for i in 0..= threshold.to_int() &&
      source_alpha < threshold.to_int() {
      result.pixels[i] = rgba(
        color.red(),
        color.green(),
        color.blue(),
        color.alpha(),
      )
    }
  }
  result
}

///|
/// Keep only pixels whose color is within a Manhattan distance of the key.
pub fn Canvas::chroma_key(self : Canvas, key : Rgba, tolerance : Int) -> Canvas {
  let safe_tolerance = if tolerance < 0 { 0 } else { tolerance }
  let result = self.copy()
  for i in 0.. Int {
  let safe_tolerance = if tolerance < 0 { 0 } else { tolerance }
  let mut count = 0
  for i in 0.. ComponentInfo {
  let queue : Array[(Int, Int)] = [(start_x, start_y)]
  let width = canvas.width
  visited[start_x + start_y * width] = true
  let mut cursor = 0
  let mut count = 0
  let mut left = start_x
  let mut right = start_x
  let mut top = start_y
  let mut bottom = start_y
  while cursor < queue.length() {
    let (x, y) = queue[cursor]
    cursor += 1
    count += 1
    if x < left {
      left = x
    }
    if x > right {
      right = x
    }
    if y < top {
      top = y
    }
    if y > bottom {
      bottom = y
    }
    let neighbors = [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]
    for pair in neighbors {
      let (nx, ny) = pair
      if nx >= 0 && ny >= 0 && nx < width && ny < canvas.height {
        let index = nx + ny * width
        if !visited[index] && alpha_at(canvas, nx, ny) >= threshold {
          visited[index] = true
          queue.push((nx, ny))
        }
      }
    }
  }
  ComponentInfo::{
    label: start_x + start_y * width + 1,
    pixels: count,
    left,
    top,
    right,
    bottom,
  }
}

///|
/// Find four-connected opaque components for sprite and mask extraction.
pub fn Canvas::components(
  self : Canvas,
  threshold? : Byte = 1,
  minimum_pixels? : Int = 1,
) -> Array[ComponentInfo] {
  let minimum = if minimum_pixels < 1 { 1 } else { minimum_pixels }
  let visited = Array::make(self.pixels.length(), false)
  let result : Array[ComponentInfo] = []
  for y in 0..= threshold.to_int() {
        let component = component_neighbors(
          self,
          x,
          y,
          visited,
          threshold.to_int(),
        )
        if component.pixels >= minimum {
          result.push(component)
        }
      }
    }
  }
  result
}

///|
/// Paint a connected region starting at a coordinate, returning changed pixels.
pub fn Canvas::flood_fill(
  self : Canvas,
  x : Int,
  y : Int,
  replacement : Rgba,
  tolerance? : Int = 0,
) -> Int {
  match self.get(x~, y~) {
    None => 0
    Some(seed) => {
      let safe_tolerance = if tolerance < 0 { 0 } else { tolerance }
      if seed.distance(replacement) <= safe_tolerance && seed == replacement {
        0
      } else {
        let queue : Array[(Int, Int)] = [(x, y)]
        let visited = Array::make(self.pixels.length(), false)
        let mut cursor = 0
        let mut changed = 0
        while cursor < queue.length() {
          let (px, py) = queue[cursor]
          cursor += 1
          if px >= 0 && py >= 0 && px < self.width && py < self.height {
            let index = px + py * self.width
            if !visited[index] {
              visited[index] = true
              let current = self.pixels[index]
              if current.distance(seed) <= safe_tolerance {
                self.pixels[index] = replacement
                changed += 1
                queue.push((px - 1, py))
                queue.push((px + 1, py))
                queue.push((px, py - 1))
                queue.push((px, py + 1))
              }
            }
          }
        }
        changed
      }
    }
  }
}

///|
/// Build a binary alpha mask with an explicit threshold.
pub fn Canvas::binary_mask(self : Canvas, threshold : Byte) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=rgba(0, 0, 0, 0),
  )
  for i in 0..= threshold {
      rgba(255, 255, 255, 255)
    } else {
      rgba(0, 0, 0, 0)
    }
  }
  result
}

///|
/// Remove connected components smaller than a pixel threshold.
pub fn Canvas::remove_small_components(
  self : Canvas,
  minimum_pixels : Int,
  threshold? : Byte = 1,
) -> Canvas {
  let result = self.copy()
  let components = self.components(threshold~, minimum_pixels~)
  let allowed : Map[Int, Bool] = Map([])
  for component in components {
    allowed[component.label()] = true
  }
  let visited = Array::make(self.pixels.length(), false)
  for y in 0..= threshold.to_int() {
        let component = component_neighbors(
          self,
          x,
          y,
          visited,
          threshold.to_int(),
        )
        if allowed.get(component.label()) is None {
          for py in component.top..<(component.bottom + 1) {
            for px in component.left..<(component.right + 1) {
              if px >= 0 && py >= 0 && px < self.width && py < self.height {
                let offset = px + py * self.width
                if self.pixels[offset].alpha().to_int() >= threshold.to_int() {
                  result.pixels[offset] = result.pixels[offset].with_alpha(0)
                }
              }
            }
          }
        }
      }
    }
  }
  result
}