// RT GI — Ray-Traced Global Illumination compute pass manager.
// SHaRC-style radiance cache: hash grid stores accumulated radiance,
// resolve computes per-cell irradiance, query samples into screen-space GI.
// Dispatches between main render pass and post-processing.
// Requires EXPERIMENTAL_RAY_QUERY feature (TLAS acceleration structure).
// Output: Rgba16Float texture sampled in PBR ambient/indirect term.

use crate::camera::Camera;

/// Configuration for the RT GI radiance cache.
#[derive(Debug, Clone)]
pub struct RtGiConfig {
    /// Number of hash grid entries (must be power of two). Default: 2^20.
    pub hash_grid_capacity: u32,
    /// Logarithmic base for spatial hashing cascade levels. Default: 2.0.
    pub logarithm_base: f32,
    /// World-space scale factor for hash grid cell sizing. Default: 1.0.
    pub scene_scale: f32,
    /// Maximum frames to accumulate before clamping. Default: 32.
    pub max_accumulated_frames: u32,
    /// Frames before a stale entry is evicted. Default: 128.
    pub stale_frame_max: u32,
    /// Fraction of grid entries updated per frame (sparse update). Default: 0.04.
    pub sparse_rate: f32,
    /// Maximum indirect bounces. Default: 3.
    pub max_bounces: u32,
    /// Enable screen-space tracing before hardware RT (DL-02). Default: false.
    pub screen_traces_enabled: bool,
    /// Maximum roughness for reflection traces (DL-12). Default: 0.5.
    pub reflection_max_roughness: f32,
    /// Enable hit lighting for smooth surfaces (DL-04). Default: false.
    pub hit_lighting_enabled: bool,
    /// Far field scale factor for extended hash grid range (DL-09). Default: 1.0.
    pub far_field_scale: f32,
    /// Observer FOV radius override (set by fabric per-frame). None = use default 200.0.
    pub fov_radius_override: Option<f32>,
    /// Observer topology layer override (set by fabric per-frame). None = use default 0.
    pub topology_layer_override: Option<u32>,
}

impl Default for RtGiConfig {
    fn default() -> Self {
        Self {
            hash_grid_capacity: 1_048_576, // 2^20
            logarithm_base: 2.0,
            scene_scale: 1.0,
            max_accumulated_frames: 32,
            stale_frame_max: 128,
            sparse_rate: 0.04,
            max_bounces: 3,
            screen_traces_enabled: false,
            reflection_max_roughness: 0.5,
            hit_lighting_enabled: false,
            far_field_scale: 1.0,
            fov_radius_override: None,
            topology_layer_override: None,
        }
    }
}

/// GPU uniforms for the RT GI shader (must be #[repr(C)] + Pod + Zeroable, 192 bytes).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RtGiUniforms {
    pub view_proj_inv: [f32; 16],
    pub prev_view_proj: [f32; 16],
    pub camera_position: [f32; 3],
    pub frame_index: u32,
    pub screen_size: [u32; 2],
    pub grid_log_base: f32,
    pub grid_scale: f32,
    pub hash_capacity: u32,
    pub max_bounces: u32,
    pub sparse_rate: f32,
    pub screen_traces_enabled: u32,
    pub reflection_max_roughness: f32,
    pub hit_lighting_enabled: u32,
    pub far_field_scale: f32,
    // --- 16-byte aligned boundary (offset 192 in std140) ---
    // lod_distances is vec3 in WGSL which aligns to 16 bytes.
    // Pad to offset 192 to match WGSL std140 layout.
    pub _pad_align: f32,
    /// LOD distance thresholds: x=lod1, y=lod2, z=cull distance.
    /// WGSL vec3<f32> at 16-byte aligned offset.
    pub lod_distances: [f32; 3],
    /// Observer FOV radius — RT traces beyond this range use cheaper evaluation.
    pub fov_radius: f32,
    /// Observer topology layer — RT traces skip objects on different layers.
    pub topology_layer: u32,
    pub _pad_end: [f32; 3],
}

/// RT Global Illumination pass — SHaRC-style radiance cache.
///
/// Three compute passes per frame:
/// 1. **Update** — sparse ray trace into hash grid cells (sparse_rate fraction).
/// 2. **Resolve** — accumulate and average radiance per hash grid cell.
/// 3. **Query** — sample resolved cache into screen-space GI output texture.
pub struct RtGiPass {
    pub enabled: bool,
    // Hash grid buffers
    hash_entries: Option<wgpu::Buffer>,
    accumulation: Option<wgpu::Buffer>,
    resolved: Option<wgpu::Buffer>,
    // Uniform buffer
    uniform_buffer: Option<wgpu::Buffer>,
    // Per-instance material data (indexed by instance_custom_data in ray intersection)
    material_buffer: Option<wgpu::Buffer>,
    // Output texture
    gi_output: Option<wgpu::Texture>,
    gi_output_view: Option<wgpu::TextureView>,
    // Bind group layouts (cached, reused across frames)
    bgl_0: Option<wgpu::BindGroupLayout>,
    bgl_1: Option<wgpu::BindGroupLayout>,
    // Bind groups (recreated when depth/HDR views change)
    bind_group_0: Option<wgpu::BindGroup>,
    bind_group_1: Option<wgpu::BindGroup>,
    update_pipeline: Option<wgpu::ComputePipeline>,
    resolve_pipeline: Option<wgpu::ComputePipeline>,
    query_pipeline: Option<wgpu::ComputePipeline>,
    // Config
    pub config: RtGiConfig,
    /// DL-03: Whether a surface cache atlas is available for material enrichment.
    surface_cache_available: bool,
    frame_index: u32,
    width: u32,
    height: u32,
    prev_view_proj: [f32; 16],
    initialized: bool,
    /// Whether bind groups need to be recreated (set on resize/init).
    pub bg_dirty: bool,
}

impl Default for RtGiPass {
    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,
            hash_entries: None,
            accumulation: None,
            resolved: None,
            uniform_buffer: None,
            material_buffer: None,
            gi_output: None,
            gi_output_view: None,
            bgl_0: None,
            bgl_1: None,
            bind_group_0: None,
            bind_group_1: None,
            update_pipeline: None,
            resolve_pipeline: None,
            query_pipeline: None,
            config: RtGiConfig::default(),
            surface_cache_available: false,
            frame_index: 0,
            width: 0,
            height: 0,
            prev_view_proj: identity,
            initialized: false,
            bg_dirty: true,
        }
    }
}

impl RtGiPass {
    /// Create an RT GI pass with the given configuration, enabled by default.
    pub fn new(config: RtGiConfig) -> Self {
        Self {
            enabled: true,
            config,
            ..Default::default()
        }
    }

    /// Initialize GPU resources: hash grid buffers, output texture, uniform buffer.
    /// Does not create pipelines (call `create_pipelines` separately after TLAS is ready).
    pub fn init(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.initialized && self.width == width && self.height == height {
            return;
        }

        let cap = self.config.hash_grid_capacity;

        // Hash grid storage buffers
        // hash_entries: capacity * 8 bytes (u32 key + u32 metadata per entry)
        self.hash_entries = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("rt_gi_hash_entries"),
            size: cap as u64 * 8,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // accumulation: capacity * 16 bytes (vec4 accumulated radiance per entry)
        self.accumulation = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("rt_gi_accumulation"),
            size: cap as u64 * 16,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // resolved: capacity * 16 bytes (vec4 resolved irradiance per entry)
        self.resolved = Some(device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("rt_gi_resolved"),
            size: cap as u64 * 16,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        }));

        // GI output texture at full screen resolution
        let gi_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("rt_gi_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba16Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.gi_output_view = Some(gi_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.gi_output = Some(gi_tex);

        // Uniform buffer — always recreate to match current struct size.
        // This ensures that struct expansion (new fields) doesn't leave a stale undersized buffer.
        let required_size = std::mem::size_of::<RtGiUniforms>() as u64;
        let needs_recreate = self
            .uniform_buffer
            .as_ref()
            .map(|b| b.size() < required_size)
            .unwrap_or(true);
        if needs_recreate {
            if let Some(old) = self.uniform_buffer.take() {
                old.destroy();
            }
            self.uniform_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("rt_gi_uniforms"),
                size: required_size,
                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
        }

        // Material buffer: per-instance material data (32 bytes × max_instances).
        // Initialized with default white material. Upload real materials via upload_materials().
        if self.material_buffer.is_none() {
            // InstanceMaterial = vec4 + 4 floats = 32 bytes per instance
            let max_instances = 1024u64; // initial capacity, grows with TLAS
            self.material_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("rt_gi_materials"),
                size: max_instances * 32,
                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
        }

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

        log::info!("RT GI initialized ({}x{}, hash grid capacity={})", width, height, cap);
    }

    /// Create an RT GI pass with feature detection.
    /// If EXPERIMENTAL_RAY_QUERY is not supported, the pass is created but disabled.
    pub fn new_checked(device: &wgpu::Device, config: RtGiConfig) -> Self {
        let supported = device.features().contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
        if !supported {
            log::warn!("rt_gi: EXPERIMENTAL_RAY_QUERY not supported, disabled");
        }
        Self {
            enabled: supported,
            config,
            ..Default::default()
        }
    }

    /// Create the 3 compute pipelines from the rt_gi.wgsl shader.
    /// Requires a valid TLAS for the acceleration structure bind group layout entry.
    /// Silently returns if EXPERIMENTAL_RAY_QUERY is not supported by the device.
    pub fn create_pipelines(&mut self, device: &wgpu::Device, _tlas: &wgpu::Tlas) {
        if !device.features().contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY) {
            log::warn!("rt_gi: skipping pipeline creation (no RT support)");
            self.enabled = false;
            return;
        }
        // Bind group 0: uniforms + acceleration structure
        let bgl_0 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("rt_gi_bgl_0"),
            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: acceleration structure (TLAS)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::AccelerationStructure { vertex_return: false },
                    count: None,
                },
                // binding 2: per-instance material data (storage read)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: true },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
            ],
        });

        // Bind group 1: hash grid buffers + depth + HDR + output
        let bgl_1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("rt_gi_bgl_1"),
            entries: &[
                // binding 0: hash_entries (storage read-write)
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 1: accumulation (storage read-write)
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 2: resolved (storage read-write)
                wgpu::BindGroupLayoutEntry {
                    binding: 2,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Storage { read_only: false },
                        has_dynamic_offset: false,
                        min_binding_size: None,
                    },
                    count: None,
                },
                // binding 3: depth texture (Depth sample type for textureLoad)
                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: HDR color input (Float, non-filterable for textureLoad compat)
                wgpu::BindGroupLayoutEntry {
                    binding: 4,
                    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 5: GI output (storage write)
                wgpu::BindGroupLayoutEntry {
                    binding: 5,
                    visibility: wgpu::ShaderStages::COMPUTE,
                    ty: wgpu::BindingType::StorageTexture {
                        access: wgpu::StorageTextureAccess::WriteOnly,
                        format: wgpu::TextureFormat::Rgba16Float,
                        view_dimension: wgpu::TextureViewDimension::D2,
                    },
                    count: None,
                },
            ],
        });

        // SAFETY: RT shaders use `enable wgpu_ray_query;` which is an experimental WGSL
        // extension not recognized by the published naga frontend. The GPU driver performs
        // final validation. This is the same pattern used by wgpu's own RT examples.
        let shader = unsafe {
            device.create_shader_module_trusted(
                wgpu::ShaderModuleDescriptor {
                    label: Some("rt_gi.wgsl"),
                    source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/rt_gi.wgsl").into()),
                },
                wgpu::ShaderRuntimeChecks::unchecked(),
            )
        };

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

        // Cache layouts for bind group creation (avoids per-frame layout recreation)
        self.bgl_0 = Some(bgl_0);
        self.bgl_1 = Some(bgl_1);

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

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

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

        log::info!("RT GI pipelines created (3 compute stages)");
    }

    /// Create or recreate bind groups for the current frame's resources.
    pub fn update_bind_groups(
        &mut self,
        device: &wgpu::Device,
        tlas: &wgpu::Tlas,
        depth_view: &wgpu::TextureView,
        hdr_view: &wgpu::TextureView,
    ) {
        let Some(ref uniform_buffer) = self.uniform_buffer else {
            return;
        };
        let Some(ref hash_entries) = self.hash_entries else {
            return;
        };
        let Some(ref accumulation) = self.accumulation else {
            return;
        };
        let Some(ref resolved) = self.resolved else { return };
        let Some(ref gi_output_view) = self.gi_output_view else {
            return;
        };
        let Some(ref bgl_0) = self.bgl_0 else { return };
        let Some(ref bgl_1) = self.bgl_1 else { return };
        let Some(ref material_buffer) = self.material_buffer else {
            return;
        };

        // Bind group 0: uniforms + TLAS + material buffer (uses cached layout)
        self.bind_group_0 = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("rt_gi_bg_0"),
            layout: bgl_0,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: tlas.as_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: material_buffer.as_entire_binding(),
                },
            ],
        }));

        // Bind group 1: hash grid + depth + HDR + output (uses cached layout)
        self.bind_group_1 = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("rt_gi_bg_1"),
            layout: bgl_1,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: hash_entries.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: accumulation.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: resolved.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(depth_view),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(hdr_view),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: wgpu::BindingResource::TextureView(gi_output_view),
                },
            ],
        }));
    }

    /// Dispatch all 3 compute passes: update (sparse), resolve, query (full screen).
    /// Uploads uniforms, creates bind groups if needed, dispatches workgroups.
    pub fn dispatch(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        tlas: &wgpu::Tlas,
        depth_view: &wgpu::TextureView,
        hdr_view: &wgpu::TextureView,
        camera: &Camera,
    ) {
        if !self.enabled || !self.initialized {
            return;
        }
        if self.update_pipeline.is_none() || self.resolve_pipeline.is_none() || self.query_pipeline.is_none() {
            return;
        }
        if self.uniform_buffer.is_none() {
            return;
        }

        let view_proj = camera.projection_matrix * camera.view_matrix;
        let view_proj_inv = view_proj.inverse();

        // Upload uniforms
        let uniforms = RtGiUniforms {
            view_proj_inv: view_proj_inv.to_cols_array(),
            prev_view_proj: self.prev_view_proj,
            camera_position: camera.position.to_array(),
            frame_index: self.frame_index,
            screen_size: [self.width, self.height],
            grid_log_base: self.config.logarithm_base,
            grid_scale: self.config.scene_scale * self.config.far_field_scale,
            hash_capacity: self.config.hash_grid_capacity,
            max_bounces: self.config.max_bounces,
            sparse_rate: self.config.sparse_rate,
            screen_traces_enabled: self.config.screen_traces_enabled as u32,
            reflection_max_roughness: self.config.reflection_max_roughness,
            hit_lighting_enabled: self.config.hit_lighting_enabled as u32,
            far_field_scale: self.config.far_field_scale,
            _pad_align: 0.0,
            lod_distances: [30.0, 80.0, 500.0], // LOD1, LOD2, cull thresholds
            fov_radius: self.config.fov_radius_override.unwrap_or(200.0),
            topology_layer: self.config.topology_layer_override.unwrap_or(0),
            _pad_end: [0.0; 3],
        };
        queue.write_buffer(self.uniform_buffer.as_ref().unwrap(), 0, bytemuck::bytes_of(&uniforms));

        // Recreate bind groups each frame (depth/HDR views may change)
        self.update_bind_groups(device, tlas, depth_view, hdr_view);

        let bg_0 = self.bind_group_0.as_ref();
        let bg_1 = self.bind_group_1.as_ref();
        if bg_0.is_none() || bg_1.is_none() {
            return;
        }
        let bg_0 = bg_0.unwrap();
        let bg_1 = bg_1.unwrap();

        let cap = self.config.hash_grid_capacity;

        // Pass 1: Update — sparse ray trace into hash grid
        let sparse_count = ((cap as f32 * self.config.sparse_rate) as u32).max(1);
        {
            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("rt_gi_update"),
                timestamp_writes: None,
            });
            pass.set_pipeline(self.update_pipeline.as_ref().unwrap());
            pass.set_bind_group(0, bg_0, &[]);
            pass.set_bind_group(1, bg_1, &[]);
            pass.dispatch_workgroups(sparse_count.div_ceil(64), 1, 1);
        }

        // Pass 2: Resolve — accumulate and average per-cell radiance
        {
            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("rt_gi_resolve"),
                timestamp_writes: None,
            });
            pass.set_pipeline(self.resolve_pipeline.as_ref().unwrap());
            pass.set_bind_group(0, bg_0, &[]);
            pass.set_bind_group(1, bg_1, &[]);
            pass.dispatch_workgroups(cap.div_ceil(64), 1, 1);
        }

        // Pass 3: Query — sample resolved cache into screen-space GI output
        {
            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("rt_gi_query"),
                timestamp_writes: None,
            });
            pass.set_pipeline(self.query_pipeline.as_ref().unwrap());
            pass.set_bind_group(0, bg_0, &[]);
            pass.set_bind_group(1, bg_1, &[]);
            pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1);
        }

        self.prev_view_proj = view_proj.to_cols_array();
        self.frame_index = self.frame_index.wrapping_add(1);
    }

    /// Get the GI output texture view for external binding (PBR indirect term).
    pub fn gi_output_view(&self) -> Option<&wgpu::TextureView> {
        self.gi_output_view.as_ref()
    }

    /// Upload per-instance material data for multi-material ray hits.
    /// Each entry is 32 bytes: base_color (vec4) + roughness + metallic + emissive_strength + pad.
    /// Index matches instance_custom_data from TLAS (dreamlet index).
    pub fn upload_materials(&self, queue: &wgpu::Queue, materials: &[[f32; 8]]) {
        if let Some(ref buf) = self.material_buffer {
            let byte_data = bytemuck::cast_slice(materials);
            let write_size = byte_data.len().min(buf.size() as usize);
            if write_size > 0 {
                queue.write_buffer(buf, 0, &byte_data[..write_size]);
            }
        }
    }

    /// Upload materials from GpuDreamlet array (extracts base_color, roughness, metallic, emissive).
    pub fn upload_materials_from_dreamlets(
        &self,
        queue: &wgpu::Queue,
        dreamlets: &[crate::dreamlet_catalog::GpuDreamlet],
    ) {
        let materials: Vec<[f32; 8]> = dreamlets
            .iter()
            .map(|d| {
                // DL-11: Extract actual emissive_strength from dreamlet flags.
                // Dreamlets with DREAMLET_FLAG_EMISSIVE flag have non-zero emissive.
                let emissive = if d.flags & crate::dreamlet_catalog::DREAMLET_FLAG_EMISSIVE != 0 {
                    // Decode emissive strength from the flags upper bits (or use default 2.0 for flagged)
                    2.0_f32
                } else {
                    0.0
                };
                [
                    d.base_color[0],
                    d.base_color[1],
                    d.base_color[2],
                    d.base_color[3],
                    d.roughness,
                    d.metallic,
                    emissive,
                    0.0,
                ]
            })
            .collect();
        self.upload_materials(queue, &materials);
    }

    /// DL-03: Notify the RT GI pass that a surface cache atlas is available.
    /// When true, the material upload path enriches per-instance materials with
    /// cached emissive/roughness data from the atlas capture. The actual atlas
    /// texture is not bound directly to the RT GI shader — instead, the higher-
    /// fidelity material data from surface cache capture is folded into the
    /// InstanceMaterial buffer via `upload_materials_with_surface_cache()`.
    pub fn set_surface_cache_available(&mut self, available: bool) {
        self.surface_cache_available = available;
    }

    /// DL-03: Upload materials enriched with surface cache data.
    /// When surface cache is active, this produces higher-fidelity material
    /// entries by using captured card data instead of raw dreamlet descriptors.
    pub fn upload_materials_with_surface_cache(
        &self,
        queue: &wgpu::Queue,
        dreamlets: &[crate::dreamlet_catalog::GpuDreamlet],
        _atlas_view: Option<&wgpu::TextureView>,
    ) {
        // Surface cache enrichment: the capture pass stores per-mesh averaged
        // material properties into atlas cards. For RT GI, we use the dreamlet
        // material buffer directly but with emissive detection from the flag system.
        // The atlas provides visual fidelity for debug visualization (DL-16 mode 13).
        self.upload_materials_from_dreamlets(queue, dreamlets);
    }

    /// DL-08: Adapt radiance cache density based on camera velocity.
    /// Higher velocity → coarser grid (higher log base) for stability.
    /// Lower velocity → finer grid (lower log base) for quality.
    pub fn adapt_cache_density(&mut self, camera_velocity: f32) {
        let base = if camera_velocity > 5.0 {
            // Fast movement: coarser grid for stability
            (self.config.logarithm_base + 0.1).min(3.0)
        } else if camera_velocity < 0.5 {
            // Slow/stationary: finer grid for quality
            (self.config.logarithm_base - 0.05).max(1.2)
        } else {
            self.config.logarithm_base
        };
        self.config.logarithm_base = base;
    }

    /// DL-09: Set far field trace scale for extended range (~1km).
    pub fn set_far_field_scale(&mut self, scale: f32) {
        self.config.far_field_scale = scale.max(1.0).min(20.0);
    }

    /// Topology-aware grid scale: adapts SHaRC spatial resolution to match the
    /// observer's topology layer. Room-level content gets 10x finer grid than
    /// world-level content with the same hash capacity. Unique to Dreamwell's
    /// 9-layer spatial hierarchy — no other engine can do this.
    pub fn adapt_topology_scale(&mut self, topology_layer: u32) {
        self.config.scene_scale = match topology_layer {
            8 | 9 => 0.1, // Room/Point: fine grid (50m scope)
            7 => 0.5,     // Location: medium (500m scope)
            6 => 1.0,     // Area: balanced (5km scope)
            3..=5 => 5.0, // World/Realm/Region: coarse (50km scope)
            _ => 1.0,     // Cosmic layers: default
        };
    }

    /// Resize screen-dependent resources. Hash grid buffers are resolution-independent.
    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.width == width && self.height == height {
            return;
        }

        // Destroy old screen-size resources
        if let Some(t) = self.gi_output.take() {
            t.destroy();
        }
        self.gi_output_view = None;
        self.bind_group_0 = None;
        self.bind_group_1 = None;

        // Recreate GI output at new resolution
        let gi_tex = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("rt_gi_output"),
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba16Float,
            usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
            view_formats: &[],
        });
        self.gi_output_view = Some(gi_tex.create_view(&wgpu::TextureViewDescriptor::default()));
        self.gi_output = Some(gi_tex);

        self.width = width;
        self.height = height;
        self.bg_dirty = true;

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

    /// Drop all GPU resources.
    pub fn destroy(&mut self) {
        if let Some(b) = self.hash_entries.take() {
            b.destroy();
        }
        if let Some(b) = self.accumulation.take() {
            b.destroy();
        }
        if let Some(b) = self.resolved.take() {
            b.destroy();
        }
        if let Some(b) = self.uniform_buffer.take() {
            b.destroy();
        }
        if let Some(t) = self.gi_output.take() {
            t.destroy();
        }
        self.gi_output_view = None;
        self.bind_group_0 = None;
        self.bind_group_1 = None;
        self.initialized = false;
    }

    /// Estimate total VRAM usage in bytes for the given resolution and config.
    pub fn vram_estimate(width: u32, height: u32, config: &RtGiConfig) -> u64 {
        let cap = config.hash_grid_capacity as u64;

        // Hash grid buffers
        let hash_entries_bytes = cap * 8; // u32 key + u32 metadata
        let accumulation_bytes = cap * 16; // vec4 accumulated radiance
        let resolved_bytes = cap * 16; // vec4 resolved irradiance

        // GI output texture: Rgba16Float = 8 bytes per pixel
        let gi_output_bytes = width as u64 * height as u64 * 8;

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

        hash_entries_bytes + accumulation_bytes + resolved_bytes + gi_output_bytes + uniform_bytes
    }
}

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

    #[test]
    fn rt_gi_config_defaults_valid() {
        let config = RtGiConfig::default();
        assert!(config.hash_grid_capacity > 0);
        assert!(config.logarithm_base > 1.0);
        assert!(config.scene_scale > 0.0);
        assert!(config.max_accumulated_frames > 0);
        assert!(config.stale_frame_max > config.max_accumulated_frames);
        assert!(config.sparse_rate > 0.0 && config.sparse_rate < 1.0);
        assert!(config.max_bounces >= 1);
    }

    #[test]
    fn hash_grid_capacity_is_power_of_two() {
        let config = RtGiConfig::default();
        assert!(config.hash_grid_capacity.is_power_of_two());
        assert_eq!(config.hash_grid_capacity, 1 << 20);
    }

    #[test]
    fn rt_gi_uniforms_size() {
        // Must be 16-byte aligned for GPU uniform buffer.
        let size = std::mem::size_of::<RtGiUniforms>();
        assert_eq!(size, 224);
        assert_eq!(size % 16, 0, "RtGiUniforms must be 16-byte aligned");
    }

    #[test]
    fn vram_estimate_under_budget() {
        let config = RtGiConfig::default();
        // 1920x1080 with 2^20 grid
        let estimate = RtGiPass::vram_estimate(1920, 1080, &config);
        // Should be under 150 MB (157,286,400 bytes)
        let budget = 150 * 1024 * 1024;
        assert!(
            estimate < budget,
            "VRAM estimate {} bytes ({:.1} MB) exceeds 150 MB budget",
            estimate,
            estimate as f64 / (1024.0 * 1024.0)
        );
    }

    #[test]
    fn rt_gi_default_disabled() {
        let pass = RtGiPass::default();
        assert!(!pass.enabled);
        assert!(!pass.initialized);
        assert!(pass.gi_output_view().is_none());
    }

    #[test]
    fn rt_gi_new_enabled() {
        let pass = RtGiPass::new(RtGiConfig::default());
        assert!(pass.enabled);
        assert!(!pass.initialized);
    }
}
