// DebugVis — Debug visualization compute pass manager.
// Replaces PBR output with false-color diagnostic views (depth, normals, edges, etc.).
// Zero overhead when disabled: no dispatch, no bind groups, no allocations.

/// Debug visualization mode — controls what the viewport displays.
/// When not None, replaces the standard PBR output with a false-color diagnostic view.
/// Zero overhead when disabled (no dispatch, no bind groups, no allocations).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DebugVisMode {
    /// Standard rendering (no debug overlay).
    None,
    /// Depth buffer — warm-to-cool gradient (near=amber, far=blue).
    Depth,
    /// World-space normals — RGB normal map from depth derivatives.
    Normals,
    /// Edge detection — Canny-inspired depth+normal discontinuities.
    Edges,
    /// Ambient occlusion — SSAO output visualization.
    AmbientOcclusion,
    /// Object ID — pastel color per object from picking buffer.
    ObjectId,
    /// Motion vectors — velocity direction as hue, magnitude as brightness.
    MotionVectors,
    /// RT shadow mask — sunlight gold (lit) to deep blue (shadow).
    RtShadows,
    /// RT GI — indirect radiance with ACES tonemapping.
    RtGi,
    /// LOD level — green/amber/red/purple by distance.
    LodLevel,
    /// Overdraw — heat map from alpha channel.
    Overdraw,
    /// Meshlet ID — per-meshlet pastel coloring.
    MeshletId,
    /// LOD alias (same as LodLevel, alternate binding).
    LodAlias,
    /// DL-16: Surface cache atlas — visualize cached material tiles.
    SurfaceCache,
    /// DL-16: GI contribution — indirect lighting from RT GI or SSGI.
    GiContribution,
    /// DL-16: Shadow mask — combined RT shadow + CSM shadow visualization.
    ShadowMask,
    /// DL-16: Screen trace hits — green=screen hit, red=hardware RT, blue=miss.
    ScreenTraceHits,
    /// DL-16: SDF distance field — distance-to-surface gradient.
    SdfDistance,
    /// DL-16: Quality preset — color-coded by active Dream Lighting quality.
    QualityPreset,
}

impl DebugVisMode {
    pub const ALL: &'static [DebugVisMode] = &[
        Self::None,             // 0
        Self::Depth,            // 1
        Self::Normals,          // 2
        Self::Edges,            // 3
        Self::AmbientOcclusion, // 4
        Self::ObjectId,         // 5
        Self::MotionVectors,    // 6
        Self::RtShadows,        // 7
        Self::RtGi,             // 8
        Self::LodLevel,         // 9
        Self::Overdraw,         // 10
        Self::MeshletId,        // 11
        Self::LodAlias,         // 12
        Self::SurfaceCache,     // 13
        Self::GiContribution,   // 14
        Self::ShadowMask,       // 15
        Self::ScreenTraceHits,  // 16
        Self::SdfDistance,      // 17
        Self::QualityPreset,    // 18
    ];

    /// Human-readable label for UI display.
    pub fn label(self) -> &'static str {
        match self {
            Self::None => "None",
            Self::Depth => "Depth",
            Self::Normals => "Normals",
            Self::Edges => "Edges",
            Self::AmbientOcclusion => "Ambient Occlusion",
            Self::ObjectId => "Object ID",
            Self::MotionVectors => "Motion Vectors",
            Self::RtShadows => "RT Shadows",
            Self::RtGi => "RT GI",
            Self::LodLevel => "LOD Level",
            Self::Overdraw => "Overdraw",
            Self::MeshletId => "Meshlet ID",
            Self::LodAlias => "LOD (Alias)",
            Self::SurfaceCache => "Surface Cache",
            Self::GiContribution => "GI Contribution",
            Self::ShadowMask => "Shadow Mask",
            Self::ScreenTraceHits => "Screen Trace Hits",
            Self::SdfDistance => "SDF Distance",
            Self::QualityPreset => "Quality Preset",
        }
    }

    /// Shader uniform index. Matches case N in debug_vis.wgsl exactly.
    pub fn mode_index(self) -> u32 {
        match self {
            Self::None => 0,
            Self::Depth => 1,
            Self::Normals => 2,
            Self::Edges => 3,
            Self::AmbientOcclusion => 4,
            Self::ObjectId => 5,
            Self::MotionVectors => 6,
            Self::RtShadows => 7,
            Self::RtGi => 8,
            Self::LodLevel => 9,
            Self::Overdraw => 10,
            Self::MeshletId => 11,
            Self::LodAlias => 12,
            Self::SurfaceCache => 13,
            Self::GiContribution => 14,
            Self::ShadowMask => 15,
            Self::ScreenTraceHits => 16,
            Self::SdfDistance => 17,
            Self::QualityPreset => 18,
        }
    }

    /// Cycle to the next mode.
    pub fn next(self) -> Self {
        let idx = self.mode_index() as usize;
        let next_idx = (idx + 1) % Self::ALL.len();
        Self::ALL[next_idx]
    }

    /// Cycle to the previous mode.
    pub fn prev(self) -> Self {
        let idx = self.mode_index() as usize;
        let prev_idx = if idx == 0 { Self::ALL.len() - 1 } else { idx - 1 };
        Self::ALL[prev_idx]
    }
}

impl Default for DebugVisMode {
    fn default() -> Self {
        Self::None
    }
}

/// GPU uniforms for the debug visualization shader (matches debug_vis.wgsl DebugVisParams).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DebugVisUniforms {
    pub screen_size: [u32; 2],
    pub mode: u32,
    pub near: f32,
    pub far: f32,
    pub time: f32,
    pub _pad: [f32; 2],
}

/// Debug visualization compute pass.
/// Reads depth + color textures, writes a false-color Rgba8Unorm output.
pub struct DebugVisPass {
    pub mode: DebugVisMode,
    // Output texture (Rgba8Unorm at screen resolution)
    output_tex: Option<wgpu::Texture>,
    output_view: Option<wgpu::TextureView>,
    // Uniform buffer
    uniform_buffer: Option<wgpu::Buffer>,
    // Pipeline + bind group layout (cached)
    pipeline: Option<wgpu::ComputePipeline>,
    bgl: Option<wgpu::BindGroupLayout>,
    // Screen dimensions
    width: u32,
    height: u32,
    pub initialized: bool,
    elapsed: f32,
}

impl Default for DebugVisPass {
    fn default() -> Self {
        Self {
            mode: DebugVisMode::None,
            output_tex: None,
            output_view: None,
            uniform_buffer: None,
            pipeline: None,
            bgl: None,
            width: 0,
            height: 0,
            initialized: false,
            elapsed: 0.0,
        }
    }
}

impl DebugVisPass {
    /// Initialize output texture and uniform buffer for the given resolution.
    pub fn init(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.initialized && self.width == width && self.height == height {
            return;
        }

        let format = wgpu::TextureFormat::Rgba8Unorm;

        let tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("debug_vis_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.output_view = Some(tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.output_tex = Some(tex);

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

        self.width = width;
        self.height = height;
        self.initialized = true;

        log::info!("DebugVis initialized ({}x{})", width, height);
    }

    /// Compile the debug_vis.wgsl shader and create the compute pipeline.
    /// Call after `init`. Separated so callers can defer shader compilation.
    pub fn create_pipeline(&mut self, device: &wgpu::Device) {
        // Bind group layout: uniform + depth + color + output
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("debug_vis_bgl"),
            entries: &[
                // @binding(0) uniform DebugVisParams
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // @binding(1) texture_2d<f32> (depth buffer, read as float)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // @binding(2) texture_2d<f32> (color / HDR input)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // @binding(3) texture_storage_2d<rgba8unorm, write> (output)
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::WriteOnly,
                        format: wgpu::TextureFormat::Rgba8Unorm,
                        view_dimension: wgpu::TextureViewDimension::D2,
                    },
                    count: None,
                },
            ],
        });

        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("debug_vis.wgsl"),
            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/debug_vis.wgsl").into()),
        });

        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("debug_vis_layout"),
            bind_group_layouts: &[Some(&bgl)],
            immediate_size: 0,
        });

        self.pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("debug_vis_compute"),
            layout: Some(&layout),
            module: &shader,
            entry_point: Some("main"),
            compilation_options: Default::default(),
            cache: None,
        }));

        self.bgl = Some(bgl);
    }

    /// Dispatch the debug visualization compute pass.
    ///
    /// If mode is `None`, returns `Option::None` immediately (zero overhead).
    /// Otherwise uploads uniforms, binds depth+color inputs, and dispatches
    /// workgroups at ceil(width/8) x ceil(height/8).
    pub fn dispatch(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        depth_view: &wgpu::TextureView,
        color_view: &wgpu::TextureView,
        camera: &crate::camera::Camera,
        dt: f32,
    ) -> Option<&wgpu::TextureView> {
        if self.mode == DebugVisMode::None {
            return None;
        }
        if !self.initialized {
            return None;
        }
        let Some(ref pipeline) = self.pipeline else { return None };
        let Some(ref bgl) = self.bgl else { return None };
        let Some(ref uniform_buffer) = self.uniform_buffer else {
            return None;
        };
        let Some(ref output_view) = self.output_view else {
            return None;
        };

        self.elapsed += dt;

        // Upload uniforms
        let uniforms = DebugVisUniforms {
            screen_size: [self.width, self.height],
            mode: self.mode.mode_index(),
            near: camera.near_far.x,
            far: camera.near_far.y,
            time: self.elapsed,
            _pad: [0.0; 2],
        };
        queue.write_buffer(uniform_buffer, 0, bytemuck::bytes_of(&uniforms));

        // Create bind group with current frame's depth + color views
        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("debug_vis_bg"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(depth_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(color_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(output_view),
                },
            ],
        });

        {
            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("debug_vis_compute"),
                timestamp_writes: None,
            });
            pass.set_pipeline(pipeline);
            pass.set_bind_group(0, &bg, &[]);
            pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1);
        }

        Some(output_view)
    }

    /// Returns the debug output texture view for compositing into the final frame.
    pub fn output_view(&self) -> Option<&wgpu::TextureView> {
        self.output_view.as_ref()
    }

    /// Recreate the output texture at new dimensions. Preserves pipeline and uniform buffer.
    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.width == width && self.height == height {
            return;
        }

        // Destroy old output texture
        if let Some(t) = self.output_tex.take() {
            t.destroy();
        }
        self.output_view = None;

        let format = wgpu::TextureFormat::Rgba8Unorm;
        let tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("debug_vis_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.output_view = Some(tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.output_tex = Some(tex);

        self.width = width;
        self.height = height;

        log::info!("DebugVis resized ({}x{})", width, height);
    }

    /// Drop all GPU resources. Resets to uninitialized state.
    pub fn destroy(&mut self) {
        if let Some(t) = self.output_tex.take() {
            t.destroy();
        }
        if let Some(b) = self.uniform_buffer.take() {
            b.destroy();
        }
        self.output_view = None;
        self.pipeline = None;
        self.bgl = None;
        self.initialized = false;
        self.elapsed = 0.0;
    }

    /// Returns `true` if a debug visualization mode is active (not `None`).
    pub fn is_active(&self) -> bool {
        self.mode != DebugVisMode::None
    }
}

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

    #[test]
    fn mode_cycling_next_wraps() {
        let mut mode = DebugVisMode::None;
        for expected in DebugVisMode::ALL.iter().skip(1) {
            mode = mode.next();
            assert_eq!(mode, *expected);
        }
        // After last mode, wrap back to None
        mode = mode.next();
        assert_eq!(mode, DebugVisMode::None);
    }

    #[test]
    fn mode_cycling_prev_wraps() {
        let mut mode = DebugVisMode::None;
        // Prev from None wraps to last mode
        mode = mode.prev();
        assert_eq!(mode, *DebugVisMode::ALL.last().unwrap());
        // Walk all the way back to None
        for expected in DebugVisMode::ALL.iter().rev() {
            assert_eq!(mode, *expected);
            mode = mode.prev();
        }
    }

    #[test]
    fn mode_labels_all_non_empty() {
        for mode in DebugVisMode::ALL {
            assert!(!mode.label().is_empty(), "label for {:?} is empty", mode);
        }
    }

    #[test]
    fn mode_index_matches_all_ordering() {
        for (i, mode) in DebugVisMode::ALL.iter().enumerate() {
            assert_eq!(
                mode.mode_index() as usize,
                i,
                "mode_index for {:?} is {} but ALL position is {}",
                mode,
                mode.mode_index(),
                i
            );
        }
    }

    #[test]
    fn uniforms_size_32_bytes() {
        assert_eq!(std::mem::size_of::<DebugVisUniforms>(), 32);
    }

    #[test]
    fn debug_vis_mode_default_is_none() {
        assert_eq!(DebugVisMode::default(), DebugVisMode::None);
    }

    #[test]
    fn debug_vis_pass_default_inactive() {
        let pass = DebugVisPass::default();
        assert!(!pass.is_active());
        assert!(!pass.initialized);
        assert_eq!(pass.mode, DebugVisMode::None);
        assert_eq!(pass.width, 0);
        assert_eq!(pass.height, 0);
        assert_eq!(pass.elapsed, 0.0);
    }

    #[test]
    fn next_prev_roundtrip() {
        // Every mode should satisfy mode.next().prev() == mode
        for mode in DebugVisMode::ALL {
            assert_eq!(
                mode.next().prev(),
                *mode,
                "next().prev() roundtrip failed for {:?}",
                mode
            );
        }
    }
}
