use abi::error::Error;
use alloc::sync::Arc;
use device_api::{
    device::Device,
    interrupt::{InterruptHandler, Irq, IrqVector},
};
use kernel_arch_x86::{
    intrinsics::{IoPort, IoPortAccess},
    ISA_IRQ_OFFSET,
};
use libk::{device::external_interrupt_controller, task::runtime, time};
use libk_util::{sync::IrqSafeSpinlock, OneTimeInit};

const FREQUENCY: u32 = 1193180;

const CMD_CH0: u8 = 0 << 6;
const CMD_ACC_WORD: u8 = 3 << 4;
const CMD_MODE_RATE: u8 = 2 << 1;

struct Inner {
    ch0_data: IoPort<u8>,
    #[allow(unused)]
    ch1_data: IoPort<u8>,
    #[allow(unused)]
    ch2_data: IoPort<u8>,
    cmd: IoPort<u8>,
}

pub struct I8253 {
    inner: IrqSafeSpinlock<Inner>,
}

impl InterruptHandler for I8253 {
    fn handle_irq(self: Arc<Self>, _vector: IrqVector) -> bool {
        self.irq_handler_fastpath();
        true
    }
}

impl Device for I8253 {
    fn display_name(&self) -> &'static str {
        "i8253 PIT"
    }

    unsafe fn init_irq(self: Arc<Self>) -> Result<(), Error> {
        let intc = external_interrupt_controller()?;
        let inner = self.inner.lock();

        let div: u16 = (FREQUENCY / 1000).try_into().unwrap();

        inner.cmd.write(CMD_CH0 | CMD_ACC_WORD | CMD_MODE_RATE);
        inner.ch0_data.write(div as u8);
        inner.ch0_data.write((div >> 8) as u8);

        let irq = Irq::External(ISA_IRQ_OFFSET);
        intc.register_irq(irq, Default::default(), self.clone())?;
        intc.enable_irq(irq)?;

        Ok(())
    }
}

impl I8253 {
    const fn new() -> Self {
        Self {
            inner: IrqSafeSpinlock::new(Inner {
                ch0_data: IoPort::new(0x40),
                ch1_data: IoPort::new(0x41),
                ch2_data: IoPort::new(0x42),
                cmd: IoPort::new(0x43),
            }),
        }
    }

    pub fn setup() -> Result<Arc<Self>, Error> {
        let this = Arc::new(Self::new());
        unsafe { this.clone().init_irq() }?;
        INSTANCE.init(this.clone());
        Ok(this)
    }

    pub fn irq_handler_fastpath(&self) {
        time::add_milliseconds(1);
        runtime::tick();

        #[cfg(any(target_arch = "x86", rust_analyzer))]
        {
            use kernel_arch::task::Scheduler;
            use libk::arch::Cpu;

            let cpu = Cpu::local();

            if let Some(queue) = cpu.try_get_scheduler() {
                unsafe { queue.yield_cpu() };
            }
        }
    }
}

static INSTANCE: OneTimeInit<Arc<I8253>> = OneTimeInit::new();
