use crate::core::camera::{CameraBase, CameraRay, CameraTrait, CameraTransform};
use crate::core::film::Film;
use crate::core::filter::FilterTrait;
use crate::core::geometry::{
    Bounds2f, Point2f, Point3f, Ray, RayDifferential, Vector2f, Vector3f, VectorLike,
};
use crate::core::medium::Medium;
use crate::core::pbrt::Float;
use crate::core::sampler::CameraSample;
use crate::spectra::{SampledSpectrum, SampledWavelengths};
use crate::utils::sampling::sample_uniform_disk_concentric;
use crate::utils::transform::Transform;

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PerspectiveCamera {
    pub base: CameraBase,
    pub screen_from_camera: Transform,
    pub camera_from_raster: Transform,
    pub raster_from_screen: Transform,
    pub screen_from_raster: Transform,
    pub lens_radius: Float,
    pub focal_distance: Float,
    pub dx_camera: Vector3f,
    pub dy_camera: Vector3f,
    pub cos_total_width: Float,
}

#[cfg(not(target_os = "cuda"))]
impl PerspectiveCamera {
    pub fn new(
        base: CameraBase,
        fov: Float,
        screen_window: Bounds2f,
        lens_radius: Float,
        focal_distance: Float,
    ) -> Self {
        let screen_from_camera =
            Transform::perspective(fov, 1e-2, 1000.).expect("Could not create perspective");
        let ndc_from_screen = Transform::scale(
            1. / (screen_window.p_max.x() - screen_window.p_min.x()),
            1. / (screen_window.p_max.y() - screen_window.p_min.y()),
            1.,
        ) * Transform::translate(Vector3f::new(
            -screen_window.p_min.x(),
            -screen_window.p_max.y(),
            0.,
        ));
        let raster_from_ndc = Transform::scale(
            base.film.full_resolution().x() as Float,
            -base.film.full_resolution().y() as Float,
            1.,
        );
        let raster_from_screen = raster_from_ndc * ndc_from_screen;
        let screen_from_raster = raster_from_screen.inverse();
        let camera_from_raster = screen_from_camera.inverse() * screen_from_raster;
        let dx_camera = camera_from_raster.apply_to_point(Point3f::new(1., 0., 0.))
            - camera_from_raster.apply_to_point(Point3f::new(0., 0., 0.));
        let dy_camera = camera_from_raster.apply_to_point(Point3f::new(0., 1., 0.))
            - camera_from_raster.apply_to_point(Point3f::new(0., 0., 0.));
        let radius = base.film.get_filter().radius();
        let p_corner = Point3f::new(-radius.x(), -radius.y(), 0.);
        let w_corner_camera =
            (camera_from_raster.apply_to_point(p_corner) - Point3f::new(0., 0., 0.)).normalize();
        let cos_total_width = w_corner_camera.z();

        Self {
            base,
            screen_from_camera,
            camera_from_raster,
            raster_from_screen,
            screen_from_raster,
            lens_radius,
            focal_distance,
            dx_camera,
            dy_camera,
            cos_total_width,
        }
    }
}

impl CameraTrait for PerspectiveCamera {
    fn base(&self) -> &CameraBase {
        &self.base
    }

    fn generate_ray(
        &self,
        sample: CameraSample,
        _lambda: &SampledWavelengths,
    ) -> Option<CameraRay> {
        // Compute raster and camera sample positions
        let p_film = Point3f::new(sample.p_film.x(), sample.p_film.y(), 0.);
        let p_camera = self.camera_from_raster.apply_to_point(p_film);
        let p_vector = p_camera - Point3f::new(0., 0., 0.);

        let mut r = Ray::new(
            Point3f::new(0., 0., 0.),
            p_vector.normalize(),
            Some(self.sample_time(sample.time)),
            &*self.base().medium,
        );
        // Modify ray for depth of field
        if self.lens_radius > 0. {
            // Sample point on lens
            let p_lens =
                self.lens_radius * Vector2f::from(sample_uniform_disk_concentric(sample.p_lens));

            // Compute point on plane of focus
            let ft = self.focal_distance / r.d.z();
            let p_focus = r.at(ft);

            // Update ray for effect of lens
            r.o = Point3f::new(p_lens.x(), p_lens.y(), 0.);
            r.d = (p_focus - r.o).normalize();
        }

        let ray = self.render_from_camera(&r, &mut None);
        Some(CameraRay {
            ray,
            weight: SampledSpectrum::default(),
        })
    }
}
