///|
/// Append the ASCII bytes of `s` to `out`.
fn push_ascii(out : Array[Byte], s : String) -> Unit {
  for c in s.iter() {
    out.push(c.to_int().to_byte())
  }
}

///|
/// Encode as a binary PPM (P6) byte stream.
pub fn to_ppm(img : Image) -> Array[Byte] {
  let out : Array[Byte] = []
  push_ascii(out, "P6\n\{img.w} \{img.h}\n255\n")
  let n = img.h * img.w
  for i = 0; i < n; i = i + 1 {
    let o = i * 4
    out.push(img.data[o])
    out.push(img.data[o + 1])
    out.push(img.data[o + 2])
  }
  out
}

///|
/// Encode as a binary PGM (P5) grayscale byte stream.
pub fn to_pgm(img : Image) -> Array[Byte] {
  let out : Array[Byte] = []
  push_ascii(out, "P5\n\{img.w} \{img.h}\n255\n")
  let n = img.h * img.w
  for i = 0; i < n; i = i + 1 {
    let o = i * 4
    out.push(luma(img.data[o], img.data[o + 1], img.data[o + 2]).to_byte())
  }
  out
}

///|
/// RGB-only byte buffer (alpha dropped), length `h * w * 3`.
pub fn to_display_bytes(img : Image) -> Array[Byte] {
  let n = img.h * img.w
  let out = Array::make(n * 3, (0 : Byte))
  for i = 0; i < n; i = i + 1 {
    out[i * 3] = img.data[i * 4]
    out[i * 3 + 1] = img.data[i * 4 + 1]
    out[i * 3 + 2] = img.data[i * 4 + 2]
  }
  out
}

///|
/// Human-readable summary of the image.
pub fn info(img : Image) -> String {
  "Image \{img.h}x\{img.w}, \{img.len()} bytes"
}

///|
/// Pixel-exact equality (same dimensions and bytes).
pub fn equals(a : Image, b : Image) -> Bool {
  if a.h != b.h || a.w != b.w {
    return false
  }
  for i = 0; i < a.data.length(); i = i + 1 {
    if a.data[i] != b.data[i] {
      return false
    }
  }
  true
}

///|
/// Mean squared error over all RGBA channels; `-1.0` on size mismatch.
pub fn mse(a : Image, b : Image) -> Double {
  if a.h != b.h || a.w != b.w || a.is_empty() {
    return -1.0
  }
  let mut acc = 0.0
  let len = a.data.length()
  for i = 0; i < len; i = i + 1 {
    let d = a.data[i].to_double() - b.data[i].to_double()
    acc = acc + d * d
  }
  acc / len.to_double()
}

///|
/// Peak signal-to-noise ratio in dB. Returns a large value for identical input.
pub fn psnr(a : Image, b : Image) -> Double {
  let m = mse(a, b)
  if m <= 0.0 {
    return 99.0
  }
  10.0 * @math.log10(255.0 * 255.0 / m)
}

///|
/// Downscale to fit within `max_h x max_w` (nearest-neighbor).
pub fn thumbnail(img : Image, max_h : Int, max_w : Int) -> Image {
  resize_to_fit(img, max_h, max_w, Nearest)
}

///|
/// Structural Similarity Index (SSIM) between two images.
/// Returns a value in `[-1, 1]` where 1 means identical.
pub fn ssim(a : Image, b : Image) -> Double {
  if a.h != b.h || a.w != b.w || a.is_empty() {
    return -1.0
  }
  let c1 = 0.01 * 255.0 * (0.01 * 255.0)
  let c2 = 0.03 * 255.0 * (0.03 * 255.0)
  let n = a.h * a.w
  let mut sum_a = 0.0
  let mut sum_b = 0.0
  let mut sum_ab = 0.0
  let mut sum_a2 = 0.0
  let mut sum_b2 = 0.0
  for i = 0; i < n; i = i + 1 {
    let o = i * 4
    let va = luma(a.data[o], a.data[o + 1], a.data[o + 2]).to_double()
    let vb = luma(b.data[o], b.data[o + 1], b.data[o + 2]).to_double()
    sum_a = sum_a + va
    sum_b = sum_b + vb
    sum_ab = sum_ab + va * vb
    sum_a2 = sum_a2 + va * va
    sum_b2 = sum_b2 + vb * vb
  }
  let nf = n.to_double()
  let mean_a = sum_a / nf
  let mean_b = sum_b / nf
  let var_a = sum_a2 / nf - mean_a * mean_a
  let var_b = sum_b2 / nf - mean_b * mean_b
  let cov_ab = sum_ab / nf - mean_a * mean_b
  let num = (2.0 * mean_a * mean_b + c1) * (2.0 * cov_ab + c2)
  let den = (mean_a * mean_a + mean_b * mean_b + c1) * (var_a + var_b + c2)
  num / den
}