use super::{Arc, Color, Material, Metal, Point3, Ray, Vec3};

pub struct HitRecord {
    pub p: Point3,    // hit location
    pub normal: Vec3, // normal of hit location
    pub mat_ptr: Arc<dyn Material>,
    pub t: f64,           // ray steps (distance) from camera to hit
    pub front_face: bool, // whether object was hitted from front or back
}

pub struct Sphere {
    center: Point3,
    radius: f64,
    pub mat_ptr: Arc<dyn Material>,
}

pub struct Triangle {
    a: Point3,
    b: Point3,
    c: Point3,
    normal: Vec3,
    pub mat_ptr: Arc<dyn Material>,
}

pub trait Hittable: Sync + Send {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64, rec: &mut HitRecord) -> bool;
}

impl HitRecord {
    fn set_face_normal(&mut self, r: &Ray, outward_normal: Vec3) {
        self.front_face = true; //Vec3::dot(r.direction(), outward_normal) < 0.0;
        if self.front_face {
            self.normal = outward_normal;
        } else {
            self.normal = (-1.0) * outward_normal;
        }
    }

    pub fn new(p: Point3, normal: Vec3, m: Arc<dyn Material>, t: f64, front_face: bool) -> Self {
        HitRecord {
            p: p,
            normal: normal,
            mat_ptr: m,
            t: t,
            front_face: front_face,
        }
    }

    pub fn empty() -> Self {
        Self::new(
            Point3::null(),
            Vec3::null(),
            Arc::new(Metal::new(&Color::null(), 0.0)),
            0.0,
            false,
        )
    }
}

impl Sphere {
    pub fn new(cen: Point3, r: f64, m: Arc<dyn Material>) -> Self {
        Sphere {
            center: cen,
            radius: r,
            mat_ptr: m,
        }
    }
}

impl Hittable for Sphere {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64, rec: &mut HitRecord) -> bool {
        let oc = r.origin() - self.center;
        let a = r.direction().length_squared(); // gleiches Ergebnis wie Skalarprodukt
        let half_b = Vec3::dot(oc, r.direction());
        let c = oc.length_squared() - self.radius * self.radius;

        let discriminant = half_b * half_b - a * c;
        if discriminant < 0.0 {
            return false;
        }

        let sqrtd = discriminant.sqrt();

        let mut root = (-half_b - sqrtd) / a;
        let normal = (r.at(root) - self.center) / self.radius;
        if root < t_min || t_max < root || Vec3::dot(normal, r.direction()) > 0.0 {
            root = (-half_b + sqrtd) / a;
            let normal = (r.at(root) - self.center) / self.radius;
            if root < t_min || t_max < root || Vec3::dot(normal, r.direction()) > 0.0 {
                return false;
            }
        }

        rec.t = root;
        rec.p = r.at(rec.t);
        let outward_normal = (rec.p - self.center) / self.radius;
        rec.set_face_normal(r, outward_normal);
        rec.mat_ptr = self.mat_ptr.clone();

        return true;
    }
}

impl Triangle {
    pub fn new(a: Point3, b: Point3, c: Point3, normal: Vec3, m: Arc<dyn Material>) -> Self {
        Triangle {
            a,
            b,
            c,
            normal,
            mat_ptr: m,
        }
    }
}

impl Hittable for Triangle {
    fn hit(&self, r: &Ray, t_min: f64, t_max: f64, rec: &mut HitRecord) -> bool {
        let epsilon = 0.0000001;

        let a_to_b = self.b - self.a;
        let a_to_c = self.c - self.a;

        let h = Vec3::cross(r.direction(), a_to_c);

        let a = Vec3::dot(a_to_b, h);

        if a > -epsilon && a < epsilon {
            // ray is parallel to triangle
            return false;
        }

        let f = 1.0 / a;

        let a_to_origin = r.origin() - self.a;

        let u = Vec3::dot(a_to_origin, h) * f;

        if u < 0.0 || u > 1.0 {
            //check if outside of triangle
            return false;
        }

        let q = Vec3::cross(a_to_origin, a_to_b);

        let v = Vec3::dot(r.direction(), q) * f;

        if v < 0.0 || u + v > 1.0 {
            // intersection outside of triangle
            return false;
        }

        let t = Vec3::dot(a_to_c, q) * f; // where is intersection on ray?

        if t <= epsilon {
            // line intersection but no ray intersection
            return false;
        }

        // ray intersection
        rec.t = t;
        rec.p = r.at(t);
        rec.mat_ptr = self.mat_ptr.clone();

        rec.normal = self.normal;
        return true;

        /* old normal calculation code if normal is not known

        // triangle normal alg from https://stackoverflow.com/questions/19350792/calculate-normal-of-a-single-triangle-in-3d-space
        let normal_to_camera = Vec3::new(
            a_to_b.y() * a_to_c.z() - a_to_b.z() * a_to_c.y(),
            a_to_b.z() * a_to_c.x() - a_to_b.x() * a_to_c.z(),
            a_to_b.x() * a_to_c.y() - a_to_b.y() * a_to_c.x(),
        );
        let unit_normal = normal_to_camera / normal_to_camera.length();

        println!("From obj: {},{},{} - calculated: {},{},{}", self.normal.x(),self.normal.y(),self.normal.z(),unit_normal.x(),unit_normal.y(),unit_normal.z());


        if Vec3::dot(unit_normal, r.direction()) >= 0.0 {
            // normal towards camera
            rec.set_face_normal(r, unit_normal);
        } else {
            // normal not towards camera, has to be flipped
            rec.set_face_normal(r, -1.0 * unit_normal);
        }

        return true;
        */
    }
}
