use crate::codecs::avif::yuv::{
    check_rgb_preconditions, check_yuv_plane_preconditions, qrshr, yuv420_to_rgbx_invoker,
    yuv422_to_rgbx_invoker, CbCrInverseTransform, HalvedRowHandler, PlaneDefinition,
    YuvChromaRange, YuvIntensityRange, YuvPlanarImage, YuvStandardMatrix,
};
use crate::ImageError;
use num_traits::AsPrimitive;

/// Computes YCgCo inverse in limited range
/// # Arguments
/// - `dst` - dest buffer
/// - `y_value` - Y value with subtracted bias
/// - `cb` - Cb value with subtracted bias
/// - `cr` - Cr value with subtracted bias
#[inline(always)]
fn ycgco_execute_limited<
    V: Copy + AsPrimitive<i32> + 'static + Sized,
    const PRECISION: i32,
    const CHANNELS: usize,
    const BIT_DEPTH: usize,
>(
    dst: &mut [V; CHANNELS],
    y_value: i32,
    cg: i32,
    co: i32,
    scale: i32,
) where
    i32: AsPrimitive<V>,
{
    let t0 = y_value - cg;

    let r = qrshr::<PRECISION, BIT_DEPTH>((t0 + co) * scale);
    let b = qrshr::<PRECISION, BIT_DEPTH>((t0 - co) * scale);
    let g = qrshr::<PRECISION, BIT_DEPTH>((y_value + cg) * scale);

    if CHANNELS == 4 {
        dst[0] = r.as_();
        dst[1] = g.as_();
        dst[2] = b.as_();
        dst[3] = ((1i32 << BIT_DEPTH) - 1).as_();
    } else if CHANNELS == 3 {
        dst[0] = r.as_();
        dst[1] = g.as_();
        dst[2] = b.as_();
    } else {
        unreachable!();
    }
}

/// Computes YCgCo inverse in full range
/// # Arguments
/// - `dst` - dest buffer
/// - `y_value` - Y value with subtracted bias
/// - `cb` - Cb value with subtracted bias
/// - `cr` - Cr value with subtracted bias
#[inline(always)]
fn ycgco_execute_full<
    V: Copy + AsPrimitive<i32> + 'static + Sized,
    const PRECISION: i32,
    const CHANNELS: usize,
    const BIT_DEPTH: usize,
>(
    dst: &mut [V; CHANNELS],
    y_value: i32,
    cg: i32,
    co: i32,
) where
    i32: AsPrimitive<V>,
{
    let t0 = y_value - cg;

    let max_value = (1i32 << BIT_DEPTH) - 1;

    let r = (t0 + co).clamp(0, max_value);
    let b = (t0 - co).clamp(0, max_value);
    let g = (y_value + cg).clamp(0, max_value);

    if CHANNELS == 4 {
        dst[0] = r.as_();
        dst[1] = g.as_();
        dst[2] = b.as_();
        dst[3] = max_value.as_();
    } else if CHANNELS == 3 {
        dst[0] = r.as_();
        dst[1] = g.as_();
        dst[2] = b.as_();
    } else {
        unreachable!();
    }
}

#[inline(always)]
fn process_halved_chroma_row_cgco<
    V: Copy + AsPrimitive<i32> + 'static + Sized,
    const PRECISION: i32,
    const CHANNELS: usize,
    const BIT_DEPTH: usize,
>(
    image: YuvPlanarImage<V>,
    rgba: &mut [V],
    _: &CbCrInverseTransform<i32>,
    range: &YuvChromaRange,
) where
    i32: AsPrimitive<V>,
{
    let max_value = (1i32 << BIT_DEPTH) - 1;

    // If the stride is larger than the plane size,
    // it might contain junk data beyond the actual valid region.
    // To avoid processing artifacts when working with odd-sized images,
    // the buffer is reshaped to its actual size,
    // preventing accidental use of invalid values from the trailing region.

    let y_plane = &image.y_plane[..image.width];
    let chroma_size = image.width.div_ceil(2);
    let u_plane = &image.u_plane[..chroma_size];
    let v_plane = &image.v_plane[..chroma_size];
    let rgba = &mut rgba[..image.width * CHANNELS];

    let bias_y = range.bias_y as i32;
    let bias_uv = range.bias_uv as i32;
    let (y_iter, y_left) = y_plane.as_chunks::<2>();
    let mut rgb_chunks = rgba.chunks_exact_mut(CHANNELS * 2);

    let scale_coef = ((max_value as f32 / range.range_y as f32) * (1 << PRECISION) as f32) as i32;

    for (((y_src, &u_src), &v_src), rgb_dst) in
        y_iter.iter().zip(u_plane).zip(v_plane).zip(&mut rgb_chunks)
    {
        let y_value0: i32 = y_src[0].as_() - bias_y;
        let cg_value: i32 = u_src.as_() - bias_uv;
        let co_value: i32 = v_src.as_() - bias_uv;

        let dst0 = &mut rgb_dst[..CHANNELS];

        ycgco_execute_limited::<V, PRECISION, CHANNELS, BIT_DEPTH>(
            dst0.try_into().unwrap(),
            y_value0,
            cg_value,
            co_value,
            scale_coef,
        );

        let y_value1 = y_src[1].as_() - bias_y;

        let dst1 = &mut rgb_dst[CHANNELS..2 * CHANNELS];

        ycgco_execute_limited::<V, PRECISION, CHANNELS, BIT_DEPTH>(
            dst1.try_into().unwrap(),
            y_value1,
            cg_value,
            co_value,
            scale_coef,
        );
    }

    // Process remainder if width is odd.
    if image.width & 1 != 0 {
        let rgb_chunks = rgb_chunks
            .into_remainder()
            .as_chunks_mut::<CHANNELS>()
            .0
            .iter_mut();
        let u_iter = u_plane.iter().rev();
        let v_iter = v_plane.iter().rev();

        for (((y_src, u_src), v_src), rgb_dst) in
            y_left.iter().zip(u_iter).zip(v_iter).zip(rgb_chunks)
        {
            let y_value = y_src.as_() - bias_y;
            let cg_value = u_src.as_() - bias_uv;
            let co_value = v_src.as_() - bias_uv;

            ycgco_execute_limited::<V, PRECISION, CHANNELS, BIT_DEPTH>(
                rgb_dst, y_value, cg_value, co_value, scale_coef,
            );
        }
    }
}

/// Converts YCgCo 444 planar format to Rgba
///
/// # Arguments
///
/// * `image`: see [YuvPlanarImage]
/// * `rgba`: RGB image layout
/// * `range`: see [YuvIntensityRange]
///
fn ycgco444_to_rgbx_impl<
    V: Copy + AsPrimitive<i32> + 'static + Sized,
    const CHANNELS: usize,
    const BIT_DEPTH: usize,
>(
    image: YuvPlanarImage<V>,
    rgba: &mut [V],
    yuv_range: YuvIntensityRange,
) -> Result<(), ImageError>
where
    i32: AsPrimitive<V>,
{
    assert!(
        CHANNELS == 3 || CHANNELS == 4,
        "YUV 4:4:4 -> RGB is implemented only on 3 and 4 channels"
    );
    assert!(
        (8..=16).contains(&BIT_DEPTH),
        "Invalid bit depth is provided"
    );
    assert!(
        if BIT_DEPTH > 8 {
            size_of::<V>() == 2
        } else {
            size_of::<V>() == 1
        },
        "Unsupported bit depth and data type combination"
    );

    let y_plane = image.y_plane;
    let u_plane = image.u_plane;
    let v_plane = image.v_plane;
    let y_stride = image.y_stride;
    let u_stride = image.u_stride;
    let v_stride = image.v_stride;
    let height = image.height;
    let width = image.width;

    check_yuv_plane_preconditions(y_plane, PlaneDefinition::Y, y_stride, height)?;
    check_yuv_plane_preconditions(u_plane, PlaneDefinition::U, u_stride, height)?;
    check_yuv_plane_preconditions(v_plane, PlaneDefinition::V, v_stride, height)?;

    check_rgb_preconditions(rgba, image.width * CHANNELS, height)?;

    let range = yuv_range.get_yuv_range(BIT_DEPTH as u32);
    const PRECISION: i32 = 13;

    let bias_y = range.bias_y as i32;
    let bias_uv = range.bias_uv as i32;

    let rgb_stride = width * CHANNELS;

    let y_iter = y_plane.chunks_exact(y_stride);
    let rgb_iter = rgba.chunks_exact_mut(rgb_stride);
    let u_iter = u_plane.chunks_exact(u_stride);
    let v_iter = v_plane.chunks_exact(v_stride);

    let max_value: i32 = (1 << BIT_DEPTH) - 1;

    // All branches on generic const will be optimized out.
    for (((y_src, u_src), v_src), rgb) in y_iter.zip(u_iter).zip(v_iter).zip(rgb_iter) {
        let rgb_chunks = rgb.as_chunks_mut::<CHANNELS>().0.iter_mut();
        match yuv_range {
            YuvIntensityRange::Tv => {
                let y_coef =
                    ((max_value as f32 / range.range_y as f32) * (1 << PRECISION) as f32) as i32;
                for (((y_src, u_src), v_src), rgb_dst) in
                    y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks)
                {
                    let y_value = y_src.as_() - bias_y;
                    let cg_value = u_src.as_() - bias_uv;
                    let co_value = v_src.as_() - bias_uv;

                    ycgco_execute_limited::<V, PRECISION, CHANNELS, BIT_DEPTH>(
                        rgb_dst, y_value, cg_value, co_value, y_coef,
                    );
                }
            }
            YuvIntensityRange::Pc => {
                for (((y_src, u_src), v_src), rgb_dst) in
                    y_src.iter().zip(u_src).zip(v_src).zip(rgb_chunks)
                {
                    let y_value = y_src.as_() - bias_y;
                    let cg_value = u_src.as_() - bias_uv;
                    let co_value = v_src.as_() - bias_uv;

                    ycgco_execute_full::<V, PRECISION, CHANNELS, BIT_DEPTH>(
                        rgb_dst, y_value, cg_value, co_value,
                    );
                }
            }
        }
    }

    Ok(())
}

macro_rules! define_ycgco_half_chroma {
    ($name: ident, $invoker: ident, $storage: ident, $cn: expr, $bp: expr, $description: expr) => {
        #[doc = concat!($description, "
        
        # Arguments
        
        * `image`: see [YuvPlanarImage]
        * `rgb`: RGB image layout
        * `range`: see [YuvIntensityRange]
        * `matrix`: see [YuvStandardMatrix]")]
        pub(crate) fn $name(
            image: YuvPlanarImage<$storage>,
            rgb: &mut [$storage],
            range: YuvIntensityRange,
        ) -> Result<(), ImageError> {
            const P: i32 = 13;
            $invoker::<$storage, HalvedRowHandler<$storage>, P, $cn, $bp>(
                image,
                rgb,
                range,
                YuvStandardMatrix::Bt709,
                process_halved_chroma_row_cgco::<$storage, P, $cn, $bp>,
            )
        }
    };
}

const RGBA_CN: usize = 4;

define_ycgco_half_chroma!(
    ycgco420_to_rgba8,
    yuv420_to_rgbx_invoker,
    u8,
    RGBA_CN,
    8,
    "Converts YCgCo 420 8-bit planar format to Rgba 8-bit"
);

define_ycgco_half_chroma!(
    ycgco422_to_rgba8,
    yuv422_to_rgbx_invoker,
    u8,
    RGBA_CN,
    8,
    "Converts YCgCo 420 8-bit planar format to Rgba 8-bit"
);

define_ycgco_half_chroma!(
    ycgco420_to_rgba10,
    yuv420_to_rgbx_invoker,
    u16,
    RGBA_CN,
    10,
    "Converts YCgCo 420 10-bit planar format to Rgba 10-bit"
);

define_ycgco_half_chroma!(
    ycgco422_to_rgba10,
    yuv422_to_rgbx_invoker,
    u16,
    RGBA_CN,
    10,
    "Converts YCgCo 422 10-bit planar format to Rgba 10-bit"
);

define_ycgco_half_chroma!(
    ycgco420_to_rgba12,
    yuv420_to_rgbx_invoker,
    u16,
    RGBA_CN,
    12,
    "Converts YCgCo 420 12-bit planar format to Rgba 12-bit"
);

define_ycgco_half_chroma!(
    ycgco422_to_rgba12,
    yuv422_to_rgbx_invoker,
    u16,
    RGBA_CN,
    12,
    "Converts YCgCo 422 12-bit planar format to Rgba 12-bit"
);

macro_rules! define_ycgcg_full_chroma {
    ($name: ident, $storage: ident, $cn: expr, $bp: expr, $description: expr) => {
        #[doc = concat!($description, "
        
        # Arguments
        
        * `image`: see [YuvPlanarImage]
        * `rgba`: RGB image layout
        * `range`: see [YuvIntensityRange]
        * `matrix`: see [YuvStandardMatrix]
        ")]
        pub(crate) fn $name(
            image: YuvPlanarImage<$storage>,
            rgba: &mut [$storage],
            range: YuvIntensityRange,
        ) -> Result<(), ImageError> {
            ycgco444_to_rgbx_impl::<$storage, $cn, $bp>(image, rgba, range)
        }
    };
}

define_ycgcg_full_chroma!(
    ycgco444_to_rgba8,
    u8,
    RGBA_CN,
    8,
    "Converts YCgCo 444 planar format 8 bit-depth to Rgba 8 bit"
);
define_ycgcg_full_chroma!(
    ycgco444_to_rgba10,
    u16,
    RGBA_CN,
    10,
    "Converts YCgCo 444 planar format 10 bit-depth to Rgba 10 bit"
);
define_ycgcg_full_chroma!(
    ycgco444_to_rgba12,
    u16,
    RGBA_CN,
    12,
    "Converts YCgCo 444 planar format 12 bit-depth to Rgba 12 bit"
);
