///|
pub(all) struct GrayImage {
  width : Int
  height : Int
  pixels : Array[Double]
} derive(Debug)

///|
pub fn GrayImage::GrayImage(
  width~ : Int,
  height~ : Int,
  fill? : Double = 0.0,
) -> GrayImage {
  let safe_width = width.max(0)
  let safe_height = height.max(0)
  {
    width: safe_width,
    height: safe_height,
    pixels: Array::make(safe_width * safe_height, fill),
  }
}

///|
pub fn GrayImage::from_rows(rows : Array[Array[Double]]) -> GrayImage {
  match GrayImage::try_from_rows(rows) {
    Some(image) => image
    None => GrayImage::GrayImage(width=0, height=0)
  }
}

///|
pub fn GrayImage::try_from_rows(rows : Array[Array[Double]]) -> GrayImage? {
  let height = rows.length()
  let width = if height == 0 { 0 } else { rows[0].length() }
  if !rows.all(fn(row) { row.length() == width }) {
    None
  } else {
    let pixels : Array[Double] = []
    for row in rows {
      for value in row {
        pixels.push(value)
      }
    }
    Some({ width, height, pixels })
  }
}

///|
pub fn GrayImage::inside(image : GrayImage, x : Int, y : Int) -> Bool {
  x >= 0 && y >= 0 && x < image.width && y < image.height
}

///|
pub fn GrayImage::at(image : GrayImage, x : Int, y : Int) -> Double {
  if image.inside(x, y) {
    image.pixels[y * image.width + x]
  } else {
    0.0
  }
}

///|
pub fn GrayImage::set(
  image : GrayImage,
  x : Int,
  y : Int,
  value : Double,
) -> Unit {
  if image.inside(x, y) {
    image.pixels[y * image.width + x] = value
  }
}

///|
pub fn GrayImage::sample_bilinear(image : GrayImage, p : Point) -> Double {
  let x0 = p.x.floor().to_int()
  let y0 = p.y.floor().to_int()
  let x1 = x0 + 1
  let y1 = y0 + 1
  let tx = p.x - Double::from_int(x0)
  let ty = p.y - Double::from_int(y0)
  let a = image.at(x0, y0) * (1.0 - tx) + image.at(x1, y0) * tx
  let b = image.at(x0, y1) * (1.0 - tx) + image.at(x1, y1) * tx
  a * (1.0 - ty) + b * ty
}

///|
pub fn GrayImage::sobel_magnitude(
  image : GrayImage,
  x : Int,
  y : Int,
) -> Double {
  let gx = -image.at(x - 1, y - 1) +
    image.at(x + 1, y - 1) -
    2.0 * image.at(x - 1, y) +
    2.0 * image.at(x + 1, y) -
    image.at(x - 1, y + 1) +
    image.at(x + 1, y + 1)
  let gy = -image.at(x - 1, y - 1) -
    2.0 * image.at(x, y - 1) -
    image.at(x + 1, y - 1) +
    image.at(x - 1, y + 1) +
    2.0 * image.at(x, y + 1) +
    image.at(x + 1, y + 1)
  (gx * gx + gy * gy).sqrt()
}

///|
pub fn GrayImage::threshold_edges(
  image : GrayImage,
  threshold : Double,
) -> Array[EdgePoint] {
  let edges : Array[EdgePoint] = []
  for y in 1..<(image.height - 1) {
    for x in 1..<(image.width - 1) {
      let strength = image.sobel_magnitude(x, y)
      if strength >= threshold {
        edges.push({
          point: Point(x=Double::from_int(x), y=Double::from_int(y)),
          strength,
          polarity: 0,
        })
      }
    }
  }
  edges
}