//! Host-side wgpu texture cache — pairs with `xenia_gpu::texture_cache` (the
//! CPU-side decoded-bytes layer) by mapping the same [`TextureKey`] onto an
//! uploaded `wgpu::Texture` + `TextureView`.
//!
//! The xenos pipeline only binds one texture at a time (group 1 single slot);
//! this cache lets the draw-dispatch loop uphold that layout without
//! re-uploading the same texture across frames. Entries carry the
//! `version_when_uploaded` stamp that originated from
//! [`xenia_memory::GuestMemory::page_version`]; on a stale hit the caller
//! re-decodes (via the CPU cache) and [`TextureCacheHost::upload`] replaces
//! the wgpu texture in place.

use std::collections::{HashMap, VecDeque};

use xenia_gpu::texture_cache::{CachedTexture, TextureFormat, TextureKey};

pub struct HostTextureEntry {
    /// Retained so the `wgpu::Texture` outlives its `TextureView` for
    /// the lifetime of the cache entry. wgpu views internally reference
    /// their backing texture through an `Arc`, so dropping this field
    /// would technically still leave the view valid — but keeping it
    /// makes the ownership intent explicit and avoids relying on wgpu
    /// internals. Read only at construction; the `#[allow]` silences
    /// `dead_code` since Rust can't tell the field is load-bearing for
    /// drop ordering.
    #[allow(dead_code)]
    pub texture: wgpu::Texture,
    pub view: wgpu::TextureView,
    pub version_when_uploaded: u64,
}

pub struct TextureCacheHost {
    entries: HashMap<TextureKey, HostTextureEntry>,
    /// FIFO insertion order for bounded eviction. The intro video uploads
    /// ~3 new-VA textures per frame (the Y/U/V planes land at rotating guest
    /// addresses), so without a cap the wgpu texture set grows unbounded until
    /// the GPU device runs out of memory (a device-lost hard crash mid-movie).
    /// Once we exceed `MAX_ENTRIES` the oldest texture is evicted; an evicted
    /// texture simply re-uploads on demand if the guest samples it again.
    order: VecDeque<TextureKey>,
    /// HUD-surfaced counters — mirror the CPU-side cache so a session
    /// can tell whether uploads are dominated by fresh work or stale
    /// invalidations.
    pub uploads_total: u64,
    pub reuploads_total: u64,
}

impl Default for TextureCacheHost {
    fn default() -> Self {
        Self::new()
    }
}

impl TextureCacheHost {
    /// Upper bound on simultaneously-resident host textures. The movie's live
    /// working set is small (~3–9 planes across the in-flight frames) and boot
    /// uses a couple dozen; 64 leaves headroom while bounding GPU memory to a
    /// few hundred MB worst case.
    const MAX_ENTRIES: usize = 64;

    pub fn new() -> Self {
        Self {
            entries: HashMap::new(),
            order: VecDeque::new(),
            uploads_total: 0,
            reuploads_total: 0,
        }
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Record a freshly-inserted key in FIFO order and evict the oldest
    /// entries until we're back under `MAX_ENTRIES`. Never evicts the key we
    /// just inserted (it's at the back of the queue).
    fn record_and_evict(&mut self, key: TextureKey, is_new: bool) {
        if is_new {
            self.order.push_back(key);
        }
        while self.order.len() > Self::MAX_ENTRIES {
            match self.order.pop_front() {
                Some(old) if old != key => {
                    self.entries.remove(&old);
                }
                Some(old) => {
                    // The only remaining entry is the current key — keep it.
                    self.order.push_back(old);
                    break;
                }
                None => break,
            }
        }
    }

    /// Kept for API symmetry with `len` (clippy recommends both together).
    /// Unused in code today — callers check `len() == 0` via the HUD.
    #[allow(dead_code)]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Upload (or refresh) a cached texture. Returns a `&TextureView` the
    /// pipeline can bind. Idempotent when `cached.version_when_uploaded`
    /// matches an existing entry — in that case we just hand back the
    /// existing view without touching GPU bandwidth.
    pub fn upload(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        cached: &CachedTexture,
    ) -> &HostTextureEntry {
        let key = cached.key;
        let needs_refresh = match self.entries.get(&key) {
            None => true,
            Some(existing) => existing.version_when_uploaded < cached.version_when_uploaded,
        };
        if !needs_refresh {
            return self.entries.get(&key).unwrap();
        }
        // Evict + re-upload. For P5 we always recreate the texture (no
        // in-place writeback); re-uploading a 1280×720 texture is <3 MB
        // and happens only when the guest actually touches the bytes.
        let Some(descriptor) = descriptor_for(&key) else {
            // Unsupported — keep a record-keeping entry pointing at a
            // dummy and let the pipeline fall back to its own magenta
            // placeholder. We still create a minimal 1×1 magenta view
            // so `get` returns something bindable.
            let is_new = !self.entries.contains_key(&key);
            let (tex, view) = create_magenta_stub(device, queue);
            let entry = HostTextureEntry {
                texture: tex,
                view,
                version_when_uploaded: cached.version_when_uploaded,
            };
            self.entries.insert(key, entry);
            self.uploads_total += 1;
            self.record_and_evict(key, is_new);
            return self.entries.get(&key).unwrap();
        };
        let texture = device.create_texture(&descriptor);
        // Write pixel data. BCn formats require block-aligned extents;
        // uncompressed formats use byte-aligned rows. First-Pixels M5:
        // BC1 = 8 bytes/block, BC2/BC3 = 16 bytes/block.
        let bytes_per_row = match key.format {
            TextureFormat::Dxt1 => Some((key.width as u32).div_ceil(4) * 8),
            TextureFormat::Dxt2_3 | TextureFormat::Dxt4_5 => {
                Some((key.width as u32).div_ceil(4) * 16)
            }
            // K8888 and K565 (CPU-expanded to Rgba8Unorm) both produce
            // 4 bytes per texel in the wgpu-uploadable buffer.
            _ => Some(key.width as u32 * 4),
        };
        queue.write_texture(
            wgpu::ImageCopyTexture {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &cached.bytes,
            wgpu::ImageDataLayout {
                offset: 0,
                bytes_per_row,
                rows_per_image: None,
            },
            wgpu::Extent3d {
                width: key.width as u32,
                height: key.height as u32,
                depth_or_array_layers: 1,
            },
        );
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let entry = HostTextureEntry {
            texture,
            view,
            version_when_uploaded: cached.version_when_uploaded,
        };
        let had_prior = self.entries.contains_key(&key);
        self.entries.insert(key, entry);
        if had_prior {
            self.reuploads_total += 1;
        } else {
            self.uploads_total += 1;
        }
        self.record_and_evict(key, !had_prior);
        self.entries.get(&key).unwrap()
    }

    pub fn view_for(&self, key: &TextureKey) -> Option<&wgpu::TextureView> {
        self.entries.get(key).map(|e| &e.view)
    }
}

/// wgpu `TextureDescriptor` matching a Xenos [`TextureKey`]. Returns `None`
/// for unsupported formats — caller uses a magenta stub.
///
/// First-Pixels M5 adds `K565` (CPU-expanded to `Rgba8Unorm` in the
/// decoder), `Dxt2_3` → `Bc2RgbaUnorm`, and `Dxt4_5` → `Bc3RgbaUnorm`.
/// BC2/BC3 have a 16-byte block footprint (BC1 is 8), so `upload`'s
/// `bytes_per_row` computation branches on the block size too.
fn descriptor_for(key: &TextureKey) -> Option<wgpu::TextureDescriptor<'static>> {
    let format = match key.format {
        TextureFormat::K8 => wgpu::TextureFormat::Rgba8Unorm, // CPU-expanded (replicated)
        TextureFormat::K8888 => wgpu::TextureFormat::Rgba8Unorm,
        TextureFormat::K565 => wgpu::TextureFormat::Rgba8Unorm, // CPU-expanded
        TextureFormat::Dxt1 => wgpu::TextureFormat::Bc1RgbaUnorm,
        TextureFormat::Dxt2_3 => wgpu::TextureFormat::Bc2RgbaUnorm,
        TextureFormat::Dxt4_5 => wgpu::TextureFormat::Bc3RgbaUnorm,
        _ => return None,
    };
    Some(wgpu::TextureDescriptor {
        label: Some("xenos cached texture"),
        size: wgpu::Extent3d {
            width: key.width as u32,
            height: key.height as u32,
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format,
        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
        view_formats: &[],
    })
}

fn create_magenta_stub(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
) -> (wgpu::Texture, wgpu::TextureView) {
    let tex = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("xenos cached stub"),
        size: wgpu::Extent3d {
            width: 1,
            height: 1,
            depth_or_array_layers: 1,
        },
        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::ImageCopyTexture {
            texture: &tex,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        &[0xFFu8, 0x00, 0xFF, 0xFF],
        wgpu::ImageDataLayout {
            offset: 0,
            bytes_per_row: Some(4),
            rows_per_image: Some(1),
        },
        wgpu::Extent3d {
            width: 1,
            height: 1,
            depth_or_array_layers: 1,
        },
    );
    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
    (tex, view)
}
