use bevy::render::{
    mesh::{MeshVertexAttribute, VertexAttributeValues},
    render_resource::VertexFormat,
};

use crate::prelude::*;

/// Hit data for 2d sprites
#[derive(Debug)]
pub(crate) struct Hit2d {
    _point: Vec2,
}

/// Hit data for 3d objects in Global (not Local) space and the distance from the Camera
#[derive(Debug)]
pub(crate) struct Hit3d {
    distance: f32,
    _point: Vec3,
}

/// A 3D Triangle used for ray-intersection tests
#[derive(Debug)]
struct Triangle {
    v0: Vec3,
    v1: Vec3,
    v2: Vec3,
}

/// Helper functions for ray-intersect testing
impl Triangle {
    fn normal(&self) -> Vec3 {
        (self.edge_a()).cross(self.edge_b())
    }

    fn edge_a(&self) -> Vec3 {
        self.v1 - self.v0
    }

    fn edge_b(&self) -> Vec3 {
        self.v2 - self.v0
    }

    fn edge0(&self) -> Vec3 {
        self.v1 - self.v0
    }

    fn edge1(&self) -> Vec3 {
        self.v2 - self.v1
    }

    fn edge2(&self) -> Vec3 {
        self.v0 - self.v2
    }
}

/// Heavily synthesized from these two resources:
/// * Textbook: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/ray-triangle-intersection-geometric-solution.html
/// * Example: https://github.com/aevyrie/bevy_mod_raycast/blob/435d8ef100738797161ac3a9b910ea346a4ed6e6/src/raycast.rs#L43
pub(crate) fn intersects3d(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option<Hit3d> {
    let attr = MeshVertexAttribute::new("Vertex_Position", 0, VertexFormat::Float32x3);
    if let Some(verts) = mesh.attribute(attr) {
        if let Some(idxs) = mesh.indices() {
            match verts {
                VertexAttributeValues::Float32x3(vals) => {
                    idxs.iter()
                        .array_chunks::<3>()
                        // Convert arrays to vec3
                        .map(|[x, y, z]| {
                            [
                                Vec3::from_array(vals[x]),
                                Vec3::from_array(vals[y]),
                                Vec3::from_array(vals[z]),
                            ]
                        })
                        // Transform each point by the global transform
                        .map(|[a, b, c]| {
                            [
                                gt.transform_point(a),
                                gt.transform_point(b),
                                gt.transform_point(c),
                            ]
                        })
                        // Collect everything into a triangle for easy operations
                        .map(|[v0, v1, v2]| Triangle { v0, v1, v2 })
                        .filter_map(|triangle| {
                            // Calculate the distance this ray hits the plane normal to the tri
                            if let Some(d) = ray.intersect_plane(triangle.v0, triangle.normal()) {
                                // Calculate the point on that plane which intersects
                                let p = ray.get_point(d);
                                // Inside out test
                                let hit = {
                                    // Determine if p is w/in edge0
                                    let c0 = triangle.edge0().cross(p - triangle.v0);
                                    // Determine if p is w/in edge1
                                    let c1 = triangle.edge1().cross(p - triangle.v1);
                                    // Determine if p is w/in edge2
                                    let c2 = triangle.edge2().cross(p - triangle.v2);

                                    // Check all three at once
                                    triangle.normal().dot(c0) > 0.0
                                        && triangle.normal().dot(c1) > 0.0
                                        && triangle.normal().dot(c2) > 0.0
                                };
                                hit.then_some(Hit3d {
                                    distance: d,
                                    _point: p,
                                })
                            } else {
                                None
                            }
                        })
                        .min_by(|a, b| a.distance.total_cmp(&b.distance))
                }
                _ => None,
            }
        } else {
            None
        }
    } else {
        None
    }
}
