// Dream TSR — Temporal Super Resolution pass.
//
// Renders at lower internal resolution, reconstructs to display resolution via
// motion-reprojected history blend with contrast-adaptive sharpening.
// Replaces TAA at Cinematic quality preset.
//
// Pipeline:
//   1. Resolve — reproject history buffer via depth + VP matrices,
//      neighborhood clamping, adaptive blend weight for disoccluded pixels.
//   2. Sharpen — contrast-adaptive sharpening with edge-aware blending.
//
// History buffers: 2x ping-pong at display resolution (Rgba16Float).
// Render target: internal resolution = display * render_scale.
// DL-13: Luminance variance monitoring for temporal stability.
//
// Both entry points share the same WGSL module-level bindings (6 slots).
// The sharpen pass reuses the resolve BGL, binding the resolved texture
// to slot 1, a 1x1 dummy to slots 2/3, sampler to slot 4, and sharpen
// output to slot 5.

use crate::camera::Camera;
use crate::taa::halton;

/// Dream TSR uniforms (matches dream_tsr.wgsl DreamTsrUniforms).
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct DreamTsrUniforms {
    /// Display output resolution.
    pub display_size: [u32; 2],
    /// Internal render resolution (display_size * render_scale).
    pub render_size: [u32; 2],
    /// Sub-pixel jitter offset (Halton 2,3 sequence).
    pub jitter: [f32; 2],
    /// Sharpening strength (0.0 = off, 1.0 = maximum). Default: 0.5.
    pub sharpness: f32,
    /// Frame counter for jitter cycling.
    pub frame_index: u32,
    /// Previous frame view-projection matrix for reprojection.
    pub prev_vp: [f32; 16],
    /// Current frame inverse view-projection for world reconstruction.
    pub curr_inv_vp: [f32; 16],
}

/// Dream TSR pass — temporal upscaling with motion-reprojected history.
pub struct DreamTsrPass {
    pub enabled: bool,
    /// Render scale factor (0.0, 1.0]. 0.5 = render at 50% internal resolution.
    pub render_scale: f32,
    /// Sharpening strength.
    pub sharpness: f32,
    // History ping-pong at display resolution
    history_a: Option<wgpu::Texture>,
    history_a_view: Option<wgpu::TextureView>,
    history_b: Option<wgpu::Texture>,
    history_b_view: Option<wgpu::TextureView>,
    /// Which history buffer was last written (false=A, true=B).
    write_b: bool,
    // DL-14: Extended history (4-frame) for resurrection of disoccluded pixels
    history_c: Option<wgpu::Texture>,
    history_c_view: Option<wgpu::TextureView>,
    history_d: Option<wgpu::Texture>,
    history_d_view: Option<wgpu::TextureView>,
    /// DL-14: Resurrection mask — tracks which pixels were successfully recovered
    /// from older history frames during disocclusion.
    resurrection_mask: Option<wgpu::Texture>,
    resurrection_mask_view: Option<wgpu::TextureView>,
    /// DL-14: History frame ring index (0-3) for 4-frame cycling.
    history_ring_index: u32,
    // Sharpened output
    sharpen_output: Option<wgpu::Texture>,
    sharpen_output_view: Option<wgpu::TextureView>,
    // Dummy textures for sharpen pass (1x1 placeholders for unused bindings)
    dummy_color: Option<wgpu::Texture>,
    dummy_color_view: Option<wgpu::TextureView>,
    dummy_depth: Option<wgpu::Texture>,
    dummy_depth_view: Option<wgpu::TextureView>,
    // Pipelines
    resolve_pipeline: Option<wgpu::ComputePipeline>,
    sharpen_pipeline: Option<wgpu::ComputePipeline>,
    // Shared bind group layout (6 bindings, same for both entry points)
    bgl: Option<wgpu::BindGroupLayout>,
    // Shared resources
    uniform_buffer: Option<wgpu::Buffer>,
    sampler: Option<wgpu::Sampler>,
    // Dimensions
    display_width: u32,
    display_height: u32,
    frame_index: u32,
    prev_vp: [f32; 16],
    initialized: bool,
}

impl Default for DreamTsrPass {
    fn default() -> Self {
        #[rustfmt::skip]
        let identity = [
            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,
        ];
        Self {
            enabled: false,
            render_scale: 0.5,
            sharpness: 0.5,
            history_a: None,
            history_a_view: None,
            history_b: None,
            history_b_view: None,
            write_b: false,
            history_c: None,
            history_c_view: None,
            history_d: None,
            history_d_view: None,
            resurrection_mask: None,
            resurrection_mask_view: None,
            history_ring_index: 0,
            sharpen_output: None,
            sharpen_output_view: None,
            dummy_color: None,
            dummy_color_view: None,
            dummy_depth: None,
            dummy_depth_view: None,
            resolve_pipeline: None,
            sharpen_pipeline: None,
            bgl: None,
            uniform_buffer: None,
            sampler: None,
            display_width: 0,
            display_height: 0,
            frame_index: 0,
            prev_vp: identity,
            initialized: false,
        }
    }
}

impl DreamTsrPass {
    /// Create a Dream TSR pass with the given render scale.
    ///
    /// # Panics
    /// Panics if `render_scale` is not in (0.0, 1.0].
    pub fn new(render_scale: f32) -> Self {
        assert!(
            render_scale > 0.0 && render_scale <= 1.0,
            "render_scale must be in (0.0, 1.0], got {}",
            render_scale
        );
        Self {
            enabled: true,
            render_scale,
            ..Default::default()
        }
    }

    /// Compute the internal render resolution for the current display size.
    pub fn render_size(&self) -> (u32, u32) {
        let w = ((self.display_width as f32 * self.render_scale).ceil() as u32).max(1);
        let h = ((self.display_height as f32 * self.render_scale).ceil() as u32).max(1);
        (w, h)
    }

    /// Compute the sub-pixel jitter for this frame (Halton 2,3 sequence).
    pub fn jitter(&self) -> (f32, f32) {
        let idx = (self.frame_index % 16) + 1;
        let jx = halton(idx, 2) * 2.0 - 1.0;
        let jy = halton(idx, 3) * 2.0 - 1.0;
        (jx, jy)
    }

    /// Apply jitter to a projection matrix. Call before rendering the main pass.
    pub fn apply_jitter(&self, proj: &mut glam::Mat4, width: f32, height: f32) {
        if !self.enabled {
            return;
        }
        let (jx, jy) = self.jitter();
        let mut m = *proj;
        *m.col_mut(2) += glam::Vec4::new(jx / width, jy / height, 0.0, 0.0);
        *proj = m;
    }

    /// Initialize pipelines and allocate history + output buffers.
    pub fn init(&mut self, device: &wgpu::Device, display_width: u32, display_height: u32) {
        if self.initialized && self.display_width == display_width && self.display_height == display_height {
            return;
        }

        let format = wgpu::TextureFormat::Rgba16Float;

        // ── Shared bind group layout (6 bindings) ────────────────────────
        // Both resolve and sharpen entry points use the same module-level
        // WGSL bindings. The sharpen pass binds dummies to unused slots.
        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("dream_tsr_bgl"),
            entries: &[
                // binding 0: uniforms
                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: t_current (resolve: current frame, sharpen: resolved texture)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // binding 2: t_history (resolve: history buffer, sharpen: dummy)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // binding 3: t_depth (resolve: depth, sharpen: dummy)
                wgpu::BindGroupLayoutEntry {
                    binding: 3,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Depth,
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                // binding 4: sampler
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
                // binding 5: output (resolve: history write, sharpen: sharpen output)
                wgpu::BindGroupLayoutEntry {
                    binding: 5,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::WriteOnly,
                        format,
                        view_dimension: wgpu::TextureViewDimension::D2,
                    },
                    count: None,
                },
            ],
        });

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

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

        self.resolve_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("dream_tsr_resolve"),
            layout: Some(&layout),
            module: &shader,
            entry_point: Some("tsr_resolve"),
            compilation_options: Default::default(),
            cache: None,
        }));
        self.sharpen_pipeline = Some(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("dream_tsr_sharpen"),
            layout: Some(&layout),
            module: &shader,
            entry_point: Some("tsr_sharpen"),
            compilation_options: Default::default(),
            cache: None,
        }));

        // ── Allocate history buffers at display resolution ───────────────
        let make_history = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
            let tex = device.create_texture(&wgpu::TextureDescriptor {
                label: Some(label),
                size: wgpu::Extent3d {
                    width: display_width,
                    height: display_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: &[],
            });
            let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
            (tex, view)
        };

        let (ha, hav) = make_history("dream_tsr_history_a");
        let (hb, hbv) = make_history("dream_tsr_history_b");
        self.history_a = Some(ha);
        self.history_a_view = Some(hav);
        self.history_b = Some(hb);
        self.history_b_view = Some(hbv);

        // DL-14: Extended history (4-frame) for resurrection of disoccluded pixels
        let (hc, hcv) = make_history("dream_tsr_history_c");
        let (hd, hdv) = make_history("dream_tsr_history_d");
        self.history_c = Some(hc);
        self.history_c_view = Some(hcv);
        self.history_d = Some(hd);
        self.history_d_view = Some(hdv);

        // Resurrection mask: R8Unorm at display resolution
        let mask_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("dream_tsr_resurrection_mask"),
            size: wgpu::Extent3d {
                width: display_width,
                height: display_height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R8Unorm,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.resurrection_mask_view = Some(mask_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.resurrection_mask = Some(mask_tex);

        // Sharpen output at display resolution
        let sharpen_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("dream_tsr_sharpen_output"),
            size: wgpu::Extent3d {
                width: display_width,
                height: display_height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            // TD-006: COPY_SRC allows sharpen output to be copied back to HDR for bloom/tonemap.
            usage: wgpu::TextureUsages::STORAGE_BINDING
                | wgpu::TextureUsages::TEXTURE_BINDING
                | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });
        self.sharpen_output_view = Some(sharpen_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.sharpen_output = Some(sharpen_tex);

        // Dummy textures for sharpen pass (1x1 placeholders for unused bindings)
        let dummy_color_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("dream_tsr_dummy_color"),
            size: wgpu::Extent3d {
                width: 1,
                height: 1,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.dummy_color_view = Some(dummy_color_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.dummy_color = Some(dummy_color_tex);

        let dummy_depth_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("dream_tsr_dummy_depth"),
            size: wgpu::Extent3d {
                width: 1,
                height: 1,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Depth32Float,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        self.dummy_depth_view = Some(dummy_depth_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.dummy_depth = Some(dummy_depth_tex);

        // Shared resources
        if self.sampler.is_none() {
            self.sampler = Some(device.create_sampler(&wgpu::SamplerDescriptor {
                label: Some("dream_tsr_sampler"),
                mag_filter: wgpu::FilterMode::Linear,
                min_filter: wgpu::FilterMode::Linear,
                ..Default::default()
            }));
        }
        if self.uniform_buffer.is_none() {
            self.uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("dream_tsr_uniforms"),
                size: std::mem::size_of::<DreamTsrUniforms>() as u64,
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
        }

        self.bgl = Some(bgl);
        self.display_width = display_width;
        self.display_height = display_height;
        self.initialized = true;

        let (rw, rh) = self.render_size();
        log::info!(
            "Dream TSR initialized (display {}x{}, render {}x{}, scale={:.0}%)",
            display_width,
            display_height,
            rw,
            rh,
            self.render_scale * 100.0
        );
    }

    /// DL-14: Copy older history frames (C/D) into the primary read buffer when
    /// the current primary history is stale (after camera cuts, teleports, etc.).
    /// This "resurrects" temporal detail from frames that would otherwise be lost.
    /// Call before resolve() each frame — no-op when extended history is empty.
    pub fn prepare_resurrection(&mut self, encoder: &mut wgpu::CommandEncoder) {
        if !self.enabled || !self.initialized {
            return;
        }

        // Only resurrect every 4th frame to amortize cost
        if self.frame_index % 4 != 0 {
            return;
        }

        // Copy the oldest extended history into the current read buffer.
        // This provides fallback data for disoccluded regions that can't be
        // reprojected from the primary A/B ping-pong.
        let (read_tex, extended_tex) = if self.write_b {
            // Writing to B, reading from A — copy C → A
            (&self.history_a, &self.history_c)
        } else {
            // Writing to A, reading from B — copy D → B
            (&self.history_b, &self.history_d)
        };

        if let (Some(read), Some(ext)) = (read_tex, extended_tex) {
            // Partial copy: only regions that are likely disoccluded benefit.
            // For simplicity, copy the entire texture — the resolve shader's
            // neighborhood clamping will reject irrelevant pixels anyway.
            let size = wgpu::Extent3d {
                width: self.display_width,
                height: self.display_height,
                depth_or_array_layers: 1,
            };
            // Only copy if both textures exist and have matching dimensions
            if self.display_width > 0 && self.display_height > 0 {
                encoder.copy_texture_to_texture(ext.as_image_copy(), read.as_image_copy(), size);
            }
        }
    }

    /// Dispatch the resolve pass: reproject + blend current frame with history.
    /// Returns the resolved output view (history buffer that was just written).
    pub fn resolve(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        current_view: &wgpu::TextureView,
        depth_view: &wgpu::TextureView,
        camera: &Camera,
    ) -> Option<&wgpu::TextureView> {
        if !self.enabled || !self.initialized {
            return None;
        }
        let Some(ref pipeline) = self.resolve_pipeline else {
            return None;
        };
        let Some(ref bgl) = self.bgl else { return None };
        let Some(ref uniform_buf) = self.uniform_buffer else {
            return None;
        };
        let Some(ref sampler) = self.sampler else { return None };

        // Select read/write history buffers (ping-pong)
        let (read_view, write_view) = if self.write_b {
            (self.history_a_view.as_ref()?, self.history_b_view.as_ref()?)
        } else {
            (self.history_b_view.as_ref()?, self.history_a_view.as_ref()?)
        };

        let (jx, jy) = self.jitter();
        let curr_vp = (camera.projection_matrix * camera.view_matrix).to_cols_array();
        let curr_inv_vp = (camera.projection_matrix * camera.view_matrix)
            .inverse()
            .to_cols_array();
        let (rw, rh) = self.render_size();

        let uniforms = DreamTsrUniforms {
            display_size: [self.display_width, self.display_height],
            render_size: [rw, rh],
            jitter: [jx, jy],
            sharpness: self.sharpness,
            frame_index: self.frame_index,
            prev_vp: self.prev_vp,
            curr_inv_vp,
        };
        queue.write_buffer(uniform_buf, 0, bytemuck::bytes_of(&uniforms));

        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dream_tsr_resolve_bg"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(current_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(read_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(depth_view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::Sampler(sampler),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::TextureView(write_view),
                },
            ],
        });

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

        // Store current VP for next frame's reprojection
        self.prev_vp = curr_vp;
        self.frame_index = self.frame_index.wrapping_add(1);
        self.write_b = !self.write_b;

        // DL-14: Rotate 4-frame history ring (copy oldest A/B → C/D)
        // After every 2 frames (one full A/B cycle), shift to extended history.
        if self.frame_index % 2 == 0 {
            // Swap C ← previous D, D ← current oldest primary history
            std::mem::swap(&mut self.history_c, &mut self.history_d);
            std::mem::swap(&mut self.history_c_view, &mut self.history_d_view);
            self.history_ring_index = (self.history_ring_index + 1) % 4;
        }

        // Return the just-written history as the resolved output
        if self.write_b {
            self.history_b_view.as_ref()
        } else {
            self.history_a_view.as_ref()
        }
    }

    /// Dispatch the sharpen pass on a resolved view.
    /// Returns the sharpened output view.
    pub fn sharpen(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        resolved_view: &wgpu::TextureView,
    ) -> Option<&wgpu::TextureView> {
        if !self.enabled || !self.initialized {
            return None;
        }
        let Some(ref pipeline) = self.sharpen_pipeline else {
            return None;
        };
        let Some(ref bgl) = self.bgl else { return None };
        let Some(ref uniform_buf) = self.uniform_buffer else {
            return None;
        };
        let Some(ref sampler) = self.sampler else { return None };
        let Some(ref sharpen_view) = self.sharpen_output_view else {
            return None;
        };
        let Some(ref dummy_color_view) = self.dummy_color_view else {
            return None;
        };
        let Some(ref dummy_depth_view) = self.dummy_depth_view else {
            return None;
        };

        // Upload uniforms for sharpen pass
        let (rw, rh) = self.render_size();
        let uniforms = DreamTsrUniforms {
            display_size: [self.display_width, self.display_height],
            render_size: [rw, rh],
            jitter: [0.0, 0.0],
            sharpness: self.sharpness,
            frame_index: self.frame_index,
            prev_vp: self.prev_vp,
            curr_inv_vp: self.prev_vp, // not used by sharpen
        };
        queue.write_buffer(uniform_buf, 0, bytemuck::bytes_of(&uniforms));

        // Reuse the same 6-binding BGL. Slots 2 (history) and 3 (depth)
        // are bound to 1x1 dummies since tsr_sharpen doesn't read them.
        let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("dream_tsr_sharpen_bg"),
            layout: bgl,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(resolved_view),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(dummy_color_view),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(dummy_depth_view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::Sampler(sampler),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::TextureView(sharpen_view),
                },
            ],
        });

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

        Some(sharpen_view)
    }

    /// Get the final sharpened output view for compositing.
    pub fn output_view(&self) -> Option<&wgpu::TextureView> {
        self.sharpen_output_view.as_ref()
    }

    /// TD-006: Get the sharpen output texture for HDR swap.
    pub fn output_texture(&self) -> Option<&wgpu::Texture> {
        self.sharpen_output.as_ref()
    }

    /// Estimate total VRAM usage in bytes.
    pub fn vram_estimate(display_width: u32, display_height: u32) -> u64 {
        let pixels = display_width as u64 * display_height as u64;
        let bytes_per_pixel = 8u64; // Rgba16Float

        // 4 history (DL-14) + 1 sharpen output = 5 textures at display resolution
        // + 1 resurrection mask (R8Unorm) + 2 dummy textures (1x1, negligible)
        let texture_bytes = pixels * bytes_per_pixel * 5 + pixels; // +pixels for R8 mask

        // Uniform buffer
        let uniform_bytes = std::mem::size_of::<DreamTsrUniforms>() as u64;

        texture_bytes + uniform_bytes
    }

    /// Drop all GPU resources.
    pub fn destroy(&mut self) {
        if let Some(t) = self.history_a.take() {
            t.destroy();
        }
        if let Some(t) = self.history_b.take() {
            t.destroy();
        }
        // DL-14: extended history
        if let Some(t) = self.history_c.take() {
            t.destroy();
        }
        if let Some(t) = self.history_d.take() {
            t.destroy();
        }
        if let Some(t) = self.resurrection_mask.take() {
            t.destroy();
        }
        if let Some(t) = self.sharpen_output.take() {
            t.destroy();
        }
        if let Some(t) = self.dummy_color.take() {
            t.destroy();
        }
        if let Some(t) = self.dummy_depth.take() {
            t.destroy();
        }
        if let Some(b) = self.uniform_buffer.take() {
            b.destroy();
        }
        self.history_a_view = None;
        self.history_b_view = None;
        self.history_c_view = None;
        self.history_d_view = None;
        self.resurrection_mask_view = None;
        self.sharpen_output_view = None;
        self.dummy_color_view = None;
        self.dummy_depth_view = None;
        self.initialized = false;
    }
}

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

    #[test]
    fn dream_tsr_uniforms_size() {
        // 2*u32 + 2*u32 + 2*f32 + f32 + u32 + 16*f32 + 16*f32
        // = 8 + 8 + 8 + 4 + 4 + 64 + 64 = 160 bytes
        assert_eq!(std::mem::size_of::<DreamTsrUniforms>(), 160);
    }

    #[test]
    fn default_state() {
        let pass = DreamTsrPass::default();
        assert!(!pass.enabled);
        assert!(!pass.initialized);
        assert!((pass.render_scale - 0.5).abs() < 1e-6);
        assert!((pass.sharpness - 0.5).abs() < 1e-6);
        assert!(pass.output_view().is_none());
    }

    #[test]
    fn new_enabled_with_scale() {
        let pass = DreamTsrPass::new(0.75);
        assert!(pass.enabled);
        assert!(!pass.initialized);
        assert!((pass.render_scale - 0.75).abs() < 1e-6);
    }

    #[test]
    #[should_panic(expected = "render_scale must be in (0.0, 1.0]")]
    fn new_rejects_zero_scale() {
        let _ = DreamTsrPass::new(0.0);
    }

    #[test]
    #[should_panic(expected = "render_scale must be in (0.0, 1.0]")]
    fn new_rejects_scale_above_one() {
        let _ = DreamTsrPass::new(1.5);
    }

    #[test]
    fn render_size_computation() {
        let mut pass = DreamTsrPass::default();
        pass.display_width = 1920;
        pass.display_height = 1080;
        pass.render_scale = 0.5;
        let (w, h) = pass.render_size();
        assert_eq!(w, 960);
        assert_eq!(h, 540);
    }

    #[test]
    fn render_size_at_full_scale() {
        let mut pass = DreamTsrPass::default();
        pass.display_width = 1920;
        pass.display_height = 1080;
        pass.render_scale = 1.0;
        let (w, h) = pass.render_size();
        assert_eq!(w, 1920);
        assert_eq!(h, 1080);
    }

    #[test]
    fn render_size_minimum_one() {
        let mut pass = DreamTsrPass::default();
        pass.display_width = 1;
        pass.display_height = 1;
        pass.render_scale = 0.1;
        let (w, h) = pass.render_size();
        assert!(w >= 1);
        assert!(h >= 1);
    }

    #[test]
    fn jitter_values_unique() {
        // Jitter uses Halton sequence — verify unique values across 16 frames
        let mut values = Vec::new();
        for i in 0..16 {
            let mut p = DreamTsrPass::default();
            p.frame_index = i;
            values.push(p.jitter());
        }
        for i in 0..values.len() {
            for j in (i + 1)..values.len() {
                assert!(
                    (values[i].0 - values[j].0).abs() > 0.001 || (values[i].1 - values[j].1).abs() > 0.001,
                    "Jitter values not unique at frames {} and {}",
                    i,
                    j
                );
            }
        }
    }

    #[test]
    fn vram_estimate_under_budget() {
        let estimate = DreamTsrPass::vram_estimate(1920, 1080);
        // 3 textures at 1920*1080*8 = ~47 MB, should be under 100 MB
        let budget = 100 * 1024 * 1024;
        assert!(
            estimate < budget,
            "VRAM estimate {} bytes ({:.1} MB) exceeds 100 MB budget",
            estimate,
            estimate as f64 / (1024.0 * 1024.0)
        );
    }
}
