// GPU particle system — compute simulation + instanced billboard rendering.
// Particles are simulated in a compute pass and rendered as camera-facing quads.

use crate::shader;

/// Per-particle data. Must match dreammatter_*.wgsl Particle layout (64 bytes).
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Particle {
    pub position: [f32; 3],
    pub lifetime: f32,
    pub velocity: [f32; 3],
    pub age: f32,
    pub color: [f32; 4],
    pub size: f32,
    /// DreamMatter state: 0=particle, 1=converging, 2=settled, 3=dissolving.
    pub state: f32,
    /// Convergence target X coordinate.
    pub target_x: f32,
    /// Convergence target Z coordinate.
    pub target_z: f32,
}

/// Simulation parameters. Must match particle.wgsl SimParams layout.
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SimParams {
    pub delta_time: f32,
    pub gravity: f32,
    pub damping: f32,
    pub particle_count: u32,
}

/// Render uniforms for particle billboard rendering.
/// Must match physics/particle_billboard.wgsl RenderUniforms (112 bytes).
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ParticleRenderUniforms {
    pub view_proj: [f32; 16],
    pub camera_right: [f32; 3],
    pub _pad0: f32,
    pub camera_up: [f32; 3],
    pub _pad1: f32,
    pub camera_pos: [f32; 3],
    pub _pad2: f32,
}

/// GPU particle system context.
pub struct ParticleSystem {
    sim_pipeline: wgpu::ComputePipeline,
    sim_bind_group_layout: wgpu::BindGroupLayout,
    sim_bind_group: Option<wgpu::BindGroup>,
    render_bind_group_layout: wgpu::BindGroupLayout,
    particle_buffer: Option<wgpu::Buffer>,
    params_buffer: wgpu::Buffer,
    capacity: u32,
}

impl ParticleSystem {
    /// Create the particle system with embedded shaders.
    pub fn new(device: &wgpu::Device) -> Self {
        let shader_module = shader::load_wgsl_embedded(device, "particle", include_str!("../shaders/particle.wgsl"));

        // Sim bind group layout: uniform params + storage particles
        let sim_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("particle_sim"),
            entries: &[
                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,
                },
                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,
                },
            ],
        });

        let sim_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("particle_sim"),
            bind_group_layouts: &[Some(&sim_bind_group_layout)],
            immediate_size: 0,
        });

        let sim_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("particle_sim"),
            layout: Some(&sim_pipeline_layout),
            module: &shader_module,
            entry_point: Some("sim_main"),
            compilation_options: Default::default(),
            cache: None,
        });

        // Render bind group layout: storage particles (read-only)
        let render_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("particle_render_instances"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::VERTEX,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Storage { read_only: true },
                    has_dynamic_offset: false,
                    min_binding_size: None,
                },
                count: None,
            }],
        });

        let params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("particle_params"),
            size: std::mem::size_of::<SimParams>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        Self {
            sim_pipeline,
            sim_bind_group_layout,
            sim_bind_group: None,
            render_bind_group_layout,
            particle_buffer: None,
            params_buffer,
            capacity: 0,
        }
    }

    /// Ensure particle buffer capacity (power-of-two growth).
    pub fn ensure_capacity(&mut self, device: &wgpu::Device, count: u32) {
        if count <= self.capacity && self.sim_bind_group.is_some() {
            return;
        }

        const MAX_PARTICLES: u32 = 1_000_000;
        if count > MAX_PARTICLES {
            log::error!("particle_overflow:count {} exceeds max {}", count, MAX_PARTICLES);
            return;
        }
        let new_capacity = count.next_power_of_two().max(64);
        let buffer_size = (new_capacity as u64) * std::mem::size_of::<Particle>() as u64;

        let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("particles"),
            size: buffer_size,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let sim_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("particle_sim"),
            layout: &self.sim_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: self.params_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: particle_buffer.as_entire_binding(),
                },
            ],
        });

        self.particle_buffer = Some(particle_buffer);
        self.sim_bind_group = Some(sim_bind_group);
        self.capacity = new_capacity;
    }

    /// Upload particles and dispatch simulation compute pass.
    pub fn simulate(
        &self,
        queue: &wgpu::Queue,
        encoder: &mut wgpu::CommandEncoder,
        particles: &[Particle],
        params: &SimParams,
    ) {
        let Some(particle_buffer) = &self.particle_buffer else {
            return;
        };
        let Some(bind_group) = &self.sim_bind_group else {
            return;
        };

        queue.write_buffer(&self.params_buffer, 0, bytemuck::bytes_of(params));
        queue.write_buffer(particle_buffer, 0, bytemuck::cast_slice(particles));

        let workgroups = (params.particle_count).div_ceil(64);
        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
            label: Some("particle_sim"),
            timestamp_writes: None,
        });
        pass.set_pipeline(&self.sim_pipeline);
        pass.set_bind_group(0, bind_group, &[]);
        pass.dispatch_workgroups(workgroups, 1, 1);
    }

    /// Access the render bind group layout for pipeline creation by consumers.
    pub fn render_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
        &self.render_bind_group_layout
    }

    /// Get particle buffer for rendering.
    pub fn particle_buffer(&self) -> Option<&wgpu::Buffer> {
        self.particle_buffer.as_ref()
    }

    /// Current allocated capacity.
    pub fn capacity(&self) -> u32 {
        self.capacity
    }

    /// Explicitly destroy GPU resources. Call before drop for deterministic cleanup.
    pub fn destroy(&mut self) {
        if let Some(buf) = self.particle_buffer.take() {
            buf.destroy();
        }
        self.params_buffer.destroy();
        self.sim_bind_group = None;
    }
}

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

    #[test]
    fn particle_layout_size() {
        assert_eq!(std::mem::size_of::<Particle>(), 64);
    }

    #[test]
    fn sim_params_layout_size() {
        assert_eq!(std::mem::size_of::<SimParams>(), 16);
    }

    #[test]
    fn render_uniforms_layout_size() {
        assert_eq!(std::mem::size_of::<ParticleRenderUniforms>(), 112);
    }
}
