///|
/// Flood fill (seed fill): starting from a seed pixel, replaces the
/// 4-connected region whose color matches the seed's — within a per-channel
/// tolerance — with a new color. Uses an explicit stack (no recursion), so
/// it handles large regions without overflowing the call stack.

///|
/// Absolute per-channel difference test: true when every channel of the two
/// RGBA pixels is within `tol`.
fn within_tol(
  data : FixedArray[Byte],
  i : Int,
  sr : Int,
  sg : Int,
  sb : Int,
  sa : Int,
  tol : Int,
) -> Bool {
  iabs(data[i].to_int() - sr) <= tol &&
  iabs(data[i + 1].to_int() - sg) <= tol &&
  iabs(data[i + 2].to_int() - sb) <= tol &&
  iabs(data[i + 3].to_int() - sa) <= tol
}

///|
/// Fills the 4-connected region around `(x, y)` matching the seed color
/// (within `tolerance` per channel) with `(r, g, b, a)`. Returns a new image;
/// the original is untouched. An out-of-bounds seed returns a plain copy. If
/// the fill color already matches the seed, the copy is returned unchanged
/// (nothing to do), which also avoids an infinite revisit.
pub fn Image::flood_fill(
  self : Image,
  x : Int,
  y : Int,
  r : Byte,
  g : Byte,
  b : Byte,
  a : Byte,
  tolerance : Int,
) -> Image {
  let out = self.copy()
  if x < 0 || x >= self.width || y < 0 || y >= self.height {
    return out
  }
  let w = self.width
  let h = self.height
  let seed = (y * w + x) * 4
  let sr = self.data[seed].to_int()
  let sg = self.data[seed + 1].to_int()
  let sb = self.data[seed + 2].to_int()
  let sa = self.data[seed + 3].to_int()
  // If the target color already matches the seed, there is nothing to do.
  if iabs(r.to_int() - sr) == 0 &&
    iabs(g.to_int() - sg) == 0 &&
    iabs(b.to_int() - sb) == 0 &&
    iabs(a.to_int() - sa) == 0 {
    return out
  }
  let visited = Array::make(w * h, false)
  let stack : Array[Int] = []
  stack.push(y * w + x)
  visited[y * w + x] = true
  while stack.length() > 0 {
    let p = stack.unsafe_pop()
    let base = p * 4
    // Paint this pixel.
    out.data[base] = r
    out.data[base + 1] = g
    out.data[base + 2] = b
    out.data[base + 3] = a
    let px = p % w
    let py = p / w
    // Enqueue 4-neighbors that match the seed color and are unvisited.
    if px > 0 &&
      !visited[p - 1] &&
      within_tol(self.data, (p - 1) * 4, sr, sg, sb, sa, tolerance) {
      visited[p - 1] = true
      stack.push(p - 1)
    }
    if px + 1 < w &&
      !visited[p + 1] &&
      within_tol(self.data, (p + 1) * 4, sr, sg, sb, sa, tolerance) {
      visited[p + 1] = true
      stack.push(p + 1)
    }
    if py > 0 &&
      !visited[p - w] &&
      within_tol(self.data, (p - w) * 4, sr, sg, sb, sa, tolerance) {
      visited[p - w] = true
      stack.push(p - w)
    }
    if py + 1 < h &&
      !visited[p + w] &&
      within_tol(self.data, (p + w) * 4, sr, sg, sb, sa, tolerance) {
      visited[p + w] = true
      stack.push(p + w)
    }
  }
  out
}