use crate::{affine_transform::AffineTransform, prelude::*};

pub trait Camera<R: Rng> {
    fn forward(&self, x: u32, y: u32, rng: &mut R) -> Ray;
    fn width(&self) -> u32;
    fn height(&self) -> u32;
}

#[derive(Debug)]
pub struct BasicCamera {
    width: u32,
    height: u32,
    pos: Pos3,
    dir: Dir3,
    h: Dir3,
    v: Dir3,
}

impl BasicCamera {
    pub fn new(
        width: u32,
        height: u32,
        pos: Pos3,
        dir: Dir3,
        up: Dir3,
        horizontal_fov: Float,
    ) -> Self {
        assert!(horizontal_fov < FloatConsts::PI);
        let dir = dir.normalize();
        let up = up.normalize();
        let l = Float::sin(0.5 * horizontal_fov);
        let h = l * Dir3::cross(dir, up);
        let v = l * ((height as Float) / (width as Float)) * Dir3::cross(dir, h.normalize());
        Self {
            width,
            height,
            pos,
            dir,
            h,
            v,
        }
    }

    pub fn from_affine_transform(
        width: u32,
        height: u32,
        tm: AffineTransform,
        horizontal_fov: Float,
    ) -> Self {
        assert!(horizontal_fov < FloatConsts::PI);

        let l = Float::sin(0.5 * horizontal_fov);

        Self {
            width,
            height,
            pos: Pos3::new(tm.get(3, 0), tm.get(3, 1), tm.get(3, 2)),
            dir: Dir3::new(tm.get(2, 0), tm.get(2, 1), tm.get(2, 2)),
            h: l * Dir3::new(tm.get(0, 0), tm.get(0, 1), tm.get(0, 2)),
            v: l * ((height as Float) / (width as Float))
                * Dir3::new(tm.get(1, 0), tm.get(1, 1), tm.get(1, 2)),
        }
    }
    pub fn from_look_at(
        width: u32,
        height: u32,
        pos: Pos3,
        look_at: Pos3,
        up: Dir3,
        horizontal_fov: Float,
    ) -> Self {
        Self::new(width, height, pos, look_at - pos, up, horizontal_fov)
    }
}

impl<R: Rng> Camera<R> for BasicCamera {
    fn forward(&self, x: u32, y: u32, rng: &mut R) -> Ray {
        // normalize x and y to -0.5 to 0.5

        let x_float = ((x as Float + rng.gen::<Float>()) / (self.width as Float)) - 0.5;
        let y_float = ((y as Float + rng.gen::<Float>()) / (self.height as Float)) - 0.5;

        let dir = self.dir + x_float * self.h + y_float * self.v;

        Ray::new(self.pos, dir.normalize(), 0.0)
    }

    fn width(&self) -> u32 {
        self.width
    }

    fn height(&self) -> u32 {
        self.height
    }
}
