//! Primary ring buffer view.
//!
//! Games allocate a ring buffer in physical memory (via
//! `MmAllocatePhysicalMemoryEx` with WRITE_COMBINE), then hand the base
//! address + log2(size) to `VdInitializeRingBuffer`. They subsequently push
//! PM4 packets into it, advancing the write-pointer by writing to a GPU
//! register (`CP_RB_WPTR`) or via kernel-call shims.
//!
//! The GPU consumes packets from `read_offset_dwords` up to (but not past)
//! the write pointer. After consuming enough bytes it writes `read_offset`
//! into the guest-memory address registered by `VdEnableRingBufferRPtrWriteBack`
//! so the game can know how much of the ring has been consumed.

/// Tracks the primary ring buffer as set up by the guest.
#[derive(Debug, Clone, Copy, Default)]
pub struct RingBufferView {
    /// Guest physical/virtual base address. `0` means uninitialized.
    pub base: u32,
    /// Size of the ring in dwords. `0` means uninitialized.
    pub size_dwords: u32,
    /// Dword offset the GPU has consumed up to (relative to `base`).
    pub read_offset_dwords: u32,
    /// Dword offset the guest has last written into (relative to `base`).
    /// Updated either by an MMIO write to `CP_RB_WPTR` or by the kernel
    /// (`VdSwap` is a hint — the game reserves a 64-dword slot in the ring
    /// for it).
    pub write_offset_dwords: u32,
    /// Guest address where we mirror `read_offset_dwords` each time we make
    /// progress. `0` if the game never called `VdEnableRingBufferRPtrWriteBack`.
    pub rptr_writeback_addr: u32,
    /// Write-back block granularity in dwords (from the `log2` arg to
    /// `VdEnableRingBufferRPtrWriteBack`). We always write back eagerly, so
    /// we don't actually use this for scheduling — kept for observability.
    pub rptr_writeback_block_dwords: u32,
    /// True for an indirect-buffer (`INDIRECT_BUFFER`) view. An IB is a fixed
    /// *linear* sub-stream, not a circular ring: it is fully written when the
    /// GPU jumps to it, so the read pointer advances monotonically from `0` to
    /// `size_dwords` and then the buffer is exhausted (the caller ring is
    /// popped). It must NOT wrap, and the primary `CP_RB_WPTR` must not be
    /// applied to it. Mirrors canary `ExecuteIndirectBuffer`, which executes
    /// the IB through a separate `RingBuffer reader_` and restores the primary
    /// reader afterward (command_processor.cc). Circular (primary-ring)
    /// semantics are used when this is `false`.
    pub indirect: bool,
}

impl RingBufferView {
    pub fn new() -> Self {
        Self::default()
    }

    /// True if the guest has provided a base + size.
    pub fn is_initialized(&self) -> bool {
        self.base != 0 && self.size_dwords != 0
    }

    /// True if there is pending unread data to consume.
    pub fn has_pending(&self) -> bool {
        if !self.is_initialized() {
            return false;
        }
        if self.indirect {
            // Linear sub-stream: exhausted once the read pointer reaches the
            // (fixed) write pointer. Never wraps.
            self.read_offset_dwords < self.write_offset_dwords
        } else {
            self.read_offset_dwords != self.write_offset_dwords
        }
    }

    /// Number of dwords we can consume without wrapping past the write ptr.
    pub fn pending_dwords(&self) -> u32 {
        if !self.is_initialized() {
            return 0;
        }
        if self.indirect {
            self.write_offset_dwords
                .saturating_sub(self.read_offset_dwords)
        } else if self.write_offset_dwords >= self.read_offset_dwords {
            self.write_offset_dwords - self.read_offset_dwords
        } else {
            // write has wrapped — we can read up to the end of the ring.
            self.size_dwords - self.read_offset_dwords
        }
    }

    /// Advance the read pointer by `dwords`. Circular rings wrap at
    /// `size_dwords`; an indirect buffer advances linearly (no wrap) so it
    /// terminates exactly at its fixed write pointer.
    pub fn advance_read(&mut self, dwords: u32) {
        if self.size_dwords == 0 {
            return;
        }
        if self.indirect {
            self.read_offset_dwords = self.read_offset_dwords.saturating_add(dwords);
        } else {
            self.read_offset_dwords =
                (self.read_offset_dwords + dwords) % self.size_dwords;
        }
    }

    /// Guest address for the dword at relative offset `i` from the current
    /// read pointer. `None` if uninitialized.
    pub fn addr_at_offset(&self, offset_dwords: u32) -> Option<u32> {
        if !self.is_initialized() {
            return None;
        }
        let off = if self.indirect {
            self.read_offset_dwords.saturating_add(offset_dwords)
        } else {
            (self.read_offset_dwords + offset_dwords) % self.size_dwords
        };
        Some(self.base.wrapping_add(off.wrapping_mul(4)))
    }
}

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

    #[test]
    fn uninitialized_view_reports_empty() {
        let v = RingBufferView::new();
        assert!(!v.is_initialized());
        assert!(!v.has_pending());
        assert_eq!(v.pending_dwords(), 0);
    }

    #[test]
    fn wrap_around_arithmetic() {
        let mut v = RingBufferView::new();
        v.base = 0x4000_0000;
        v.size_dwords = 16;
        v.read_offset_dwords = 14;
        v.write_offset_dwords = 2; // wrapped

        // We can only read to end-of-ring in one chunk.
        assert_eq!(v.pending_dwords(), 2);
        v.advance_read(2);
        assert_eq!(v.read_offset_dwords, 0);
        // Now unwrapped, 2 more to go.
        assert_eq!(v.pending_dwords(), 2);
    }

    #[test]
    fn addr_at_offset_wraps() {
        let mut v = RingBufferView::new();
        v.base = 0x4000_0000;
        v.size_dwords = 4;
        v.read_offset_dwords = 3;
        assert_eq!(v.addr_at_offset(0), Some(0x4000_000C));
        assert_eq!(v.addr_at_offset(1), Some(0x4000_0000));
        assert_eq!(v.addr_at_offset(2), Some(0x4000_0004));
    }

    #[test]
    fn indirect_buffer_drains_linearly_and_terminates() {
        // An indirect buffer is a fixed linear sub-stream: read advances from
        // 0 to `size_dwords` and then is exhausted — it must NOT wrap back to
        // 0 (which previously caused an infinite re-read of a system command
        // buffer; iterate-2O). write_offset == size, exactly as the
        // INDIRECT_BUFFER handler sets it.
        let mut ib = RingBufferView {
            base: 0x4adf_5080,
            size_dwords: 11,
            read_offset_dwords: 0,
            write_offset_dwords: 11,
            rptr_writeback_addr: 0,
            rptr_writeback_block_dwords: 0,
            indirect: true,
        };
        assert!(ib.has_pending());
        // Drain the exact packet layout observed for Sylpheed's init IB:
        // 2 + 3 + 6 dwords = 11.
        ib.advance_read(2);
        assert!(ib.has_pending());
        ib.advance_read(3);
        assert!(ib.has_pending());
        ib.advance_read(6); // reaches 11 == write
        assert_eq!(ib.read_offset_dwords, 11);
        assert!(
            !ib.has_pending(),
            "indirect buffer must terminate at write ptr, not wrap to 0"
        );
        // addr_at_offset must not modulo-wrap for an indirect buffer.
        ib.read_offset_dwords = 9;
        assert_eq!(ib.addr_at_offset(1), Some(0x4adf_5080 + 10 * 4));
    }

    #[test]
    fn indirect_flag_does_not_affect_circular_ring() {
        // Sanity: a circular (primary) ring still wraps as before.
        let mut v = RingBufferView::new();
        v.base = 0x4adc_c000;
        v.size_dwords = 8192;
        v.read_offset_dwords = 8190;
        v.write_offset_dwords = 2;
        assert!(v.has_pending());
        v.advance_read(4); // (8190 + 4) % 8192 = 2
        assert_eq!(v.read_offset_dwords, 2);
        assert!(!v.has_pending());
    }
}
