use alloc::{boxed::Box, string::String, vec::Vec};

use ax_errno::AxError;
use rdif_display::Interface;
use rdrive::{DriverGeneric, probe::OnProbeError};

use crate::{
    BindingInfo, BindingIrq, binding_info_from_acpi, binding_info_from_fdt,
    registration::{BoundDevice, TakeRegistered, register_bound_device, take_registered_device},
};
#[cfg(feature = "pci")]
use crate::{PciIrqRequirement, binding_info_from_pci};

pub struct PlatformDisplayDevice {
    name: String,
    info: BindingInfo,
    display: Option<Box<dyn Interface>>,
}

impl PlatformDisplayDevice {
    fn new(name: String, display: Box<dyn Interface>, info: BindingInfo) -> Self {
        Self {
            name,
            info,
            display: Some(display),
        }
    }

    pub fn binding_info(&self) -> &BindingInfo {
        &self.info
    }

    pub fn irq_num(&self) -> Option<usize> {
        self.info.irq_num()
    }

    pub fn irq_cloned(&self) -> Option<BindingIrq> {
        self.info.irq_cloned()
    }
}

impl DriverGeneric for PlatformDisplayDevice {
    fn name(&self) -> &str {
        &self.name
    }
}

impl BoundDevice for PlatformDisplayDevice {
    fn binding_info(&self) -> &BindingInfo {
        &self.info
    }
}

pub struct TakenDisplayDevice {
    pub device: Box<dyn Interface>,
    pub irq: Option<BindingIrq>,
}

impl TakeRegistered for PlatformDisplayDevice {
    type Output = TakenDisplayDevice;

    fn take_registered(&mut self) -> Option<Self::Output> {
        Some(TakenDisplayDevice {
            device: self.display.take()?,
            irq: self.info.irq_cloned(),
        })
    }
}

pub trait PlatformDeviceDisplay {
    fn register_display<T>(self, dev: T) -> Option<usize>
    where
        T: Interface + 'static;

    fn register_display_with_info<T>(self, dev: T, info: BindingInfo) -> Option<usize>
    where
        T: Interface + 'static;
}

impl PlatformDeviceDisplay for rdrive::PlatformDevice {
    fn register_display<T>(self, dev: T) -> Option<usize>
    where
        T: Interface + 'static,
    {
        self.register_display_with_info(dev, BindingInfo::empty())
    }

    fn register_display_with_info<T>(self, dev: T, info: BindingInfo) -> Option<usize>
    where
        T: Interface + 'static,
    {
        register_display_with_info(self, dev, info)
    }
}

pub trait ProbeFdtDisplay {
    fn register_display<T>(self, dev: T) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static;
}

impl ProbeFdtDisplay for rdrive::probe::fdt::ProbeFdt<'_> {
    fn register_display<T>(self, dev: T) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static,
    {
        let info = binding_info_from_fdt(self.info())?;
        Ok(register_display_with_info(
            self.into_platform_device(),
            dev,
            info,
        ))
    }
}

pub trait ProbeAcpiDisplay {
    fn register_display<T>(self, dev: T) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static;
}

impl ProbeAcpiDisplay for rdrive::probe::acpi::ProbeAcpi<'_> {
    fn register_display<T>(self, dev: T) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static,
    {
        let info = binding_info_from_acpi(self.info())?;
        Ok(register_display_with_info(
            self.into_platform_device(),
            dev,
            info,
        ))
    }
}

#[cfg(feature = "pci")]
pub trait ProbePciDisplay {
    fn register_display<T>(
        self,
        dev: T,
        requirement: PciIrqRequirement,
    ) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static;
}

#[cfg(feature = "pci")]
impl ProbePciDisplay for rdrive::probe::pci::ProbePci<'_> {
    fn register_display<T>(
        self,
        dev: T,
        requirement: PciIrqRequirement,
    ) -> Result<Option<usize>, OnProbeError>
    where
        T: Interface + 'static,
    {
        let info = binding_info_from_pci(self.info(), requirement)?;
        Ok(register_display_with_info(
            self.into_platform_device(),
            dev,
            info,
        ))
    }
}

fn register_display_with_info<T>(
    plat_dev: rdrive::PlatformDevice,
    dev: T,
    info: BindingInfo,
) -> Option<usize>
where
    T: Interface + 'static,
{
    let name = dev.name().into();
    register_bound_device(
        plat_dev,
        PlatformDisplayDevice::new(name, Box::new(dev), info),
    )
}

pub fn take_display_devices() -> Result<Vec<TakenDisplayDevice>, AxError> {
    let mut devices = Vec::new();
    for dev in rdrive::get_list::<PlatformDisplayDevice>() {
        let display = take_display_device(dev)?;
        devices.push(display);
    }
    Ok(devices)
}

fn take_display_device(
    device: rdrive::Device<PlatformDisplayDevice>,
) -> Result<TakenDisplayDevice, AxError> {
    take_registered_device(device).ok_or(AxError::BadState)
}

#[cfg(test)]
mod tests {
    extern crate std;

    use rdif_display::{DisplayError, DisplayInfo, FrameBuffer, PixelFormat};

    use super::*;
    use crate::{BindingInfo, BindingIrq};

    struct TestDisplay {
        fb: [u8; 16],
    }

    impl DriverGeneric for TestDisplay {
        fn name(&self) -> &str {
            "test-display"
        }
    }

    impl Interface for TestDisplay {
        fn info(&self) -> DisplayInfo {
            DisplayInfo {
                width: 2,
                height: 2,
                stride: 8,
                format: PixelFormat::Xrgb8888,
                fb_size: self.fb.len(),
            }
        }

        fn framebuffer(&mut self) -> Result<FrameBuffer<'_>, DisplayError> {
            Ok(FrameBuffer::from_slice(&mut self.fb))
        }
    }

    #[test]
    fn platform_display_device_exposes_binding_info_irq_num() {
        let irq = 42;
        let device = PlatformDisplayDevice::new(
            "test-display".into(),
            Box::new(TestDisplay { fb: [0; 16] }),
            BindingInfo::with_irq(Some(irq)).unwrap(),
        );

        assert_eq!(device.binding_info().irq_num(), Some(irq));
        assert_eq!(device.irq_num(), Some(irq));
        assert_eq!(BoundDevice::irq_num(&device), Some(irq));
    }

    #[test]
    fn platform_display_device_empty_binding_has_no_irq_num() {
        let device = PlatformDisplayDevice::new(
            "test-display".into(),
            Box::new(TestDisplay { fb: [0; 16] }),
            BindingInfo::empty(),
        );

        assert_eq!(device.binding_info().irq_num(), None);
        assert_eq!(device.irq_num(), None);
        assert_eq!(BoundDevice::irq_num(&device), None);
    }

    #[test]
    fn platform_display_device_exposes_native_binding_irq() {
        let irq = BindingIrq::fdt_interrupt_with_controller(rdrive::DeviceId::new(), [0, 42, 4]);
        let device = PlatformDisplayDevice::new(
            "test-display".into(),
            Box::new(TestDisplay { fb: [0; 16] }),
            BindingInfo::with_binding_irq(Some(irq.clone())),
        );

        assert_eq!(device.irq_cloned(), Some(irq));
        assert_eq!(device.irq_num(), None);
        assert_eq!(BoundDevice::irq_num(&device), None);
    }
}
