//! Fixed render pass chain — no generalized render graph.
//! Each pass implements `RenderPassNode` for prepare/run lifecycle.
//!
//! ## Pass status
//!
//! | Pass | Status | Notes |
//! |------|--------|-------|
//! | `ShadowPass` | Wired | Atlas + pipeline + depth-only draw infrastructure |
//! | `OpaquePass` | Superseded | Draw dispatch via GpuScene PBR pipeline in DreamFabric |
//! | `CutoutPass` | Superseded | Draw dispatch via GpuScene PBR masked pipeline |
//! | `TransparentPass` | Superseded | Draw dispatch via GpuScene PBR transparent pipeline |
//! | `BloomPass` | Superseded | Replaced by PostProcessStack in post.rs |
//! | `TonemapPass` | Superseded | Replaced by PostProcessStack in post.rs |
//! | `ParticlePass` | Wired | Billboard pipeline + render_billboards() dispatch |
//!
//! Geometry rendering goes through `GpuScene::render()` (primitives)
//! and `MeshletRenderPass` (meshlets), both called from `DreamFabric::end_frame`.
//! The pass structs above are the correct insertion points for future work; the
//! trait contract is stable and ready to receive real implementations.

use crate::primitives::PBR_VERTEX_LAYOUT;
use crate::shader;

/// Render pass node contract.
/// `prepare()` reads immutable scene data and stages GPU work.
/// `run()` executes GPU commands via the encoder.
pub trait RenderPassNode {
    /// Prepare GPU resources for this pass. Called before the render loop.
    fn prepare(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String>;

    /// Execute the render pass.
    fn run(
        &mut self,
        device: &wgpu::Device,
        encoder: &mut wgpu::CommandEncoder,
        target: &crate::render_target::RenderTarget,
        scene: &crate::render_scene::RenderSceneData,
        camera: &crate::camera::Camera,
    );

    /// Pass name for profiling and debug labels.
    fn name(&self) -> &str;
}

// ── Concrete pass stubs ─────────────────────────────────────────────────

/// Shadow map atlas resolution.
pub const SHADOW_ATLAS_SIZE: u32 = 2048;

/// Shadow map render pass. Renders depth from light perspective into an atlas texture.
/// Set `enabled = true` to allocate the atlas on next `prepare()`. When disabled,
/// no GPU memory is allocated and `run()` is a no-op.
pub struct ShadowPass {
    /// Shadow atlas depth texture.
    atlas_texture: Option<wgpu::Texture>,
    /// Shadow atlas depth view.
    pub atlas_view: Option<wgpu::TextureView>,
    /// Light-space view-projection matrix for the primary directional light.
    pub light_view_proj: [[f32; 16]; 4],
    /// Number of active cascade splits.
    pub cascade_count: u32,
    /// Whether the atlas has been allocated.
    initialized: bool,
    /// Enable shadow rendering. When false, prepare() skips atlas allocation.
    pub enabled: bool,
    /// Shadow depth-only render pipeline.
    shadow_pipeline: Option<wgpu::RenderPipeline>,
    /// Bind group layout for shadow uniforms (light_view_proj + model).
    shadow_bgl: Option<wgpu::BindGroupLayout>,
    /// Uniform buffer for ShadowUniforms (light_view_proj + model = 128 bytes).
    shadow_uniform_buf: Option<wgpu::Buffer>,
    /// Whether the pipeline has been initialized.
    pipeline_initialized: bool,
}

impl Default for ShadowPass {
    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 {
            atlas_texture: None,
            atlas_view: None,
            light_view_proj: [identity; 4],
            cascade_count: 1,
            initialized: false,
            enabled: false,
            shadow_pipeline: None,
            shadow_bgl: None,
            shadow_uniform_buf: None,
            pipeline_initialized: false,
        }
    }
}

impl ShadowPass {
    /// Release GPU resources. Safe to call multiple times.
    pub fn destroy(&mut self) {
        if let Some(tex) = self.atlas_texture.take() {
            tex.destroy();
        }
        self.atlas_view = None;
        self.initialized = false;
        if let Some(buf) = self.shadow_uniform_buf.take() {
            buf.destroy();
        }
        self.shadow_pipeline = None;
        self.shadow_bgl = None;
        self.pipeline_initialized = false;
    }

    /// Initialize the shadow depth-only render pipeline.
    ///
    /// Creates:
    /// - Bind group layout: 1 uniform buffer (vertex stage) for ShadowUniforms
    /// - Render pipeline: depth-only, back-face culling, PrimitiveVertex layout
    /// - Uniform buffer: 128 bytes (mat4x4 light_view_proj + mat4x4 model)
    fn init_pipeline(&mut self, device: &wgpu::Device) {
        if self.pipeline_initialized {
            return;
        }

        // Bind group layout: single uniform buffer at binding 0 (vertex stage)
        let bgl = shader::uniform_bind_group_layout(device, "shadow_bgl", wgpu::ShaderStages::VERTEX);

        // Load shadow shader
        let shader_module = shader::load_wgsl_embedded(device, "shadow", include_str!("../../shaders/shadow.wgsl"));

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

        // Vertex buffer layout: PBR vertex (64 bytes). Shadow shader reads position only
        // but stride must match the PBR vertex buffer layout.
        let vertex_layout = PBR_VERTEX_LAYOUT;

        // Depth-only render pipeline (no color targets)
        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("shadow_pipeline"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader_module,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                buffers: &[vertex_layout],
            },
            fragment: None, // depth-only
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                unclipped_depth: false,
                polygon_mode: wgpu::PolygonMode::Fill,
                conservative: false,
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: crate::formats::DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        // Uniform buffer: ShadowUniforms = light_view_proj (64) + model (64) = 128 bytes
        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("shadow_uniform_buf"),
            size: 128,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        self.shadow_pipeline = Some(pipeline);
        self.shadow_bgl = Some(bgl);
        self.shadow_uniform_buf = Some(uniform_buf);
        self.pipeline_initialized = true;
    }

    /// Access the shadow render pipeline (if initialized).
    pub fn shadow_pipeline(&self) -> Option<&wgpu::RenderPipeline> {
        self.shadow_pipeline.as_ref()
    }

    /// Access the shadow bind group layout (if initialized).
    pub fn shadow_bgl(&self) -> Option<&wgpu::BindGroupLayout> {
        self.shadow_bgl.as_ref()
    }

    /// Access the shadow uniform buffer (if initialized).
    pub fn shadow_uniform_buf(&self) -> Option<&wgpu::Buffer> {
        self.shadow_uniform_buf.as_ref()
    }

    /// Compute orthographic light-space matrix for a directional light.
    pub fn compute_light_matrix(light_dir: [f32; 3], bounds_half: f32) -> [f32; 16] {
        // Orthographic projection centered on origin, looking along light_dir.
        let lx = light_dir[0];
        let ly = light_dir[1];
        let lz = light_dir[2];
        let len = (lx * lx + ly * ly + lz * lz).sqrt();
        if len < 1e-6 {
            #[rustfmt::skip]
            return [
                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,
            ];
        }

        // Forward = -light_dir (light points toward scene).
        let fwd = [-lx / len, -ly / len, -lz / len];

        // Pick up vector that isn't parallel to fwd.
        let up_ref = if fwd[1].abs() > 0.99 {
            [1.0, 0.0, 0.0]
        } else {
            [0.0, 1.0, 0.0]
        };

        // right = cross(up_ref, fwd)
        let rx = up_ref[1] * fwd[2] - up_ref[2] * fwd[1];
        let ry = up_ref[2] * fwd[0] - up_ref[0] * fwd[2];
        let rz = up_ref[0] * fwd[1] - up_ref[1] * fwd[0];
        let rlen = (rx * rx + ry * ry + rz * rz).sqrt();
        let right = [rx / rlen, ry / rlen, rz / rlen];

        // up = cross(fwd, right)
        let ux = fwd[1] * right[2] - fwd[2] * right[1];
        let uy = fwd[2] * right[0] - fwd[0] * right[2];
        let uz = fwd[0] * right[1] - fwd[1] * right[0];

        // View matrix (look along fwd from origin)
        let b = bounds_half;
        let near = -b * 2.0;
        let far = b * 2.0;
        let ortho_x = 1.0 / b;
        let ortho_y = 1.0 / b;
        let ortho_z = 1.0 / (far - near);

        // Combined ortho * view (column-major)
        [
            right[0] * ortho_x,
            ux * ortho_y,
            fwd[0] * ortho_z,
            0.0,
            right[1] * ortho_x,
            uy * ortho_y,
            fwd[1] * ortho_z,
            0.0,
            right[2] * ortho_x,
            uz * ortho_y,
            fwd[2] * ortho_z,
            0.0,
            0.0,
            0.0,
            -near * ortho_z,
            1.0,
        ]
    }
}

impl ShadowPass {
    /// Dispatch the shadow pass: renders depth-only from the first directional light's
    /// perspective into the 2048² shadow atlas. Uses existing scene PBR vertex buffers.
    pub fn dispatch_shadow(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        scene: &crate::scene::GpuScene,
    ) {
        // Ensure atlas and pipeline are initialized
        if !self.enabled {
            self.enabled = true;
        }
        if !self.initialized {
            let texture = device.create_texture(&wgpu::TextureDescriptor {
                label: Some("shadow_atlas"),
                size: wgpu::Extent3d {
                    width: SHADOW_ATLAS_SIZE,
                    height: SHADOW_ATLAS_SIZE,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: crate::formats::DEPTH_FORMAT,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            });
            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
            self.atlas_texture = Some(texture);
            self.atlas_view = Some(view);
            self.initialized = true;
        }
        self.init_pipeline(device);

        let Some(ref atlas_view) = self.atlas_view else { return };
        let Some(ref pipeline) = self.shadow_pipeline else {
            return;
        };
        let Some(ref bgl) = self.shadow_bgl else { return };
        let Some(ref uniform_buf) = self.shadow_uniform_buf else {
            return;
        };

        // Get first directional light for shadow casting
        let dir_lights = &scene.scene_lights.directional;
        if dir_lights.is_empty() {
            return;
        }
        let light = &dir_lights[0];
        let light_vp = Self::compute_light_matrix(light.direction, 20.0);

        // Shadow render pass (depth-only, clears atlas)
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("shadow_pass"),
                color_attachments: &[],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: atlas_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: wgpu::StoreOp::Store,
                    }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });

            pass.set_pipeline(pipeline);

            // Render each visible scene object into the shadow atlas
            for slot in scene.visible_slots() {
                let Some(mesh) = scene.mesh_for_slot(slot) else {
                    continue;
                };

                // Upload shadow uniforms: light_vp (64 bytes) + model (64 bytes)
                let mut shadow_data = [0u8; 128];
                shadow_data[..64].copy_from_slice(bytemuck::bytes_of(&light_vp));
                shadow_data[64..].copy_from_slice(bytemuck::bytes_of(&slot.model_matrix));
                queue.write_buffer(uniform_buf, 0, &shadow_data);

                let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
                    label: Some("shadow_per_object"),
                    layout: bgl,
                    entries: &[wgpu::BindGroupEntry {
                        binding: 0,
                        resource: uniform_buf.as_entire_binding(),
                    }],
                });

                pass.set_bind_group(0, &bg, &[]);
                pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
                pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
                pass.draw_indexed(0..mesh.index_count, 0, 0..1);
            }
        }

        // Store the light VP for PBR shader shadow sampling
        self.light_view_proj[0] = light_vp;
    }
}

impl RenderPassNode for ShadowPass {
    fn prepare(
        &mut self,
        device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        if !self.enabled {
            return Ok(());
        }
        if !self.initialized {
            let texture = device.create_texture(&wgpu::TextureDescriptor {
                label: Some("shadow_atlas"),
                size: wgpu::Extent3d {
                    width: SHADOW_ATLAS_SIZE,
                    height: SHADOW_ATLAS_SIZE,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: crate::formats::DEPTH_FORMAT,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            });
            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
            self.atlas_texture = Some(texture);
            self.atlas_view = Some(view);
            self.initialized = true;
        }
        // Initialize the shadow pipeline after atlas allocation.
        self.init_pipeline(device);
        Ok(())
    }

    fn run(
        &mut self,
        _device: &wgpu::Device,
        encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        let Some(ref atlas_view) = self.atlas_view else {
            return;
        };

        // Clear the shadow atlas depth.
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("shadow_pass"),
                color_attachments: &[],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: atlas_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: wgpu::StoreOp::Store,
                    }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });

            // Set the shadow pipeline if initialized. Actual draw calls are issued
            // by DreamFabric when it iterates scene objects — each object creates a
            // bind group from shadow_bgl + shadow_uniform_buf with its per-object
            // model matrix, then calls draw_indexed on this pass.
            if let Some(ref pipeline) = self.shadow_pipeline {
                pass.set_pipeline(pipeline);
            }
        }
    }

    fn name(&self) -> &str {
        "shadow"
    }
}

/// Opaque geometry render pass.
///
/// Superseded by PBR pipeline dispatch in `DreamFabric::end_frame()`.
/// `GpuScene::render()` handles opaque primitive draw calls with Cook-Torrance PBR.
/// Retained as a named pass node for the render graph contract.
#[derive(Default)]
pub struct OpaquePass;

impl RenderPassNode for OpaquePass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Opaque geometry rendered via GpuScene::render() with PBR shading in DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "opaque"
    }
}

/// Cutout (alpha-test) geometry render pass.
///
/// Superseded by PBR masked pipeline in GpuScene.
/// Alpha-test geometry uses the `pbr_masked_pipeline` with shader discard.
#[derive(Default)]
pub struct CutoutPass;

impl RenderPassNode for CutoutPass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Cutout geometry rendered via GpuScene with pbr_masked_pipeline in DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "cutout"
    }
}

/// Transparent geometry render pass.
///
/// Superseded by PBR transparent pipeline in GpuScene.
/// Transparent geometry uses `pbr_transparent_pipeline` with alpha blend.
#[derive(Default)]
pub struct TransparentPass;

impl RenderPassNode for TransparentPass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Transparent geometry rendered via GpuScene with pbr_transparent_pipeline in DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "transparent"
    }
}

/// Bloom post-processing pass.
///
/// Superseded by `PostProcessStack::run_bloom()` in `post.rs`.
/// The PostProcessStack provides 2-level mip chain bloom with configurable threshold.
#[derive(Default)]
pub struct BloomPass;

impl RenderPassNode for BloomPass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Bloom rendered via PostProcessStack::run_bloom() in DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "bloom"
    }
}

/// Tonemapping post-processing pass.
///
/// Superseded by `PostProcessStack::run_tonemap()` in `post.rs`.
/// Supports ACES filmic, Uncharted2, Reinhard, and passthrough operators.
#[derive(Default)]
pub struct TonemapPass;

impl RenderPassNode for TonemapPass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Tonemap rendered via PostProcessStack::run_tonemap() in DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "tonemap"
    }
}

/// Particle billboard render pass. Creates pipeline for GPU-driven billboard
/// particle rendering with additive blending. Vertices are generated entirely
/// in the vertex shader from `vertex_index` and `instance_index` — no CPU
/// vertex buffers required.
///
/// The pipeline is initialized externally by `DreamFabric` (which knows the
/// surface format). Draw calls are also issued by `DreamFabric` via the
/// pipeline accessors.
pub struct ParticlePass {
    /// Billboard render pipeline (additive blend, no depth write).
    render_pipeline: Option<wgpu::RenderPipeline>,
    /// Uniform buffer for ParticleRenderUniforms (view_proj + camera vectors = 96 bytes).
    render_uniform_buf: Option<wgpu::Buffer>,
    /// Bind group layout for render uniforms (group 0).
    render_uniform_bgl: Option<wgpu::BindGroupLayout>,
    /// Bind group layout for particle storage buffer (group 1).
    particle_bgl: Option<wgpu::BindGroupLayout>,
    /// Whether the pipeline has been initialized.
    initialized: bool,
}

impl Default for ParticlePass {
    fn default() -> Self {
        Self {
            render_pipeline: None,
            render_uniform_buf: None,
            render_uniform_bgl: None,
            particle_bgl: None,
            initialized: false,
        }
    }
}

impl ParticlePass {
    /// Initialize the particle billboard render pipeline.
    ///
    /// Called by `DreamFabric` once the surface format is known. Creates:
    /// - render_uniform_bgl: 1 uniform buffer (vertex stage) for ParticleRenderUniforms
    /// - particle_bgl: 1 read-only storage buffer (vertex stage) for particle data
    /// - Render pipeline: additive blend, no depth write, no vertex buffers
    /// - Uniform buffer: 96 bytes (ParticleRenderUniforms)
    pub fn init_pipeline(&mut self, device: &wgpu::Device, surface_format: wgpu::TextureFormat) {
        if self.initialized {
            return;
        }

        // Group 0: render uniforms (view_proj + camera vectors)
        let render_uniform_bgl =
            shader::uniform_bind_group_layout(device, "particle_render_uniform_bgl", wgpu::ShaderStages::VERTEX);

        // Group 1: particle storage buffer (read-only, vertex stage)
        let particle_bgl =
            shader::storage_bind_group_layout(device, "particle_storage_bgl", 1, wgpu::ShaderStages::VERTEX);

        // Load particle shader (render entry points: vs_main / fs_main)
        let shader_module =
            shader::load_wgsl_embedded(device, "particle_render", include_str!("../../shaders/particle.wgsl"));

        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("particle_render_pipeline_layout"),
            bind_group_layouts: &[Some(&render_uniform_bgl), Some(&particle_bgl)],
            immediate_size: 0,
        });

        // Alpha blend for Dreamlet micro-geometry (semi-transparent during dissolution)
        let blend_state = wgpu::BlendState {
            color: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::SrcAlpha,
                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                operation: wgpu::BlendOperation::Add,
            },
            alpha: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::One,
                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
                operation: wgpu::BlendOperation::Add,
            },
        };

        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("dreamlet_render_pipeline"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader_module,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                buffers: &[], // 36 vertices per Dreamlet cube generated from vertex_index
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader_module,
                entry_point: Some("fs_main"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: surface_format,
                    blend: Some(blend_state),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back), // back-face cull for solid 3D cubes
                unclipped_depth: false,
                polygon_mode: wgpu::PolygonMode::Fill,
                conservative: false,
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: crate::formats::DEPTH_FORMAT,
                depth_write_enabled: Some(true), // solid 3D geometry needs depth write
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        // Uniform buffer: ParticleRenderUniforms = 96 bytes
        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("particle_render_uniform_buf"),
            size: 96,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        self.render_pipeline = Some(pipeline);
        self.render_uniform_bgl = Some(render_uniform_bgl);
        self.particle_bgl = Some(particle_bgl);
        self.render_uniform_buf = Some(uniform_buf);
        self.initialized = true;
    }

    /// Access the particle render pipeline (if initialized).
    pub fn render_pipeline(&self) -> Option<&wgpu::RenderPipeline> {
        self.render_pipeline.as_ref()
    }

    /// Access the render uniform bind group layout (if initialized).
    pub fn render_uniform_bgl(&self) -> Option<&wgpu::BindGroupLayout> {
        self.render_uniform_bgl.as_ref()
    }

    /// Access the particle storage bind group layout (if initialized).
    pub fn particle_bgl(&self) -> Option<&wgpu::BindGroupLayout> {
        self.particle_bgl.as_ref()
    }

    /// Access the render uniform buffer (if initialized).
    pub fn render_uniform_buf(&self) -> Option<&wgpu::Buffer> {
        self.render_uniform_buf.as_ref()
    }

    /// Whether the pipeline has been initialized.
    pub fn is_initialized(&self) -> bool {
        self.initialized
    }

    /// Render particle billboards into an existing render pass.
    ///
    /// Creates per-frame bind groups (lightweight CPU objects), writes
    /// `ParticleRenderUniforms`, and issues `draw(0..6, 0..particle_count)`.
    /// Each particle is a camera-facing quad (6 vertices from vertex_index).
    ///
    /// No-op if the pipeline is not initialized or `particle_count` is 0.
    pub fn render_billboards(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        pass: &mut wgpu::RenderPass<'_>,
        camera: &crate::camera::Camera,
        particle_buffer: &wgpu::Buffer,
        particle_count: u32,
    ) {
        if particle_count == 0 {
            return;
        }
        let Some(ref pipeline) = self.render_pipeline else {
            return;
        };
        let Some(ref uniform_buf) = self.render_uniform_buf else {
            return;
        };
        let Some(ref uniform_bgl) = self.render_uniform_bgl else {
            return;
        };
        let Some(ref particle_bgl) = self.particle_bgl else {
            return;
        };

        // Compute camera right/up from the view matrix columns.
        let vm = camera.view_matrix;
        let camera_right = [vm.col(0).x, vm.col(0).y, vm.col(0).z];
        let camera_up = [vm.col(1).x, vm.col(1).y, vm.col(1).z];

        let vp = (camera.projection_matrix * camera.view_matrix).to_cols_array();

        let uniforms = crate::particle::ParticleRenderUniforms {
            view_proj: vp,
            camera_right,
            _pad0: 0.0,
            camera_up,
            _pad1: 0.0,
            camera_pos: camera.position.to_array(),
            _pad2: 0.0,
        };
        queue.write_buffer(uniform_buf, 0, bytemuck::bytes_of(&uniforms));

        // Create per-frame bind groups (CPU-side descriptors, not GPU allocations).
        let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_render_uniform_bg"),
            layout: uniform_bgl,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: uniform_buf.as_entire_binding(),
            }],
        });
        let particle_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_render_data_bg"),
            layout: particle_bgl,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: particle_buffer.as_entire_binding(),
            }],
        });

        pass.set_pipeline(pipeline);
        pass.set_bind_group(0, &uniform_bg, &[]);
        pass.set_bind_group(1, &particle_bg, &[]);
        // 36 vertices per Dreamlet cube (6 faces × 2 triangles × 3 verts)
        pass.draw(0..36, 0..particle_count);
    }

    /// Release GPU resources. Safe to call multiple times.
    pub fn destroy(&mut self) {
        if let Some(buf) = self.render_uniform_buf.take() {
            buf.destroy();
        }
        self.render_pipeline = None;
        self.render_uniform_bgl = None;
        self.particle_bgl = None;
        self.initialized = false;
    }
}

impl RenderPassNode for ParticlePass {
    fn prepare(
        &mut self,
        _device: &wgpu::Device,
        _queue: &wgpu::Queue,
        _scene: &crate::render_scene::RenderSceneData,
    ) -> Result<(), String> {
        // Pipeline initialization requires surface_format, which is not available
        // through the RenderPassNode trait. init_pipeline() is called externally
        // by DreamFabric once the surface format is known.
        Ok(())
    }
    fn run(
        &mut self,
        _device: &wgpu::Device,
        _encoder: &mut wgpu::CommandEncoder,
        _target: &crate::render_target::RenderTarget,
        _scene: &crate::render_scene::RenderSceneData,
        _camera: &crate::camera::Camera,
    ) {
        // Particle billboards rendered via render_billboards() called from DreamFabric::end_frame.
    }
    fn name(&self) -> &str {
        "particles"
    }
}

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

    #[test]
    fn pass_names() {
        assert_eq!(ShadowPass::default().name(), "shadow");
        assert_eq!(OpaquePass.name(), "opaque");
        assert_eq!(CutoutPass.name(), "cutout");
        assert_eq!(TransparentPass.name(), "transparent");
        assert_eq!(BloomPass.name(), "bloom");
        assert_eq!(TonemapPass.name(), "tonemap");
        assert_eq!(ParticlePass::default().name(), "particles");
    }

    #[test]
    fn shadow_pass_default_not_initialized() {
        let pass = ShadowPass::default();
        assert!(!pass.pipeline_initialized);
        assert!(pass.shadow_pipeline().is_none());
        assert!(pass.shadow_bgl().is_none());
        assert!(pass.shadow_uniform_buf().is_none());
    }

    #[test]
    fn particle_pass_default_not_initialized() {
        let pass = ParticlePass::default();
        assert!(!pass.is_initialized());
        assert!(pass.render_pipeline().is_none());
        assert!(pass.render_uniform_bgl().is_none());
        assert!(pass.particle_bgl().is_none());
        assert!(pass.render_uniform_buf().is_none());
    }
}
