//! Ring-buffer drainer.
//!
//! Walks a guest PM4 ring buffer from `start_offset` forward, classifying each
//! packet via [`crate::pm4`] and stopping when it either reaches the end of
//! the window it was asked to scan, walks off a NOP-fill region, or hits a
//! malformed header.
//!
//! It does **not** execute draws — that's deferred to a later phase. Its job
//! is to (a) advance the read pointer far enough that games keep making
//! progress, and (b) surface `PM4_XE_SWAP` packets so `VdSwap` can forward
//! them to the host UI.

use xenia_memory::MemoryAccess;

use crate::pm4::{self, PacketKind};

/// Outcome of a [`drain`] call.
#[derive(Default, Debug, Clone, Copy)]
pub struct DrainResult {
    /// Dword offset reached, relative to the start of the ring buffer.
    pub new_offset: u32,
    /// How many packets were walked in this call.
    pub packets_walked: u32,
    /// True if we saw `PM4_XE_SWAP` during the walk.
    pub swap_seen: bool,
    /// If `swap_seen`, the guest frontbuffer *physical* address written next
    /// to `PM4_XE_SWAP` (dword 2 of the 4-payload packet).
    pub swap_frontbuffer_phys: u32,
    /// If `swap_seen`, the width written at dword 3.
    pub swap_width: u32,
    /// If `swap_seen`, the height written at dword 4.
    pub swap_height: u32,
}

/// Walk `max_packets` packets starting at dword offset `start_offset` in the
/// ring buffer at guest address `ring_base` of size `ring_size_dwords`.
///
/// The offset is treated modulo `ring_size_dwords`. Walking stops when:
/// - `max_packets` have been walked,
/// - a `PM4_XE_SWAP` has been consumed (the swap is reported and we stop so
///   the UI sees the frame boundary before further drain),
/// - a header's declared total size would exceed the remaining budget,
/// - the ring size is zero (drainer is a no-op).
pub fn drain<M: MemoryAccess + ?Sized>(
    mem: &M,
    ring_base: u32,
    ring_size_dwords: u32,
    start_offset: u32,
    max_packets: u32,
) -> DrainResult {
    if ring_size_dwords == 0 || ring_base == 0 {
        return DrainResult::default();
    }
    let mut result = DrainResult {
        new_offset: start_offset % ring_size_dwords,
        ..DrainResult::default()
    };
    let mut offset = result.new_offset;
    for _ in 0..max_packets {
        let header_addr = ring_base.wrapping_add(offset.wrapping_mul(4));
        let header = mem.read_u32(header_addr);
        let packet = pm4::decode(header);
        // Refuse to walk past the ring in a single packet.
        if packet.total_dwords > ring_size_dwords {
            break;
        }
        // Type-3 PM4_XE_SWAP → record payload and stop.
        if let PacketKind::Type3 { opcode, .. } = packet.kind
            && opcode == pm4::PM4_XE_SWAP {
                // Payload layout (from canary VdSwap_entry):
                //   [0] XE_SWAP header
                //   [1] kSwapSignature ("XNEX" = 0x584E4558)
                //   [2] frontbuffer physical address
                //   [3] width
                //   [4] height
                let payload = |i: u32| {
                    let addr =
                        ring_base.wrapping_add(((offset + i) % ring_size_dwords).wrapping_mul(4));
                    mem.read_u32(addr)
                };
                result.swap_seen = true;
                result.swap_frontbuffer_phys = payload(2);
                result.swap_width = payload(3);
                result.swap_height = payload(4);
                offset = (offset + packet.total_dwords) % ring_size_dwords;
                result.new_offset = offset;
                result.packets_walked += 1;
                return result;
            }
        offset = (offset + packet.total_dwords) % ring_size_dwords;
        result.new_offset = offset;
        result.packets_walked += 1;
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use xenia_memory::GuestMemory;
    use xenia_memory::page_table::MemoryProtect;

    fn build_mem() -> GuestMemory {
        let mut mem = GuestMemory::new().unwrap();
        let rw = MemoryProtect::READ | MemoryProtect::WRITE;
        mem.alloc(0x4000_0000, 0x1000, rw).unwrap();
        mem
    }

    fn write_dword(mem: &GuestMemory, addr: u32, val: u32) {
        mem.write_u32(addr, val);
    }

    #[test]
    fn walks_nops_until_budget_exhausted() {
        let mut mem = build_mem();
        // Fill 10 dwords with Type-2 NOPs.
        for i in 0..10 {
            write_dword(&mut mem, 0x4000_0000 + i * 4, 0x8000_0000);
        }
        let r = drain(&mem, 0x4000_0000, 0x400, 0, 5);
        assert_eq!(r.packets_walked, 5);
        assert_eq!(r.new_offset, 5);
        assert!(!r.swap_seen);
    }

    #[test]
    fn stops_at_swap_and_reports_payload() {
        let mut mem = build_mem();
        // Two NOPs, then a PM4_XE_SWAP packet.
        write_dword(&mut mem, 0x4000_0000, 0x8000_0000);
        write_dword(&mut mem, 0x4000_0004, 0x8000_0000);
        // MakePacketType3(PM4_XE_SWAP, 4) → (3<<30) | (3<<16) | (0x64<<8)
        let swap_hdr = (3u32 << 30) | (3u32 << 16) | ((pm4::PM4_XE_SWAP as u32) << 8);
        write_dword(&mut mem, 0x4000_0008, swap_hdr);
        write_dword(&mut mem, 0x4000_000C, pm4::SWAP_SIGNATURE);
        write_dword(&mut mem, 0x4000_0010, 0xDEAD_F000); // frontbuffer phys
        write_dword(&mut mem, 0x4000_0014, 1280);
        write_dword(&mut mem, 0x4000_0018, 720);
        let r = drain(&mem, 0x4000_0000, 0x400, 0, 16);
        assert!(r.swap_seen);
        assert_eq!(r.swap_frontbuffer_phys, 0xDEAD_F000);
        assert_eq!(r.swap_width, 1280);
        assert_eq!(r.swap_height, 720);
        assert_eq!(r.packets_walked, 3);
        assert_eq!(r.new_offset, 7); // 2 NOPs (1 dword each) + 5-dword swap = 7
    }

    #[test]
    fn wraps_around_ring() {
        let mut mem = build_mem();
        // Ring size = 4 dwords. Start at offset 3 (last dword). Write a NOP
        // there, then the walker should wrap to offset 0.
        write_dword(&mut mem, 0x4000_000C, 0x8000_0000);
        write_dword(&mut mem, 0x4000_0000, 0x8000_0000);
        let r = drain(&mem, 0x4000_0000, 4, 3, 2);
        assert_eq!(r.packets_walked, 2);
        assert_eq!(r.new_offset, 1);
    }

    #[test]
    fn zero_ring_size_is_noop() {
        let mem = build_mem();
        let r = drain(&mem, 0x4000_0000, 0, 0, 10);
        assert_eq!(r.packets_walked, 0);
        assert_eq!(r.new_offset, 0);
        assert!(!r.swap_seen);
    }
}
