// MicroDreamlet — 64-byte compact render-only dreamlet for maximum density.
//
// Strips physics fields (velocity, angular_velocity, mass, restitution,
// age, lifetime, target_position, deconstruct_level) for static geometry.
// 2M MicroDreamlets fit in a single 128 MB storage buffer binding.
//
// For dynamic physics objects, use the full GpuDreamlet (128 bytes).
// The cull/render shaders detect which type via a separate dispatch.

/// Compact render-only dreamlet (64 bytes, std430).
/// Position + scale + rotation + color + roughness/metallic + mesh_id.
/// No physics state. No lifecycle. Static geometry only.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MicroDreamlet {
    pub position: [f32; 3],   // 12 bytes
    pub scale: f32,           // 4 bytes
    pub rotation: [f32; 4],   // 16 bytes (quaternion)
    pub base_color: [f32; 4], // 16 bytes
    pub roughness: f32,       // 4 bytes
    pub metallic: f32,        // 4 bytes
    pub mesh_id: u32,         // 4 bytes
    pub bounding_radius: f32, // 4 bytes
}

/// Cull parameters for micro-dreamlet visibility (64 bytes).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MicroCullParams {
    pub view_proj: [f32; 16], // 64 bytes — frustum for Gribb-Hartmann extraction
}

/// CPU coarse pre-cull region. Spatial grid cell with dreamlet index range.
/// The CPU rejects entire regions that are outside the frustum before
/// the GPU processes individual dreamlets within surviving regions.
#[derive(Debug, Clone)]
pub struct CullRegion {
    pub center: [f32; 3],
    pub radius: f32,
    pub dreamlet_start: u32,
    pub dreamlet_count: u32,
}

/// Spatial grid for CPU coarse pre-cull.
/// Divides dreamlet space into cells. Each cell covers a cubic region.
/// CPU tests cell bounding spheres against frustum — O(cells), not O(dreamlets).
/// Only surviving cells' dreamlet ranges are dispatched to GPU cull.
pub struct CoarseCullGrid {
    pub cell_size: f32,
    pub regions: Vec<CullRegion>,
}

impl CoarseCullGrid {
    /// Build grid from dreamlet positions.
    pub fn build(positions: &[[f32; 3]], radii: &[f32], cell_size: f32) -> Self {
        use std::collections::HashMap;

        let mut cells: HashMap<(i32, i32, i32), Vec<usize>> = HashMap::new();

        for (i, pos) in positions.iter().enumerate() {
            let cx = (pos[0] / cell_size).floor() as i32;
            let cy = (pos[1] / cell_size).floor() as i32;
            let cz = (pos[2] / cell_size).floor() as i32;
            cells.entry((cx, cy, cz)).or_default().push(i);
        }

        let mut regions = Vec::with_capacity(cells.len());
        for ((cx, cy, cz), indices) in &cells {
            let center = [
                (*cx as f32 + 0.5) * cell_size,
                (*cy as f32 + 0.5) * cell_size,
                (*cz as f32 + 0.5) * cell_size,
            ];
            // Bounding sphere radius = half diagonal of cell + max dreamlet radius in cell
            let half_diag = cell_size * 0.866; // sqrt(3)/2
            let max_r = indices
                .iter()
                .map(|&i| radii.get(i).copied().unwrap_or(0.5))
                .fold(0.0f32, f32::max);

            regions.push(CullRegion {
                center,
                radius: half_diag + max_r,
                dreamlet_start: *indices.first().unwrap_or(&0) as u32,
                dreamlet_count: indices.len() as u32,
            });
        }

        Self { cell_size, regions }
    }

    /// CPU frustum pre-cull: returns total dreamlet count in visible regions.
    /// This is O(regions), not O(dreamlets).
    pub fn cull_regions(&self, vp: &[f32; 16]) -> (u32, Vec<(u32, u32)>) {
        let mut visible_ranges = Vec::new();
        let mut total = 0u32;

        for region in &self.regions {
            if frustum_contains_sphere(vp, region.center, region.radius) {
                visible_ranges.push((region.dreamlet_start, region.dreamlet_count));
                total += region.dreamlet_count;
            }
        }

        (total, visible_ranges)
    }
}

/// CPU-side frustum sphere test (Gribb-Hartmann, same as GPU shader).
fn frustum_contains_sphere(vp: &[f32; 16], center: [f32; 3], radius: f32) -> bool {
    let m = vp;
    // Extract 6 frustum planes from column-major VP matrix
    let planes: [[f32; 4]; 6] = [
        [m[3] + m[0], m[7] + m[4], m[11] + m[8], m[15] + m[12]],  // left
        [m[3] - m[0], m[7] - m[4], m[11] - m[8], m[15] - m[12]],  // right
        [m[3] + m[1], m[7] + m[5], m[11] + m[9], m[15] + m[13]],  // bottom
        [m[3] - m[1], m[7] - m[5], m[11] - m[9], m[15] - m[13]],  // top
        [m[2], m[6], m[10], m[14]],                               // near
        [m[3] - m[2], m[7] - m[6], m[11] - m[10], m[15] - m[14]], // far
    ];

    for plane in &planes {
        let d = plane[0] * center[0] + plane[1] * center[1] + plane[2] * center[2] + plane[3];
        let len = (plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]).sqrt();
        if d < -radius * len {
            return false;
        }
    }
    true
}

/// Maximum MicroDreamlets in a single storage buffer binding.
/// 128 MB / 64 bytes = 2,097,152
pub const MAX_MICRO_DREAMLETS: u32 = 2_097_152;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn micro_dreamlet_size() {
        assert_eq!(std::mem::size_of::<MicroDreamlet>(), 64);
    }

    #[test]
    fn micro_cull_params_size() {
        assert_eq!(std::mem::size_of::<MicroCullParams>(), 64);
    }

    #[test]
    fn max_micro_density() {
        // 2M micro-dreamlets in 128 MB
        assert_eq!(MAX_MICRO_DREAMLETS, 2_097_152);
        assert_eq!(MAX_MICRO_DREAMLETS as u64 * 64, 128 * 1024 * 1024);
    }

    #[test]
    fn coarse_grid_build() {
        let positions = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]];
        let radii = vec![0.5, 0.5, 0.5];
        let grid = CoarseCullGrid::build(&positions, &radii, 5.0);
        assert!(grid.regions.len() >= 2); // at least 2 cells
    }

    #[test]
    fn frustum_test_origin_visible() {
        // Identity VP matrix — everything at origin is visible
        #[rustfmt::skip]
        let vp = [
            1.0, 0.0, 0.0, 0.0,
            0.0, 1.0, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0,
        ];
        assert!(frustum_contains_sphere(&vp, [0.0, 0.0, 0.0], 1.0));
    }
}
