use crate::core::image::{Image, ImageIO};
use shared::Float;
use shared::core::color::{ColorEncoding, RGB};
use shared::core::geometry::{Point2f, Point2i, Vector2f, VectorLike};
use shared::core::image::{WrapMode, WrapMode2D};
use shared::spectra::RGBColorSpace;
use shared::utils::math::{lerp, safe_sqrt, square};
use std::hash::{Hash, Hasher};
use std::ops::{Add, Mul, Sub};
use std::path::Path;

#[cfg(feature = "cuda")]
use std::sync::OnceLock;

#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FilterFunction {
    Point,
    Bilinear,
    Trilinear,
    Ewa,
}

impl std::fmt::Display for FilterFunction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            FilterFunction::Ewa => "EWA",
            FilterFunction::Trilinear => "trilinear",
            FilterFunction::Bilinear => "bilinear",
            FilterFunction::Point => "point",
        };
        write!(f, "{}", s)
    }
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MIPMapFilterOptions {
    pub filter: FilterFunction,
    pub max_anisotropy: Float,
}

impl Default for MIPMapFilterOptions {
    fn default() -> Self {
        Self {
            filter: FilterFunction::Ewa,
            max_anisotropy: 8.0,
        }
    }
}

impl PartialEq for MIPMapFilterOptions {
    fn eq(&self, other: &Self) -> bool {
        self.filter == other.filter
            && self.max_anisotropy.to_bits() == other.max_anisotropy.to_bits()
    }
}

impl Eq for MIPMapFilterOptions {}

impl Hash for MIPMapFilterOptions {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.filter.hash(state);
        self.max_anisotropy.to_bits().hash(state);
    }
}

pub trait MIPMapSample:
    Copy + Add<Output = Self> + Sub<Output = Self> + Mul<Float, Output = Self> + std::fmt::Debug
{
    fn zero() -> Self;
    fn sample_bilerp(image: &Image, st: Point2f, wrap: WrapMode2D) -> Self;
    fn sample_texel(image: &Image, st: Point2i, wrap: WrapMode2D) -> Self;
}

impl MIPMapSample for Float {
    fn zero() -> Self {
        0.
    }
    fn sample_bilerp(image: &Image, st: Point2f, wrap: WrapMode2D) -> Self {
        image.bilerp_channel_with_wrap(st, 0, wrap)
    }
    fn sample_texel(image: &Image, st: Point2i, wrap: WrapMode2D) -> Self {
        image.get_channel_with_wrap(st, 0, wrap)
    }
}

impl MIPMapSample for RGB {
    fn zero() -> Self {
        RGB::new(0., 0., 0.)
    }
    fn sample_bilerp(image: &Image, st: Point2f, wrap: WrapMode2D) -> Self {
        let nc = image.n_channels();
        if nc >= 3 {
            let r = image.bilerp_channel_with_wrap(st, 0, wrap);
            let g = image.bilerp_channel_with_wrap(st, 1, wrap);
            let b = image.bilerp_channel_with_wrap(st, 2, wrap);
            RGB::new(r, g, b)
        } else {
            let v = image.bilerp_channel_with_wrap(st, 0, wrap);
            RGB::new(v, v, v)
        }
    }
    fn sample_texel(image: &Image, st: Point2i, wrap: WrapMode2D) -> Self {
        let nc = image.n_channels();
        if nc >= 3 {
            let r = image.get_channel_with_wrap(st, 0, wrap);
            let g = image.get_channel_with_wrap(st, 1, wrap);
            let b = image.get_channel_with_wrap(st, 2, wrap);
            RGB::new(r, g, b)
        } else {
            let v = image.get_channel_with_wrap(st, 0, wrap);
            RGB::new(v, v, v)
        }
    }
}

#[derive(Clone, Debug)]
pub struct MIPMap {
    pub pyramid: Vec<Image>,
    pub color_space: Option<RGBColorSpace>,
    pub wrap_mode: WrapMode,
    pub options: MIPMapFilterOptions,
    #[cfg(feature = "cuda")]
    tex_obj: OnceLock<u64>,
}

impl MIPMap {
    pub fn new(
        image: Image,
        color_space: Option<RGBColorSpace>,
        wrap_mode: WrapMode,
        options: MIPMapFilterOptions,
    ) -> Self {
        let pyramid = Image::generate_pyramid(image, wrap_mode);
        Self {
            pyramid,
            color_space,
            wrap_mode,
            options,
            #[cfg(feature = "cuda")]
            tex_obj: OnceLock::new(),
        }
    }

    pub fn level_resolution(&self, level: usize) -> Point2i {
        self.pyramid[level].resolution()
    }

    #[inline]
    pub fn is_single_channel(&self) -> bool {
        self.pyramid[0].n_channels() == 1
    }

    pub fn levels(&self) -> usize {
        self.pyramid.len()
    }

    pub fn get_rgb_colorspace(&self) -> Option<RGBColorSpace> {
        self.color_space.clone()
    }

    pub fn get_level(&self, level: usize) -> &Image {
        &self.pyramid[level]
    }

    pub fn base_image(&self) -> &Image {
        &self.pyramid[0]
    }

    pub fn filter<T: MIPMapSample>(
        &self,
        st: Point2f,
        mut dst0: Vector2f,
        mut dst1: Vector2f,
    ) -> T {
        if self.options.filter != FilterFunction::Ewa {
            let width = 2.0
                * [
                    dst0.x().abs(),
                    dst0.y().abs(),
                    dst1.x().abs(),
                    dst1.y().abs(),
                ]
                .into_iter()
                .reduce(Float::max)
                .unwrap_or(0.0);

            let n_levels = self.levels() as Float;
            let level = n_levels - 1.0 + width.max(1e-8).log2();

            if level >= n_levels - 1.0 {
                return self.texel(self.levels() - 1, Point2i::new(0, 0));
            }

            let i_level = level.floor() as usize;
            return match self.options.filter {
                FilterFunction::Point => {
                    let resolution = self.level_resolution(i_level);
                    let sti = Point2i::new(
                        (st.x() * resolution.x() as Float - 0.5).round() as i32,
                        (st.y() * resolution.y() as Float - 0.5).round() as i32,
                    );
                    self.texel(i_level, sti)
                }
                FilterFunction::Bilinear => self.bilerp(i_level, st),
                FilterFunction::Trilinear => {
                    let v0 = self.bilerp(i_level, st);
                    let v1 = self.bilerp(i_level + 1, st);
                    let t = level - i_level as Float;
                    lerp(t, v0, v1)
                }
                FilterFunction::Ewa => unreachable!(),
            };
        }

        if dst0.norm_squared() < dst1.norm_squared() {
            std::mem::swap(&mut dst0, &mut dst1);
        }
        let longer_len = dst0.norm();
        let mut shorter_len = dst1.norm();

        if shorter_len * self.options.max_anisotropy < longer_len && shorter_len > 0.0 {
            let scale = longer_len / (shorter_len * self.options.max_anisotropy);
            dst1 *= scale;
            shorter_len *= scale;
        }

        if shorter_len == 0.0 {
            return self.bilerp(0, st);
        }

        let lod = (self.levels() as Float - 1.0 + shorter_len.log2()).max(0.0);
        let ilod = lod.floor() as usize;

        let v0 = self.ewa(ilod, st, dst0, dst1);
        let v1 = self.ewa(ilod + 1, st, dst0, dst1);
        lerp(lod - ilod as Float, v0, v1)
    }

    fn texel<T: MIPMapSample>(&self, level: usize, st: Point2i) -> T {
        if level >= self.levels() {
            panic!("MIPMap level out of bounds");
        }
        let image = &self.pyramid[level];
        let wrap_2d = WrapMode2D {
            uv: [self.wrap_mode; 2],
        };
        T::sample_texel(image, st, wrap_2d)
    }

    fn bilerp<T: MIPMapSample>(&self, level: usize, st: Point2f) -> T {
        let image = &self.pyramid[level];
        let wrap_2d = WrapMode2D {
            uv: [self.wrap_mode; 2],
        };
        T::sample_bilerp(image, st, wrap_2d)
    }

    fn ewa<T: MIPMapSample>(
        &self,
        level: usize,
        mut st: Point2f,
        mut dst0: Vector2f,
        mut dst1: Vector2f,
    ) -> T {
        if level > self.levels() {
            return self.texel(self.levels() - 1, Point2i::new(0, 0));
        }

        let level_res = self.level_resolution(level);
        st[0] = st[0] * level_res[0] as Float - 0.5;
        st[1] = st[1] * level_res[1] as Float - 0.5;
        dst0[0] *= level_res[0] as Float;
        dst0[1] *= level_res[1] as Float;
        dst1[0] *= level_res[0] as Float;
        dst1[1] *= level_res[1] as Float;

        let mut a = square(dst0[1]) + square(dst1[1]) + 1.;
        let mut b = -2. * (dst0[0] + dst0[1] + dst1[1]);
        let mut c = square(dst0[0]) + square(dst1[0]) + 1.;
        let inv_f = 1. / (a * c - square(b) * 0.25);
        a *= inv_f;
        b *= inv_f;
        c *= inv_f;

        let det = -square(b) + 4. * a * c;
        let inv_det = 1. / det;
        let u_sqrt = safe_sqrt(det * c);
        let v_sqrt = safe_sqrt(det * a);

        let s0: i32 = (st[0] - 2. * inv_det * u_sqrt).ceil() as i32;
        let s1: i32 = (st[0] + 2. * inv_det * u_sqrt).floor() as i32;
        let t0: i32 = (st[1] - 2. * inv_det * v_sqrt).ceil() as i32;
        let t1: i32 = (st[1] + 2. * inv_det * v_sqrt).floor() as i32;

        let mut sum = T::zero();
        let mut sum_wts = 0.;

        for it in t0..=t1 {
            let tt = it as Float - st[1];
            for is in s0..=s1 {
                let ss = is as Float - st[0];
                let r2 = a * square(ss) + b * ss * tt + c * square(tt);
                if r2 < 1.0 {
                    let index = (r2 * MIP_FILTER_LUT_SIZE as Float)
                        .min((MIP_FILTER_LUT_SIZE - 1) as Float)
                        as usize;
                    let weight = MIP_FILTER_LUT[index];
                    sum = sum + self.texel::<T>(level, Point2i::new(is, it)) * weight;
                    sum_wts += weight;
                }
            }
        }
        sum * (1. / sum_wts)
    }

    pub fn create_from_file(
        filename: &Path,
        options: MIPMapFilterOptions,
        wrap_mode: WrapMode,
        encoding: ColorEncoding,
    ) -> Result<MIPMap, ()> {
        let image_and_metadata = Image::read(filename, Some(encoding)).unwrap();
        let image = image_and_metadata.image;
        Ok(MIPMap::new(
            image,
            image_and_metadata.metadata.colorspace,
            wrap_mode,
            options,
        ))
    }

    #[cfg(feature = "cuda")]
    pub fn texture_object(&self) -> u64 {
        *self
            .tex_obj
            .get_or_init(|| create_cuda_texture(&self.pyramid, self.wrap_mode))
    }

    #[cfg(not(feature = "cuda"))]
    pub fn texture_object(&self) -> u64 {
        0
    }
}

#[cfg(feature = "cuda")]
fn create_cuda_texture(pyramid: &[Image], wrap_mode: WrapMode) -> u64 {
    use cuda_runtime_sys::*;

    let base = &pyramid[0];
    let (width, height) = (
        base.resolution().x() as usize,
        base.resolution().y() as usize,
    );
    let channels = base.n_channels() as usize;

    unsafe {
        let channel_desc = cudaCreateChannelDesc(
            32,
            if channels > 1 { 32 } else { 0 },
            if channels > 2 { 32 } else { 0 },
            if channels > 3 { 32 } else { 0 },
            cudaChannelFormatKind::cudaChannelFormatKindFloat,
        );

        let mut array: cudaArray_t = std::ptr::null_mut();
        cudaMallocArray(&mut array, &channel_desc, width, height, 0);

        // Get raw pixel data from the base image
        let pixel_data = base
            .as_f32_slice()
            .expect("GPU upload requires Float encoded image");
        let row_bytes = width * channels * std::mem::size_of::<f32>();

        cudaMemcpy2DToArray(
            array,
            0,
            0,
            pixel_data.as_ptr() as *const _,
            row_bytes,
            row_bytes,
            height,
            cudaMemcpyKind::cudaMemcpyHostToDevice,
        );

        let res_desc = cudaResourceDesc {
            resType: cudaResourceType::cudaResourceTypeArray,
            res: cudaResourceDesc__bindgen_ty_1 {
                array: cudaResourceDesc__bindgen_ty_1__bindgen_ty_1 { array },
            },
        };

        let address_mode = match wrap_mode {
            WrapMode::Repeat => cudaTextureAddressMode::cudaAddressModeWrap,
            WrapMode::Clamp => cudaTextureAddressMode::cudaAddressModeClamp,
            WrapMode::Black => cudaTextureAddressMode::cudaAddressModeBorder,
            WrapMode::OctahedralSphere => cudaTextureAddressMode::cudaAddressModeBorder,
        };

        let tex_desc = cudaTextureDesc {
            addressMode: [address_mode; 3],
            filterMode: cudaTextureFilterMode::cudaFilterModeLinear,
            readMode: cudaTextureReadMode::cudaReadModeElementType,
            normalizedCoords: 1,
            ..std::mem::zeroed()
        };

        let mut tex_obj: cudaTextureObject_t = 0;
        cudaCreateTextureObject(&mut tex_obj, &res_desc, &tex_desc, std::ptr::null());
        tex_obj
    }
}

static MIP_FILTER_LUT_SIZE: usize = 128;

static MIP_FILTER_LUT: [Float; MIP_FILTER_LUT_SIZE] = [
    0.864664733,
    0.849040031,
    0.83365953,
    0.818519294,
    0.80361563,
    0.788944781,
    0.774503231,
    0.760287285,
    0.746293485,
    0.732518315,
    0.718958378,
    0.705610275,
    0.692470789,
    0.679536581,
    0.666804492,
    0.654271305,
    0.641933978,
    0.629789352,
    0.617834508,
    0.606066525,
    0.594482362,
    0.583079159,
    0.571854174,
    0.560804546,
    0.549927592,
    0.539220572,
    0.528680861,
    0.518305838,
    0.50809288,
    0.498039544,
    0.488143265,
    0.478401601,
    0.468812168,
    0.45937258,
    0.450080454,
    0.440933526,
    0.431929469,
    0.423066139,
    0.414341331,
    0.405752778,
    0.397298455,
    0.388976216,
    0.380784035,
    0.372719884,
    0.364781618,
    0.356967449,
    0.34927541,
    0.341703475,
    0.334249914,
    0.32691282,
    0.319690347,
    0.312580705,
    0.305582166,
    0.298692942,
    0.291911423,
    0.285235822,
    0.278664529,
    0.272195935,
    0.265828371,
    0.259560347,
    0.253390193,
    0.247316495,
    0.241337672,
    0.235452279,
    0.229658857,
    0.223955944,
    0.21834214,
    0.212816045,
    0.207376286,
    0.202021524,
    0.196750447,
    0.191561714,
    0.186454013,
    0.181426153,
    0.176476851,
    0.171604887,
    0.166809067,
    0.162088141,
    0.157441005,
    0.152866468,
    0.148363426,
    0.143930718,
    0.139567271,
    0.135272011,
    0.131043866,
    0.126881793,
    0.122784719,
    0.11875169,
    0.114781633,
    0.11087364,
    0.107026696,
    0.103239879,
    0.0995122194,
    0.0958427936,
    0.0922307223,
    0.0886750817,
    0.0851749927,
    0.0817295909,
    0.0783380121,
    0.0749994367,
    0.0717130303,
    0.0684779733,
    0.0652934611,
    0.0621587038,
    0.0590728968,
    0.0560353249,
    0.0530452281,
    0.0501018465,
    0.0472044498,
    0.0443523228,
    0.0415447652,
    0.0387810767,
    0.0360605568,
    0.0333825648,
    0.0307464004,
    0.0281514227,
    0.0255970061,
    0.0230824798,
    0.0206072628,
    0.0181707144,
    0.0157722086,
    0.013411209,
    0.0110870898,
    0.0087992847,
    0.0065472275,
    0.00433036685,
    0.0021481365,
    0.,
];
