// GPU picking — renders object IDs to a separate texture for mouse selection.

use crate::formats::CAST_TEXTURE_FORMAT;

/// GPU picking state — manages the ID render target.
#[derive(Default)]
pub struct PickingState {
    pub texture: Option<wgpu::Texture>,
    pub texture_view: Option<wgpu::TextureView>,
    pub readback_buffer: Option<wgpu::Buffer>,
    pub width: u32,
    pub height: u32,
    pub last_picked_id: u32,
}

impl PickingState {
    /// Ensure the picking texture matches the viewport size.
    pub fn ensure_texture(&mut self, device: &wgpu::Device, width: u32, height: u32) {
        if self.width == width && self.height == height && self.texture.is_some() {
            return;
        }

        let w = width.max(1);
        let h = height.max(1);

        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("picking_texture"),
            size: wgpu::Extent3d {
                width: w,
                height: h,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: CAST_TEXTURE_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
            view_formats: &[],
        });

        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());

        // Readback buffer for single pixel (Rgba32Uint = 4 x u32 = 16 bytes of data).
        // wgpu requires bytes_per_row aligned to COPY_BYTES_PER_ROW_ALIGNMENT (256),
        // so the buffer must be at least 256 bytes even though only 16 are meaningful.
        let readback = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("picking_readback"),
            size: 256,
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });

        self.texture = Some(texture);
        self.texture_view = Some(view);
        self.readback_buffer = Some(readback);
        self.width = w;
        self.height = h;
    }

    /// Get the color attachment for the picking render pass.
    pub fn color_attachment(&self) -> Option<wgpu::RenderPassColorAttachment<'_>> {
        self.texture_view.as_ref().map(|view| wgpu::RenderPassColorAttachment {
            view,
            resolve_target: None,
            depth_slice: None,
            ops: wgpu::Operations {
                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                store: wgpu::StoreOp::Store,
            },
        })
    }

    /// Copy a single pixel from the picking texture to the readback buffer.
    pub fn copy_pixel(&self, encoder: &mut wgpu::CommandEncoder, pixel_x: u32, pixel_y: u32) {
        let (Some(texture), Some(buffer)) = (&self.texture, &self.readback_buffer) else {
            return;
        };

        let x = pixel_x.min(self.width.saturating_sub(1));
        let y = pixel_y.min(self.height.saturating_sub(1));

        encoder.copy_texture_to_buffer(
            wgpu::TexelCopyTextureInfo {
                texture,
                mip_level: 0,
                origin: wgpu::Origin3d { x, y, z: 0 },
                aspect: wgpu::TextureAspect::All,
            },
            wgpu::TexelCopyBufferInfo {
                buffer,
                layout: wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(256), // Must be COPY_BYTES_PER_ROW_ALIGNMENT (256) aligned
                    rows_per_image: None,
                },
            },
            wgpu::Extent3d {
                width: 1,
                height: 1,
                depth_or_array_layers: 1,
            },
        );
    }
}

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

    #[test]
    fn default_state() {
        let ps = PickingState::default();
        assert_eq!(ps.last_picked_id, 0);
        assert!(ps.texture.is_none());
    }

    #[test]
    fn color_attachment_none_without_texture() {
        let ps = PickingState::default();
        assert!(ps.color_attachment().is_none());
    }
}
