// CSM — Cascaded Shadow Maps (4 cascade, PSSM splits).
//
// Replaces the single 2048² shadow atlas with a 4096×4096 atlas divided into
// 4 × 2048×2048 cascade tiles. Each cascade covers a different depth range
// using practical split scheme (logarithmic-linear blend).
//
// PBR shader selects cascade by fragment depth and samples with PCF.

use crate::camera::Camera;

/// Number of shadow cascades.
pub const CASCADE_COUNT: u32 = 4;

/// Atlas size (each cascade gets atlas_size/2 × atlas_size/2).
pub const CSM_ATLAS_SIZE: u32 = 4096;

/// Per-cascade data for shadow rendering.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CascadeData {
    /// Light-space view-projection matrix for this cascade.
    pub light_vp: [f32; 16],
    /// Split depth (view-space Z where this cascade ends).
    pub split_depth: f32,
    /// Viewport offset X in atlas (normalized 0..1).
    pub viewport_x: f32,
    /// Viewport offset Y in atlas (normalized 0..1).
    pub viewport_y: f32,
    pub _pad: f32,
}

/// CSM uniform data uploaded to PBR shader (4 cascades).
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CsmUniforms {
    pub cascades: [CascadeData; CASCADE_COUNT as usize],
    pub cascade_count: u32,
    pub shadow_bias: f32,
    pub normal_bias: f32,
    pub _pad: f32,
}

/// Cascaded shadow map manager.
pub struct CsmPass {
    pub atlas_texture: Option<wgpu::Texture>,
    pub atlas_view: Option<wgpu::TextureView>,
    pub uniform_buf: Option<wgpu::Buffer>,
    pub pipeline: Option<wgpu::RenderPipeline>,
    pub bgl: Option<wgpu::BindGroupLayout>,
    pub per_object_buf: Option<wgpu::Buffer>,
    pub enabled: bool,
    /// DL-15: Multiview cascade rendering (4-layer texture array, single draw).
    /// Requires EXPERIMENTAL_MESH_SHADER_MULTIVIEW. Falls back to 4-pass when unavailable.
    pub multiview_enabled: bool,
    multiview_texture: Option<wgpu::Texture>,
    multiview_view: Option<wgpu::TextureView>,
    initialized: bool,
}

impl Default for CsmPass {
    fn default() -> Self {
        Self {
            atlas_texture: None,
            atlas_view: None,
            uniform_buf: None,
            pipeline: None,
            bgl: None,
            per_object_buf: None,
            enabled: true,
            multiview_enabled: false,
            multiview_texture: None,
            multiview_view: None,
            initialized: false,
        }
    }
}

impl CsmPass {
    /// Compute PSSM split depths (practical split scheme).
    /// Lambda blends between logarithmic and uniform splits.
    pub fn compute_splits(near: f32, far: f32, lambda: f32) -> [f32; CASCADE_COUNT as usize] {
        let mut splits = [0.0f32; CASCADE_COUNT as usize];
        let count = CASCADE_COUNT as f32;
        for i in 0..CASCADE_COUNT as usize {
            let p = (i as f32 + 1.0) / count;
            let log_split = near * (far / near).powf(p);
            let uniform_split = near + (far - near) * p;
            splits[i] = lambda * log_split + (1.0 - lambda) * uniform_split;
        }
        splits
    }

    /// Compute light-space matrix for a cascade covering the given frustum slice.
    pub fn compute_cascade_matrix(light_dir: [f32; 3], camera: &Camera, near_split: f32, far_split: f32) -> [f32; 16] {
        // Compute frustum corners for this cascade's depth range
        let inv_vp = (camera.projection_matrix * camera.view_matrix).inverse();

        // NDC corners for the near and far planes of this cascade
        let ndc_corners = [
            [-1.0, -1.0, 0.0],
            [1.0, -1.0, 0.0],
            [1.0, 1.0, 0.0],
            [-1.0, 1.0, 0.0], // near
            [-1.0, -1.0, 1.0],
            [1.0, -1.0, 1.0],
            [1.0, 1.0, 1.0],
            [-1.0, 1.0, 1.0], // far
        ];

        let cam_near = camera.near_far.x;
        let cam_far = camera.near_far.y;

        // Interpolate depth range for this cascade
        let t_near = (near_split - cam_near) / (cam_far - cam_near);
        let t_far = (far_split - cam_near) / (cam_far - cam_near);

        let mut world_corners = [[0.0f32; 3]; 8];
        for (i, ndc) in ndc_corners.iter().enumerate() {
            let t = if i < 4 { t_near } else { t_far };
            let depth_ndc = t; // linear depth in NDC

            let clip = glam::Vec4::new(ndc[0], ndc[1], depth_ndc, 1.0);
            let world = inv_vp * clip;
            let w = world.w.max(0.0001);
            world_corners[i] = [world.x / w, world.y / w, world.z / w];
        }

        // Compute center and radius of the frustum slice
        let mut center = [0.0f32; 3];
        for c in &world_corners {
            center[0] += c[0];
            center[1] += c[1];
            center[2] += c[2];
        }
        center[0] /= 8.0;
        center[1] /= 8.0;
        center[2] /= 8.0;

        let mut max_radius = 0.0f32;
        for c in &world_corners {
            let dx = c[0] - center[0];
            let dy = c[1] - center[1];
            let dz = c[2] - center[2];
            let r = (dx * dx + dy * dy + dz * dz).sqrt();
            max_radius = max_radius.max(r);
        }

        // Use the bounding sphere radius as the ortho bounds
        crate::passes::ShadowPass::compute_light_matrix(light_dir, max_radius.max(1.0))
    }

    /// Initialize the CSM atlas and pipeline.
    pub fn init(&mut self, device: &wgpu::Device) {
        if self.initialized {
            return;
        }

        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("csm_atlas"),
            size: wgpu::Extent3d {
                width: CSM_ATLAS_SIZE,
                height: CSM_ATLAS_SIZE,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: crate::formats::DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.atlas_view = Some(texture.create_view(&wgpu::TextureViewDescriptor::default()));
        self.atlas_texture = Some(texture);

        self.uniform_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("csm_uniforms"),
            size: std::mem::size_of::<CsmUniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        self.per_object_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("csm_per_object"),
            size: 128,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        self.initialized = true;
        log::info!(
            "CSM initialized ({}×{} atlas, {} cascades)",
            CSM_ATLAS_SIZE,
            CSM_ATLAS_SIZE,
            CASCADE_COUNT
        );
    }

    /// Compute cascade splits and light matrices for the current frame.
    pub fn update(&mut self, queue: &wgpu::Queue, camera: &Camera, light_dir: [f32; 3]) {
        let Some(ref buf) = self.uniform_buf else { return };

        let near = camera.near_far.x.max(0.1);
        let far = camera.near_far.y.min(500.0);
        let splits = Self::compute_splits(near, far, 0.75);

        let viewport_offsets = [[0.0, 0.0], [0.5, 0.0], [0.0, 0.5], [0.5, 0.5]];

        let mut cascades = [CascadeData {
            light_vp: [0.0; 16],
            split_depth: 0.0,
            viewport_x: 0.0,
            viewport_y: 0.0,
            _pad: 0.0,
        }; CASCADE_COUNT as usize];

        let mut prev_split = near;
        for i in 0..CASCADE_COUNT as usize {
            let light_vp = Self::compute_cascade_matrix(light_dir, camera, prev_split, splits[i]);
            cascades[i] = CascadeData {
                light_vp,
                split_depth: splits[i],
                viewport_x: viewport_offsets[i][0],
                viewport_y: viewport_offsets[i][1],
                _pad: 0.0,
            };
            prev_split = splits[i];
        }

        let uniforms = CsmUniforms {
            cascades,
            cascade_count: CASCADE_COUNT,
            shadow_bias: 0.005,
            normal_bias: 0.02,
            _pad: 0.0,
        };

        queue.write_buffer(buf, 0, bytemuck::bytes_of(&uniforms));
    }

    /// DL-15: Initialize multiview cascade texture array (4 layers at cascade resolution).
    /// Only allocates when multiview_enabled and device supports multiview rendering.
    pub fn init_multiview(&mut self, device: &wgpu::Device) {
        if !self.multiview_enabled {
            return;
        }

        let cascade_size = CSM_ATLAS_SIZE / 2; // Each cascade gets half atlas dimension
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("csm_multiview_array"),
            size: wgpu::Extent3d {
                width: cascade_size,
                height: cascade_size,
                depth_or_array_layers: CASCADE_COUNT,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: crate::formats::DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.multiview_view = Some(texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        }));
        self.multiview_texture = Some(texture);

        log::info!(
            "CSM multiview initialized ({}×{} × {} layers)",
            cascade_size,
            cascade_size,
            CASCADE_COUNT
        );
    }

    /// DL-15: Render all 4 cascades in a single multiview draw call.
    /// Requires `@builtin(view_index)` in shadow_multiview.wgsl.
    /// Falls back to standard 4-pass when multiview not available.
    pub fn render_multiview(&self) -> bool {
        self.multiview_enabled && self.multiview_texture.is_some()
    }

    pub fn destroy(&mut self) {
        if let Some(t) = self.atlas_texture.take() {
            t.destroy();
        }
        if let Some(t) = self.multiview_texture.take() {
            t.destroy();
        }
        if let Some(b) = self.uniform_buf.take() {
            b.destroy();
        }
        if let Some(b) = self.per_object_buf.take() {
            b.destroy();
        }
        self.atlas_view = None;
        self.multiview_view = None;
        self.initialized = false;
    }
}

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

    #[test]
    fn cascade_data_size() {
        assert_eq!(std::mem::size_of::<CascadeData>(), 80);
    }

    #[test]
    fn csm_uniforms_size() {
        assert_eq!(std::mem::size_of::<CsmUniforms>(), 336);
    }

    #[test]
    fn pssm_splits_monotonic() {
        let splits = CsmPass::compute_splits(0.1, 200.0, 0.75);
        assert!(splits[0] < splits[1]);
        assert!(splits[1] < splits[2]);
        assert!(splits[2] < splits[3]);
        assert!(splits[3] <= 200.0);
    }

    #[test]
    fn pssm_splits_cover_range() {
        let splits = CsmPass::compute_splits(0.5, 200.0, 0.75);
        assert!(splits[0] > 0.5, "First split must be > near");
        assert!(splits[3] <= 200.0, "Last split must be <= far");
    }
}
