//! Sprite-Pass: das 2D-Overlay (UI) als getintete Textur-Quads.
//!
//! Immediate-Mode: der Vertex-Buffer wird jeden Frame neu befüllt (kleine
//! Datenmenge). Geometrie kommt von [`crate::render::ui`] als fertige
//! [`SpriteVertex`]-Liste + [`SpriteBatch`]es (ein Batch je Textur). Die
//! Texturen (weißer Pixel, Font-Atlas, …) werden einmal beim Bau
//! hochgeladen — wie beim Szenen-Pass: Decode (CPU) ≠ Upload (GPU).

use crate::engine::tga::Image;

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct SpriteVertex {
    /// Interne Pixel, Ursprung oben-links.
    pub(crate) pos:   [f32; 2],
    pub(crate) uv:    [f32; 2],
    /// Tint, mit dem Texel multipliziert (RGBA, 0..1).
    pub(crate) color: [f32; 4],
}

/// Ein zusammenhängender Vertex-Bereich, der mit *einer* Textur gezeichnet
/// wird (zwei Dreiecke je Quad, also Vielfache von 6).
#[derive(Clone, Copy)]
pub(crate) struct SpriteBatch {
    pub(crate) texture: usize,
    pub(crate) start:   u32,
    pub(crate) count:   u32,
}

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
    inv_res: [f32; 2],
    _pad:    [f32; 2],
}

/// Obergrenze des dynamischen Vertex-Buffers. 8192 Verts = ~1365 Quads pro
/// Frame — reichlich für Dialog-Panels, Text und Cursor.
const MAX_VERTS: u64 = 8192;

const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
    array_stride: size_of::<SpriteVertex>() as u64,
    step_mode: wgpu::VertexStepMode::Vertex,
    attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4],
};

pub struct SpritePass {
    pipeline:     wgpu::RenderPipeline,
    vbuf:         wgpu::Buffer,
    uniform_bind: wgpu::BindGroup,       // group 0
    tex_binds:    Vec<wgpu::BindGroup>,  // group 1, pro Textur
    /// Im aktuellen Frame hochgeladene Vertex-Anzahl (für `draw`-Schutz).
    loaded:       u32,
}

impl SpritePass {
    pub fn new(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        color_format: wgpu::TextureFormat,
        depth_format: wgpu::TextureFormat,
        internal: [f32; 2],
        textures: &[Image],
    ) -> Self {
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("sprite"),
            source: wgpu::ShaderSource::Wgsl(include_str!("sprite.wgsl").into()),
        });
        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("sprite"),
            layout: None,
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                buffers: &[VERTEX_LAYOUT],
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                compilation_options: Default::default(),
                targets: &[Some(wgpu::ColorTargetState {
                    format: color_format,
                    // Standard-Alpha-Blending fürs Overlay.
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
            }),
            primitive: wgpu::PrimitiveState::default(),
            // Der interne Pass hat ein Tiefen-Attachment → die Pipeline muss
            // ein kompatibles Format deklarieren. UI testet/schreibt aber
            // keine Tiefe (Always, kein Write) → liegt immer obenauf.
            depth_stencil: Some(wgpu::DepthStencilState {
                format: depth_format,
                depth_write_enabled: Some(false),
                depth_compare: Some(wgpu::CompareFunction::Always),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        let vbuf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("sprite vertices"),
            size: MAX_VERTS * size_of::<SpriteVertex>() as u64,
            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });

        let ubuf = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("sprite uniforms"),
            size: size_of::<Uniforms>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let u = Uniforms { inv_res: [1.0 / internal[0], 1.0 / internal[1]], _pad: [0.0; 2] };
        queue.write_buffer(&ubuf, 0, bytemuck::bytes_of(&u));
        let uniform_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("sprite uniforms"),
            layout: &pipeline.get_bind_group_layout(0),
            entries: &[wgpu::BindGroupEntry { binding: 0, resource: ubuf.as_entire_binding() }],
        });

        // Nearest + Clamp: harte Pixel, kein Wrap an den Atlas-Rändern.
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("sprite nearest"),
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });
        let tex_layout = pipeline.get_bind_group_layout(1);
        let tex_binds = textures.iter()
            .map(|img| upload_texture(device, queue, &tex_layout, &sampler, img))
            .collect();

        Self { pipeline, vbuf, uniform_bind, tex_binds, loaded: 0 }
    }

    /// Vertex-Daten dieses Frames hochladen — vor dem Render-Pass rufen.
    pub fn prepare(&mut self, queue: &wgpu::Queue, verts: &[SpriteVertex]) {
        let n = (verts.len() as u64).min(MAX_VERTS) as usize;
        queue.write_buffer(&self.vbuf, 0, bytemuck::cast_slice(&verts[..n]));
        self.loaded = n as u32;
    }

    pub fn draw(&self, pass: &mut wgpu::RenderPass, batches: &[SpriteBatch]) {
        if self.loaded == 0 { return; }
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, &self.uniform_bind, &[]);
        pass.set_vertex_buffer(0, self.vbuf.slice(..));
        for b in batches {
            if b.start + b.count > self.loaded { continue; }
            pass.set_bind_group(1, &self.tex_binds[b.texture], &[]);
            pass.draw(b.start..b.start + b.count, 0..1);
        }
    }
}

/// Ein RGBA8-`Image` als GPU-Textur hochladen und die Bind-Group (Textur +
/// Sampler, group 1) bauen.
fn upload_texture(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    layout: &wgpu::BindGroupLayout,
    sampler: &wgpu::Sampler,
    img: &Image,
) -> wgpu::BindGroup {
    let size = wgpu::Extent3d { width: img.width, height: img.height, depth_or_array_layers: 1 };
    let texture = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("sprite texture"),
        size,
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: wgpu::TextureFormat::Rgba8Unorm,
        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
        view_formats: &[],
    });
    queue.write_texture(
        wgpu::TexelCopyTextureInfo {
            texture: &texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        &img.rgba,
        wgpu::TexelCopyBufferLayout {
            offset: 0,
            bytes_per_row: Some(img.width * 4),
            rows_per_image: Some(img.height),
        },
        size,
    );
    let view = texture.create_view(&Default::default());
    device.create_bind_group(&wgpu::BindGroupDescriptor {
        label: Some("sprite texture"),
        layout,
        entries: &[
            wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) },
        ],
    })
}
