use std::collections::HashSet;

use nalgebra_glm::{IVec3, Vec3};

use super::marching_cubes;
use super::resources::SdfWorld;

pub struct SdfCollisionMesh {
    pub chunk_size: f32,
    pub cell_size: f32,
    pub dirty_chunks: HashSet<IVec3>,
    pub max_chunks_per_frame: usize,
    pub terrain_octaves: u32,
}

impl SdfCollisionMesh {
    pub fn new(chunk_size: f32, cell_size: f32) -> Self {
        Self {
            chunk_size,
            cell_size,
            dirty_chunks: HashSet::new(),
            max_chunks_per_frame: 8,
            terrain_octaves: 11,
        }
    }

    pub fn mark_dirty_in_bounds(&mut self, bounds_min: Vec3, bounds_max: Vec3) {
        let chunk_min = IVec3::new(
            (bounds_min.x / self.chunk_size).floor() as i32,
            (bounds_min.y / self.chunk_size).floor() as i32,
            (bounds_min.z / self.chunk_size).floor() as i32,
        );
        let chunk_max = IVec3::new(
            (bounds_max.x / self.chunk_size).ceil() as i32,
            (bounds_max.y / self.chunk_size).ceil() as i32,
            (bounds_max.z / self.chunk_size).ceil() as i32,
        );

        for cz in chunk_min.z..=chunk_max.z {
            for cy in chunk_min.y..=chunk_max.y {
                for cx in chunk_min.x..=chunk_max.x {
                    self.dirty_chunks.insert(IVec3::new(cx, cy, cz));
                }
            }
        }
    }

    pub fn update(
        &mut self,
        sdf_world: &SdfWorld,
        camera_pos: Vec3,
        excluded_edit_indices: &HashSet<usize>,
    ) -> Vec<(IVec3, CollisionChunkResult)> {
        if self.dirty_chunks.is_empty() {
            return Vec::new();
        }

        let count = self.max_chunks_per_frame.min(self.dirty_chunks.len());
        let mut candidates: Vec<IVec3> = self.dirty_chunks.iter().copied().collect();

        if candidates.len() > count {
            candidates.select_nth_unstable_by(count - 1, |a, b| {
                let dist_a = chunk_center_distance(*a, self.chunk_size, camera_pos);
                let dist_b = chunk_center_distance(*b, self.chunk_size, camera_pos);
                dist_a.partial_cmp(&dist_b).unwrap()
            });
            candidates.truncate(count);
        }

        let mut results = Vec::with_capacity(count);
        let octaves = self.terrain_octaves;

        for chunk_coord in candidates {
            self.dirty_chunks.remove(&chunk_coord);

            let region_min = Vec3::new(
                chunk_coord.x as f32 * self.chunk_size,
                chunk_coord.y as f32 * self.chunk_size,
                chunk_coord.z as f32 * self.chunk_size,
            );
            let region_max =
                region_min + Vec3::new(self.chunk_size, self.chunk_size, self.chunk_size);

            let (vertices, indices) = if excluded_edit_indices.is_empty() {
                marching_cubes::extract_mesh(
                    &|point| sdf_world.evaluate_at_lod(point, octaves),
                    region_min,
                    region_max,
                    self.cell_size,
                )
            } else {
                marching_cubes::extract_mesh(
                    &|point| {
                        sdf_world.evaluate_at_lod_excluding(point, octaves, excluded_edit_indices)
                    },
                    region_min,
                    region_max,
                    self.cell_size,
                )
            };

            if vertices.is_empty() || indices.is_empty() {
                results.push((chunk_coord, CollisionChunkResult::Empty));
            } else {
                results.push((
                    chunk_coord,
                    CollisionChunkResult::Mesh { vertices, indices },
                ));
            }
        }

        results
    }

    pub fn clear(&mut self) {
        self.dirty_chunks.clear();
    }
}

pub enum CollisionChunkResult {
    Empty,
    Mesh {
        vertices: Vec<[f32; 3]>,
        indices: Vec<[u32; 3]>,
    },
}

fn chunk_center_distance(chunk_coord: IVec3, chunk_size: f32, camera_pos: Vec3) -> f32 {
    let center = Vec3::new(
        (chunk_coord.x as f32 + 0.5) * chunk_size,
        (chunk_coord.y as f32 + 0.5) * chunk_size,
        (chunk_coord.z as f32 + 0.5) * chunk_size,
    );
    nalgebra_glm::length(&(center - camera_pos))
}
