// LOD — Level-of-Detail system for distance-based mesh selection.
//
// Primitives generate up to 3 LOD levels (Full, Medium, Low) with
// decreasing triangle counts. LOD selection is distance-based on CPU
// for scene objects, screen-area-based on GPU for meshlets.
//
// MATTER.md: "LOD and draw bins built" (step 3 of runtime pass flow),
// "budget-aware LOD" (safety section), "per-cluster culling and LOD"
// (mesh shader section).

use crate::primitives::{PbrMesh, PrimitiveShape};

/// LOD level identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum LodLevel {
    /// Full detail — original mesh (LOD 0).
    Full = 0,
    /// Medium detail — ~25% triangle count (LOD 1).
    Medium = 1,
    /// Low detail — ~6% triangle count (LOD 2).
    Low = 2,
}

impl LodLevel {
    pub const COUNT: usize = 3;

    pub const ALL: [LodLevel; 3] = [Self::Full, Self::Medium, Self::Low];
}

/// Per-object LOD configuration with distance thresholds.
#[derive(Debug, Clone, Copy)]
pub struct LodConfig {
    /// Distance beyond which LOD 1 (Medium) is used.
    pub lod1_distance: f32,
    /// Distance beyond which LOD 2 (Low) is used.
    pub lod2_distance: f32,
    /// Distance beyond which the object is culled entirely.
    pub cull_distance: f32,
}

impl Default for LodConfig {
    fn default() -> Self {
        Self {
            lod1_distance: 30.0,
            lod2_distance: 80.0,
            cull_distance: 500.0,
        }
    }
}

impl LodConfig {
    /// Select LOD level for a given camera distance.
    pub fn select(&self, distance: f32) -> Option<LodLevel> {
        if distance > self.cull_distance {
            None
        } else if distance > self.lod2_distance {
            Some(LodLevel::Low)
        } else if distance > self.lod1_distance {
            Some(LodLevel::Medium)
        } else {
            Some(LodLevel::Full)
        }
    }
}

/// Set of up to 3 LOD mesh levels for a single PrimitiveKind.
/// Low-poly shapes (cube, plane, pyramid, wedge) use the same mesh
/// for all LOD levels.
pub struct LodMeshSet {
    pub meshes: [PbrMesh; LodLevel::COUNT],
    pub index_counts: [u32; LodLevel::COUNT],
}

impl LodMeshSet {
    /// Get mesh for the requested LOD level.
    pub fn mesh(&self, level: LodLevel) -> &PbrMesh {
        &self.meshes[level as usize]
    }

    /// Get index count for the requested LOD level.
    pub fn index_count(&self, level: LodLevel) -> u32 {
        self.index_counts[level as usize]
    }
}

/// GPU buffer set for a LOD mesh (vertex + index per level).
pub struct GpuLodMesh {
    pub vertex_buffers: [wgpu::Buffer; LodLevel::COUNT],
    pub index_buffers: [wgpu::Buffer; LodLevel::COUNT],
    pub index_counts: [u32; LodLevel::COUNT],
}

impl GpuLodMesh {
    /// Upload a LodMeshSet to GPU.
    pub fn upload(device: &wgpu::Device, set: &LodMeshSet) -> Self {
        use wgpu::util::DeviceExt;

        let mut vbufs = Vec::with_capacity(3);
        let mut ibufs = Vec::with_capacity(3);
        let mut icounts = [0u32; 3];

        for (i, mesh) in set.meshes.iter().enumerate() {
            let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(mesh.name),
                contents: bytemuck::cast_slice(&mesh.vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
            let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some(mesh.name),
                contents: bytemuck::cast_slice(&mesh.indices),
                usage: wgpu::BufferUsages::INDEX,
            });
            icounts[i] = mesh.indices.len() as u32;
            vbufs.push(vb);
            ibufs.push(ib);
        }

        Self {
            vertex_buffers: [vbufs.remove(0), vbufs.remove(0), vbufs.remove(0)],
            index_buffers: [ibufs.remove(0), ibufs.remove(0), ibufs.remove(0)],
            index_counts: icounts,
        }
    }

    /// Get vertex buffer for LOD level.
    pub fn vertex_buffer(&self, level: LodLevel) -> &wgpu::Buffer {
        &self.vertex_buffers[level as usize]
    }

    /// Get index buffer for LOD level.
    pub fn index_buffer(&self, level: LodLevel) -> &wgpu::Buffer {
        &self.index_buffers[level as usize]
    }

    /// Get index count for LOD level.
    pub fn index_count(&self, level: LodLevel) -> u32 {
        self.index_counts[level as usize]
    }

    /// Destroy all GPU resources.
    pub fn destroy(self) {
        for buf in self.vertex_buffers {
            buf.destroy();
        }
        for buf in self.index_buffers {
            buf.destroy();
        }
    }
}

/// Tessellation parameters for LOD generation.
/// Shapes that can vary tessellation use these to produce LOD variants.
struct LodTessellation {
    slices: [u32; 3],
    stacks: [u32; 3],
}

impl PrimitiveShape {
    /// Tessellation counts per LOD level for parameterized shapes.
    fn lod_tessellation(self) -> Option<LodTessellation> {
        match self {
            Self::Sphere => Some(LodTessellation {
                slices: [16, 8, 4],
                stacks: [12, 6, 3],
            }),
            Self::Cylinder | Self::Cone => Some(LodTessellation {
                slices: [16, 8, 4],
                stacks: [1, 1, 1], // unused for cylinder/cone
            }),
            Self::Torus => Some(LodTessellation {
                slices: [16, 8, 4], // ring_segments
                stacks: [8, 4, 3],  // tube_segments
            }),
            Self::Capsule => Some(LodTessellation {
                slices: [12, 6, 4],
                stacks: [8, 4, 3],
            }),
            // Cube, Plane, Pyramid, Wedge have fixed low geometry — same mesh all LODs.
            _ => None,
        }
    }

    /// Generate a set of LOD meshes (3 levels) for this primitive.
    /// Low-poly shapes return the same mesh for all levels.
    pub fn generate_lod_set(self, color: [f32; 4]) -> LodMeshSet {
        if let Some(tess) = self.lod_tessellation() {
            let m0 = self.generate_pbr_with_tessellation(color, tess.slices[0], tess.stacks[0]);
            let m1 = self.generate_pbr_with_tessellation(color, tess.slices[1], tess.stacks[1]);
            let m2 = self.generate_pbr_with_tessellation(color, tess.slices[2], tess.stacks[2]);
            let index_counts = [
                m0.indices.len() as u32,
                m1.indices.len() as u32,
                m2.indices.len() as u32,
            ];
            LodMeshSet {
                meshes: [m0, m1, m2],
                index_counts,
            }
        } else {
            // Single-LOD shape — replicate
            let m0 = self.generate_pbr(color);
            let m1 = self.generate_pbr(color);
            let m2 = self.generate_pbr(color);
            let ic = m0.indices.len() as u32;
            LodMeshSet {
                meshes: [m0, m1, m2],
                index_counts: [ic, ic, ic],
            }
        }
    }
}

/// Camera distance squared from an object's world-space position.
pub fn distance_squared(camera_pos: [f32; 3], object_pos: [f32; 3]) -> f32 {
    let dx = camera_pos[0] - object_pos[0];
    let dy = camera_pos[1] - object_pos[1];
    let dz = camera_pos[2] - object_pos[2];
    dx * dx + dy * dy + dz * dz
}

/// Extract world-space position from a column-major 4×4 model matrix.
pub fn position_from_matrix(m: &[f32; 16]) -> [f32; 3] {
    [m[12], m[13], m[14]]
}

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

    #[test]
    fn lod_config_select() {
        let config = LodConfig::default();
        assert_eq!(config.select(10.0), Some(LodLevel::Full));
        assert_eq!(config.select(50.0), Some(LodLevel::Medium));
        assert_eq!(config.select(100.0), Some(LodLevel::Low));
        assert_eq!(config.select(600.0), None);
    }

    #[test]
    fn lod_config_boundary() {
        let config = LodConfig {
            lod1_distance: 30.0,
            lod2_distance: 80.0,
            cull_distance: 500.0,
        };
        assert_eq!(config.select(30.0), Some(LodLevel::Full));
        assert_eq!(config.select(30.001), Some(LodLevel::Medium));
        assert_eq!(config.select(80.0), Some(LodLevel::Medium));
        assert_eq!(config.select(80.001), Some(LodLevel::Low));
        assert_eq!(config.select(500.0), Some(LodLevel::Low));
        assert_eq!(config.select(500.001), None);
    }

    #[test]
    fn sphere_lod_triangle_reduction() {
        let set = PrimitiveShape::Sphere.generate_lod_set([1.0; 4]);
        let lod0 = set.index_counts[0];
        let lod1 = set.index_counts[1];
        let lod2 = set.index_counts[2];
        // Each level should have strictly fewer triangles
        assert!(lod1 < lod0, "LOD1 ({lod1}) must be < LOD0 ({lod0})");
        assert!(lod2 < lod1, "LOD2 ({lod2}) must be < LOD1 ({lod1})");
    }

    #[test]
    fn cube_lod_same_all_levels() {
        let set = PrimitiveShape::Cube.generate_lod_set([1.0; 4]);
        assert_eq!(set.index_counts[0], set.index_counts[1]);
        assert_eq!(set.index_counts[1], set.index_counts[2]);
    }

    #[test]
    fn torus_lod_reduction() {
        let set = PrimitiveShape::Torus.generate_lod_set([1.0; 4]);
        assert!(set.index_counts[1] < set.index_counts[0]);
        assert!(set.index_counts[2] < set.index_counts[1]);
    }

    #[test]
    fn distance_squared_basic() {
        let d = distance_squared([0.0, 0.0, 0.0], [3.0, 4.0, 0.0]);
        assert!((d - 25.0).abs() < 0.001);
    }

    #[test]
    fn position_from_matrix_identity() {
        #[rustfmt::skip]
        let m = [
            1.0, 0.0, 0.0, 0.0,
            0.0, 1.0, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            5.0, 6.0, 7.0, 1.0,
        ];
        let pos = position_from_matrix(&m);
        assert_eq!(pos, [5.0, 6.0, 7.0]);
    }

    #[test]
    fn lod_level_count() {
        assert_eq!(LodLevel::COUNT, 3);
        assert_eq!(LodLevel::ALL.len(), 3);
    }
}
