use crate::ecs::camera::queries::query_active_camera_matrices;
use crate::ecs::sprite_particles::components::EmitterShape2D;
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use freecs::Entity;
use nalgebra_glm::{Mat4, Vec2};
use std::collections::HashMap;
use wgpu::util::DeviceExt;

const MAX_SPRITE_PARTICLES: u32 = 500_000;
const MAX_SPRITE_EMITTERS: u32 = 256;
const WORKGROUP_SIZE: u32 = 256;

struct EmitterSpawnState {
    accumulated_spawn: f32,
    has_fired: bool,
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GpuSpriteParticle {
    position: [f32; 4],
    velocity: [f32; 4],
    color: [f32; 4],
    size_lifetime: [f32; 4],
    emitter_data: [f32; 4],
    physics: [f32; 4],
    color_start: [f32; 4],
    color_end: [f32; 4],
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GpuSpriteEmitterData {
    position_anchor: [f32; 4],
    velocity_min: [f32; 4],
    velocity_max: [f32; 4],
    lifetime_range: [f32; 4],
    size_start: [f32; 4],
    size_end: [f32; 4],
    gravity_drag: [f32; 4],
    rotation_range: [f32; 4],
    rotation_speed_range: [f32; 4],
    color_start: [f32; 4],
    color_end: [f32; 4],
    uv_min_max: [f32; 4],
    texture_depth: [f32; 4],
    shape_params: [f32; 4],
    spawn_count: u32,
    shape_type: u32,
    blend_mode: u32,
    _padding: u32,
}

impl Default for GpuSpriteEmitterData {
    fn default() -> Self {
        Self {
            position_anchor: [0.0; 4],
            velocity_min: [0.0; 4],
            velocity_max: [0.0; 4],
            lifetime_range: [1.0, 2.0, 0.0, 0.0],
            size_start: [8.0, 8.0, 0.0, 0.0],
            size_end: [2.0, 2.0, 0.0, 0.0],
            gravity_drag: [0.0, -200.0, 0.1, 0.0],
            rotation_range: [0.0; 4],
            rotation_speed_range: [0.0; 4],
            color_start: [1.0, 1.0, 1.0, 1.0],
            color_end: [1.0, 1.0, 1.0, 0.0],
            uv_min_max: [0.0, 0.0, 1.0, 1.0],
            texture_depth: [0.0; 4],
            shape_params: [0.0; 4],
            spawn_count: 0,
            shape_type: 0,
            blend_mode: 0,
            _padding: 0,
        }
    }
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SimParams {
    delta_time: f32,
    time: f32,
    max_particles: u32,
    _padding: u32,
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct DrawIndirect {
    vertex_count: u32,
    instance_count: u32,
    first_vertex: u32,
    first_instance: u32,
}

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SpriteParticleUniforms {
    view_projection: Mat4,
    screen_size: Vec2,
    atlas_slots_per_row: f32,
    atlas_slot_uv_size: f32,
}

pub struct SpriteParticlePass {
    _particle_buffer: wgpu::Buffer,
    emitter_buffer: wgpu::Buffer,
    params_buffer: wgpu::Buffer,
    _free_indices_buffer: wgpu::Buffer,
    _free_count_buffer: wgpu::Buffer,
    _alive_indices_buffer: wgpu::Buffer,
    _alive_count_buffer: wgpu::Buffer,
    draw_indirect_buffer: wgpu::Buffer,
    uniform_buffer: wgpu::Buffer,
    update_pipeline: wgpu::ComputePipeline,
    spawn_pipeline: wgpu::ComputePipeline,
    reset_pipeline: wgpu::ComputePipeline,
    render_pipeline_alpha: wgpu::RenderPipeline,
    render_pipeline_additive: wgpu::RenderPipeline,
    _compute_bind_group_layout: wgpu::BindGroupLayout,
    compute_bind_group: wgpu::BindGroup,
    _render_bind_group_layout: wgpu::BindGroupLayout,
    render_bind_group: wgpu::BindGroup,
    texture_bind_group_layout: wgpu::BindGroupLayout,
    texture_bind_group: Option<wgpu::BindGroup>,
    max_particles: u32,
    emitter_data: Vec<GpuSpriteEmitterData>,
    time: f32,
    delta_time: f32,
    emitter_spawn_states: HashMap<Entity, EmitterSpawnState>,
    cached_view_projection: Option<Mat4>,
    use_additive_blending: bool,
}

impl SpriteParticlePass {
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let max_particles = MAX_SPRITE_PARTICLES;

        let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Sprite Particle Buffer"),
            size: (std::mem::size_of::<GpuSpriteParticle>() * max_particles as usize) as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let emitter_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Sprite Emitter Buffer"),
            size: (std::mem::size_of::<GpuSpriteEmitterData>() * MAX_SPRITE_EMITTERS as usize)
                as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

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

        let mut free_indices: Vec<u32> = (0..max_particles).collect();
        free_indices.reverse();

        let free_indices_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Sprite Particle Free Indices"),
            contents: bytemuck::cast_slice(&free_indices),
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
        });

        let free_count_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Sprite Particle Free Count"),
            contents: bytemuck::cast_slice(&[max_particles]),
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
        });

        let alive_indices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("Sprite Particle Alive Indices"),
            size: (std::mem::size_of::<u32>() * max_particles as usize) as u64,
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let alive_count_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Sprite Particle Alive Count"),
            contents: bytemuck::cast_slice(&[0u32]),
            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
        });

        let draw_indirect_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Sprite Particle Draw Indirect"),
            contents: bytemuck::cast_slice(&[DrawIndirect {
                vertex_count: 6,
                instance_count: 0,
                first_vertex: 0,
                first_instance: 0,
            }]),
            usage: wgpu::BufferUsages::STORAGE
                | wgpu::BufferUsages::INDIRECT
                | wgpu::BufferUsages::COPY_DST,
        });

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

        let compute_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Sprite Particle Compute BGL"),
                entries: &[
                    bgl_storage_rw(0, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_ro(1, wgpu::ShaderStages::COMPUTE),
                    bgl_uniform(2, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_rw(3, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_rw(4, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_rw(5, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_rw(6, wgpu::ShaderStages::COMPUTE),
                    bgl_storage_rw(7, wgpu::ShaderStages::COMPUTE),
                ],
            });

        let compute_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Sprite Particle Compute BG"),
            layout: &compute_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: particle_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: emitter_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: params_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: free_indices_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 4,
                    resource: free_count_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 5,
                    resource: alive_indices_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 6,
                    resource: alive_count_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 7,
                    resource: draw_indirect_buffer.as_entire_binding(),
                },
            ],
        });

        let render_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Sprite Particle Render BGL"),
                entries: &[
                    bgl_uniform(0, wgpu::ShaderStages::VERTEX),
                    bgl_storage_ro(1, wgpu::ShaderStages::VERTEX),
                    bgl_storage_ro(2, wgpu::ShaderStages::VERTEX),
                ],
            });

        let render_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Sprite Particle Render BG"),
            layout: &render_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: uniform_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: particle_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: alive_indices_buffer.as_entire_binding(),
                },
            ],
        });

        let texture_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("Sprite Particle Texture BGL"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                        count: None,
                    },
                ],
            });

        let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("Sprite Particle Compute Shader"),
            source: wgpu::ShaderSource::Wgsl(
                include_str!("../../shaders/sprite_particle_update.wgsl").into(),
            ),
        });

        let render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("Sprite Particle Render Shader"),
            source: wgpu::ShaderSource::Wgsl(
                include_str!("../../shaders/sprite_particle_render.wgsl").into(),
            ),
        });

        let compute_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Sprite Particle Compute PL"),
                bind_group_layouts: &[&compute_bind_group_layout],
                push_constant_ranges: &[],
            });

        let update_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Sprite Particle Update"),
            layout: Some(&compute_pipeline_layout),
            module: &compute_shader,
            entry_point: Some("update"),
            compilation_options: Default::default(),
            cache: None,
        });

        let spawn_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Sprite Particle Spawn"),
            layout: Some(&compute_pipeline_layout),
            module: &compute_shader,
            entry_point: Some("spawn"),
            compilation_options: Default::default(),
            cache: None,
        });

        let reset_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
            label: Some("Sprite Particle Reset"),
            layout: Some(&compute_pipeline_layout),
            module: &compute_shader,
            entry_point: Some("reset_counters"),
            compilation_options: Default::default(),
            cache: None,
        });

        let render_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("Sprite Particle Render PL"),
                bind_group_layouts: &[&render_bind_group_layout, &texture_bind_group_layout],
                push_constant_ranges: &[],
            });

        let render_pipeline_alpha = create_sprite_particle_pipeline(
            device,
            &render_pipeline_layout,
            &render_shader,
            color_format,
            wgpu::BlendState::ALPHA_BLENDING,
            "fs_main",
            "Sprite Particle Pipeline (Alpha)",
        );

        let additive_blend = wgpu::BlendState {
            color: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::SrcAlpha,
                dst_factor: wgpu::BlendFactor::One,
                operation: wgpu::BlendOperation::Add,
            },
            alpha: wgpu::BlendComponent {
                src_factor: wgpu::BlendFactor::One,
                dst_factor: wgpu::BlendFactor::One,
                operation: wgpu::BlendOperation::Add,
            },
        };

        let render_pipeline_additive = create_sprite_particle_pipeline(
            device,
            &render_pipeline_layout,
            &render_shader,
            color_format,
            additive_blend,
            "fs_main_additive",
            "Sprite Particle Pipeline (Additive)",
        );

        Self {
            _particle_buffer: particle_buffer,
            emitter_buffer,
            params_buffer,
            _free_indices_buffer: free_indices_buffer,
            _free_count_buffer: free_count_buffer,
            _alive_indices_buffer: alive_indices_buffer,
            _alive_count_buffer: alive_count_buffer,
            draw_indirect_buffer,
            uniform_buffer,
            update_pipeline,
            spawn_pipeline,
            reset_pipeline,
            render_pipeline_alpha,
            render_pipeline_additive,
            _compute_bind_group_layout: compute_bind_group_layout,
            compute_bind_group,
            _render_bind_group_layout: render_bind_group_layout,
            render_bind_group,
            texture_bind_group_layout,
            texture_bind_group: None,
            max_particles,
            emitter_data: Vec::new(),
            time: 0.0,
            delta_time: 0.0,
            emitter_spawn_states: HashMap::new(),
            cached_view_projection: None,
            use_additive_blending: true,
        }
    }

    pub fn set_atlas_bind_group(
        &mut self,
        device: &wgpu::Device,
        atlas_view: &wgpu::TextureView,
        atlas_sampler: &wgpu::Sampler,
    ) {
        self.texture_bind_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Sprite Particle Texture BG"),
            layout: &self.texture_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(atlas_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(atlas_sampler),
                },
            ],
        }));
    }

    fn update_emitters(&mut self, world: &World) {
        self.emitter_data.clear();

        let emitter_entities: Vec<_> = world
            .query_entities(crate::ecs::SPRITE_PARTICLE_EMITTER)
            .collect();

        for entity in &emitter_entities {
            if self.emitter_data.len() >= MAX_SPRITE_EMITTERS as usize {
                break;
            }

            let Some(emitter) = world.get_sprite_particle_emitter(*entity) else {
                continue;
            };

            if !emitter.enabled {
                self.emitter_spawn_states.remove(entity);
                continue;
            }

            if let Some(visibility) = world.get_visibility(*entity)
                && !visibility.visible
            {
                continue;
            }

            let render_layer = world
                .get_render_layer(*entity)
                .map(|layer| layer.0)
                .unwrap_or(crate::ecs::render_layer::components::RenderLayer::WORLD);

            let should_render = match render_layer {
                crate::ecs::render_layer::components::RenderLayer::WORLD => {
                    world.resources.graphics.render_layer_world_enabled
                }
                crate::ecs::render_layer::components::RenderLayer::OVERLAY => {
                    world.resources.graphics.render_layer_overlay_enabled
                }
                _ => true,
            };
            if !should_render {
                continue;
            }

            let state = self
                .emitter_spawn_states
                .entry(*entity)
                .or_insert(EmitterSpawnState {
                    accumulated_spawn: 0.0,
                    has_fired: false,
                });

            if emitter.one_shot && state.has_fired {
                continue;
            }

            if emitter.burst_count > 0 && !state.has_fired {
                state.accumulated_spawn += emitter.burst_count as f32;
                state.has_fired = true;
            } else if emitter.spawn_rate > 0.0 {
                state.accumulated_spawn += emitter.spawn_rate * self.delta_time;
            }

            let spawn_count = state.accumulated_spawn.floor().min(256.0) as u32;
            state.accumulated_spawn -= spawn_count as f32;

            if spawn_count == 0 {
                continue;
            }

            let (shape_type, shape_params) = match emitter.shape {
                EmitterShape2D::Point => (0u32, [0.0f32; 4]),
                EmitterShape2D::Circle { radius } => (1u32, [radius, 0.0, 0.0, 0.0]),
                EmitterShape2D::Rectangle { half_extents } => {
                    (2u32, [half_extents.x, half_extents.y, 0.0, 0.0])
                }
            };

            let blend_mode_id = match emitter.blend_mode {
                crate::ecs::sprite::components::SpriteBlendMode::Alpha => 0u32,
                crate::ecs::sprite::components::SpriteBlendMode::Additive => 1u32,
                crate::ecs::sprite::components::SpriteBlendMode::Multiply => 2u32,
                crate::ecs::sprite::components::SpriteBlendMode::Screen => 3u32,
                _ => 0u32,
            };

            let gpu_emitter = GpuSpriteEmitterData {
                position_anchor: [emitter.anchor.x, emitter.anchor.y, 0.0, 0.0],
                velocity_min: [emitter.velocity_min.x, emitter.velocity_min.y, 0.0, 0.0],
                velocity_max: [emitter.velocity_max.x, emitter.velocity_max.y, 0.0, 0.0],
                lifetime_range: [emitter.lifetime_min, emitter.lifetime_max, 0.0, 0.0],
                size_start: [emitter.size_start.x, emitter.size_start.y, 0.0, 0.0],
                size_end: [emitter.size_end.x, emitter.size_end.y, 0.0, 0.0],
                gravity_drag: [emitter.gravity.x, emitter.gravity.y, emitter.drag, 0.0],
                rotation_range: [emitter.rotation_min, emitter.rotation_max, 0.0, 0.0],
                rotation_speed_range: [
                    emitter.rotation_speed_min,
                    emitter.rotation_speed_max,
                    0.0,
                    0.0,
                ],
                color_start: emitter.color.start,
                color_end: emitter.color.end,
                uv_min_max: [
                    emitter.uv_min.x,
                    emitter.uv_min.y,
                    emitter.uv_max.x,
                    emitter.uv_max.y,
                ],
                texture_depth: [emitter.texture_index as f32, emitter.depth, 0.0, 0.0],
                shape_params,
                spawn_count,
                shape_type,
                blend_mode: blend_mode_id,
                _padding: 0,
            };

            self.emitter_data.push(gpu_emitter);
        }

        self.emitter_spawn_states
            .retain(|entity, _| emitter_entities.contains(entity));
    }
}

impl PassNode<World> for SpriteParticlePass {
    fn name(&self) -> &str {
        "sprite_particle_pass"
    }

    fn reads(&self) -> Vec<&str> {
        vec![]
    }

    fn writes(&self) -> Vec<&str> {
        vec![]
    }

    fn reads_writes(&self) -> Vec<&str> {
        vec!["color"]
    }

    fn prepare(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
        self.delta_time = world.resources.window.timing.delta_time;
        self.time += self.delta_time;

        self.update_emitters(world);

        if !self.emitter_data.is_empty() {
            queue.write_buffer(
                &self.emitter_buffer,
                0,
                bytemuck::cast_slice(&self.emitter_data),
            );
        }

        let params = SimParams {
            delta_time: self.delta_time,
            time: self.time,
            max_particles: self.max_particles,
            _padding: 0,
        };
        queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[params]));

        let camera_matrices = query_active_camera_matrices(world);

        let (view_projection, screen_size) =
            if let Some((w, h)) = world.resources.window.cached_viewport_size {
                let width = w as f32;
                let height = h as f32;

                let vp = if let Some(ref matrices) = camera_matrices {
                    matrices.projection * matrices.view
                } else {
                    nalgebra_glm::ortho_rh_zo(0.0, width, 0.0, height, -1.0, 1.0)
                };
                (vp, Vec2::new(width, height))
            } else {
                (
                    nalgebra_glm::ortho_rh_zo(0.0, 1.0, 0.0, 1.0, -1.0, 1.0),
                    Vec2::new(1.0, 1.0),
                )
            };

        self.cached_view_projection = Some(view_projection);

        let atlas_slots_per_row =
            (crate::render::wgpu::sprite_texture_atlas::SPRITE_ATLAS_TOTAL_SLOTS as f32)
                .sqrt()
                .ceil();

        let uniforms = SpriteParticleUniforms {
            view_projection,
            screen_size,
            atlas_slots_per_row,
            atlas_slot_uv_size: 1.0 / atlas_slots_per_row,
        };
        queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
    }

    fn execute<'r, 'e>(
        &mut self,
        context: PassExecutionContext<'r, 'e, World>,
    ) -> crate::render::wgpu::rendergraph::Result<
        Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
    > {
        {
            let mut compute_pass =
                context
                    .encoder
                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
                        label: Some("Sprite Particle Reset"),
                        timestamp_writes: None,
                    });
            compute_pass.set_pipeline(&self.reset_pipeline);
            compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
            compute_pass.dispatch_workgroups(1, 1, 1);
        }

        {
            let mut compute_pass =
                context
                    .encoder
                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
                        label: Some("Sprite Particle Update"),
                        timestamp_writes: None,
                    });
            compute_pass.set_pipeline(&self.update_pipeline);
            compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
            let workgroups = self.max_particles.div_ceil(WORKGROUP_SIZE);
            compute_pass.dispatch_workgroups(workgroups, 1, 1);
        }

        if !self.emitter_data.is_empty() {
            let mut compute_pass =
                context
                    .encoder
                    .begin_compute_pass(&wgpu::ComputePassDescriptor {
                        label: Some("Sprite Particle Spawn"),
                        timestamp_writes: None,
                    });
            compute_pass.set_pipeline(&self.spawn_pipeline);
            compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
            compute_pass.dispatch_workgroups(self.emitter_data.len() as u32, 1, 1);
        }

        if let Some(ref texture_bind_group) = self.texture_bind_group {
            let color_view = context.get_texture_view("color")?;

            let mut render_pass = context
                .encoder
                .begin_render_pass(&wgpu::RenderPassDescriptor {
                    label: Some("Sprite Particle Render"),
                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                        view: color_view,
                        resolve_target: None,
                        ops: wgpu::Operations {
                            load: wgpu::LoadOp::Load,
                            store: wgpu::StoreOp::Store,
                        },
                        depth_slice: None,
                    })],
                    depth_stencil_attachment: None,
                    timestamp_writes: None,
                    occlusion_query_set: None,
                });

            let pipeline = if self.use_additive_blending {
                &self.render_pipeline_additive
            } else {
                &self.render_pipeline_alpha
            };
            render_pass.set_pipeline(pipeline);
            render_pass.set_bind_group(0, &self.render_bind_group, &[]);
            render_pass.set_bind_group(1, texture_bind_group, &[]);
            render_pass.draw_indirect(&self.draw_indirect_buffer, 0);
        }

        Ok(vec![])
    }
}

fn bgl_storage_rw(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility,
        ty: wgpu::BindingType::Buffer {
            ty: wgpu::BufferBindingType::Storage { read_only: false },
            has_dynamic_offset: false,
            min_binding_size: None,
        },
        count: None,
    }
}

fn bgl_storage_ro(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility,
        ty: wgpu::BindingType::Buffer {
            ty: wgpu::BufferBindingType::Storage { read_only: true },
            has_dynamic_offset: false,
            min_binding_size: None,
        },
        count: None,
    }
}

fn bgl_uniform(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
    wgpu::BindGroupLayoutEntry {
        binding,
        visibility,
        ty: wgpu::BindingType::Buffer {
            ty: wgpu::BufferBindingType::Uniform,
            has_dynamic_offset: false,
            min_binding_size: None,
        },
        count: None,
    }
}

fn create_sprite_particle_pipeline(
    device: &wgpu::Device,
    layout: &wgpu::PipelineLayout,
    shader: &wgpu::ShaderModule,
    color_format: wgpu::TextureFormat,
    blend_state: wgpu::BlendState,
    fragment_entry: &str,
    label: &str,
) -> wgpu::RenderPipeline {
    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some(label),
        layout: Some(layout),
        vertex: wgpu::VertexState {
            module: shader,
            entry_point: Some("vs_main"),
            buffers: &[],
            compilation_options: Default::default(),
        },
        fragment: Some(wgpu::FragmentState {
            module: shader,
            entry_point: Some(fragment_entry),
            targets: &[Some(wgpu::ColorTargetState {
                format: color_format,
                blend: Some(blend_state),
                write_mask: wgpu::ColorWrites::ALL,
            })],
            compilation_options: Default::default(),
        }),
        primitive: wgpu::PrimitiveState {
            topology: wgpu::PrimitiveTopology::TriangleList,
            strip_index_format: None,
            front_face: wgpu::FrontFace::Ccw,
            cull_mode: None,
            polygon_mode: wgpu::PolygonMode::Fill,
            unclipped_depth: false,
            conservative: false,
        },
        depth_stencil: None,
        multisample: wgpu::MultisampleState::default(),
        multiview: None,
        cache: None,
    })
}
