//! PCI Express ECAM interface
use libk_mm::{address::PhysicalAddress, device::DeviceMemoryMapping};
use yggdrasil_abi::error::Error;

use super::{PciAddress, PciConfigurationSpace};

/// PCI Express Enhanced Configuration Access Mechanism
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct PciEcam {
    mapping: DeviceMemoryMapping,
}

impl PciConfigurationSpace for PciEcam {
    fn read_u32(&self, offset: usize) -> u32 {
        assert_eq!(offset & 3, 0);
        unsafe { ((self.mapping.address() + offset) as *const u32).read_volatile() }
    }

    fn write_u32(&self, offset: usize, value: u32) {
        assert_eq!(offset & 3, 0);
        unsafe { ((self.mapping.address() + offset) as *mut u32).write_volatile(value) }
    }
}

impl PciEcam {
    /// Maps the physical address of a ECAM space for kernel access.
    ///
    /// # Safety
    ///
    /// The `phys_addr` must be a valid ECAM address. The address must not alias any other mapped
    /// regions. The address must be aligned to a 4KiB boundary and be valid for accesses within a
    /// 4KiB-sized range.
    pub unsafe fn map(phys_addr: PhysicalAddress) -> Result<Self, Error> {
        let mapping = DeviceMemoryMapping::map(phys_addr, 0x1000, Default::default())?;
        Ok(Self { mapping })
    }

    /// Checks if the ECAM contains a valid device configuration space, mapping and returning a
    /// [PciEcam] if it does.
    ///
    /// # Safety
    ///
    /// See [PciEcam::map].
    pub unsafe fn probe_raw_parts(
        segment_phys_addr: PhysicalAddress,
        bus_offset: u8,
        address: PciAddress,
    ) -> Result<Option<Self>, Error> {
        let phys_addr = segment_phys_addr.add(
            ((address.bus - bus_offset) as usize * 256
                + address.device as usize * 8
                + address.function as usize)
                * 0x1000,
        );
        let this = Self::map(phys_addr)?;

        Ok(if this.is_valid() { Some(this) } else { None })
    }
}
