// Sprite atlas batch renderer via instancing.
// SoA-friendly design for efficient batch submission.

/// Per-sprite instance data. Must match sprite.wgsl SpriteInstance layout.
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SpriteInstance {
    pub position: [f32; 3],
    pub size: f32,
    pub uv_rect: [f32; 4], // x, y, width, height in atlas UV space
    pub color: [f32; 4],
}

/// Sprite render uniforms.
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct SpriteUniforms {
    pub view_proj: [f32; 16],
}

/// A region within a sprite atlas.
#[derive(Debug, Clone, Copy)]
pub struct AtlasRegion {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

impl AtlasRegion {
    /// Convert to normalized UV rect given atlas dimensions.
    pub fn to_uv_rect(&self, atlas_width: u32, atlas_height: u32) -> [f32; 4] {
        let aw = atlas_width as f32;
        let ah = atlas_height as f32;
        [
            self.x as f32 / aw,
            self.y as f32 / ah,
            self.width as f32 / aw,
            self.height as f32 / ah,
        ]
    }
}

/// Sprite batch — collects instances for a single draw call.
pub struct SpriteBatch {
    instances: Vec<SpriteInstance>,
    buffer: Option<wgpu::Buffer>,
    capacity: u32,
    dirty: bool,
}

impl Default for SpriteBatch {
    fn default() -> Self {
        Self {
            instances: Vec::with_capacity(256),
            buffer: None,
            capacity: 0,
            dirty: false,
        }
    }
}

impl SpriteBatch {
    /// Clear all instances for the next frame.
    pub fn clear(&mut self) {
        self.instances.clear();
        self.dirty = true;
    }

    /// Add a sprite instance.
    pub fn push(&mut self, instance: SpriteInstance) {
        self.instances.push(instance);
        self.dirty = true;
    }

    /// Number of sprites in the batch.
    pub fn len(&self) -> usize {
        self.instances.len()
    }

    /// Whether the batch is empty.
    pub fn is_empty(&self) -> bool {
        self.instances.is_empty()
    }

    /// Upload instances to GPU buffer, growing capacity as needed.
    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
        if self.instances.is_empty() {
            return;
        }

        let count = self.instances.len() as u32;
        if count > self.capacity || self.buffer.is_none() {
            let new_cap = count.next_power_of_two().max(64);
            let size = (new_cap as u64) * std::mem::size_of::<SpriteInstance>() as u64;
            self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
                label: Some("sprite_instances"),
                size,
                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }));
            self.capacity = new_cap;
        }

        debug_assert!(
            self.instances.len() <= self.capacity as usize,
            "sprite instances exceed buffer capacity"
        );
        if let Some(buf) = &self.buffer {
            queue.write_buffer(buf, 0, bytemuck::cast_slice(&self.instances));
        }
        self.dirty = false;
    }

    /// Get the GPU buffer for binding.
    pub fn buffer(&self) -> Option<&wgpu::Buffer> {
        self.buffer.as_ref()
    }

    /// Get current instances slice.
    pub fn instances(&self) -> &[SpriteInstance] {
        &self.instances
    }

    /// Explicitly destroy GPU resources.
    pub fn destroy(&mut self) {
        if let Some(buf) = self.buffer.take() {
            buf.destroy();
        }
        self.instances.clear();
        self.capacity = 0;
    }
}

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

    #[test]
    fn sprite_instance_layout_size() {
        assert_eq!(std::mem::size_of::<SpriteInstance>(), 48);
    }

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

    #[test]
    fn atlas_region_to_uv() {
        let region = AtlasRegion {
            x: 64,
            y: 128,
            width: 32,
            height: 32,
        };
        let uv = region.to_uv_rect(256, 256);
        assert!((uv[0] - 0.25).abs() < 0.001);
        assert!((uv[1] - 0.5).abs() < 0.001);
        assert!((uv[2] - 0.125).abs() < 0.001);
        assert!((uv[3] - 0.125).abs() < 0.001);
    }

    #[test]
    fn batch_clear_and_push() {
        let mut batch = SpriteBatch::default();
        assert!(batch.is_empty());

        batch.push(SpriteInstance {
            position: [0.0, 0.0, 0.0],
            size: 1.0,
            uv_rect: [0.0, 0.0, 1.0, 1.0],
            color: [1.0, 1.0, 1.0, 1.0],
        });
        assert_eq!(batch.len(), 1);

        batch.clear();
        assert!(batch.is_empty());
    }
}
