use crate::core::color::RGB;
use crate::core::geometry::{
    Bounds2f, Bounds3f, Normal3f, Point2f, Point2i, Point3f, Ray, Vector2f, Vector3f,
};
use crate::core::geometry::{Frame, VectorLike};
use crate::core::image::{Image, PixelFormat, WrapMode};
use crate::core::interaction::InteractionBase;
use crate::core::interaction::{Interaction, SimpleInteraction};
use crate::core::light::{
    LightBase, LightBounds, LightLiSample, LightSampleContext, LightTrait, LightType,
};
use crate::core::medium::{Medium, MediumInterface};
use crate::core::spectrum::{Spectrum, SpectrumTrait};
use crate::spectra::{DenselySampledSpectrum, SampledSpectrum, SampledWavelengths};
use crate::spectra::{RGBColorSpace, RGBIlluminantSpectrum};
use crate::utils::math::{clamp, equal_area_sphere_to_square, equal_area_square_to_sphere, square};
use crate::utils::sampling::{
    sample_uniform_sphere, uniform_sphere_pdf, AliasTable, PiecewiseConstant2D,
    WindowedPiecewiseConstant2D,
};
use crate::utils::{Ptr, Transform};
use crate::{Float, PI};
use num_traits::Float as NumFloat;

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct UniformInfiniteLight {
    pub base: LightBase,
    pub lemit: Ptr<DenselySampledSpectrum>,
    pub scale: Float,
    pub scene_center: Point3f,
    pub scene_radius: Float,
}

unsafe impl Send for UniformInfiniteLight {}
unsafe impl Sync for UniformInfiniteLight {}

impl UniformInfiniteLight {
    pub fn new(
        render_from_light: Transform,
        scale: Float,
        lemit: Ptr<DenselySampledSpectrum>,
    ) -> Self {
        let base = LightBase::new(
            LightType::Infinite,
            render_from_light,
            MediumInterface::default(),
        );
        Self {
            base,
            lemit,
            scale,
            scene_center: Point3f::default(),
            scene_radius: 0.0,
        }
    }
}

impl LightTrait for UniformInfiniteLight {
    fn base(&self) -> &LightBase {
        &self.base
    }
    fn sample_li(
        &self,
        ctx: &LightSampleContext,
        u: Point2f,
        lambda: &SampledWavelengths,
        allow_incomplete_pdf: bool,
    ) -> Option<LightLiSample> {
        if allow_incomplete_pdf {
            return None;
        }
        let wi = sample_uniform_sphere(u);
        let pdf = uniform_sphere_pdf();
        let base = InteractionBase::new_boundary(
            ctx.p() + wi * (2. * self.scene_radius),
            0.,
            MediumInterface::default(),
        );
        let intr_simple = SimpleInteraction::new(base);

        let intr = Interaction::Simple(intr_simple);
        Some(LightLiSample::new(
            self.scale * self.lemit.sample(lambda),
            wi,
            pdf,
            intr,
        ))
    }

    fn pdf_li(
        &self,
        _ctx: &LightSampleContext,
        _wi: Vector3f,
        allow_incomplete_pdf: bool,
    ) -> Float {
        if allow_incomplete_pdf {
            return 0.;
        }
        uniform_sphere_pdf()
    }

    fn l(
        &self,
        _p: Point3f,
        _n: Normal3f,
        _uv: Point2f,
        _w: Vector3f,
        _lambda: &SampledWavelengths,
    ) -> SampledSpectrum {
        todo!()
    }

    fn le(&self, _ray: &Ray, lambda: &SampledWavelengths) -> SampledSpectrum {
        self.scale * self.lemit.sample(lambda)
    }

    #[cfg(not(target_os = "cuda"))]
    fn preprocess(&mut self, _scene_bounds: &Bounds3f) {
        todo!()
    }

    #[cfg(not(target_os = "cuda"))]
    fn bounds(&self) -> Option<LightBounds> {
        todo!()
    }

    #[cfg(not(target_os = "cuda"))]
    fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
        4. * PI * PI * square(self.scene_radius) * self.scale * self.lemit.sample(&lambda)
    }
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ImageInfiniteLight {
    pub base: LightBase,
    pub image: Ptr<Image>,
    pub image_color_space: Ptr<RGBColorSpace>,
    pub distrib: Ptr<PiecewiseConstant2D>,
    pub compensated_distrib: Ptr<PiecewiseConstant2D>,
    pub scale: Float,
    pub scene_radius: Float,
    pub scene_center: Point3f,
}

unsafe impl Send for ImageInfiniteLight {}
unsafe impl Sync for ImageInfiniteLight {}

impl ImageInfiniteLight {
    pub fn new(
        render_from_light: Transform,
        scale: Float,
        image: Ptr<Image>,
        image_color_space: Ptr<RGBColorSpace>,
        distrib: Ptr<PiecewiseConstant2D>,
        compensated_distrib: Ptr<PiecewiseConstant2D>,
    ) -> Self {
        let base = LightBase::new(
            LightType::Infinite,
            render_from_light,
            MediumInterface::default(),
        );
        Self {
            base,
            image,
            image_color_space,
            scale,
            distrib,
            compensated_distrib,
            scene_center: Point3f::default(),
            scene_radius: 0.0,
        }
    }

    fn image_le(&self, uv: Point2f, lambda: &SampledWavelengths) -> SampledSpectrum {
        let mut rgb = RGB::default();
        for c in 0..3 {
            rgb[c] = self.image.lookup_nearest_channel_with_wrap(
                uv,
                c,
                WrapMode::OctahedralSphere.into(),
            );
        }
        let spec = RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero());
        self.scale * spec.sample(lambda)
    }
}

impl LightTrait for ImageInfiniteLight {
    fn base(&self) -> &LightBase {
        &self.base
    }

    fn sample_li(
        &self,
        ctx: &LightSampleContext,
        u: Point2f,
        lambda: &SampledWavelengths,
        allow_incomplete_pdf: bool,
    ) -> Option<LightLiSample> {
        let (uv, map_pdf, _) = if allow_incomplete_pdf {
            self.compensated_distrib.sample(u)
        } else {
            self.distrib.sample(u)
        };

        if map_pdf == 0. {
            return None;
        }
        // Convert infinite light sample point to direction
        let w_light = equal_area_square_to_sphere(uv);
        let wi = self.base.render_from_light.apply_to_vector(w_light);
        let pdf = map_pdf / (4. * PI);

        // Return radiance value for infinite light direction
        let base = InteractionBase::new_boundary(
            ctx.p() + wi * (2. * self.scene_radius),
            0.,
            self.base.medium_interface,
        );
        let simple_intr = SimpleInteraction::new(base);
        let intr = Interaction::Simple(simple_intr);

        Some(LightLiSample::new(self.image_le(uv, lambda), wi, pdf, intr))
    }

    fn pdf_li(&self, _ctx: &LightSampleContext, wi: Vector3f, allow_incomplete_pdf: bool) -> Float {
        let w_light = self.base.render_from_light.apply_inverse_vector(wi);
        let uv = equal_area_sphere_to_square(w_light);
        let pdf = if allow_incomplete_pdf {
            self.compensated_distrib.pdf(uv)
        } else {
            self.distrib.pdf(uv)
        };
        pdf / (4. * PI)
    }

    fn l(
        &self,
        _p: Point3f,
        _n: Normal3f,
        _uv: Point2f,
        _w: Vector3f,
        _lambda: &SampledWavelengths,
    ) -> SampledSpectrum {
        todo!()
    }

    fn le(&self, ray: &Ray, lambda: &SampledWavelengths) -> SampledSpectrum {
        let w_light = self
            .base
            .render_from_light
            .apply_inverse_vector(ray.d)
            .normalize();
        let uv = equal_area_sphere_to_square(w_light);
        self.image_le(uv, lambda)
    }

    #[cfg(not(target_os = "cuda"))]
    fn phi(&self, lambda: SampledWavelengths) -> SampledSpectrum {
        let mut sum_l = SampledSpectrum::new(0.);
        let width = self.image.resolution().x();
        let height = self.image.resolution().y();
        for v in 0..height {
            for u in 0..width {
                let mut rgb = RGB::default();
                for c in 0..3 {
                    rgb[c] = self.image.get_channel_with_wrap(
                        Point2i::new(u, v),
                        c,
                        WrapMode::OctahedralSphere.into(),
                    );
                }
                sum_l += RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero())
                    .sample(&lambda);
            }
        }
        4. * PI * PI * square(self.scene_radius) * self.scale * sum_l / (width * height) as Float
    }

    #[cfg(not(target_os = "cuda"))]
    fn preprocess(&mut self, scene_bounds: &Bounds3f) {
        let (scene_center, scene_radius) = scene_bounds.bounding_sphere();
        self.scene_center = scene_center;
        self.scene_radius = scene_radius;
    }

    #[cfg(not(target_os = "cuda"))]
    fn bounds(&self) -> Option<LightBounds> {
        None
    }
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct PortalInfiniteLight {
    pub base: LightBase,
    pub image: Ptr<Image>,
    pub image_color_space: Ptr<RGBColorSpace>,
    pub scale: Float,
    pub portal: [Point3f; 4],
    pub portal_frame: Frame,
    pub distribution: Ptr<WindowedPiecewiseConstant2D>,
    pub scene_center: Point3f,
    pub scene_radius: Float,
}

impl PortalInfiniteLight {
    pub fn new(
        render_from_light: Transform,
        scale: Float,
        image: Ptr<Image>,
        image_color_space: Ptr<RGBColorSpace>,
        portal: [Point3f; 4],
        portal_frame: Frame,
        distribution: Ptr<WindowedPiecewiseConstant2D>,
    ) -> Self {
        let base = LightBase::new(
            LightType::Infinite,
            render_from_light,
            MediumInterface::default(),
        );
        Self {
            base,
            image,
            image_color_space,
            scale,
            portal,
            portal_frame,
            distribution,
            scene_center: Point3f::default(),
            scene_radius: 0.0,
        }
    }

    pub fn image_lookup(&self, uv: Point2f, lambda: &SampledWavelengths) -> SampledSpectrum {
        let mut rgb = RGB::default();
        for c in 0..3 {
            rgb[c] = self.image.lookup_nearest_channel(uv, c)
        }
        let spec = RGBIlluminantSpectrum::new(&self.image_color_space, rgb.clamp_zero());
        self.scale * spec.sample(lambda)
    }

    pub fn image_from_render(&self, w_render: Vector3f) -> Option<(Point2f, Float)> {
        let w = self.portal_frame.to_local(w_render);

        if w.z() <= 0.0 {
            return None;
        }

        let alpha = w.x().atan2(w.z());
        let beta = w.y().atan2(w.z());

        let duv_dw = square(PI) * (1. - square(w.x())) * (1. - square(w.y())) / w.z();

        Some((
            Point2f::new(
                clamp((alpha + PI / 2.0) / PI, 0.0, 1.0),
                clamp((beta + PI / 2.0) / PI, 0.0, 1.0),
            ),
            duv_dw,
        ))
    }

    pub fn image_bounds(&self, p: Point3f) -> Option<Bounds2f> {
        let (p0, _) = self.image_from_render((self.portal[0] - p).normalize())?;
        let (p1, _) = self.image_from_render((self.portal[2] - p).normalize())?;

        Some(Bounds2f::from_points(p0, p1))
    }

    pub fn area(&self) -> Float {
        (self.portal[1] - self.portal[0]).norm() * (self.portal[3] - self.portal[0]).norm()
    }

    pub fn render_from_image(portal_frame: Frame, uv: Point2f) -> (Vector3f, Float) {
        let alpha = -PI / 2.0 + uv.x() * PI;
        let beta = -PI / 2.0 + uv.y() * PI;

        let x = alpha.tan();
        let y = beta.tan();

        let w = Vector3f::new(x, y, 1.0).normalize();

        let duv_dw = square(PI) * (1.0 - square(w.x())) * (1.0 - square(w.y())) / w.z();

        (portal_frame.from_local(w), duv_dw)
    }
}

impl LightTrait for PortalInfiniteLight {
    fn base(&self) -> &LightBase {
        &self.base
    }

    fn sample_li(
        &self,
        ctx: &LightSampleContext,
        u: Point2f,
        lambda: &SampledWavelengths,
        _allow_incomplete_pdf: bool,
    ) -> Option<LightLiSample> {
        let b = self.image_bounds(ctx.p())?;
        let (uv, map_pdf) = self.distribution.sample(u, b)?;
        let (wi, duv_dw) = Self::render_from_image(self.portal_frame, uv);
        if duv_dw == 0. {
            return None;
        }
        let pdf = map_pdf / duv_dw;
        let l = self.image_lookup(uv, lambda);
        let pl = ctx.p() + 2. * self.scene_radius * wi;
        let base = InteractionBase::new_boundary(pl, 0., self.base.medium_interface);
        let sintr = SimpleInteraction::new(base);
        let intr = Interaction::Simple(sintr);
        Some(LightLiSample::new(l, wi, pdf, intr))
    }

    fn pdf_li(&self, ctx: &LightSampleContext, wi: Vector3f, _allow_incomplete_pdf: bool) -> Float {
        let Some((uv, duv_dw)) = self.image_from_render(wi) else {
            return 0.;
        };
        let Some(b) = self.image_bounds(ctx.p()) else {
            return 0.;
        };
        let pdf = self.distribution.pdf(uv, b);
        pdf / duv_dw
    }

    fn l(
        &self,
        _p: Point3f,
        _n: Normal3f,
        _uv: Point2f,
        _w: Vector3f,
        _lambda: &SampledWavelengths,
    ) -> SampledSpectrum {
        todo!()
    }

    fn le(&self, ray: &Ray, lambda: &SampledWavelengths) -> SampledSpectrum {
        let uv = self.image_from_render(ray.d.normalize());
        let b = self.image_bounds(ray.o);
        match (uv, b) {
            (Some((p, duv_dw)), Some(bounds)) if bounds.contains(p) => self.image_lookup(p, lambda),
            _ => SampledSpectrum::new(0.0),
        }
    }

    #[cfg(not(target_os = "cuda"))]
    fn phi(&self, _lambda: SampledWavelengths) -> SampledSpectrum {
        todo!()
    }

    #[cfg(not(target_os = "cuda"))]
    fn preprocess(&mut self, scene_bounds: &Bounds3f) {
        (self.scene_center, self.scene_radius) = scene_bounds.bounding_sphere();
    }

    #[cfg(not(target_os = "cuda"))]
    fn bounds(&self) -> Option<LightBounds> {
        None
    }
}
