//! x86-64 APIC interface (Local + I/O)

use core::arch::global_asm;

use kernel_arch::task::Scheduler;
use kernel_arch_x86_64::context::IrqFrame;
use libk::{
    arch::Cpu,
    device::external_interrupt_controller,
    task::{runtime, thread::Thread},
};
use static_assertions::{const_assert, const_assert_eq};

use crate::arch::x86_64::X86_64;

use super::exception;

pub mod ioapic;
pub mod local;

// I/O APIC 0..MAX_EXTERNAL_VECTORS range is mapped to BSP Local APIC 2..

/// Fixed IRQ vector for Local APIC timer
pub const APIC_TIMER_VECTOR: u32 = 0x00;
/// Fixed IRQ vector for LINT0 line
pub const APIC_LINT0_VECTOR: u32 = 0x01;
/// Fixed IRQ vector for LINT1 line
pub const APIC_LINT1_VECTOR: u32 = 0x02;
/// Fixed vector for inter-processor interrupt
pub const APIC_IPI_VECTOR: u32 = 0x03;
/// Fixed vector for spurious interrupt
pub const APIC_SPURIOUS_VECTOR: u32 = 0xDF;
/// Start of the I/O APIC IRQ range
pub const APIC_EXTERNAL_OFFSET: u32 = 4;
/// Start of the MSI range
pub const APIC_MSI_OFFSET: u32 = APIC_EXTERNAL_OFFSET + MAX_EXTERNAL_VECTORS;
/// Maximum number of APIC vectors allocated for handling IRQs from I/O APIC
pub const MAX_EXTERNAL_VECTORS: u32 = APIC_SPURIOUS_VECTOR - APIC_EXTERNAL_OFFSET - MAX_MSI_VECTORS;
/// Number of I/O APIC IRQ vectors that are actually populated
pub const POPULATED_EXTERNAL_VECTORS: u32 = 16;
/// Maximum number of APIC vectors allocated for handling MSIs
pub const MAX_MSI_VECTORS: u32 = 16;

const_assert!(POPULATED_EXTERNAL_VECTORS <= MAX_EXTERNAL_VECTORS);
const_assert_eq!(APIC_MSI_OFFSET + MAX_MSI_VECTORS, APIC_SPURIOUS_VECTOR);

/// Fills the IDT with interrupt vectors for this APIC
pub fn setup_vectors(idt: &mut [exception::Entry]) {
    extern "C" {
        // IRQ vectors
        static __x86_64_apic_vectors: [usize; 224];
    }

    for (i, &entry) in unsafe { __x86_64_apic_vectors.iter() }.enumerate() {
        idt[i] = exception::Entry::new(
            entry,
            0x08,
            exception::Entry::PRESENT | exception::Entry::INT32,
        );
    }
}

unsafe extern "C" fn irq_handler(vector: usize, frame: *mut IrqFrame) {
    if vector >= POPULATED_EXTERNAL_VECTORS as _ {
        unreachable!("Got a weird IRQ with vector {}", vector);
    }

    let cpu = Cpu::local();
    let frame = &mut *frame;

    if let Ok(intc) = external_interrupt_controller() {
        intc.handle_specific_irq(vector);
    }
    cpu.local_apic().clear_interrupt();

    if let Some(thread) = Thread::get_current() {
        thread.handle_pending_signals(frame);
    }
}

unsafe extern "C" fn msi_handler(vector: usize, frame: *mut IrqFrame) {
    if vector >= MAX_MSI_VECTORS as _ {
        unreachable!("Got a weird MSI with vector {}", vector);
    }

    let cpu = Cpu::local();
    let frame = &mut *frame;

    cpu.local_apic().handle_msi(vector);
    cpu.local_apic().clear_interrupt();

    if let Some(thread) = Thread::get_current() {
        thread.handle_pending_signals(frame);
    }
}

unsafe extern "C" fn local_timer_irq_handler(frame: *mut IrqFrame) {
    let frame = &mut *frame;

    runtime::tick();

    let cpu = Cpu::local();
    // Clear interrupt before switching, because otherwise we won't receive the next one
    cpu.local_apic().clear_interrupt();

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

    if let Some(thread) = Thread::get_current() {
        thread.handle_pending_signals(frame);
    }
}

unsafe extern "C" fn dummy_irq_handler() {
    unreachable!("dummy_irq_handler entered")
}

unsafe extern "C" fn ipi_handler() {
    let ipi = Cpu::local().get_ipi();
    X86_64::handle_ipi(ipi);
}

global_asm!(
    include_str!("vectors.S"),
    local_timer_irq_handler = sym local_timer_irq_handler,
    irq_handler = sym irq_handler,
    msi_handler = sym msi_handler,
    ipi_handler = sym ipi_handler,
    dummy_irq_handler = sym dummy_irq_handler,
    irq_vector_offset = const APIC_EXTERNAL_OFFSET,
    irq_vector_count = const MAX_EXTERNAL_VECTORS,
    msi_vector_offset = const APIC_MSI_OFFSET,
    msi_vector_count = const MAX_MSI_VECTORS,
    options(att_syntax)
);
