use crate::{Point3, Ray, Vec3};
use crate::material::{Material};
use crate::aabb::Aabb;
use std::f64::consts::PI;

pub type HittableList = Vec<Box<dyn Hittable + Sync>>;

//#[derive(Debug, Clone, Copy)]
#[derive(Clone, Copy)]
pub struct HitRecord<'material> {
    pub point: Point3,
    pub normal: Vec3,
    pub t: f64,
    pub u: f64,
    pub v: f64,
    pub front_face: bool,
    pub material: &'material Material
}

pub trait Hittable {
    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord>;
    fn bounding_box(&self, time0: f64, time1: f64) -> Option<Aabb>;
}

//#[derive(Clone)]
pub struct Sphere {
    pub center: Point3,
    pub radius: f64,
    pub material: Material
}
impl Sphere {
    pub fn new(center: Point3, radius: f64, material: Material) -> Sphere {
        Sphere { center, radius, material }
    }
    pub fn uv(point: &Point3) -> (f64, f64) {
        // p: a given point on the sphere of radius one, centered at the origin.
        // u: returned value [0,1] of angle around the Y axis from X=-1.
        // v: returned value [0,1] of angle from Y=-1 to Y=+1.
        //     <1 0 0> yields <0.50 0.50>       <-1  0  0> yields <0.00 0.50>
        //     <0 1 0> yields <0.50 1.00>       < 0 -1  0> yields <0.50 0.00>
        //     <0 0 1> yields <0.25 0.50>       < 0  0 -1> yields <0.75 0.50>

        let theta = -(point.y()).acos();
        let phi = -(point.z()).atan2(point.x()) + PI;

        (phi / (2.0 * PI), theta / PI)
    }
}
impl Default for Sphere {
    fn default() -> Self {
        Sphere {
            center: Point3::default(),
            radius: 0.0,
            material: Material::default()
        }
    }
}
impl Hittable for Sphere {
    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let oc = &ray.origin() - &self.center;
        let a = ray.direction().length_squared();
        let half_b = oc.dot(&ray.direction());
        let c = oc.length_squared() - &self.radius * &self.radius;
        let discriminant = half_b * half_b - a * c;
        if discriminant < 0.0 { return None; }

        let sqrtd = discriminant.sqrt();
        let mut root = (-half_b - sqrtd) / a;
        if root < t_min || t_max < root {
            root = (-half_b + sqrtd) / a;
            if root < t_min || t_max < root { return None; }
        }
        let point = ray.at(root);
        let normal = (point - self.center) / self.radius;
        let dot = ray.direction().dot(&normal);
        let front_face = dot < 0.0;
        let (u, v) = Sphere::uv(&normal);
        let normal = if front_face { normal } else { -normal };
        Some(HitRecord {
            point,
            normal,
            t: root,
            u,
            v,
            front_face,
            material: &self.material
        })
    }
    fn bounding_box(&self, _time0: f64, _time1: f64) -> Option<Aabb> {
        let s = Vec3::new(self.radius, self.radius, self.radius);
        Some(Aabb {
            minimum: self.center - s,
            maximum: self.center + s,
        })
    }
}

//#[derive(Clone)]
pub struct MovableSphere {
    pub center0: Point3,
    pub center1: Point3,
    pub radius: f64,
    pub material: Material,
    pub time0: f64,
    pub time1: f64,
}
impl MovableSphere {
    pub fn new(
        center0: Point3,
        center1: Point3,
        radius: f64,
        material: Material,
        time0: f64,
        time1: f64) -> Self {
        MovableSphere { center0, center1, radius, material, time0, time1 }
    }
    pub fn center(&self, time: f64) -> Point3 {
        self.center0 + ((time - self.time0) / (self.time1 - self.time0))*(self.center1 - self.center0)
    }
}
impl Default for MovableSphere {
    fn default() -> Self {
        MovableSphere {
            center0: Point3::default(),
            center1: Point3::default(),
            radius: 0.0,
            material: Material::default(),
            time0: 0.0,
            time1: 0.0
        }
    }
}

impl Hittable for MovableSphere {
    fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
        let oc = &ray.origin() - &self.center(ray.time());
        let a = ray.direction().length_squared();
        let half_b = oc.dot(&ray.direction());
        let c = oc.length_squared() - &self.radius * &self.radius;
        let discriminant = half_b * half_b - a * c;
        if discriminant < 0.0 { return None; }

        let sqrtd = discriminant.sqrt();
        let mut root = (-half_b - sqrtd) / a;
        if root < t_min || t_max < root {
            root = (-half_b + sqrtd) / a;
            if root < t_min || t_max < root { return None; }
        }
        let point = ray.at(root);
        let normal = (point - self.center(ray.time())) / self.radius;
        let dot = ray.direction().dot(&normal);
        let front_face = dot < 0.0;
        let (u, v) = Sphere::uv(&normal);
        let normal = if front_face { normal } else { -normal };
        Some(HitRecord {
            point,
            normal,
            t: root,
            u,
            v,
            front_face,
            material: &self.material,
        })
    }
    fn bounding_box(&self, time0: f64, time1: f64) -> Option<Aabb> {
        let s = Vec3::new(self.radius, self.radius, self.radius);
        let center0 = self.center(time0);
        let center1 = self.center(time1);
        Some(Aabb {
            minimum: center0 - s,
            maximum: center0 + s,
        }.surrounding(&Aabb {
            minimum: center1 - s,
            maximum: center1 + s
        }))
    }
}