//! Post-processing chain — HDR framebuffer, bloom, tonemapping.
//!
//! Zero-overhead bypass: when `is_enabled() == false`, rendering targets the surface
//! directly. No HDR allocation, no bloom passes, no tonemap pass.

use dreamwell_engine::material::TonemapOperator;

use crate::formats::DEPTH_FORMAT;
use crate::shader;

pub const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;

const BLOOM_SHADER_SRC: &str = include_str!("../shaders/bloom.wgsl");
const TONEMAP_SHADER_SRC: &str = include_str!("../shaders/tonemap.wgsl");

// ---------------------------------------------------------------------------
// Uniform structs (GPU-uploadable)
// ---------------------------------------------------------------------------

#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct BloomUniforms {
    threshold: f32,
    intensity: f32,
    texel_size: [f32; 2],
}

#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct TonemapUniforms {
    exposure: f32,
    bloom_intensity: f32,
    tonemap_operator: u32,
    dof_enabled: u32,
    fog_enabled: u32,
    // WGSL: _pad: vec3<f32> has alignment 16 → placed at offset 32 in std140.
    // 12 bytes implicit padding (offset 20→32) + 12 bytes vec3<f32> + 4 bytes struct tail.
    _pad_align: [f32; 3], // 12B gap for vec3 alignment (offset 20→32)
    _pad: [f32; 3],       // matches WGSL vec3<f32> at offset 32 (12B)
    _pad_tail: f32,       // struct alignment padding to 48 (WGSL rounds to 16)
}
// Total: 48 bytes = 5*4 + 12 + 12 + 4 = 48 ✓

// ---------------------------------------------------------------------------
// PostProcessConfig
// ---------------------------------------------------------------------------

/// Post-processing configuration.
#[derive(Debug, Clone)]
pub struct PostProcessConfig {
    pub bloom_enabled: bool,
    pub bloom_threshold: f32,
    pub bloom_intensity: f32,
    pub tonemap_enabled: bool,
    pub tonemap_operator: TonemapOperator,
    pub exposure: f32,
}

impl Default for PostProcessConfig {
    fn default() -> Self {
        Self {
            bloom_enabled: false,
            bloom_threshold: 1.0,
            bloom_intensity: 0.5,
            tonemap_enabled: false,
            tonemap_operator: TonemapOperator::AcesFilmic,
            exposure: 1.0,
        }
    }
}

impl PostProcessConfig {
    /// PBR-optimized configuration: HDR + bloom + ACES filmic tonemapping.
    /// Suitable as the default when SceneDreamMode is PbrDefault or PbrLightweight.
    pub fn for_pbr() -> Self {
        Self {
            bloom_enabled: true,
            bloom_threshold: 1.0,
            bloom_intensity: 0.04,
            tonemap_enabled: true,
            tonemap_operator: TonemapOperator::AcesFilmic,
            exposure: 1.0,
        }
    }

    /// Demo-friendly configuration: subtle bloom, restrained exposure.
    /// Avoids the washed-out look on bright scenes.
    pub fn for_demo() -> Self {
        Self {
            bloom_enabled: true,
            bloom_threshold: 1.5,
            bloom_intensity: 0.02,
            tonemap_enabled: true,
            tonemap_operator: TonemapOperator::AcesFilmic,
            exposure: 0.8,
        }
    }
}

// ---------------------------------------------------------------------------
// BloomMip (internal)
// ---------------------------------------------------------------------------

struct BloomMip {
    /// Retained for GPU resource lifetime — dropping frees the underlying allocation.
    _texture: wgpu::Texture,
    view: wgpu::TextureView,
    width: u32,
    height: u32,
}

// ---------------------------------------------------------------------------
// PostProcessStack
// ---------------------------------------------------------------------------

/// Post-processing stack — HDR framebuffer, bloom mip chain, tonemapping.
pub struct PostProcessStack {
    config: PostProcessConfig,
    // HDR scene target
    hdr_texture: Option<wgpu::Texture>,
    hdr_view: Option<wgpu::TextureView>,
    hdr_depth_texture: Option<wgpu::Texture>,
    hdr_depth_view: Option<wgpu::TextureView>,
    width: u32,
    height: u32,
    // Bloom (2-level mip chain)
    bloom_mips: [Option<BloomMip>; 2],
    bloom_downsample_pipeline: Option<wgpu::RenderPipeline>,
    bloom_upsample_pipeline: Option<wgpu::RenderPipeline>,
    bloom_bgl: Option<wgpu::BindGroupLayout>,
    bloom_uniform_buf: Option<wgpu::Buffer>,
    // Tonemap
    tonemap_pipeline: Option<wgpu::RenderPipeline>,
    tonemap_bgl: Option<wgpu::BindGroupLayout>,
    tonemap_uniform_buf: Option<wgpu::Buffer>,
    // Shared
    sampler: Option<wgpu::Sampler>,
    black_texture: Option<(wgpu::Texture, wgpu::TextureView)>,
    surface_format: wgpu::TextureFormat,
    initialized: bool,
    // Bind group caches (invalidated on resize)
    cached_tonemap_bg: Option<wgpu::BindGroup>,
    cached_bloom_bgs: [Option<wgpu::BindGroup>; 3],
}

impl PostProcessStack {
    /// Create a new uninitialized post-processing stack.
    pub fn new(surface_format: wgpu::TextureFormat) -> Self {
        Self {
            config: PostProcessConfig::default(),
            hdr_texture: None,
            hdr_view: None,
            hdr_depth_texture: None,
            hdr_depth_view: None,
            width: 0,
            height: 0,
            bloom_mips: [None, None],
            bloom_downsample_pipeline: None,
            bloom_upsample_pipeline: None,
            bloom_bgl: None,
            bloom_uniform_buf: None,
            tonemap_pipeline: None,
            tonemap_bgl: None,
            tonemap_uniform_buf: None,
            sampler: None,
            black_texture: None,
            surface_format,
            initialized: false,
            cached_tonemap_bg: None,
            cached_bloom_bgs: [None, None, None],
        }
    }

    /// Current configuration.
    pub fn config(&self) -> &PostProcessConfig {
        &self.config
    }

    /// Update configuration.
    pub fn set_config(&mut self, config: PostProcessConfig) {
        self.config = config;
    }

    /// Whether any post-processing is active.
    pub fn is_enabled(&self) -> bool {
        self.config.bloom_enabled || self.config.tonemap_enabled
    }

    // -----------------------------------------------------------------------
    // Resource allocation
    // -----------------------------------------------------------------------

    /// Allocate (or reallocate) GPU resources for the given framebuffer dimensions.
    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if width == 0 || height == 0 {
            return;
        }
        if self.initialized && self.width == width && self.height == height {
            return;
        }

        // Invalidate cached bind groups.
        self.cached_tonemap_bg = None;
        self.cached_bloom_bgs = [None, None, None];

        // Drop old resources.
        self.hdr_texture = None;
        self.hdr_view = None;
        self.hdr_depth_texture = None;
        self.hdr_depth_view = None;
        self.bloom_mips = [None, None];

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

        // HDR color target.
        let hdr_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("post_hdr_color"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: HDR_FORMAT,
            // TD-006: COPY_DST allows TAA/Dream TSR resolved output to be copied back to HDR.
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                | wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        self.hdr_view = Some(hdr_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.hdr_texture = Some(hdr_tex);

        // HDR depth target.
        // FORMAT CONTRACT: Uses DEPTH_FORMAT (Depth32Float) from crate::formats.
        // All passes that reference this depth buffer MUST use the same format:
        //   - Main render pass depth attachment (fabric frame.rs)
        //   - Hi-Z downsample input (hiz.rs)
        //   - Screen-space effect depth reads (ssao, ssr, dof, ssgi, taa)
        //   - RT shadow/GI depth texture binding (rt_shadows.rs, rt_gi.rs)
        // Mismatched formats cause wgpu validation errors at render pass creation.
        let depth_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("post_hdr_depth"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.hdr_depth_view = Some(depth_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.hdr_depth_texture = Some(depth_tex);

        // Bloom mip chain: half-res and quarter-res.
        // .max(1) on results prevents zero-dimension textures at very small framebuffer sizes.
        let mip_sizes = [
            ((width / 2).max(1), (height / 2).max(1)),
            ((width / 4).max(1), (height / 4).max(1)),
        ];
        for (i, &(mw, mh)) in mip_sizes.iter().enumerate() {
            let tex = device.create_texture(&wgpu::TextureDescriptor {
                label: Some(&format!("post_bloom_mip{i}")),
                size: wgpu::Extent3d {
                    width: mw,
                    height: mh,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: HDR_FORMAT,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            });
            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
            self.bloom_mips[i] = Some(BloomMip {
                _texture: tex,
                view,
                width: mw,
                height: mh,
            });
        }

        // Shared sampler (create once).
        if self.sampler.is_none() {
            self.sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
                label: Some("post_sampler"),
                address_mode_u: wgpu::AddressMode::ClampToEdge,
                address_mode_v: wgpu::AddressMode::ClampToEdge,
                address_mode_w: wgpu::AddressMode::ClampToEdge,
                mag_filter: wgpu::FilterMode::Linear,
                min_filter: wgpu::FilterMode::Linear,
                mipmap_filter: wgpu::MipmapFilterMode::Linear,
                ..Default::default()
            }));
        }

        // 1x1 black fallback for bloom (create once).
        if self.black_texture.is_none() {
            let tex = device.create_texture(&wgpu::TextureDescriptor {
                label: Some("post_black_1x1"),
                size: wgpu::Extent3d {
                    width: 1,
                    height: 1,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: HDR_FORMAT,
                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
                view_formats: &[],
            });
            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
            self.black_texture = Some((tex, view));
        }

        self.initialized = true;
    }

    // -----------------------------------------------------------------------
    // Pipeline creation
    // -----------------------------------------------------------------------

    /// Create pipelines and bind group layouts if they do not exist yet.
    pub fn ensure_pipelines(&mut self, device: &wgpu::Device) {
        if self.bloom_downsample_pipeline.is_some() {
            return;
        }

        // --- Bloom BGL ---
        let bloom_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("post_bloom_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        let bloom_shader = shader::load_wgsl(device, "bloom", BLOOM_SHADER_SRC).0;

        let bloom_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("post_bloom_pipeline_layout"),
            bind_group_layouts: &[Some(&bloom_bgl)],
            immediate_size: 0,
        });

        // Bloom downsample pipeline.
        self.bloom_downsample_pipeline = Some(device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("post_bloom_downsample"),
            layout: Some(&bloom_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &bloom_shader,
                entry_point: Some("vs_main"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &bloom_shader,
                entry_point: Some("fs_downsample"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: HDR_FORMAT,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                ..Default::default()
            },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        }));

        // Bloom upsample pipeline (additive blend: src One, dst One).
        self.bloom_upsample_pipeline = Some(device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("post_bloom_upsample"),
            layout: Some(&bloom_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &bloom_shader,
                entry_point: Some("vs_main"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &bloom_shader,
                entry_point: Some("fs_upsample"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: HDR_FORMAT,
                    blend: Some(wgpu::BlendState {
                        color: wgpu::BlendComponent {
                            src_factor: wgpu::BlendFactor::One,
                            dst_factor: wgpu::BlendFactor::One,
                            operation: wgpu::BlendOperation::Add,
                        },
                        alpha: wgpu::BlendComponent {
                            src_factor: wgpu::BlendFactor::One,
                            dst_factor: wgpu::BlendFactor::One,
                            operation: wgpu::BlendOperation::Add,
                        },
                    }),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                ..Default::default()
            },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        }));

        // Bloom uniform buffer.
        self.bloom_uniform_buf = Some(shader::create_uniform_buffer(
            device,
            "post_bloom_uniforms",
            &BloomUniforms {
                threshold: self.config.bloom_threshold,
                intensity: self.config.bloom_intensity,
                texel_size: [0.0, 0.0],
            },
        ));

        self.bloom_bgl = Some(bloom_bgl);

        // --- Tonemap BGL ---
        let tonemap_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("post_tonemap_bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // TD-004: DOF texture (binding 4)
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // TD-008: Volumetric fog texture (binding 5)
                wgpu::BindGroupLayoutEntry {
                    binding: 5,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
            ],
        });

        let tonemap_shader = shader::load_wgsl(device, "tonemap", TONEMAP_SHADER_SRC).0;

        let tonemap_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("post_tonemap_pipeline_layout"),
            bind_group_layouts: &[Some(&tonemap_bgl)],
            immediate_size: 0,
        });

        self.tonemap_pipeline = Some(device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("post_tonemap"),
            layout: Some(&tonemap_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &tonemap_shader,
                entry_point: Some("vs_main"),
                buffers: &[],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &tonemap_shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: self.surface_format,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                ..Default::default()
            },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        }));

        // Tonemap uniform buffer.
        self.tonemap_uniform_buf = Some(shader::create_uniform_buffer(
            device,
            "post_tonemap_uniforms",
            &TonemapUniforms {
                exposure: self.config.exposure,
                bloom_intensity: self.config.bloom_intensity,
                tonemap_operator: tonemap_operator_to_u32(self.config.tonemap_operator),
                dof_enabled: 0,
                fog_enabled: 0,
                _pad_align: [0.0; 3],
                _pad: [0.0; 3],
                _pad_tail: 0.0,
            },
        ));

        self.tonemap_bgl = Some(tonemap_bgl);
    }

    // -----------------------------------------------------------------------
    // Accessors
    // -----------------------------------------------------------------------

    /// HDR color view for the main-pass render target.
    pub fn hdr_view(&self) -> Option<&wgpu::TextureView> {
        self.hdr_view.as_ref()
    }

    /// TD-006: HDR texture for TAA/Dream TSR output swap (copy-to-texture target).
    pub fn hdr_texture(&self) -> Option<&wgpu::Texture> {
        self.hdr_texture.as_ref()
    }

    /// HDR depth view for the main-pass depth attachment.
    pub fn hdr_depth_view(&self) -> Option<&wgpu::TextureView> {
        self.hdr_depth_view.as_ref()
    }

    // -----------------------------------------------------------------------
    // Bloom pass
    // -----------------------------------------------------------------------

    /// Execute the bloom downsample + upsample passes.
    ///
    /// Expects `resize` and `ensure_pipelines` to have been called.
    pub fn run_bloom(&self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder) {
        let (Some(bloom_bgl), Some(sampler), Some(bloom_buf)) =
            (&self.bloom_bgl, &self.sampler, &self.bloom_uniform_buf)
        else {
            return;
        };
        let (Some(downsample_pipe), Some(upsample_pipe)) =
            (&self.bloom_downsample_pipeline, &self.bloom_upsample_pipeline)
        else {
            return;
        };
        let (Some(hdr_view), Some(mip0), Some(mip1)) = (&self.hdr_view, &self.bloom_mips[0], &self.bloom_mips[1])
        else {
            return;
        };

        // -- Pass 1: Downsample HDR -> mip[0] --
        let texel_size_0 = [1.0 / mip0.width as f32, 1.0 / mip0.height as f32];
        shader::update_uniform_buffer(
            queue,
            bloom_buf,
            &BloomUniforms {
                threshold: self.config.bloom_threshold,
                intensity: self.config.bloom_intensity,
                texel_size: texel_size_0,
            },
        );

        let bg_down0 = create_bloom_bind_group(device, bloom_bgl, hdr_view, sampler, bloom_buf);
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("bloom_downsample_0"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &mip0.view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(downsample_pipe);
            pass.set_bind_group(0, &bg_down0, &[]);
            pass.draw(0..3, 0..1);
        }

        // -- Pass 2: Downsample mip[0] -> mip[1] --
        let texel_size_1 = [1.0 / mip1.width as f32, 1.0 / mip1.height as f32];
        shader::update_uniform_buffer(
            queue,
            bloom_buf,
            &BloomUniforms {
                threshold: 0.0, // already thresholded
                intensity: self.config.bloom_intensity,
                texel_size: texel_size_1,
            },
        );

        let bg_down1 = create_bloom_bind_group(device, bloom_bgl, &mip0.view, sampler, bloom_buf);
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("bloom_downsample_1"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &mip1.view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(downsample_pipe);
            pass.set_bind_group(0, &bg_down1, &[]);
            pass.draw(0..3, 0..1);
        }

        // -- Pass 3: Upsample mip[1] -> mip[0] (additive) --
        shader::update_uniform_buffer(
            queue,
            bloom_buf,
            &BloomUniforms {
                threshold: 0.0,
                intensity: self.config.bloom_intensity,
                texel_size: texel_size_0,
            },
        );

        let bg_up = create_bloom_bind_group(device, bloom_bgl, &mip1.view, sampler, bloom_buf);
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("bloom_upsample"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &mip0.view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(upsample_pipe);
            pass.set_bind_group(0, &bg_up, &[]);
            pass.draw(0..3, 0..1);
        }
    }

    // -----------------------------------------------------------------------
    // Tonemap pass
    // -----------------------------------------------------------------------

    /// Execute the tonemapping pass, writing to the surface view.
    ///
    /// Expects `resize` and `ensure_pipelines` to have been called.
    pub fn run_tonemap(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        surface_view: &wgpu::TextureView,
    ) {
        let (Some(tonemap_bgl), Some(sampler), Some(tonemap_buf)) =
            (&self.tonemap_bgl, &self.sampler, &self.tonemap_uniform_buf)
        else {
            return;
        };
        let (Some(tonemap_pipe), Some(hdr_view)) = (&self.tonemap_pipeline, &self.hdr_view) else {
            return;
        };

        // Update tonemap uniforms (TD-004 + TD-008: DOF + fog flags).
        shader::update_uniform_buffer(
            queue,
            tonemap_buf,
            &TonemapUniforms {
                exposure: self.config.exposure,
                bloom_intensity: if self.config.bloom_enabled {
                    self.config.bloom_intensity
                } else {
                    0.0
                },
                tonemap_operator: tonemap_operator_to_u32(self.config.tonemap_operator),
                dof_enabled: 0, // DOF compositing controlled by caller via config
                fog_enabled: 0, // Fog compositing controlled by caller via config
                _pad_align: [0.0; 3],
                _pad: [0.0; 3],
                _pad_tail: 0.0,
            },
        );

        // Bloom source: mip[0] if bloom ran, else 1x1 black fallback.
        let bloom_view = if self.config.bloom_enabled {
            self.bloom_mips[0].as_ref().map(|m| &m.view)
        } else {
            None
        };
        let bloom_view = bloom_view
            .or_else(|| self.black_texture.as_ref().map(|(_, v)| v))
            .expect("black_texture should be initialized after resize");

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("post_tonemap_bg"),
            layout: tonemap_bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(hdr_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(sampler),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: tonemap_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(bloom_view),
                },
                // TD-004: DOF fallback (1x1 black = no DOF)
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(
                        self.black_texture
                            .as_ref()
                            .map(|(_, v)| v)
                            .expect("black_texture should be initialized after resize"),
                    ),
                },
                // TD-008: Volumetric fog fallback (1x1 black = no fog)
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::TextureView(
                        self.black_texture
                            .as_ref()
                            .map(|(_, v)| v)
                            .expect("black_texture should be initialized after resize"),
                    ),
                },
            ],
        });

        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("tonemap"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: surface_view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            pass.set_pipeline(tonemap_pipe);
            pass.set_bind_group(0, &bind_group, &[]);
            pass.draw(0..3, 0..1);
        }
    }

    // -----------------------------------------------------------------------
    // Cleanup
    // -----------------------------------------------------------------------

    /// Destroy all GPU resources held by this stack.
    pub fn destroy(&mut self) {
        self.hdr_texture = None;
        self.hdr_view = None;
        self.hdr_depth_texture = None;
        self.hdr_depth_view = None;
        self.bloom_mips = [None, None];
        self.bloom_downsample_pipeline = None;
        self.bloom_upsample_pipeline = None;
        self.bloom_bgl = None;
        self.bloom_uniform_buf = None;
        self.tonemap_pipeline = None;
        self.tonemap_bgl = None;
        self.tonemap_uniform_buf = None;
        self.sampler = None;
        self.black_texture = None;
        self.initialized = false;
        self.width = 0;
        self.height = 0;
    }
}

// ---------------------------------------------------------------------------
// Free-standing helpers
// ---------------------------------------------------------------------------

fn create_bloom_bind_group(
    device: &wgpu::Device,
    layout: &wgpu::BindGroupLayout,
    src_view: &wgpu::TextureView,
    sampler: &wgpu::Sampler,
    uniform_buf: &wgpu::Buffer,
) -> wgpu::BindGroup {
    device.create_bind_group(&wgpu::BindGroupDescriptor {
        label: Some("post_bloom_bg"),
        layout,
        entries: &[
            wgpu::BindGroupEntry {
                binding: 0,
                resource: wgpu::BindingResource::TextureView(src_view),
            },
            wgpu::BindGroupEntry {
                binding: 1,
                resource: wgpu::BindingResource::Sampler(sampler),
            },
            wgpu::BindGroupEntry {
                binding: 2,
                resource: uniform_buf.as_entire_binding(),
            },
        ],
    })
}

fn tonemap_operator_to_u32(op: TonemapOperator) -> u32 {
    match op {
        TonemapOperator::AcesFilmic => 0,
        TonemapOperator::Uncharted2 => 1,
        TonemapOperator::Reinhard => 2,
        TonemapOperator::None => 3,
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn default_config_disabled() {
        let config = PostProcessConfig::default();
        assert!(!config.bloom_enabled);
        assert!(!config.tonemap_enabled);
        assert_eq!(config.bloom_threshold, 1.0);
        assert_eq!(config.bloom_intensity, 0.5);
        assert_eq!(config.exposure, 1.0);
    }

    #[test]
    fn config_with_bloom_enabled() {
        let mut stack = PostProcessStack::new(wgpu::TextureFormat::Bgra8UnormSrgb);
        stack.set_config(PostProcessConfig {
            bloom_enabled: true,
            ..Default::default()
        });
        assert!(stack.is_enabled());
    }

    #[test]
    fn config_with_tonemap_enabled() {
        let mut stack = PostProcessStack::new(wgpu::TextureFormat::Bgra8UnormSrgb);
        stack.set_config(PostProcessConfig {
            tonemap_enabled: true,
            ..Default::default()
        });
        assert!(stack.is_enabled());
    }

    #[test]
    fn hdr_format_is_rgba16float() {
        assert_eq!(HDR_FORMAT, wgpu::TextureFormat::Rgba16Float);
    }

    #[test]
    fn for_pbr_config() {
        let config = PostProcessConfig::for_pbr();
        assert!(config.bloom_enabled);
        assert!(config.tonemap_enabled);
        assert_eq!(config.bloom_threshold, 1.0);
        assert_eq!(config.bloom_intensity, 0.04);
        assert_eq!(config.exposure, 1.0);
    }

    #[test]
    fn new_not_initialized() {
        let stack = PostProcessStack::new(wgpu::TextureFormat::Bgra8UnormSrgb);
        assert!(!stack.initialized);
        assert!(!stack.is_enabled());
        assert!(stack.hdr_view().is_none());
        assert!(stack.hdr_depth_view().is_none());
    }
}
