use crate::core::camera::{CameraBase, CameraRay, CameraTrait, CameraTransform};
use crate::core::film::Film;
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::Transform;
use crate::utils::sampling::sample_uniform_disk_concentric;

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct OrthographicCamera {
    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,
}

#[cfg(not(target_os = "cuda"))]
impl OrthographicCamera {
    pub fn new(
        base: CameraBase,
        screen_window: Bounds2f,
        lens_radius: Float,
        focal_distance: Float,
    ) -> Self {
        let ndc_from_screen: Transform = 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 mut base_ortho = base;
        let film = base.film;
        if film.is_null() {
            panic!("Camera must have a film");
        }

        let raster_from_ndc = Transform::scale(
            film.full_resolution().x() as Float,
            -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 screen_from_camera = Transform::orthographic(0., 1.);
        let camera_from_raster = screen_from_camera.inverse() * screen_from_raster;
        let dx_camera = camera_from_raster.apply_to_vector(Vector3f::new(1., 0., 0.));
        let dy_camera = camera_from_raster.apply_to_vector(Vector3f::new(0., 1., 0.));
        base_ortho.min_dir_differential_x = Vector3f::new(0., 0., 0.);
        base_ortho.min_dir_differential_y = Vector3f::new(0., 0., 0.);
        base_ortho.min_pos_differential_x = dx_camera;
        base_ortho.min_pos_differential_y = dy_camera;
        Self {
            base: base_ortho,
            screen_from_camera,
            camera_from_raster,
            raster_from_screen,
            screen_from_raster,
            lens_radius,
            focal_distance,
            dx_camera,
            dy_camera,
        }
    }
}

impl CameraTrait for OrthographicCamera {
    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 mut ray = Ray::new(
            p_camera,
            Vector3f::new(0., 0., 1.),
            Some(self.sample_time(sample.time)),
            &self.base().medium,
        );
        if self.lens_radius > 0. {
            let p_lens_vec =
                self.lens_radius * Vector2f::from(sample_uniform_disk_concentric(sample.p_lens));
            let p_lens = Point2f::from(p_lens_vec);

            let ft = self.focal_distance / ray.d.z();
            let p_focus = ray.at(ft);

            ray.o = Point3f::new(p_lens.x(), p_lens.y(), 0.);
            ray.d = (p_focus - ray.o).normalize();
        }

        let camera_ray = self.render_from_camera(&ray, &mut None);

        Some(CameraRay {
            ray: camera_ray,
            weight: SampledSpectrum::default(),
        })
    }

    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();
        if self.lens_radius > 0.0 {
            let mut sample_x = sample;
            sample_x.p_film[0] += 1.0;
            let rx = self.generate_ray(sample_x, lambda)?;

            let mut sample_y = sample;
            sample_y.p_film[1] += 1.0;
            let ry = self.generate_ray(sample_y, lambda)?;

            rd.rx_origin = rx.ray.o;
            rd.ry_origin = ry.ray.o;
            rd.rx_direction = rx.ray.d;
            rd.ry_direction = ry.ray.d;
        } else {
            let time = self.sample_time(sample.time);
            let world_dx = self
                .base
                .camera_transform
                .render_from_camera_vector(self.dx_camera, time);
            let world_dy = self
                .base
                .camera_transform
                .render_from_camera_vector(self.dy_camera, time);

            rd.rx_origin = central_cam_ray.ray.o + world_dx;
            rd.ry_origin = central_cam_ray.ray.o + world_dy;

            rd.rx_direction = central_cam_ray.ray.d;
            rd.ry_direction = central_cam_ray.ray.d;
        }
        central_cam_ray.ray.differential = rd;
        Some(central_cam_ray)
    }
}
