use alloc::format;

use log::info;
use rdrive::{
    probe::{
        OnProbeError,
        acpi::{AcpiId, AcpiInfo, ProbeAcpi},
    },
    register::ProbeFdt,
};
use some_serial::ns16550 as serial_ns16550;

use super::{
    PlatformSerialDevice, SerialProbeRuntime, acpi_serial_device_info, prop_u32,
    serial_device_info, serial_runtime,
};

const ACPI_NS16550_CLOCK: u32 = 1_843_200;
const ACPI_NS16550_REG_WIDTH: usize = 1;

model_register!(
    name: "NS16550 serial",
    level: ProbeLevel::PreKernel,
    priority: ProbePriority::DEFAULT,
    probe_kinds: &[
        ProbeKind::Fdt {
            compatibles: &["snps,dw-apb-uart", "ns16550a", "ns16550"],
            on_probe: probe
        },
        ProbeKind::Acpi {
            ids: &[
                AcpiId {
                    hid: "PNP0501",
                    cids: &[],
                },
                AcpiId {
                    hid: "PNP0500",
                    cids: &[],
                },
            ],
            on_probe: probe_acpi
        },
    ],
);

fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> {
    let (info, plat_dev) = probe.into_parts();

    info!("Probing NS16550 serial device: {}", info.node.name());
    let base_reg = info
        .node
        .regs()
        .into_iter()
        .next()
        .ok_or_else(|| OnProbeError::other(format!("[{}] has no reg", info.node.name())))?;

    let mmio_size = base_reg.size.unwrap_or(0x1000) as usize;
    let mmio_base = crate::mmio::iomap(base_reg.address as usize, mmio_size)?;
    let node = info.node.as_node();
    let reg_width = prop_u32(node, "reg-io-width").unwrap_or(1) as usize;
    let reg_shift = prop_u32(node, "reg-shift").map(|shift| 1usize << shift);
    let ns16550_width = reg_shift.unwrap_or(reg_width);
    let mut serial: Option<SerialProbeRuntime> = None;

    for compatible in node.compatibles() {
        if compatible == "snps,dw-apb-uart" {
            let clock_freq = prop_u32(node, "clock-frequency")
                .unwrap_or(serial_ns16550::dw_apb::SG2002_UART_CLOCK);
            let raw = serial_ns16550::DwApbUart::new_raw(mmio_base, clock_freq);
            serial = Some(serial_runtime(raw));
            break;
        }

        if matches!(compatible, "ns16550a" | "ns16550") {
            let clock_freq = prop_u32(node, "clock-frequency").unwrap_or(24_000_000);
            let raw = serial_ns16550::Ns16550::new_mmio(mmio_base, clock_freq, ns16550_width);
            serial = Some(serial_runtime(raw));
            break;
        }
    }

    let serial = serial.ok_or(OnProbeError::NotMatch)?;
    let device_info = serial_device_info(&info, &base_reg, serial.base_addr, serial.baudrate);

    info!(
        "NS16550 serial@{:#x} registered successfully",
        serial.base_addr
    );
    plat_dev.register(PlatformSerialDevice::new(
        serial.name.into(),
        device_info,
        serial.runtime,
    ));
    Ok(())
}

struct AcpiSerialResource {
    serial: SerialProbeRuntime,
    paddr: usize,
    mapped_base: usize,
}

fn probe_acpi(probe: ProbeAcpi<'_>) -> Result<(), OnProbeError> {
    let info = probe.info();

    info!("Probing ACPI NS16550 serial device: {}", info.path);
    let resource = if let Some(resource) = acpi_io_serial(info)? {
        resource
    } else {
        acpi_mmio_serial(info)?
    };
    let device_info = acpi_serial_device_info(
        info,
        resource.paddr,
        resource.mapped_base,
        resource.serial.baudrate,
    );
    let serial_name = resource.serial.name.into();
    let plat_dev = probe.into_platform_device();

    info!(
        "ACPI NS16550 serial@{:#x} registered successfully",
        resource.paddr
    );
    plat_dev.register(PlatformSerialDevice::new(
        serial_name,
        device_info,
        resource.serial.runtime,
    ));
    Ok(())
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn acpi_io_serial(info: &AcpiInfo<'_>) -> Result<Option<AcpiSerialResource>, OnProbeError> {
    let Some(range) = info.io_ranges().first() else {
        return Ok(None);
    };
    let port = u16::try_from(range.base).map_err(|_| {
        OnProbeError::other(format!(
            "{} has invalid ACPI serial I/O base {:#x}",
            info.path, range.base
        ))
    })?;
    let raw = serial_ns16550::Ns16550::new_port(port, ACPI_NS16550_CLOCK);
    let serial = serial_runtime(raw);
    let mapped_base = serial.base_addr;
    Ok(Some(AcpiSerialResource {
        serial,
        paddr: usize::from(port),
        mapped_base,
    }))
}

#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
fn acpi_io_serial(_info: &AcpiInfo<'_>) -> Result<Option<AcpiSerialResource>, OnProbeError> {
    Ok(None)
}

fn acpi_mmio_serial(info: &AcpiInfo<'_>) -> Result<AcpiSerialResource, OnProbeError> {
    let range = info.memory_ranges().first().ok_or_else(|| {
        OnProbeError::other(format!(
            "{} has no ACPI serial I/O port or MMIO resource",
            info.path
        ))
    })?;
    let paddr = usize::try_from(range.base).map_err(|_| {
        OnProbeError::other(format!(
            "{} has invalid ACPI serial MMIO base {:#x}",
            info.path, range.base
        ))
    })?;
    let mmio_size = usize::try_from(range.size).unwrap_or(0x1000).max(0x1000);
    let mmio_base = crate::mmio::iomap(paddr, mmio_size)?;
    let raw =
        serial_ns16550::Ns16550::new_mmio(mmio_base, ACPI_NS16550_CLOCK, ACPI_NS16550_REG_WIDTH);
    let serial = serial_runtime(raw);
    let mapped_base = serial.base_addr;
    Ok(AcpiSerialResource {
        serial,
        paddr,
        mapped_base,
    })
}
