use std::{
    io,
    path::{Path, PathBuf},
    process::Command,
};

use qemu::{
    aarch64,
    device::{IntelGigabitRev, QemuDevice, QemuDrive, QemuNic, QemuSerialTarget},
    riscv64, x86_64, Qemu,
};

use crate::{
    build::{self, AllBuilt, ImageBuilt, InitrdGenerated, KernelBin},
    env::{Board, BuildEnv},
    error::Error,
};

#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
enum QemuIgbeRev {
    I82574L,
    I82544GC,
    I82545EM,
    I82540EM,
}

#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
enum QemuNetworkInterface {
    #[default]
    VirtioNet,
    Rtl8139,
    IntelGigabit {
        rev: Option<QemuIgbeRev>,
    },
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuNetworkConfig {
    enable: bool,
    interface_name: String,
    mac: String,
    interface: QemuNetworkInterface,
}

#[derive(Debug, Default, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
enum QemuDiskInterface {
    #[default]
    Nvme,
    Ahci,
    VirtioBlk,
}

#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuDiskConfig {
    interface: QemuDiskInterface,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuX86_64MachineConfig {
    enable_kvm: bool,
    memory: usize,
    uefi_code_path: PathBuf,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuAArch64MachineConfig {
    memory: usize,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(default)]
struct QemuMachineConfig {
    x86_64: QemuX86_64MachineConfig,
    aarch64: QemuAArch64MachineConfig,
    smp: usize,
}

#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuConfig {
    network: QemuNetworkConfig,
    disk: QemuDiskConfig,
    machine: QemuMachineConfig,
}

impl Default for QemuMachineConfig {
    fn default() -> Self {
        Self {
            x86_64: Default::default(),
            aarch64: Default::default(),
            smp: 4,
        }
    }
}

impl Default for QemuAArch64MachineConfig {
    fn default() -> Self {
        Self { memory: 512 }
    }
}

impl Default for QemuX86_64MachineConfig {
    fn default() -> Self {
        Self {
            enable_kvm: true,
            memory: 1024,
            uefi_code_path: PathBuf::from("/usr/share/edk2-ovmf/x64/OVMF_CODE.fd"),
        }
    }
}

impl Default for QemuNetworkConfig {
    fn default() -> Self {
        Self {
            enable: true,
            interface_name: "qemu-tap0".into(),
            mac: "12:34:56:65:43:21".into(),
            interface: QemuNetworkInterface::default(),
        }
    }
}

impl From<QemuDiskInterface> for QemuDrive {
    fn from(value: QemuDiskInterface) -> Self {
        match value {
            QemuDiskInterface::Nvme => Self::Nvme,
            QemuDiskInterface::Ahci => Self::Sata,
            QemuDiskInterface::VirtioBlk => Self::VirtioBlk,
        }
    }
}

// TODO device settings

fn run_aarch64(
    config: &QemuConfig,
    env: &BuildEnv,
    qemu_bin: Option<PathBuf>,
    devices: Vec<QemuDevice>,
    kernel_bin: PathBuf,
    initrd: PathBuf,
) -> Result<Command, Error> {
    let mut qemu = Qemu::new_aarch64();
    if let Some(qemu_bin) = qemu_bin {
        qemu.override_qemu(qemu_bin);
    }

    qemu.with_smp(config.machine.smp)
        .with_boot_image(aarch64::Image::Kernel {
            kernel: kernel_bin,
            initrd: Some(initrd),
        })
        .disable_display();

    match env.board {
        Board::raspi4b => qemu
            .with_machine(aarch64::Machine::Raspi4b)
            .with_serial(QemuSerialTarget::None)
            .with_serial(QemuSerialTarget::MonStdio),
        Board::virt | Board::default => qemu
            .with_machine(aarch64::Machine::Virt { virtualize: true })
            .with_serial(QemuSerialTarget::MonStdio)
            .with_cpu(aarch64::Cpu::Max)
            .with_memory_megabytes(config.machine.aarch64.memory),
        _ => {
            return Err(Error::UnsupportedEmulation(env.board));
        }
    };

    if env.board != Board::raspi4b {
        for device in devices {
            qemu.with_device(device);
        }
    }

    let mut command = qemu.into_command();

    if env.board == Board::raspi4b {
        // Provide the dtb
        command.args([
            "-dtb",
            &format!(
                "{}/etc/dtb/bcm2711-rpi-4-b.dtb",
                env.workspace_root.display()
            ),
        ]);
    }

    Ok(command)
}

fn run_x86_64(
    config: &QemuConfig,
    qemu_bin: Option<PathBuf>,
    devices: Vec<QemuDevice>,
    image: PathBuf,
) -> Result<Command, Error> {
    let mut qemu = Qemu::new_x86_64();
    if let Some(qemu_bin) = qemu_bin {
        qemu.override_qemu(qemu_bin);
    }
    qemu.with_serial(QemuSerialTarget::MonStdio)
        .with_cpu(x86_64::Cpu::Host {
            enable_kvm: config.machine.x86_64.enable_kvm,
        })
        .with_smp(config.machine.smp)
        .with_machine(x86_64::Machine::Q35)
        .with_boot_slot('a')
        .with_bios_image(config.machine.x86_64.uefi_code_path.clone())
        .with_boot_image(x86_64::Image::Drive(image))
        .with_memory_megabytes(config.machine.x86_64.memory);

    for device in devices {
        qemu.with_device(device);
    }

    Ok(qemu.into_command())
}

fn run_riscv64(
    config: &QemuConfig,
    env: &BuildEnv,
    qemu_bin: Option<PathBuf>,
    devices: Vec<QemuDevice>,
    kernel: PathBuf,
    initrd: PathBuf,
) -> Result<Command, Error> {
    let _ = config;
    let _ = devices;

    let mut qemu = Qemu::new_riscv64();
    if let Some(qemu_bin) = qemu_bin {
        qemu.override_qemu(qemu_bin);
    }
    let bios = env.workspace_root.join("boot/riscv/fw_jump.bin");
    qemu.with_serial(QemuSerialTarget::MonStdio)
        .with_machine(riscv64::Machine::Virt)
        .with_cpu(riscv64::Cpu::Rv64)
        .with_memory_megabytes(1024)
        .disable_display()
        .with_boot_image(riscv64::Image::OpenSBI {
            kernel,
            initrd,
            bios,
        });

    for device in devices {
        qemu.with_device(device);
    }

    Ok(qemu.into_command())
}

fn load_qemu_config<P: AsRef<Path>>(path: P) -> Result<QemuConfig, Error> {
    let path = path.as_ref();

    if path.exists() {
        let config = std::fs::read_to_string(path)?;
        let config =
            toml::from_str(&config).map_err(|e| Error::TomlParseError(path.to_owned(), e))?;

        Ok(config)
    } else {
        Ok(QemuConfig::default())
    }
}

fn add_devices_from_config(
    devices: &mut Vec<QemuDevice>,
    disk: Option<&PathBuf>,
    config: &QemuConfig,
) -> Result<(), Error> {
    if config.network.enable {
        let mac = Some(config.network.mac.clone());
        let nic = match config.network.interface {
            QemuNetworkInterface::VirtioNet => QemuNic::VirtioPci { mac },
            QemuNetworkInterface::Rtl8139 => QemuNic::Rtl8139 { mac },
            QemuNetworkInterface::IntelGigabit { rev } => QemuNic::IntelGigabit {
                mac,
                rev: rev.map(Into::into),
            },
        };
        devices.push(QemuDevice::NetworkTap {
            nic,
            script: Some("xtask/scripts/qemu-ifup".into()),
            ifname: Some(config.network.interface_name.clone()),
        });
    }
    if let Some(disk) = disk {
        devices.push(QemuDevice::Drive {
            ty: config.disk.interface.into(),
            file: disk.clone(),
            serial: Some("deadbeef".into()),
        });
    }
    Ok(())
}

pub fn dump_config<W: io::Write>(env: &BuildEnv, output: &mut W) {
    let config_path = env.workspace_root.join("qemu.toml");
    if config_path.exists() {
        writeln!(output, " * Using qemu.toml:").ok();
    } else {
        writeln!(output, " * Using default qemu config:").ok();
    }
    let config = load_qemu_config(config_path);

    writeln!(output).ok();
    match config.as_ref().map(toml::to_string_pretty) {
        Ok(Ok(string)) => writeln!(output, "{string}").ok(),
        Ok(Err(error)) => writeln!(output, "<Serialization error: {error}>").ok(),
        Err(error) => writeln!(output, "<Load error: {error}>").ok(),
    };
}

pub fn run(
    env: BuildEnv,
    qemu: Option<PathBuf>,
    disk: Option<PathBuf>,
    extra_args: Vec<String>,
) -> Result<(), Error> {
    let config = load_qemu_config(env.workspace_root.join("qemu.toml"))?;

    // Rebuild the image
    let built = build::build_all(&env)?;

    let mut devices = vec![];
    add_devices_from_config(&mut devices, disk.as_ref(), &config)?;

    let mut command = match built {
        AllBuilt::Riscv64(KernelBin(kernel), InitrdGenerated(initrd)) => {
            run_riscv64(&config, &env, qemu, devices, kernel, initrd)?
        }
        AllBuilt::AArch64(KernelBin(kernel), InitrdGenerated(initrd)) => {
            run_aarch64(&config, &env, qemu, devices, kernel, initrd)?
        }
        AllBuilt::X86_64(ImageBuilt(image)) => run_x86_64(&config, qemu, devices, image)?,
    };

    command.args(extra_args);

    log::trace!("Run QEMU: {:?}", command);
    log::info!("Start qemu");

    command.status()?;

    Ok(())
}

impl From<QemuIgbeRev> for IntelGigabitRev {
    fn from(value: QemuIgbeRev) -> Self {
        match value {
            QemuIgbeRev::I82574L => IntelGigabitRev::I82574L,
            QemuIgbeRev::I82544GC => IntelGigabitRev::I82544GC,
            QemuIgbeRev::I82545EM => IntelGigabitRev::I82545EM,
            QemuIgbeRev::I82540EM => IntelGigabitRev::I82540EM,
        }
    }
}
