#![allow(clippy::missing_safety_doc)]

use x86_64::instructions::port::Port;

const CMD_INIT: u8 = 0x11;
const CMD_END_OF_INT: u8 = 0x20;
const MODE_8086: u8 = 0x01;

struct Pic {
    offset: u8,
    data: Port<u8>,
    command: Port<u8>,
}

impl Pic {
    /// Are we in charge of handling the specified interrupt?
    /// (Each PIC handles 8 interrupts.)
    const fn handles_interrupt(&self, interrupt_id: u8) -> bool {
        self.offset <= interrupt_id && interrupt_id < self.offset + 8
    }

    /// Notify us that an interrupt has been handled and that we're ready
    /// for more.
    unsafe fn end_of_interrupt(&mut self) {
        unsafe { self.command.write(CMD_END_OF_INT) };
    }

    /// Reads the interrupt mask of this PIC.
    unsafe fn read_mask(&mut self) -> u8 {
        unsafe { self.data.read() }
    }

    /// Writes the interrupt mask of this PIC.
    unsafe fn write_mask(&mut self, mask: u8) {
        unsafe { self.data.write(mask) }
    }
}

/// A pair of chained PICs.  This is the standard setup on x86.
pub struct ChainedPics {
    pics: [Pic; 2],
}

impl ChainedPics {
    /// Construct a new `ChainedPics` given the base offsets of the two PICs.
    ///
    /// # Safety
    ///
    /// This function is unsafe because it requires you to pass valid offsets.
    /// If you pass offsets that are out of range, or if you pass offsets that
    /// conflict with one another, the resulting object will be useless and
    /// will likely cause problems for your system.
    pub const unsafe fn new(offset1: u8, offset2: u8) -> Self {
        Self {
            pics: [
                Pic {
                    offset: offset1,
                    command: Port::new(0x20),
                    data: Port::new(0x21),
                },
                Pic {
                    offset: offset2,
                    command: Port::new(0xA0),
                    data: Port::new(0xA1),
                },
            ],
        }
    }

    /// .
    ///
    /// # Safety
    ///
    /// .
    pub const unsafe fn new_contiguous(primary_offset: u8) -> Self {
        unsafe { Self::new(primary_offset, primary_offset + 8) }
    }

    /// Returns a new instance of [`ChainedPics`].
    ///
    /// # Safety
    ///
    /// this is unsafe because it required writing to CPU I/O ports.
    pub unsafe fn initialize(&mut self) {
        unsafe {
            let mut wait_port: Port<u8> = Port::new(0x80);
            let mut wait = || wait_port.write(0);

            // Save our original interrupt masks, because I'm too lazy to
            // figure out reasonable values. We'll restore these when we're
            // done.
            let saved_masks = self.read_masks();

            // Tell each PIC that we're going to send it a three-byte
            // initialization sequence on its data port.
            self.pics[0].command.write(CMD_INIT);
            wait();
            self.pics[1].command.write(CMD_INIT);
            wait();

            // Byte 1: Set up our base offsets.
            self.pics[0].data.write(self.pics[0].offset);
            wait();
            self.pics[1].data.write(self.pics[1].offset);
            wait();

            // Byte 2: Configure chaining between PIC1 and PIC2.
            self.pics[0].data.write(4);
            wait();
            self.pics[1].data.write(2);
            wait();

            // Byte 3: Set our mode.
            self.pics[0].data.write(MODE_8086);
            wait();
            self.pics[1].data.write(MODE_8086);
            wait();

            // Restore our saved masks.
            self.write_masks(saved_masks[0], saved_masks[1])
        }
    }

    /// Reads the interrupt masks of both PICs.
    pub unsafe fn read_masks(&mut self) -> [u8; 2] {
        unsafe { [self.pics[0].read_mask(), self.pics[1].read_mask()] }
    }

    /// Writes the interrupt masks of both PICs.
    pub unsafe fn write_masks(&mut self, mask1: u8, mask2: u8) {
        unsafe {
            self.pics[0].write_mask(mask1);
            self.pics[1].write_mask(mask2);
        }
    }

    /// Disables both PICs by masking all interrupts.
    pub unsafe fn disable(&mut self) {
        unsafe { self.write_masks(u8::MAX, u8::MAX) }
    }

    /// Do we handle this interrupt?
    pub fn handles_interrupt(&self, interrupt_id: u8) -> bool {
        self.pics.iter().any(|p| p.handles_interrupt(interrupt_id))
    }

    /// Figure out which (if any) PICs in our chain need to know about this
    /// interrupt.  This is tricky, because all interrupts from `pics[1]`
    /// get chained through `pics[0]`.
    pub unsafe fn notify_end_of_interrupt(&mut self, interrupt_id: u8) {
        if self.handles_interrupt(interrupt_id) {
            if self.pics[1].handles_interrupt(interrupt_id) {
                unsafe {
                    self.pics[1].end_of_interrupt();
                }
            }
            unsafe {
                self.pics[0].end_of_interrupt();
            }
        }
    }
}
