use derive_more::{IsVariant, TryUnwrap, Unwrap};
use thiserror::Error;

use super::{
  GeometryOverflow, InsufficientPlane, InsufficientStride, WidthAlignment, ZeroDimension,
};

/// A validated YUV 4:2:0 planar frame.
///
/// Three planes:
/// - `y` — full-size luma, `y_stride >= width`, length `>= y_stride * height`.
/// - `u` / `v` — half-width, half-height chroma,
///   `u_stride >= (width + 1) / 2`, length `>= u_stride * ((height + 1) / 2)`.
///
/// `width` must be even (4:2:0 subsamples chroma 2:1 in width, and the
/// SIMD kernels assume `width & 1 == 0`). `height` may be odd — chroma
/// row sizing uses `height.div_ceil(2)` and the row walker maps Y row
/// `r` to chroma row `r / 2`, so the final Y row of an odd-height
/// frame shares chroma with its single chroma row. Odd-width input is
/// rejected at construction.
#[derive(Debug, Clone, Copy)]
pub struct Yuv420pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv420pFrame<'a> {
  /// Constructs a new [`Yuv420pFrame`], validating dimensions and
  /// plane lengths.
  ///
  /// Returns [`Yuv420pFrameError`] if any of:
  /// - `width` or `height` is zero,
  /// - `width` is odd (odd height is allowed and handled via
  ///   `height.div_ceil(2)` in chroma-row sizing),
  /// - `y_stride < width`, `u_stride < (width + 1) / 2`, or
  ///   `v_stride < (width + 1) / 2`,
  /// - any plane is too short to cover its declared rows.
  #[cfg_attr(not(tarpaulin), inline(always))]
  // The 3-plane × (slice, stride, dim) shape is intrinsic to YUV 4:2:0;
  // `div_ceil` on u32 isn't const-stable yet, so the `(x + 1) / 2`
  // idiom stays.
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv420pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv420pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    // 4:2:0 subsamples chroma 2:1 in width (one chroma sample covers
    // two Y columns), so odd widths have no paired chroma for the
    // rightmost column and the SIMD kernels assume `width & 1 == 0`.
    // Height is allowed to be odd: plane sizing uses
    // `height.div_ceil(2)` and the row walker maps every Y row `r`
    // to chroma row `r / 2`, so a frame like 640x481 works — the last
    // Y row shares chroma with the final chroma row alone.
    if width & 1 != 0 {
      return Err(Yuv420pFrameError::WidthAlignment(WidthAlignment::odd(
        width as usize,
      )));
    }
    if y_stride < width {
      return Err(Yuv420pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    let chroma_width = width.div_ceil(2);
    if u_stride < chroma_width {
      return Err(Yuv420pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, chroma_width),
      ));
    }
    if v_stride < chroma_width {
      return Err(Yuv420pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, chroma_width),
      ));
    }

    // Plane sizes use `checked_mul` because `stride * height` can
    // wrap `usize` on 32‑bit targets (wasm32, i686) for large inputs
    // — without this guard, an undersized plane could pass validation
    // and panic later during row slicing. The declared geometry must
    // fit in `usize` to be usable at all.
    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv420pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv420pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    let chroma_height = height.div_ceil(2);
    let u_min = match (u_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv420pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride,
          chroma_height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv420pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv420pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride,
          chroma_height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv420pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv420pFrame`], panicking on invalid inputs.
  /// Prefer [`Self::try_new`] when inputs may be invalid at runtime.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv420pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes. Row `r` starts at byte offset `r * y_stride()`.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }

  /// U (Cb) plane bytes. Row `r` starts at byte offset `r * u_stride()`.
  /// U has half the width and half the height of the frame.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }

  /// V (Cr) plane bytes. Row `r` starts at byte offset `r * v_stride()`.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }

  /// Frame width in pixels. Always even.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }

  /// Frame height in pixels.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }

  /// Byte stride of the Y plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }

  /// Byte stride of the U plane (`>= width / 2`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }

  /// Byte stride of the V plane (`>= width / 2`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// A validated YUV 4:2:2 planar frame.
///
/// Three planes. Same per-row kernel contract as [`Yuv420pFrame`] —
/// the 4:2:0 → 4:2:2 difference is purely vertical. `Nv16Frame`
/// has the same axis difference versus `Nv12Frame`.
///
/// - `y` — full-size luma, `y_stride >= width`, length
///   `>= y_stride * height`.
/// - `u` / `v` — **half-width**, **full-height** chroma,
///   `u_stride >= (width + 1) / 2`, length `>= u_stride * height`.
///
/// `width` must be even (4:2:2 still pairs chroma columns 2:1). No
/// height parity constraint — chroma is full-height.
///
/// Canonical for `libx264 -pix_fmt yuv422p`, pro-video intermediates,
/// and ProRes SW decode at 8 bits.
#[derive(Debug, Clone, Copy)]
pub struct Yuv422pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv422pFrame<'a> {
  /// Constructs a new [`Yuv422pFrame`], validating dimensions and
  /// plane lengths.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv422pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv422pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    if width & 1 != 0 {
      return Err(Yuv422pFrameError::WidthAlignment(WidthAlignment::odd(
        width as usize,
      )));
    }
    if y_stride < width {
      return Err(Yuv422pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    let chroma_width = width.div_ceil(2);
    if u_stride < chroma_width {
      return Err(Yuv422pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, chroma_width),
      ));
    }
    if v_stride < chroma_width {
      return Err(Yuv422pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, chroma_width),
      ));
    }

    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv422pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv422pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    // 4:2:2: chroma is **full-height** — no `div_ceil(2)`.
    let u_min = match (u_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv422pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride, height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv422pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv422pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride, height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv422pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv422pFrame`], panicking on invalid inputs.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv422pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }

  /// U (Cb) plane bytes. Half-width, full-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }

  /// V (Cr) plane bytes. Half-width, full-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }

  /// Frame width in pixels. Always even.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }

  /// Frame height in pixels.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }

  /// Byte stride of the Y plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }

  /// Byte stride of the U plane (`>= width / 2`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }

  /// Byte stride of the V plane (`>= width / 2`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// Errors returned by [`Yuv422pFrame::try_new`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, TryUnwrap, Unwrap, Error)]
#[non_exhaustive]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Yuv422pFrameError {
  /// `width` or `height` was zero.
  #[error(transparent)]
  ZeroDimension(ZeroDimension),
  /// `width` was odd. 4:2:2 subsamples chroma 2:1 in width.
  #[error(transparent)]
  WidthAlignment(WidthAlignment),
  /// `y_stride < width`.
  #[error(transparent)]
  InsufficientYStride(InsufficientStride),
  /// `u_stride` is smaller than the half-width chroma row.
  #[error(transparent)]
  InsufficientUStride(InsufficientStride),
  /// `v_stride` is smaller than the half-width chroma row.
  #[error(transparent)]
  InsufficientVStride(InsufficientStride),
  /// Y plane is shorter than `y_stride * height` bytes.
  #[error(transparent)]
  InsufficientYPlane(InsufficientPlane),
  /// U plane is shorter than `u_stride * height` bytes.
  #[error(transparent)]
  InsufficientUPlane(InsufficientPlane),
  /// V plane is shorter than `v_stride * height` bytes.
  #[error(transparent)]
  InsufficientVPlane(InsufficientPlane),
  /// `stride * rows` does not fit in `usize` (32‑bit targets only).
  #[error(transparent)]
  GeometryOverflow(GeometryOverflow),
}

/// A validated YUV 4:4:4 planar frame.
///
/// Three planes, all full-size. Same per-row arithmetic as
/// `Nv24Frame` / `Nv42Frame` but with U and V read from separate
/// slices instead of an interleaved plane.
///
/// - `y` / `u` / `v` — full-size, `*_stride >= width`, length
///   `>= *_stride * height`.
///
/// No width parity constraint (4:4:4 chroma is 1:1 with Y).
///
/// Canonical for ProRes 4444 SW decode, CUDA/NVDEC hardware-decode
/// download of 4:4:4 content, and `libx264 -pix_fmt yuv444p`.
#[derive(Debug, Clone, Copy)]
pub struct Yuv444pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv444pFrame<'a> {
  /// Constructs a new [`Yuv444pFrame`], validating dimensions and
  /// plane lengths. Odd widths are accepted — 4:4:4 chroma pairs
  /// nothing.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv444pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv444pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    if y_stride < width {
      return Err(Yuv444pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    if u_stride < width {
      return Err(Yuv444pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, width),
      ));
    }
    if v_stride < width {
      return Err(Yuv444pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, width),
      ));
    }

    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv444pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    let u_min = match (u_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride, height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv444pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride, height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv444pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv444pFrame`], panicking on invalid inputs.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv444pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }

  /// U (Cb) plane bytes. Full-width, full-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }

  /// V (Cr) plane bytes. Full-width, full-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }

  /// Frame width in pixels. No parity constraint.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }

  /// Frame height in pixels.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }

  /// Byte stride of the Y plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }

  /// Byte stride of the U plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }

  /// Byte stride of the V plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// Errors returned by [`Yuv444pFrame::try_new`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, TryUnwrap, Unwrap, Error)]
#[non_exhaustive]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Yuv444pFrameError {
  /// `width` or `height` was zero.
  #[error(transparent)]
  ZeroDimension(ZeroDimension),
  /// `y_stride < width`.
  #[error(transparent)]
  InsufficientYStride(InsufficientStride),
  /// `u_stride < width`.
  #[error(transparent)]
  InsufficientUStride(InsufficientStride),
  /// `v_stride < width`.
  #[error(transparent)]
  InsufficientVStride(InsufficientStride),
  /// Y plane is shorter than `y_stride * height` bytes.
  #[error(transparent)]
  InsufficientYPlane(InsufficientPlane),
  /// U plane is shorter than `u_stride * height` bytes.
  #[error(transparent)]
  InsufficientUPlane(InsufficientPlane),
  /// V plane is shorter than `v_stride * height` bytes.
  #[error(transparent)]
  InsufficientVPlane(InsufficientPlane),
  /// `stride * rows` does not fit in `usize` (32‑bit targets only).
  #[error(transparent)]
  GeometryOverflow(GeometryOverflow),
}

/// A validated YUV 4:4:0 planar frame.
///
/// **4:4:0 = full-width chroma, half-height chroma.** Axis-flipped
/// counterpart to 4:2:2: chroma is fully sampled horizontally
/// (1:1 with Y) but subsampled 2:1 vertically (one chroma row per
/// two Y rows). FFmpeg names: `yuv440p`, `yuvj440p`. Mostly seen
/// from JPEG decoders that subsampled vertically only.
///
/// Three planes:
/// - `y` — full-size luma.
/// - `u` / `v` — full-width, **half-height** chroma. `u_stride >=
///   width`, length `>= u_stride * ((height + 1) / 2)`.
///
/// `width` accepts any value (4:4:0 has no horizontal subsampling
/// — same as 4:4:4). `height` may be odd: chroma row sizing uses
/// `height.div_ceil(2)` and the row walker maps Y row `r` to
/// chroma row `r / 2`, so a frame like 1280x481 works.
///
/// Per-row kernel reuses [`Yuv444pFrame`]'s `yuv_444_to_rgb_row`:
/// per-row math is identical (full-width chroma, no horizontal
/// duplication); only the walker reads chroma row `r / 2` instead
/// of `r`.
///
/// Validation errors surface as [`Yuv440pFrameError`] (a transparent
/// alias of [`Yuv444pFrameError`] — same variants apply since 4:4:0
/// uses the same chroma-width and overflow contracts as 4:4:4).
#[derive(Debug, Clone, Copy)]
pub struct Yuv440pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv440pFrame<'a> {
  /// Constructs a new [`Yuv440pFrame`], validating dimensions and
  /// plane lengths. Errors surface as [`Yuv440pFrameError`] (a
  /// transparent alias of [`Yuv444pFrameError`] — same variants apply
  /// since 4:4:0 has full-width chroma like 4:4:4 and no width-parity
  /// constraint).
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv440pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv444pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    if y_stride < width {
      return Err(Yuv444pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    if u_stride < width {
      return Err(Yuv444pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, width),
      ));
    }
    if v_stride < width {
      return Err(Yuv444pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, width),
      ));
    }

    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv444pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    // 4:4:0: chroma is half-height (same as 4:2:0 vertical axis).
    let chroma_height = height.div_ceil(2);
    let u_min = match (u_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride,
          chroma_height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv444pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv444pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride,
          chroma_height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv444pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv440pFrame`], panicking on invalid inputs.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv440pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }
  /// U (Cb) plane bytes. **Full-width, half-height** — one row per
  /// two Y rows.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }
  /// V (Cr) plane bytes. Full-width, half-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }
  /// Frame width in pixels. No parity constraint.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }
  /// Frame height in pixels. May be odd.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }
  /// Y plane stride.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }
  /// U plane stride (full-width).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }
  /// V plane stride (full-width).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// Errors returned by [`Yuv440pFrame::try_new`]. Transparent alias of
/// [`Yuv444pFrameError`] — 4:4:0 has the same full-width chroma and
/// no width-parity constraint, so the variants apply unchanged. The
/// alias keeps the public API self-descriptive.
pub type Yuv440pFrameError = Yuv444pFrameError;

/// Errors returned by [`Yuv420pFrame::try_new`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, TryUnwrap, Unwrap, Error)]
#[non_exhaustive]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Yuv420pFrameError {
  /// `width` or `height` was zero.
  #[error(transparent)]
  ZeroDimension(ZeroDimension),
  /// `width` was odd. YUV420p / 4:2:0 subsamples chroma 2:1 in width,
  /// so each chroma column pairs two Y columns — odd widths leave the
  /// last Y column without a paired chroma sample, and the SIMD
  /// kernels assume `width & 1 == 0`. Height is allowed to be odd
  /// (handled by `height.div_ceil(2)` in chroma‑row sizing).
  #[error(transparent)]
  WidthAlignment(WidthAlignment),
  /// `y_stride < width`.
  #[error(transparent)]
  InsufficientYStride(InsufficientStride),
  /// `u_stride < ceil(width / 2)`.
  #[error(transparent)]
  InsufficientUStride(InsufficientStride),
  /// `v_stride < ceil(width / 2)`.
  #[error(transparent)]
  InsufficientVStride(InsufficientStride),
  /// Y plane is shorter than `y_stride * height` bytes.
  #[error(transparent)]
  InsufficientYPlane(InsufficientPlane),
  /// U plane is shorter than `u_stride * (height / 2)` bytes.
  #[error(transparent)]
  InsufficientUPlane(InsufficientPlane),
  /// V plane is shorter than `v_stride * (height / 2)` bytes.
  #[error(transparent)]
  InsufficientVPlane(InsufficientPlane),
  /// `stride * rows` does not fit in `usize` (can only fire on 32‑bit
  /// targets — wasm32, i686 — with extreme dimensions).
  #[error(transparent)]
  GeometryOverflow(GeometryOverflow),
}

/// A validated YUV 4:1:0 planar frame.
///
/// **4:1:0 = quarter-width chroma, quarter-height chroma.** Y is at
/// full resolution; U / V are subsampled 4:1 in both axes, so one
/// chroma sample covers a 4×4 block of luma (16 pixels share one
/// chroma pair). FFmpeg names: `yuv410p`. Historical interest only —
/// Cinepak, Sorenson, and other legacy codecs from the 1990s; modern
/// pipelines almost never see it.
///
/// Three planes:
/// - `y` — full-size luma, `y_stride >= width`, length
///   `>= y_stride * height`.
/// - `u` / `v` — **quarter-width**, **quarter-height** chroma.
///   `u_stride >= width / 4`, length
///   `>= u_stride * height.div_ceil(4)`.
///
/// `width` must be a multiple of 4 (the per-row kernels assume
/// `width % 4 == 0`). `height` may be any non-zero value: the chroma
/// plane height is computed as `height.div_ceil(4)`, so a partial
/// 4-row chroma group at the bottom is handled by the walker
/// (`chroma_row = y_row / 4`) reading the trailing chroma row for the
/// final 1..=3 Y rows. This mirrors how `Yuv420pFrame` admits odd
/// heights and lets cropped / non-aligned 4:1:0 frames be wrapped
/// without copying.
///
/// Per-row kernel: `yuv_410_to_rgb_row` — the same Q15 chroma+Y math
/// as 4:2:0, but each (U, V) sample is duplicated across four
/// adjacent Y columns instead of two. The walker passes the same
/// chroma row to four consecutive Y rows (vertical 4× duplication).
/// Structural analog to [`Yuv420pFrame`] (vertical subsampling)
/// combined with 4× horizontal subsampling.
///
/// Validation errors surface as [`Yuv410pFrameError`].
#[derive(Debug, Clone, Copy)]
pub struct Yuv410pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv410pFrame<'a> {
  /// Constructs a new [`Yuv410pFrame`], validating dimensions and
  /// plane lengths.
  ///
  /// Returns [`Yuv410pFrameError`] if any of:
  /// - `width` or `height` is zero,
  /// - `width % 4 != 0` (the per-row kernels operate on 4-pixel chroma
  ///   groups; partial horizontal chroma blocks have no defined
  ///   coverage),
  /// - `y_stride < width`, `u_stride < width / 4`, or
  ///   `v_stride < width / 4`,
  /// - any plane is too short to cover its declared rows (chroma plane
  ///   length is checked against `chroma_stride * height.div_ceil(4)`).
  ///
  /// `height` need not be a multiple of 4 — heights such as 6 or 10
  /// are accepted and the walker reuses the final chroma row group for
  /// the trailing 1..=3 Y rows.
  #[cfg_attr(not(tarpaulin), inline(always))]
  // The 3-plane × (slice, stride, dim) shape is intrinsic to YUV 4:1:0.
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv410pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv410pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    // 4:1:0 chroma is subsampled 4:1 in both axes. Width must be a
    // multiple of 4 because the row kernels operate on 4-pixel chroma
    // groups (no partial horizontal block coverage). Height may be
    // any non-zero value: chroma_height is `height.div_ceil(4)` and
    // the walker (`chroma_row = y_row / 4`) reuses the final chroma
    // row group for the trailing 1..=3 Y rows. This matches how
    // `Yuv420pFrame` admits odd heights.
    if width & 3 != 0 {
      return Err(Yuv410pFrameError::WidthAlignment(
        WidthAlignment::multiple_of_four(width as usize),
      ));
    }
    if y_stride < width {
      return Err(Yuv410pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    let chroma_width = width / 4;
    if u_stride < chroma_width {
      return Err(Yuv410pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, chroma_width),
      ));
    }
    if v_stride < chroma_width {
      return Err(Yuv410pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, chroma_width),
      ));
    }

    // `checked_mul` for `stride * rows` — same overflow rationale as
    // [`Yuv420pFrame::try_new`]. Wraps on 32-bit targets for extreme
    // dimensions and would otherwise admit an undersized plane.
    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv410pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv410pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    // `div_ceil(4)` matches the walker, which maps Y row → chroma row
    // via `y_row / 4` (so a height of 6 yields chroma rows 0 and 1, with
    // chroma row 1 covering Y rows 4..6).
    let chroma_height = height.div_ceil(4);
    let u_min = match (u_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv410pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride,
          chroma_height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv410pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(chroma_height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv410pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride,
          chroma_height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv410pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv410pFrame`], panicking on invalid inputs.
  /// Prefer [`Self::try_new`] when inputs may be invalid at runtime.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv410pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes. Row `r` starts at byte offset `r * y_stride()`.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }
  /// U (Cb) plane bytes. **Quarter-width, quarter-height** — one
  /// chroma row per four Y rows, one chroma sample per four Y
  /// columns. `u_stride()` bytes per row, `height.div_ceil(4)` rows
  /// total (a partial 4-row chroma group at the bottom is reused for
  /// the trailing 1..=3 Y rows).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }
  /// V (Cr) plane bytes. Quarter-width, quarter-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }
  /// Frame width in pixels. Always a multiple of 4.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }
  /// Frame height in pixels. Any non-zero value; chroma plane carries
  /// `height.div_ceil(4)` rows.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }
  /// Byte stride of the Y plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }
  /// Byte stride of the U plane (`>= width / 4`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }
  /// Byte stride of the V plane (`>= width / 4`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// Errors returned by [`Yuv410pFrame::try_new`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, TryUnwrap, Unwrap, Error)]
#[non_exhaustive]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Yuv410pFrameError {
  /// `width` or `height` was zero.
  #[error(transparent)]
  ZeroDimension(ZeroDimension),

  /// `width` is not a multiple of 4. 4:1:0 subsamples chroma 4:1 in
  /// width — partial 4-column chroma blocks have no defined coverage.
  #[error(transparent)]
  WidthAlignment(WidthAlignment),

  /// `y_stride < width`.
  #[error(transparent)]
  InsufficientYStride(InsufficientStride),

  /// `u_stride < width / 4`.
  #[error(transparent)]
  InsufficientUStride(InsufficientStride),

  /// `v_stride < width / 4`.
  #[error(transparent)]
  InsufficientVStride(InsufficientStride),

  /// Y plane is shorter than `y_stride * height` bytes.
  #[error(transparent)]
  InsufficientYPlane(InsufficientPlane),

  /// U plane is shorter than `u_stride * height.div_ceil(4)` bytes.
  #[error(transparent)]
  InsufficientUPlane(InsufficientPlane),

  /// V plane is shorter than `v_stride * height.div_ceil(4)` bytes.
  #[error(transparent)]
  InsufficientVPlane(InsufficientPlane),

  /// `stride * rows` does not fit in `usize` (32‑bit targets only,
  /// extreme dimensions).
  #[error(transparent)]
  GeometryOverflow(GeometryOverflow),
}

/// A validated YUV 4:1:1 planar frame (`AV_PIX_FMT_YUV411P`).
///
/// 4:1:1 = **quarter-width**, full-height chroma. Legacy DV-NTSC
/// subsampling: every chroma sample covers four Y columns horizontally
/// while chroma rows are fully sampled vertically. Compared to 4:2:2
/// (half-width chroma) the only change is the horizontal stride: U/V
/// planes are `width.div_ceil(4)` wide instead of `width / 2`.
///
/// Three planes:
/// - `y` — full-size luma, `y_stride >= width`, length `>=
///   y_stride * height`.
/// - `u` / `v` — **quarter-width**, **full-height** chroma,
///   `u_stride >= width.div_ceil(4)`, length `>= u_stride * height`.
///
/// `width` may be any non-zero value — non-4-aligned widths are
/// accepted. Chroma row width is `width.div_ceil(4)`, matching
/// FFmpeg's `AV_PIX_FMT_YUV411P` descriptor (`log2_chroma_w = 2`,
/// ceiling right shift). For widths not divisible by 4, the trailing
/// 1..3 Y columns share the final chroma sample (a partial 1..3-pixel
/// chroma group). The SIMD kernels stride a multiple-of-4 Y block and
/// the per-row scalar tail picks up that 1..3-pixel remainder.
/// `height` has no parity constraint — chroma is full-height.
///
/// Common in DV-NTSC video (legacy). Extremely rare on modern
/// pipelines; the format is included for FFmpeg ingest completeness.
#[derive(Debug, Clone, Copy)]
pub struct Yuv411pFrame<'a> {
  y: &'a [u8],
  u: &'a [u8],
  v: &'a [u8],
  width: u32,
  height: u32,
  y_stride: u32,
  u_stride: u32,
  v_stride: u32,
}

impl<'a> Yuv411pFrame<'a> {
  /// Constructs a new [`Yuv411pFrame`], validating dimensions and
  /// plane lengths.
  ///
  /// Returns [`Yuv411pFrameError`] if any of:
  /// - `width` or `height` is zero,
  /// - `y_stride < width`, `u_stride < width.div_ceil(4)`, or
  ///   `v_stride < width.div_ceil(4)`,
  /// - any plane is too short to cover its declared rows.
  ///
  /// Non-4-aligned widths are accepted: chroma row width is
  /// `width.div_ceil(4)`, matching FFmpeg's `AV_PIX_FMT_YUV411P`
  /// descriptor (`log2_chroma_w = 2`, ceiling right shift). For
  /// e.g. `width = 641`, the chroma row carries 161 samples and the
  /// final chroma sample covers the trailing 1 Y column. Per-row
  /// scalar / SIMD kernels handle the partial-width tail.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn try_new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Result<Self, Yuv411pFrameError> {
    if width == 0 || height == 0 {
      return Err(Yuv411pFrameError::ZeroDimension(ZeroDimension::new(
        width, height,
      )));
    }
    if y_stride < width {
      return Err(Yuv411pFrameError::InsufficientYStride(
        InsufficientStride::new(y_stride, width),
      ));
    }
    // 4:1:1 subsamples chroma 4:1 in width. FFmpeg's
    // `AV_PIX_FMT_YUV411P` defines chroma width via a ceiling right
    // shift (`(width + 3) >> 2`), so widths not divisible by 4 leave
    // the trailing 1..3 Y columns paired with the last chroma sample
    // (a partial 1..3-pixel chroma group).
    let chroma_width = width.div_ceil(4);
    if u_stride < chroma_width {
      return Err(Yuv411pFrameError::InsufficientUStride(
        InsufficientStride::new(u_stride, chroma_width),
      ));
    }
    if v_stride < chroma_width {
      return Err(Yuv411pFrameError::InsufficientVStride(
        InsufficientStride::new(v_stride, chroma_width),
      ));
    }

    // Plane sizes use `checked_mul` because `stride * height` can
    // wrap `usize` on 32-bit targets (wasm32, i686) for extreme
    // dimensions. Same rationale as [`Yuv420pFrame::try_new`].
    let y_min = match (y_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv411pFrameError::GeometryOverflow(GeometryOverflow::new(
          y_stride, height,
        )));
      }
    };
    if y.len() < y_min {
      return Err(Yuv411pFrameError::InsufficientYPlane(
        InsufficientPlane::new(y_min, y.len()),
      ));
    }
    // 4:1:1: chroma is full-height — no `div_ceil(2)`.
    let u_min = match (u_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv411pFrameError::GeometryOverflow(GeometryOverflow::new(
          u_stride, height,
        )));
      }
    };
    if u.len() < u_min {
      return Err(Yuv411pFrameError::InsufficientUPlane(
        InsufficientPlane::new(u_min, u.len()),
      ));
    }
    let v_min = match (v_stride as usize).checked_mul(height as usize) {
      Some(v) => v,
      None => {
        return Err(Yuv411pFrameError::GeometryOverflow(GeometryOverflow::new(
          v_stride, height,
        )));
      }
    };
    if v.len() < v_min {
      return Err(Yuv411pFrameError::InsufficientVPlane(
        InsufficientPlane::new(v_min, v.len()),
      ));
    }

    Ok(Self {
      y,
      u,
      v,
      width,
      height,
      y_stride,
      u_stride,
      v_stride,
    })
  }

  /// Constructs a new [`Yuv411pFrame`], panicking on invalid inputs.
  /// Prefer [`Self::try_new`] when inputs may be invalid at runtime.
  #[cfg_attr(not(tarpaulin), inline(always))]
  #[allow(clippy::too_many_arguments)]
  pub const fn new(
    y: &'a [u8],
    u: &'a [u8],
    v: &'a [u8],
    width: u32,
    height: u32,
    y_stride: u32,
    u_stride: u32,
    v_stride: u32,
  ) -> Self {
    match Self::try_new(y, u, v, width, height, y_stride, u_stride, v_stride) {
      Ok(frame) => frame,
      Err(_) => panic!("invalid Yuv411pFrame dimensions or plane lengths"),
    }
  }

  /// Y (luma) plane bytes. Row `r` starts at byte offset `r * y_stride()`.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y(&self) -> &'a [u8] {
    self.y
  }

  /// U (Cb) plane bytes. Quarter-width, full-height (one row per Y row).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u(&self) -> &'a [u8] {
    self.u
  }

  /// V (Cr) plane bytes. Quarter-width, full-height.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v(&self) -> &'a [u8] {
    self.v
  }

  /// Frame width in pixels.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn width(&self) -> u32 {
    self.width
  }

  /// Frame height in pixels.
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn height(&self) -> u32 {
    self.height
  }

  /// Byte stride of the Y plane (`>= width`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn y_stride(&self) -> u32 {
    self.y_stride
  }

  /// Byte stride of the U plane (`>= width.div_ceil(4)`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn u_stride(&self) -> u32 {
    self.u_stride
  }

  /// Byte stride of the V plane (`>= width.div_ceil(4)`).
  #[cfg_attr(not(tarpaulin), inline(always))]
  pub const fn v_stride(&self) -> u32 {
    self.v_stride
  }
}

/// Errors returned by [`Yuv411pFrame::try_new`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, TryUnwrap, Unwrap, Error)]
#[non_exhaustive]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Yuv411pFrameError {
  /// `width` or `height` was zero.
  #[error(transparent)]
  ZeroDimension(ZeroDimension),

  /// **No longer produced.** Originally rejected `width % 4 != 0`,
  /// but [`Yuv411pFrame::try_new`] now accepts arbitrary widths via
  /// FFmpeg-compatible `chroma_width = width.div_ceil(4)`. The variant
  /// is preserved for backward compatibility with external code that
  /// matches it explicitly. The enum is `#[non_exhaustive]`, so
  /// downstream `match` arms must already include a wildcard.
  #[error(transparent)]
  WidthAlignment(WidthAlignment),

  /// `y_stride < width`.
  #[error(transparent)]
  InsufficientYStride(InsufficientStride),

  /// `u_stride < width.div_ceil(4)`.
  #[error(transparent)]
  InsufficientUStride(InsufficientStride),

  /// `v_stride < width.div_ceil(4)`.
  #[error(transparent)]
  InsufficientVStride(InsufficientStride),

  /// Y plane is shorter than `y_stride * height` bytes.
  #[error(transparent)]
  InsufficientYPlane(InsufficientPlane),

  /// U plane is shorter than `u_stride * height` bytes.
  #[error(transparent)]
  InsufficientUPlane(InsufficientPlane),

  /// V plane is shorter than `v_stride * height` bytes.
  #[error(transparent)]
  InsufficientVPlane(InsufficientPlane),

  /// `stride * rows` does not fit in `usize` (can only fire on 32-bit
  /// targets — wasm32, i686 — with extreme dimensions).
  #[error(transparent)]
  GeometryOverflow(GeometryOverflow),
}
