pub struct Console;

const COM1_PORT: u16 = 0x3f8;
const COM1_CLOCK_HZ: u32 = 1_843_200;
const COM1_IRQ_VECTOR: usize = 0x30 + 4;

const UART_IER: u16 = 1;
const UART_IIR: u16 = 2;
const UART_LCR: u16 = 3;
const UART_MCR: u16 = 4;
const UART_LSR: u16 = 5;

const IER_RECEIVED_DATA_AVAILABLE: u8 = 1 << 0;
const IER_RECEIVER_LINE_STATUS: u8 = 1 << 2;

const LCR_DIVISOR_LATCH_ACCESS: u8 = 1 << 7;
const MCR_INTERRUPT_OUTPUT_ENABLE: u8 = 1 << 3;

const IIR_NO_INTERRUPT_PENDING: u8 = 1 << 0;
const IIR_INTERRUPT_ID_MASK: u8 = 0x0e;
const IIR_RECEIVER_LINE_STATUS: u8 = 0x06;
const IIR_RECEIVED_DATA_AVAILABLE: u8 = 0x04;
const IIR_CHARACTER_TIMEOUT: u8 = 0x0c;

const LSR_DATA_READY: u8 = 1 << 0;
const LSR_OVERRUN_ERROR: u8 = 1 << 1;
const LSR_RX_ERROR_MASK: u8 = 0x1e;

impl crate::console::ArchConsoleOps for Console {
    fn init() -> bool {
        // someboot runs before the normal allocator is available, so the early
        // serial path must stay on the raw register-level driver. Do not use
        // the rdif/ax-driver runtime wrapper here: it allocates `Box`/`Arc`
        // state around the raw UART for the OS driver model.
        let mut uart = some_serial::ns16550::Ns16550::new_port(COM1_PORT, COM1_CLOCK_HZ);
        uart.open();
        crate::console::set_earlycon_serial(crate::console::EarlySerial::new(
            crate::console::EarlySerialRaw::Ns16550Port(uart),
        ));
        true
    }

    fn read_byte() -> Option<u8> {
        unsafe {
            let status = x86::io::inb(COM1_PORT + UART_LSR);
            if status & LSR_DATA_READY == 0 {
                None
            } else {
                Some(x86::io::inb(COM1_PORT))
            }
        }
    }

    fn irq_num() -> Option<usize> {
        Some(COM1_IRQ_VECTOR)
    }

    fn set_input_irq_enabled(enabled: bool) {
        unsafe {
            let lcr = x86::io::inb(COM1_PORT + UART_LCR);
            x86::io::outb(COM1_PORT + UART_LCR, lcr & !LCR_DIVISOR_LATCH_ACCESS);

            let mut mcr = x86::io::inb(COM1_PORT + UART_MCR);
            if enabled {
                mcr |= MCR_INTERRUPT_OUTPUT_ENABLE;
            } else {
                mcr &= !MCR_INTERRUPT_OUTPUT_ENABLE;
            }
            x86::io::outb(COM1_PORT + UART_MCR, mcr);

            x86::io::outb(
                COM1_PORT + UART_IER,
                if enabled {
                    IER_RECEIVED_DATA_AVAILABLE | IER_RECEIVER_LINE_STATUS
                } else {
                    0
                },
            );
        }
    }

    fn handle_irq() -> u32 {
        let iir = unsafe { x86::io::inb(COM1_PORT + UART_IIR) };
        let lsr = unsafe { x86::io::inb(COM1_PORT + UART_LSR) };
        let mut events = 0;

        if lsr & LSR_DATA_READY != 0 {
            events |= crate::console::CONSOLE_IRQ_RX_READY;
        }
        if lsr & LSR_OVERRUN_ERROR != 0 {
            events |= crate::console::CONSOLE_IRQ_OVERRUN;
        }
        if lsr & LSR_RX_ERROR_MASK != 0 {
            events |= crate::console::CONSOLE_IRQ_RX_ERROR;
        }

        if iir & IIR_NO_INTERRUPT_PENDING == 0 {
            match iir & IIR_INTERRUPT_ID_MASK {
                IIR_RECEIVED_DATA_AVAILABLE | IIR_CHARACTER_TIMEOUT => {
                    events |= crate::console::CONSOLE_IRQ_RX_READY;
                }
                IIR_RECEIVER_LINE_STATUS => {
                    events |= crate::console::CONSOLE_IRQ_RX_ERROR;
                }
                _ => {}
            }
        }

        events
    }
}
