use crate::cameras::*;
use crate::core::film::Film;
use crate::core::geometry::{
    Normal3f, Point2f, Point2i, Point3f, Ray, RayDifferential, Vector3f, VectorLike,
};
use crate::core::interaction::Interaction;
use crate::core::medium::Medium;
use crate::core::pbrt::Float;
use crate::core::sampler::CameraSample;
use crate::spectra::{SampledSpectrum, SampledWavelengths};
use crate::utils::math::lerp;
use crate::utils::options::RenderingCoordinateSystem;
use crate::utils::ptr::Ptr;
use crate::utils::transform::{AnimatedTransform, Transform};
use enum_dispatch::enum_dispatch;
use num_traits::Float as NumFloat;

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct CameraRay {
    pub ray: Ray,
    pub weight: SampledSpectrum,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct CameraWiSample {
    pub wi_spec: SampledSpectrum,
    pub wi: Vector3f,
    pub pdf: Float,
    pub p_raster: Point2f,
    pub p_ref: Interaction,
    pub p_lens: Interaction,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct CameraTransform {
    pub render_from_camera: AnimatedTransform,
    pub world_from_render: Transform,
}

impl CameraTransform {
    pub fn from_world(
        world_from_camera: AnimatedTransform,
        rendering_space: RenderingCoordinateSystem,
    ) -> Self {
        let world_from_render = match rendering_space {
            RenderingCoordinateSystem::Camera => {
                let t_mid = (world_from_camera.start_time + world_from_camera.end_time) / 2.;
                world_from_camera.interpolate(t_mid)
            }
            RenderingCoordinateSystem::CameraWorld => {
                let t_mid = (world_from_camera.start_time + world_from_camera.end_time) / 2.;
                let p_camera = world_from_camera.apply_point(Point3f::default(), t_mid);
                Transform::translate(p_camera - Point3f::new(0., 0., 0.))
            }
            RenderingCoordinateSystem::World => Transform::identity(),
        };
        let render_from_world = world_from_render.inverse();
        let rfc = [
            render_from_world * world_from_camera.start_transform,
            render_from_world * world_from_camera.end_transform,
        ];
        let render_from_camera = AnimatedTransform::new(
            &rfc[0],
            world_from_camera.start_time,
            &rfc[1],
            world_from_camera.end_time,
        );
        Self {
            render_from_camera,
            world_from_render,
        }
    }

    pub fn camera_from_world(&self, time: Float) -> Transform {
        (self.world_from_render * self.render_from_camera.interpolate(time)).inverse()
    }

    pub fn render_from_camera(&self, p: Point3f, time: Float) -> Point3f {
        self.render_from_camera.apply_point(p, time)
    }

    pub fn render_from_camera_vector(&self, v: Vector3f, time: Float) -> Vector3f {
        self.render_from_camera.apply_vector(v, time)
    }

    pub fn render_from_camera_ray(&self, r: &Ray, t_max: &mut Option<Float>) -> Ray {
        self.render_from_camera.apply_ray(r, t_max)
    }

    pub fn camera_from_render(&self, p: Point3f, time: Float) -> Point3f {
        self.render_from_camera.apply_inverse_point(p, time)
    }

    pub fn camera_from_render_vector(&self, v: Vector3f, time: Float) -> Vector3f {
        self.render_from_camera.apply_inverse_vector(v, time)
    }

    pub fn camera_from_render_normal(&self, n: Normal3f, time: Float) -> Normal3f {
        self.render_from_camera.apply_inverse_normal(n, time)
    }
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct CameraBase {
    pub camera_transform: CameraTransform,
    pub shutter_open: Float,
    pub shutter_close: Float,
    pub min_pos_differential_x: Vector3f,
    pub min_pos_differential_y: Vector3f,
    pub min_dir_differential_x: Vector3f,
    pub min_dir_differential_y: Vector3f,
    pub film: Ptr<Film>,
    pub medium: Ptr<Medium>,
}

#[repr(C)]
#[derive(Debug, Clone)]
#[enum_dispatch(CameraTrait)]
pub enum Camera {
    Perspective(PerspectiveCamera),
    Orthographic(OrthographicCamera),
    Spherical(SphericalCamera),
    Realistic(RealisticCamera),
}

#[enum_dispatch]
pub trait CameraTrait {
    fn base(&self) -> &CameraBase;
    fn generate_ray(&self, sample: CameraSample, lambda: &SampledWavelengths) -> Option<CameraRay>;

    fn get_film(&self) -> &Film {
        #[cfg(not(target_os = "cuda"))]
        {
            if self.base().film.is_null() {
                panic!(
                    "FilmBase error: PixelSensor pointer is null. This should have been checked during construction."
                );
            }
        }
        &self.base().film
    }

    fn sample_time(&self, u: Float) -> Float {
        lerp(u, self.base().shutter_open, self.base().shutter_close)
    }

    fn resolution(&self) -> Point2i {
        self.base().film.full_resolution()
    }

    fn render_from_camera(&self, r: &Ray, t_max: &mut Option<Float>) -> Ray {
        self.base()
            .camera_transform
            .render_from_camera_ray(r, t_max)
    }

    fn generate_ray_differential(
        &self,
        sample: CameraSample,
        lambda: &SampledWavelengths,
    ) -> Option<CameraRay> {
        let mut central_cam_ray = self.generate_ray(sample, lambda)?;
        let mut rd = RayDifferential::default();
        let mut rx_found = false;
        let mut ry_found = false;

        for eps in [0.05, -0.05] {
            let mut s_shift = sample;
            s_shift.p_film[0] += eps;

            if let Some(rx_cam_ray) = self.generate_ray(s_shift, lambda) {
                rd.rx_origin =
                    central_cam_ray.ray.o + (rx_cam_ray.ray.o - central_cam_ray.ray.o) / eps;
                rd.rx_direction =
                    central_cam_ray.ray.d + (rx_cam_ray.ray.d - central_cam_ray.ray.d) / eps;
                rx_found = true;
                break;
            }
        }

        for eps in [0.05, -0.05] {
            let mut s_shift = sample;
            s_shift.p_film[1] += eps;

            if let Some(ry_cam_ray) = self.generate_ray(s_shift, lambda) {
                rd.ry_origin =
                    central_cam_ray.ray.o + (ry_cam_ray.ray.o - central_cam_ray.ray.o) / eps;
                rd.ry_direction =
                    central_cam_ray.ray.d + (ry_cam_ray.ray.d - central_cam_ray.ray.d) / eps;
                ry_found = true;
                break;
            }
        }

        if rx_found && ry_found {
            central_cam_ray.ray.differential = rd;
        }

        Some(central_cam_ray)
    }

    fn approximate_dp_dxy(
        &self,
        p: Point3f,
        n: Normal3f,
        time: Float,
        samples_per_pixel: i32,
        dpdx: &mut Vector3f,
        dpdy: &mut Vector3f,
    ) {
        let p_camera = self.base().camera_transform.camera_from_render(p, time);
        let p_camera_vec = p_camera - Point3f::new(0., 0., 0.);
        let down_z_from_camera =
            Transform::rotate_from_to(p_camera_vec.normalize(), Vector3f::new(1., 0., 0.));
        let p_down_z = down_z_from_camera.apply_to_point(p_camera);
        let n_down_z = down_z_from_camera.apply_to_normal(
            self.base()
                .camera_transform
                .camera_from_render_normal(n, time),
        );
        let d = n_down_z.z() * p_down_z.z();
        let x_ray = Ray::new(
            Point3f::new(0., 0., 0.) + self.base().min_pos_differential_x,
            Vector3f::new(0., 0., 1.) + self.base().min_dir_differential_x,
            None,
            &Ptr::default(),
        );
        let y_ray = Ray::new(
            Point3f::new(0., 0., 0.) + self.base().min_pos_differential_y,
            Vector3f::new(0., 0., 1.) + self.base().min_dir_differential_y,
            None,
            &Ptr::default(),
        );
        let n_down = Vector3f::from(n_down_z);
        let tx = -(n_down.dot(y_ray.o.into())) / n_down.dot(x_ray.d);
        let ty = -(n_down.dot(x_ray.o.into()) - d) / n_down.dot(y_ray.d);
        let px = x_ray.at(tx);
        let py = y_ray.at(ty);
        let spp_scale = 0.125_f32.max((samples_per_pixel as Float).sqrt());
        *dpdx = spp_scale
            * self.base().camera_transform.render_from_camera_vector(
                down_z_from_camera.apply_inverse_vector(px - p_down_z),
                time,
            );
        *dpdy = spp_scale
            * self.base().camera_transform.render_from_camera_vector(
                down_z_from_camera.apply_inverse_vector(py - p_down_z),
                time,
            );
    }
}
