//! x86-common stuff

#![allow(missing_docs)]

use abi::error::Error;
use alloc::{sync::Arc, vec::Vec};
use device_api::{
    device::{Device, DeviceInitContext},
    interrupt::Irq,
};
use kernel_arch_x86::ISA_IRQ_OFFSET;
use libk::{
    config, debug,
    dma::DummyDmaAllocator,
    fs::{devfs, sysfs},
    task::runtime,
};
use libk_mm::{
    address::{PhysicalAddress, Virtualize},
    phys::{self, PhysicalMemoryRegion},
    table::EntryLevelExt,
};
use peripherals::{i8253::I8253, ps2::PS2Controller, rtc::Rtc, serial::ComPort};
use ygg_driver_pci::PciBusManager;

use crate::fs::{Initrd, INITRD_DATA};

use super::L3;

mod pci;
pub mod peripherals;

pub type ProbeClockSourceFn = fn() -> Result<Arc<dyn Device>, Error>;

pub trait InitrdSource {
    fn start(&self) -> PhysicalAddress;
    fn end(&self) -> PhysicalAddress;
}

struct ProbeClockSource {
    probe: ProbeClockSourceFn,
    name: &'static str,
}

pub struct EarlyPlatformDevices {
    pub com1_3: Arc<ComPort>,

    clock_sources: Vec<ProbeClockSource>,
}

pub enum SelectedClockSource {
    Proper(Arc<dyn Device>),
    Fallback(Arc<I8253>),
}

pub fn dummy_init_context() -> DeviceInitContext {
    DeviceInitContext {
        dma_allocator: Arc::new(DummyDmaAllocator),
    }
}

// Initialize the bare minimum required to:
// * Allocate/manage interrupts
// * Print debug output
pub fn init_platform_early(cmdline: &str) -> Result<EarlyPlatformDevices, Error> {
    sysfs::init();
    devfs::init();
    debug::init();

    config::parse_boot_arguments(cmdline);

    // Initialize async executor queue
    runtime::init_task_queue();

    let com1_3 = ComPort::setup(
        dummy_init_context(),
        0x3F8,
        0x3E8,
        Irq::External(ISA_IRQ_OFFSET + 4),
    )?;
    // let i8259 = I8259::setup().expect("Could not initialize i8259 PIC");

    // disable_i8259();
    // I8259.disable();

    Ok(EarlyPlatformDevices {
        com1_3,
        clock_sources: Vec::new(),
    })
}

pub fn add_legacy_pci() {
    if let Err(error) = PciBusManager::add_legacy_segment(&pci::PCI) {
        log::error!("Couldn't add legacy x86 PCI: {error:?}");
    }
}

pub fn init_platform_devices(early: EarlyPlatformDevices) {
    match init_clock_source(&early.clock_sources) {
        Ok(SelectedClockSource::Proper(source)) => {
            log::info!(
                "Selected {:?} as the primary clock source",
                source.display_name()
            );
        }
        Ok(SelectedClockSource::Fallback(_i8253)) => {
            log::info!("Selected i8253 as the primary clock source");
        }
        Err(error) => {
            log::error!("Could not select a clock source: {error:?}");
        }
    }
    if let Err(error) = PS2Controller::setup() {
        log::error!("PS/2 controller init error: {error:?}");
    }
    if let Err(error) = Rtc::setup() {
        log::error!("RTC init error: {error:?}");
    }
    if let Err(error) = unsafe { early.com1_3.port_a().clone().init_irq() } {
        log::error!("COM port IRQ init error: {error:?}");
    }

    if let Err(error) = PciBusManager::probe_bus_devices() {
        log::error!("PCI bus device setup error(s): {error:?}");
    }
}

fn init_clock_source(sources: &[ProbeClockSource]) -> Result<SelectedClockSource, Error> {
    for source in sources {
        match (source.probe)() {
            Ok(source) => {
                return Ok(SelectedClockSource::Proper(source));
            }
            Err(error) => {
                log::error!(
                    "Could not pick {:?} as the primary clock source: {error:?}",
                    source.name
                );
            }
        }
    }

    // Fall back to i8253 (or use it as the default one in i686)
    let i8253 = I8253::setup()?;

    Ok(SelectedClockSource::Fallback(i8253))
}

pub fn set_initrd(initrd: &impl InitrdSource, reserve: bool) {
    let initrd_start = initrd.start();
    let initrd_end = initrd.end();

    if initrd_start.is_zero() || initrd_end <= initrd_start {
        log::info!("No initrd loaded");
        return;
    }

    let start_aligned = initrd_start.page_align_down::<L3>();
    let end_aligned = initrd_end.page_align_up::<L3>();
    let size = end_aligned - start_aligned;

    if reserve {
        phys::reserved::reserve_region(
            "initrd",
            PhysicalMemoryRegion {
                base: start_aligned,
                size,
            },
        );
    }

    let data = unsafe {
        core::slice::from_raw_parts(
            start_aligned.virtualize() as *const u8,
            initrd_end - initrd_start,
        )
    };

    let initrd = Initrd {
        phys_page_start: start_aligned,
        phys_page_len: end_aligned - start_aligned,
        data,
    };

    INITRD_DATA.init(initrd);
}

impl EarlyPlatformDevices {
    pub fn add_preferred_clock_source(&mut self, name: &'static str, probe: ProbeClockSourceFn) {
        self.clock_sources.push(ProbeClockSource { probe, name });
    }
}
