use glam::{Mat4, Vec3, Vec4};

use crate::components::TreeInstancesComponent;
use crate::entity::EntityHandle;
use crate::loaders::mesh::Mesh;
use crate::world::World;

pub struct Ray
{
    pub origin: Vec3,
    pub direction: Vec3,
}

impl Ray
{
    pub fn from_screen_position(
        screen_x: f32,
        screen_y: f32,
        screen_width: u32,
        screen_height: u32,
        view: &Mat4,
        projection: &Mat4,
    ) -> Self
    {
        let ndc_x = (2.0 * screen_x) / screen_width as f32 - 1.0;
        let ndc_y = 1.0 - (2.0 * screen_y) / screen_height as f32;

        let clip_coords = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);

        let inv_projection = projection.inverse();
        let inv_view = view.inverse();

        let eye_coords = inv_projection * clip_coords;
        let eye_coords = Vec4::new(eye_coords.x, eye_coords.y, -1.0, 0.0);

        let world_coords = inv_view * eye_coords;
        let direction = Vec3::new(world_coords.x, world_coords.y, world_coords.z).normalize();

        let origin = inv_view.col(3).truncate();

        Ray { origin, direction }
    }

    pub fn intersects_aabb(&self, min: Vec3, max: Vec3) -> Option<f32>
    {
        let inv_dir = Vec3::new(
            1.0 / self.direction.x,
            1.0 / self.direction.y,
            1.0 / self.direction.z,
        );

        let t1 = (min - self.origin) * inv_dir;
        let t2 = (max - self.origin) * inv_dir;

        let tmin = t1.min(t2);
        let tmax = t1.max(t2);

        let tenter = tmin.x.max(tmin.y).max(tmin.z);
        let texit = tmax.x.min(tmax.y).min(tmax.z);

        if tenter <= texit && texit >= 0.0
        {
            Some(tenter.max(0.0))
        }
        else
        {
            None
        }
    }
}

fn transform_aabb(aabb_min: Vec3, aabb_max: Vec3, model: &Mat4) -> (Vec3, Vec3)
{
    let corners = [
        Vec3::new(aabb_min.x, aabb_min.y, aabb_min.z),
        Vec3::new(aabb_max.x, aabb_min.y, aabb_min.z),
        Vec3::new(aabb_min.x, aabb_max.y, aabb_min.z),
        Vec3::new(aabb_max.x, aabb_max.y, aabb_min.z),
        Vec3::new(aabb_min.x, aabb_min.y, aabb_max.z),
        Vec3::new(aabb_max.x, aabb_min.y, aabb_max.z),
        Vec3::new(aabb_min.x, aabb_max.y, aabb_max.z),
        Vec3::new(aabb_max.x, aabb_max.y, aabb_max.z),
    ];

    let mut world_min = Vec3::splat(f32::MAX);
    let mut world_max = Vec3::splat(f32::MIN);
    for corner in &corners
    {
        let world_corner = model.transform_point3(*corner);
        world_min = world_min.min(world_corner);
        world_max = world_max.max(world_corner);
    }
    (world_min, world_max)
}

fn intersect_triangle(origin: Vec3, dir: Vec3, v0: Vec3, v1: Vec3, v2: Vec3) -> Option<f32>
{
    let edge1 = v1 - v0;
    let edge2 = v2 - v0;
    let h = dir.cross(edge2);
    let a = edge1.dot(h);
    if a.abs() < 1e-7
    {
        return None;
    }
    let f = 1.0 / a;
    let s = origin - v0;
    let u = f * s.dot(h);
    if !(0.0..=1.0).contains(&u)
    {
        return None;
    }
    let q = s.cross(edge1);
    let v = f * dir.dot(q);
    if v < 0.0 || u + v > 1.0
    {
        return None;
    }
    let t = f * edge2.dot(q);
    if t > 1e-7
    {
        Some(t)
    }
    else
    {
        None
    }
}

/// Ray–mesh triangle intersection in local space, returning world-space distance.
///
/// Transforms the ray into local mesh space, tests every triangle with
/// Möller–Trumbore, then maps the closest local hit back to a world-space
/// distance so results can be compared across entities.
fn intersect_mesh_triangles(ray: &Ray, mesh: &Mesh, model: &Mat4) -> Option<f32>
{
    if mesh.cpu_indices.is_empty()
    {
        return None;
    }

    let inv_model = model.inverse();
    let local_origin = inv_model.transform_point3(ray.origin);
    let local_dir = inv_model.transform_vector3(ray.direction);

    let mut closest_t = f32::MAX;
    let mut hit = false;

    for tri in mesh.cpu_indices.chunks_exact(3)
    {
        let v0 = Vec3::from(mesh.cpu_positions[tri[0] as usize]);
        let v1 = Vec3::from(mesh.cpu_positions[tri[1] as usize]);
        let v2 = Vec3::from(mesh.cpu_positions[tri[2] as usize]);

        if let Some(t) = intersect_triangle(local_origin, local_dir, v0, v1, v2)
        {
            if t < closest_t
            {
                closest_t = t;
                hit = true;
            }
        }
    }

    if hit
    {
        let local_hit = local_origin + local_dir * closest_t;
        let world_hit = model.transform_point3(local_hit);
        Some((world_hit - ray.origin).length())
    }
    else
    {
        None
    }
}

fn intersect_instances(
    ray: &Ray,
    mesh: &Mesh,
    tree_instances: &TreeInstancesComponent,
) -> Option<f32>
{
    let mut closest: Option<f32> = None;

    for instance in &tree_instances.instances
    {
        let instance_model = Mat4::from_scale_rotation_translation(
            instance.scale,
            instance.rotation,
            instance.position,
        );
        let (world_min, world_max) = transform_aabb(mesh.aabb_min, mesh.aabb_max, &instance_model);

        if let Some(d) = ray.intersects_aabb(world_min, world_max)
        {
            if closest.map_or(true, |c| d < c)
            {
                closest = Some(d);
            }
        }
    }

    closest
}

pub fn pick_entity(ray: &Ray, world: &World) -> Option<EntityHandle>
{
    let mut closest_entity = None;
    let mut closest_distance = f32::MAX;

    for (&entity, mesh_component) in &world.meshes.components
    {
        let transform = match world.transforms.get(entity)
        {
            Some(t) => t,
            None => continue,
        };

        let model = transform.to_matrix();

        let distance = if let Some(tree_instances) = world.tree_instances.get(entity)
        {
            intersect_instances(ray, &mesh_component.mesh, tree_instances)
        }
        else
        {
            let (world_min, world_max) = transform_aabb(
                mesh_component.mesh.aabb_min,
                mesh_component.mesh.aabb_max,
                &model,
            );
            if ray.intersects_aabb(world_min, world_max).is_none()
            {
                continue;
            }
            intersect_mesh_triangles(ray, &mesh_component.mesh, &model)
        };

        if let Some(d) = distance
        {
            if d < closest_distance
            {
                closest_distance = d;
                closest_entity = Some(entity);
            }
        }
    }

    closest_entity
}
