use crate::{canvas::Canvas, matrices::Matrix4x4, rays::Ray, tuples::Tuple, world::World, BLACK};

pub struct Camera {
    hsize: usize,
    vsize: usize,
    field_of_view: f32,
    pub transform: Matrix4x4,
    pixel_size: f32,
    half_width: f32,
    half_height: f32,
}

impl Camera {
    /// Create a camera with a canvas of pixel hsize (height) and vsize (width)
    /// with the given field of view (in radians).
    ///
    /// # Examples
    /// ```
    /// use std::f32::consts::PI;
    ///
    /// use rtchallenge::{camera::Camera, matrices::Matrix4x4};
    ///
    /// let hsize = 160;
    /// let vsize = 120;
    /// let field_of_view = PI / 2.;
    /// let c = Camera::new(hsize, vsize, field_of_view);
    /// assert_eq!(c.hsize(), 160);
    /// assert_eq!(c.vsize(), 120);
    /// assert_eq!(c.transform(), Matrix4x4::identity());
    ///
    /// // Pixel size for a horizontal canvas.
    /// let c = Camera::new(200, 150, PI / 2.);
    /// assert_eq!(c.pixel_size(), 0.01);
    ///
    /// // Pixel size for a horizontal canvas.
    /// let c = Camera::new(150, 200, PI / 2.);
    /// assert_eq!(c.pixel_size(), 0.01);
    /// ```
    pub fn new(hsize: usize, vsize: usize, field_of_view: f32) -> Camera {
        let half_view = (field_of_view / 2.).tan();
        let aspect = hsize as f32 / vsize as f32;
        let (half_width, half_height) = if aspect >= 1. {
            (half_view, half_view / aspect)
        } else {
            (half_view * aspect, half_view)
        };
        let pixel_size = 2. * half_width / hsize as f32;
        Camera {
            hsize,
            vsize,
            field_of_view,
            transform: Matrix4x4::identity(),
            pixel_size,
            half_height,
            half_width,
        }
    }
    pub fn hsize(&self) -> usize {
        self.hsize
    }
    pub fn vsize(&self) -> usize {
        self.vsize
    }
    pub fn field_of_view(&self) -> f32 {
        self.field_of_view
    }
    pub fn transform(&self) -> Matrix4x4 {
        self.transform
    }
    pub fn pixel_size(&self) -> f32 {
        self.pixel_size
    }

    /// Calculate ray that starts at the camera and passes through the (x,y)
    /// pixel on the canvas.
    ///
    /// # Examples
    /// ```
    /// use std::f32::consts::PI;
    ///
    /// use rtchallenge::{camera::Camera, matrices::Matrix4x4, tuples::Tuple};
    ///
    /// // Constructing a ray through the center of the canvas.
    /// let c = Camera::new(201, 101, PI / 2.);
    /// let r = c.ray_for_pixel(100, 50);
    /// assert_eq!(r.origin, Tuple::point(0., 0., 0.));
    /// assert_eq!(r.direction, Tuple::vector(0., 0., -1.));
    ///
    /// // Constructing a ray through the corner of the canvas.
    /// let c = Camera::new(201, 101, PI / 2.);
    /// let r = c.ray_for_pixel(0, 0);
    /// assert_eq!(r.origin, Tuple::point(0., 0., 0.));
    /// assert_eq!(r.direction, Tuple::vector(0.66519, 0.33259, -0.66851));
    ///
    /// // Constructing a ray when the camera is transformed.
    /// let mut c = Camera::new(201, 101, PI / 2.);
    /// c.transform = Matrix4x4::rotation_y(PI / 4.) * Matrix4x4::translation(0., -2., 5.);
    /// let r = c.ray_for_pixel(100, 50);
    /// assert_eq!(r.origin, Tuple::point(0., 2., -5.));
    /// assert_eq!(
    ///     r.direction,
    ///     Tuple::vector(2_f32.sqrt() / 2., 0., -2_f32.sqrt() / 2.)
    /// );
    /// ```
    pub fn ray_for_pixel(&self, px: usize, py: usize) -> Ray {
        // The offset from the edge of the canvas to the pixel's corner.
        let xoffset = (px as f32 + 0.5) * self.pixel_size;
        let yoffset = (py as f32 + 0.5) * self.pixel_size;

        // The untransformed coordinates of the pixle in world space.
        // (Remember that the camera looks toward -z, so +x is to the left.)
        let world_x = self.half_width - xoffset;
        let world_y = self.half_height - yoffset;

        // Using the camera matrix, transofmrm the canvas point and the origin,
        // and then compute the ray's direction vector.
        // (Remember that the canves is at z>=-1).
        let pixel = self.transform.inverse() * Tuple::point(world_x, world_y, -1.);
        let origin = self.transform.inverse() * Tuple::point(0., 0., 0.);
        let direction = (pixel - origin).normalize();

        Ray::new(origin, direction)
    }

    /// Use camera to render an image of the given world.
    ///
    /// # Examples
    /// ```
    /// use std::f32::consts::PI;
    ///
    /// use rtchallenge::{
    ///     camera::Camera,
    ///     transformations::view_transform,
    ///     tuples::{Color, Tuple},
    ///     world::World,
    /// };
    ///
    /// // Rendering a world with a camera.
    /// let w = World::test_world();
    /// let mut c = Camera::new(11, 11, PI / 2.);
    /// let from = Tuple::point(0., 0., -5.);
    /// let to = Tuple::point(0., 0., 0.);
    /// let up = Tuple::vector(0., 1., 0.);
    /// c.transform = view_transform(from, to, up);
    /// let image = c.render(&w);
    /// assert_eq!(image.get(5, 5), Color::new(0.38066, 0.47583, 0.2855));
    /// ```
    pub fn render(&self, w: &World) -> Canvas {
        let mut image = Canvas::new(self.hsize, self.vsize, BLACK);
        for y in 0..self.vsize {
            for x in 0..self.hsize {
                let ray = self.ray_for_pixel(x, y);
                let color = w.color_at(&ray);
                image.set(x, y, color);
            }
        }
        image
    }
}
