///|
/// Raw axis-aligned image blit. Samples per destination pixel via nearest or
/// bilinear interpolation, applies the matrix to map dest rect coordinates,
/// and composites via source-over `blend_over`.
///
/// This is the low-level primitive used by `Context::draw_image`. It doesn't
/// handle rotation or shear cleanly (the destination rect is computed from
/// the matrix-transformed axis-aligned corners and sampled as an axis-aligned
/// rectangle — acceptable for v0).
pub fn draw_image_raw(
  pixels : FixedArray[Byte],
  dst_w : Int,
  dst_h : Int,
  src : @image.ImageData,
  dst_x : Double,
  dst_y : Double,
  scale_x : Double,
  scale_y : Double,
  src_x : Double,
  src_y : Double,
  src_w : Double,
  src_h : Double,
  matrix : Matrix2D,
  global_alpha : Double,
  smoothing : Bool,
) -> Unit {
  let out_w = src_w * scale_x
  let out_h = src_h * scale_y
  let (tl_x, tl_y) = matrix.transform_point(dst_x, dst_y)
  let (br_x, br_y) = matrix.transform_point(dst_x + out_w, dst_y + out_h)
  let x_min = if tl_x < br_x { tl_x } else { br_x }
  let x_max = if tl_x < br_x { br_x } else { tl_x }
  let y_min = if tl_y < br_y { tl_y } else { br_y }
  let y_max = if tl_y < br_y { br_y } else { tl_y }
  let ix0 = x_min.floor().to_int()
  let ix1 = x_max.ceil().to_int()
  let iy0 = y_min.floor().to_int()
  let iy1 = y_max.ceil().to_int()
  let width_d = x_max - x_min
  let height_d = y_max - y_min
  if width_d <= 0.0 || height_d <= 0.0 {
    return
  }
  let scale_u = src_w / width_d
  let scale_v = src_h / height_d
  for py in iy0..= 0 && py < dst_h {
      for px in ix0..= 0 && px < dst_w {
          let u = (px.to_double() + 0.5 - x_min) * scale_u + src_x
          let v = (py.to_double() + 0.5 - y_min) * scale_v + src_y
          let color = if smoothing {
            sample_bilinear(src, u, v)
          } else {
            sample_nearest(src, u, v)
          }
          let offset = (py * dst_w + px) * 4
          blend_over(pixels, offset, color, 256, global_alpha)
        }
      }
    }
  }
}

///|
fn sample_nearest(img : @image.ImageData, u : Double, v : Double) -> Color {
  let x = u.floor().to_int()
  let y = v.floor().to_int()
  if x < 0 || x >= img.width || y < 0 || y >= img.height {
    Color::transparent()
  } else {
    let o = (y * img.width + x) * 4
    let r = img.data[o].to_int()
    let g = img.data[o + 1].to_int()
    let b = img.data[o + 2].to_int()
    let a = img.data[o + 3].to_int().to_double() / 255.0
    { r, g, b, a }
  }
}

///|
fn sample_bilinear(img : @image.ImageData, u : Double, v : Double) -> Color {
  let x0 = u.floor().to_int()
  let y0 = v.floor().to_int()
  let fx = u - x0.to_double()
  let fy = v - y0.to_double()
  let c00 = sample_nearest(img, x0.to_double() + 0.5, y0.to_double() + 0.5)
  let c10 = sample_nearest(
    img,
    (x0 + 1).to_double() + 0.5,
    y0.to_double() + 0.5,
  )
  let c01 = sample_nearest(
    img,
    x0.to_double() + 0.5,
    (y0 + 1).to_double() + 0.5,
  )
  let c11 = sample_nearest(
    img,
    (x0 + 1).to_double() + 0.5,
    (y0 + 1).to_double() + 0.5,
  )
  let w00 = (1.0 - fx) * (1.0 - fy)
  let w10 = fx * (1.0 - fy)
  let w01 = (1.0 - fx) * fy
  let w11 = fx * fy
  let r = (c00.r.to_double() * w00 +
  c10.r.to_double() * w10 +
  c01.r.to_double() * w01 +
  c11.r.to_double() * w11).to_int()
  let g = (c00.g.to_double() * w00 +
  c10.g.to_double() * w10 +
  c01.g.to_double() * w01 +
  c11.g.to_double() * w11).to_int()
  let b = (c00.b.to_double() * w00 +
  c10.b.to_double() * w10 +
  c01.b.to_double() * w01 +
  c11.b.to_double() * w11).to_int()
  let a = c00.a * w00 + c10.a * w10 + c01.a * w01 + c11.a * w11
  { r, g, b, a }
}