use nalgebra_glm::{Vec3, vec3};
use serde::{Deserialize, Serialize};

use crate::ecs::world::Entity;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Lattice {
    pub base_points: Vec<Vec3>,
    pub displacements: Vec<Vec3>,
    pub dimensions: [usize; 3],
    pub bounds_min: Vec3,
    pub bounds_max: Vec3,
    pub falloff: f32,
    pub version: u32,
}

impl Lattice {
    pub fn new(bounds_min: Vec3, bounds_max: Vec3, dimensions: [usize; 3]) -> Self {
        let [nx, ny, nz] = dimensions;
        let point_count = nx * ny * nz;

        let mut base_points = Vec::with_capacity(point_count);
        let size = bounds_max - bounds_min;

        for z in 0..nz {
            for y in 0..ny {
                for x in 0..nx {
                    let u = if nx > 1 {
                        x as f32 / (nx - 1) as f32
                    } else {
                        0.5
                    };
                    let v = if ny > 1 {
                        y as f32 / (ny - 1) as f32
                    } else {
                        0.5
                    };
                    let w = if nz > 1 {
                        z as f32 / (nz - 1) as f32
                    } else {
                        0.5
                    };

                    let position = bounds_min + vec3(u * size.x, v * size.y, w * size.z);
                    base_points.push(position);
                }
            }
        }

        let displacements = vec![Vec3::zeros(); point_count];

        Self {
            base_points,
            displacements,
            dimensions,
            bounds_min,
            bounds_max,
            falloff: 0.0,
            version: 0,
        }
    }

    pub fn with_falloff(mut self, falloff: f32) -> Self {
        self.falloff = falloff;
        self
    }

    pub fn get_index(&self, x: usize, y: usize, z: usize) -> usize {
        let [nx, ny, _nz] = self.dimensions;
        z * (nx * ny) + y * nx + x
    }

    pub fn set_displacement(&mut self, x: usize, y: usize, z: usize, displacement: Vec3) {
        let index = self.get_index(x, y, z);
        if index < self.displacements.len() {
            self.displacements[index] = displacement;
            self.version = self.version.wrapping_add(1);
        }
    }

    pub fn get_displacement(&self, x: usize, y: usize, z: usize) -> Vec3 {
        let index = self.get_index(x, y, z);
        self.displacements
            .get(index)
            .copied()
            .unwrap_or(Vec3::zeros())
    }

    pub fn get_point(&self, x: usize, y: usize, z: usize) -> Vec3 {
        let index = self.get_index(x, y, z);
        if index < self.base_points.len() {
            self.base_points[index] + self.displacements[index]
        } else {
            Vec3::zeros()
        }
    }

    pub fn world_to_uvw(&self, world_pos: Vec3) -> Vec3 {
        let size = self.bounds_max - self.bounds_min;
        let relative = world_pos - self.bounds_min;

        vec3(
            if size.x > 0.0 {
                relative.x / size.x
            } else {
                0.5
            },
            if size.y > 0.0 {
                relative.y / size.y
            } else {
                0.5
            },
            if size.z > 0.0 {
                relative.z / size.z
            } else {
                0.5
            },
        )
    }

    pub fn sample(&self, world_pos: Vec3) -> Vec3 {
        let uvw = self.world_to_uvw(world_pos);

        let inside = uvw.x >= 0.0
            && uvw.x <= 1.0
            && uvw.y >= 0.0
            && uvw.y <= 1.0
            && uvw.z >= 0.0
            && uvw.z <= 1.0;

        let blend = if inside {
            1.0
        } else if self.falloff > 0.0 {
            let clamped = vec3(
                uvw.x.clamp(0.0, 1.0),
                uvw.y.clamp(0.0, 1.0),
                uvw.z.clamp(0.0, 1.0),
            );
            let distance = nalgebra_glm::length(&(uvw - clamped));
            let size = self.bounds_max - self.bounds_min;
            let avg_size = (size.x + size.y + size.z) / 3.0;
            let normalized_distance = distance * avg_size;
            (1.0 - normalized_distance / self.falloff).max(0.0)
        } else {
            0.0
        };

        if blend <= 0.0 {
            return Vec3::zeros();
        }

        let [nx, ny, nz] = self.dimensions;

        let fx = uvw.x.clamp(0.0, 1.0) * (nx - 1) as f32;
        let fy = uvw.y.clamp(0.0, 1.0) * (ny - 1) as f32;
        let fz = uvw.z.clamp(0.0, 1.0) * (nz - 1) as f32;

        let x0 = (fx.floor() as usize).min(nx - 2);
        let y0 = (fy.floor() as usize).min(ny - 2);
        let z0 = (fz.floor() as usize).min(nz - 2);

        let x1 = x0 + 1;
        let y1 = y0 + 1;
        let z1 = z0 + 1;

        let tx = fx - x0 as f32;
        let ty = fy - y0 as f32;
        let tz = fz - z0 as f32;

        let d000 = self.get_displacement(x0, y0, z0);
        let d100 = self.get_displacement(x1, y0, z0);
        let d010 = self.get_displacement(x0, y1, z0);
        let d110 = self.get_displacement(x1, y1, z0);
        let d001 = self.get_displacement(x0, y0, z1);
        let d101 = self.get_displacement(x1, y0, z1);
        let d011 = self.get_displacement(x0, y1, z1);
        let d111 = self.get_displacement(x1, y1, z1);

        let d00 = nalgebra_glm::lerp(&d000, &d100, tx);
        let d10 = nalgebra_glm::lerp(&d010, &d110, tx);
        let d01 = nalgebra_glm::lerp(&d001, &d101, tx);
        let d11 = nalgebra_glm::lerp(&d011, &d111, tx);

        let d0 = nalgebra_glm::lerp(&d00, &d10, ty);
        let d1 = nalgebra_glm::lerp(&d01, &d11, ty);

        let displacement = nalgebra_glm::lerp(&d0, &d1, tz);

        displacement * blend
    }

    pub fn reset_displacements(&mut self) {
        for displacement in &mut self.displacements {
            *displacement = Vec3::zeros();
        }
        self.version = self.version.wrapping_add(1);
    }

    pub fn point_count(&self) -> usize {
        self.base_points.len()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatticeInfluenced {
    pub lattice_entity: Entity,
    pub last_lattice_version: u32,
    pub last_position: Vec3,
}

impl Default for LatticeInfluenced {
    fn default() -> Self {
        Self {
            lattice_entity: Entity::default(),
            last_lattice_version: u32::MAX,
            last_position: vec3(f32::MAX, f32::MAX, f32::MAX),
        }
    }
}

impl LatticeInfluenced {
    pub fn new(lattice_entity: Entity) -> Self {
        Self {
            lattice_entity,
            last_lattice_version: u32::MAX,
            last_position: vec3(f32::MAX, f32::MAX, f32::MAX),
        }
    }

    pub fn needs_update(&self, lattice_version: u32, current_position: Vec3) -> bool {
        self.last_lattice_version != lattice_version
            || (self.last_position - current_position).norm() > 0.0001
    }

    pub fn mark_updated(&mut self, lattice_version: u32, position: Vec3) {
        self.last_lattice_version = lattice_version;
        self.last_position = position;
    }
}
