///|
/// High-bit-depth planar YUV conversion for the staged AV1 path.
///
/// The public Image type stores 8-bit channels; coded 8..12-bit samples are
/// converted with the high-bit-depth matrix and narrowed only for final RGBA
/// storage. Codec reconstruction remains integer-only; this adapter performs
/// no browser or FFI fallback.
///
/// `subsampling_x`/`subsampling_y` select 4:4:4, 4:2:2 or 4:2:0; the defaults
/// keep the historical 4:2:0 behaviour of `av1_yuv420_highbd_to_rgba`.
/// Chroma uses nearest-neighbour replication. Output is source-primary,
/// source-transfer nonlinear RGB, clipped and rounded once to RGBA8. There
/// is no display tone mapping or gamut conversion, except that XYZ primaries
/// (10) are explicitly converted to BT.709/sRGB. Unspecified matrix (2) uses
/// the AVIF/libavif BT.601 default; matrices needing primaries or a transfer
/// reject unspecified metadata rather than guessing those parameters.
pub fn av1_yuv_highbd_to_rgba(
  width : Int,
  height : Int,
  y : Array[Int],
  u : Array[Int],
  v : Array[Int],
  bit_depth : Int,
  subsampling_x? : Bool = true,
  subsampling_y? : Bool = true,
  full_range? : Bool = false,
  matrix_coefficients? : Int = 6,
  color_primaries? : Int = 2,
  transfer_characteristics? : Int = 2,
) -> Image? {
  av1_yuv_highbd_to_rgba_core(
    width,
    height,
    y,
    u,
    v,
    bit_depth,
    subsampling_x~,
    subsampling_y~,
    full_range~,
    matrix_coefficients~,
    color_primaries~,
    transfer_characteristics~,
  )
}

///|
/// The native-alpha path is used where libavif fuses unpremultiplication into
/// its floating-point matrix conversion, before narrowing the color channels.
fn av1_yuv_highbd_to_rgba_core(
  width : Int,
  height : Int,
  y : Array[Int],
  u : Array[Int],
  v : Array[Int],
  bit_depth : Int,
  subsampling_x? : Bool = true,
  subsampling_y? : Bool = true,
  full_range? : Bool = false,
  matrix_coefficients? : Int = 6,
  color_primaries? : Int = 2,
  transfer_characteristics? : Int = 2,
  native_alpha? : Array[Int]? = None,
  monochrome? : Bool = false,
) -> Image? {
  if (bit_depth != 8 && bit_depth != 10 && bit_depth != 12) ||
    width <= 0 ||
    height <= 0 ||
    width > MAX_IMAGE_PIXELS / height ||
    (subsampling_y && !subsampling_x) ||
    (matrix_coefficients == 0 && (subsampling_x || subsampling_y)) ||
    !av1_color_supported(
      matrix_coefficients, color_primaries, transfer_characteristics,
    ) {
    return None
  }
  let sub_x = if subsampling_x { 1 } else { 0 }
  let sub_y = if subsampling_y { 1 } else { 0 }
  let cw = (width + (1 << sub_x) - 1) >> sub_x
  let ch = (height + (1 << sub_y) - 1) >> sub_y
  if y.length() != width * height ||
    (!monochrome && (u.length() != cw * ch || v.length() != cw * ch)) {
    return None
  }
  // Keep coded precision through the matrix conversion. Narrowing each
  // plane first loses limited-range offsets and adds a second rounding step.
  let scale = 1 << (bit_depth - 8)
  let max_value = (1 << bit_depth) - 1
  if native_alpha is Some(alpha) {
    if alpha.length() != width * height {
      return None
    }
    for value in alpha {
      if value < 0 || value > max_value {
        return None
      }
    }
  }
  let clamp_sample = fn(sample : Int) -> Double {
    if sample < 0 {
      0.0
    } else if sample > max_value {
      max_value.to_double()
    } else {
      sample.to_double()
    }
  }
  let out = Image::new(width, height)
  let black = if full_range { 0.0 } else { (16 * scale).to_double() }
  let midpoint = (128 * scale).to_double()
  let luma_range = if full_range {
    max_value.to_double()
  } else {
    (219 * scale).to_double()
  }
  // YCgCo operates on quantized RGB channels (H.273 equations 27-29,
  // 51-57): undo its signed differences before the RGB range expansion.
  // This is equivalent to a chroma denominator of 219, not YCbCr's 224.
  let chroma_range = if full_range {
    max_value.to_double()
  } else if matrix_coefficients == 8 {
    (219 * scale).to_double()
  } else {
    (224 * scale).to_double()
  }
  // The libavif slow identity/YCgCo path uses float32 LUT entries, matrix
  // operations and alpha division. Keeping those rounding boundaries matters
  // at exact half-channel ties after unpremultiplication (for example 212.5).
  let scalar_float = native_alpha is Some(_) &&
    (matrix_coefficients == 0 || matrix_coefficients == 8)
  let normalize = fn(sample : Int, bias : Double, span : Double) -> Double {
    if scalar_float {
      ((Float::from_double(clamp_sample(sample)) - Float::from_double(bias)) /
      Float::from_double(span)).to_double()
    } else {
      (clamp_sample(sample) - bias) / span
    }
  }
  let to_byte = fn(value : Double) -> Byte {
    let clamped = if value < 0.0 {
      0.0
    } else if value > 1.0 {
      1.0
    } else {
      value
    }
    if scalar_float {
      (Float::from_double(clamped) * 255.0 + 0.5).to_int().to_byte()
    } else {
      (clamped * 255.0 + 0.5).to_int().to_byte()
    }
  }
  for yy in 0..> sub_y) * cw + (xx >> sub_x)
      let y0 = normalize(y[yi], black, luma_range)
      let (r, g, b) = if monochrome {
        (y0, y0, y0)
      } else if matrix_coefficients == 0 {
        // Identity stores G in Y, B in U and R in V; all three are RGB-range
        // components, so neither chroma centering nor 224 scaling applies.
        (
          normalize(v[ci], black, luma_range),
          y0,
          normalize(u[ci], black, luma_range),
        )
      } else if scalar_float && matrix_coefficients == 8 {
        let yy = Float::from_double(y0)
        let cg = Float::from_double(normalize(u[ci], midpoint, chroma_range))
        let co = Float::from_double(normalize(v[ci], midpoint, chroma_range))
        let t = yy - cg
        ((t + co).to_double(), (yy + cg).to_double(), (t - co).to_double())
      } else {
        match
          av1_color_rgb(
            y0,
            (clamp_sample(u[ci]) - midpoint) / chroma_range,
            (clamp_sample(v[ci]) - midpoint) / chroma_range,
            matrix_coefficients,
            color_primaries,
            transfer_characteristics,
          ) {
          Some(rgb) => rgb
          None => return None
        }
      }
      let (r, g, b) = match native_alpha {
        None => (r, g, b)
        Some(alpha) => {
          let coverage = if scalar_float {
            (Float::from_int(alpha[yi]) / Float::from_int(max_value)).to_double()
          } else {
            alpha[yi].to_double() / max_value.to_double()
          }
          let straight = fn(value : Double) -> Double {
            if coverage == 0.0 || value <= 0.0 {
              0.0
            } else {
              let clamped = if value > 1.0 { 1.0 } else { value }
              let result = if scalar_float {
                (Float::from_double(clamped) / Float::from_double(coverage)).to_double()
              } else {
                clamped / coverage
              }
              if result > 1.0 {
                1.0
              } else {
                result
              }
            }
          }
          (straight(r), straight(g), straight(b))
        }
      }
      let (r, g, b) = if color_primaries == 10 {
        match av1_color_xyz_to_srgb(r, g, b, transfer_characteristics) {
          Some(rgb) => rgb
          None => return None
        }
      } else {
        (r, g, b)
      }
      out.set_pixel(xx, yy, to_byte(r), to_byte(g), to_byte(b), b'\xFF')
    }
  }
  Some(out)
}

///|
/// 4:2:0 convenience wrapper over `av1_yuv_highbd_to_rgba`.
pub fn av1_yuv420_highbd_to_rgba(
  width : Int,
  height : Int,
  y : Array[Int],
  u : Array[Int],
  v : Array[Int],
  bit_depth : Int,
  full_range? : Bool = false,
  matrix_coefficients? : Int = 6,
  color_primaries? : Int = 2,
  transfer_characteristics? : Int = 2,
) -> Image? {
  av1_yuv_highbd_to_rgba(
    width,
    height,
    y,
    u,
    v,
    bit_depth,
    full_range~,
    matrix_coefficients~,
    color_primaries~,
    transfer_characteristics~,
  )
}