use std::f32::consts::PI;

use crate::aabb::AABB;
use crate::hitable::Hit;
use crate::hitable::HitRecord;
use crate::material::Material;
use crate::ray::Ray;
use crate::vec3::dot;
use crate::vec3::Vec3;

pub struct Sphere<M>
where
    M: Material,
{
    center: Vec3,
    radius: f32,
    material: M,
}

pub fn get_sphere_uv(p: Vec3) -> (f32, f32) {
    let phi = p.z.atan2(p.x);
    let theta = p.y.asin();
    let u = 1. - (phi + PI) / (2. * PI);
    let v = (theta + PI / 2.) / PI;
    (u, v)
}

impl<M> Sphere<M>
where
    M: Material,
{
    pub fn new<V>(center: V, radius: f32, material: M) -> Sphere<M>
    where
        V: Into<Vec3>,
    {
        Sphere {
            center: center.into(),
            radius,
            material,
        }
    }
}

impl<M> Hit for Sphere<M>
where
    M: Material,
{
    fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
        let oc = r.origin - self.center;
        let a = dot(r.direction, r.direction);
        let b = dot(oc, r.direction);
        let c = dot(oc, oc) - self.radius * self.radius;
        let discriminant = b * b - a * c;
        if discriminant > 0. {
            let temp = (-b - (b * b - a * c).sqrt()) / a;
            if temp < t_max && temp > t_min {
                let point = r.point_at_parameter(temp);
                let uv = get_sphere_uv((point - self.center) / self.radius);
                return Some(HitRecord {
                    t: temp,
                    uv,
                    p: point,
                    normal: (point - self.center) / self.radius,
                    material: &self.material,
                });
            }
            let temp = (-b + (b * b - a * c).sqrt()) / a;
            if temp < t_max && temp > t_min {
                let point = r.point_at_parameter(temp);
                let uv = get_sphere_uv((point - self.center) / self.radius);
                return Some(HitRecord {
                    t: temp,
                    uv,
                    p: point,
                    normal: (point - self.center) / self.radius,
                    material: &self.material,
                });
            }
        }
        None
    }

    fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
        Some(AABB::new(
            self.center - Vec3::new(self.radius, self.radius, self.radius),
            self.center + Vec3::new(self.radius, self.radius, self.radius),
        ))
    }
}
