#![feature(let_chains)]

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

use device::{QemuDevice, QemuSerialTarget};

pub mod aarch64;
pub mod i386;
pub mod riscv64;
pub mod x86_64;

pub mod device;

pub trait Architecture: IntoArgs {
    type MachineType: IntoArgs;
    type CpuType: IntoArgs;
    type ImageType: IntoArgs;

    const DEFAULT_COMMAND: &'static str;
}

pub trait IntoArgs {
    fn add_args(&self, command: &mut Command);
}

impl<T: IntoArgs> IntoArgs for Option<T> {
    fn add_args(&self, command: &mut Command) {
        if let Some(val) = self {
            val.add_args(command);
        }
    }
}

#[derive(Debug, Default)]
struct QemuCommon {
    devices: Vec<QemuDevice>,
    serial: Vec<QemuSerialTarget>,
}

#[derive(Debug, Default)]
pub struct Qemu<A: Architecture> {
    binary_override: Option<PathBuf>,
    common: QemuCommon,
    cpu: Option<A::CpuType>,
    machine: Option<A::MachineType>,
    boot_image: Option<A::ImageType>,
    cdrom: Option<PathBuf>,
    no_display: bool,
    memory_megabytes: Option<usize>,
    smp: Option<usize>,
    arch: A,
}

impl Qemu<aarch64::QemuAArch64> {
    pub fn new_aarch64() -> Self {
        Qemu::new(aarch64::QemuAArch64)
    }
}

impl Qemu<x86_64::QemuX86_64> {
    pub fn new_x86_64() -> Self {
        Qemu::new(x86_64::QemuX86_64::default())
    }

    pub fn with_bios_image(&mut self, image: PathBuf) -> &mut Self {
        self.arch.bios_image = Some(image);
        self
    }

    pub fn with_boot_slot(&mut self, slot: char) -> &mut Self {
        self.arch.boot_slot = Some(slot);
        self
    }
}

impl Qemu<i386::QemuI386> {
    pub fn new_i386() -> Self {
        Qemu::new(i386::QemuI386::default())
    }
}

impl Qemu<riscv64::QemuRiscv64> {
    pub fn new_riscv64() -> Self {
        Qemu::new(riscv64::QemuRiscv64)
    }
}

impl<A: Architecture> Qemu<A> {
    pub fn new(arch: A) -> Self {
        Self {
            binary_override: None,
            common: QemuCommon::default(),

            memory_megabytes: None,
            no_display: false,
            cpu: None,
            machine: None,
            boot_image: None,
            cdrom: None,
            smp: None,
            arch,
        }
    }

    pub fn with_device(&mut self, device: QemuDevice) -> &mut Self {
        self.common.devices.push(device);
        self
    }

    pub fn with_memory_megabytes(&mut self, value: usize) -> &mut Self {
        self.memory_megabytes = Some(value);
        self
    }

    pub fn with_serial(&mut self, serial: QemuSerialTarget) -> &mut Self {
        self.common.serial.push(serial);
        self
    }

    pub fn with_boot_image(&mut self, image: A::ImageType) -> &mut Self {
        self.boot_image = Some(image);
        self
    }

    pub fn with_cdrom<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cdrom = Some(path.as_ref().into());
        self
    }

    pub fn with_cpu(&mut self, cpu: A::CpuType) -> &mut Self {
        self.cpu = Some(cpu);
        self
    }

    pub fn with_machine(&mut self, machine: A::MachineType) -> &mut Self {
        self.machine = Some(machine);
        self
    }

    pub fn with_smp(&mut self, smp: usize) -> &mut Self {
        assert_ne!(smp, 0);
        self.smp = Some(smp);
        self
    }

    pub fn disable_display(&mut self) -> &mut Self {
        self.no_display = true;
        self
    }

    pub fn override_qemu<S: Into<PathBuf>>(&mut self, qemu: S) -> &mut Self {
        self.binary_override = Some(qemu.into());
        self
    }

    pub fn into_command(self) -> Command {
        let qemu = self
            .binary_override
            .clone()
            .unwrap_or_else(|| A::DEFAULT_COMMAND.into());

        let mut command = Command::new(qemu);

        self.add_args(&mut command);

        command
    }
}

impl IntoArgs for QemuCommon {
    fn add_args(&self, command: &mut Command) {
        for serial in self.serial.iter() {
            serial.add_args(command);
        }
    }
}

impl<A: Architecture> IntoArgs for Qemu<A> {
    fn add_args(&self, command: &mut Command) {
        self.common.add_args(command);

        for device in &self.common.devices {
            device.add_args(command);
        }

        if let Some(mem) = self.memory_megabytes {
            command.args(["-m", &format!("{}m", mem)]);
        }

        self.machine.add_args(command);
        self.cpu.add_args(command);
        self.boot_image.add_args(command);

        if let Some(cdrom) = &self.cdrom {
            command.args(["-cdrom", &format!("{}", cdrom.display())]);
        }

        if self.no_display {
            command.args(["-display", "none"]);
        }

        if let Some(smp) = self.smp {
            command.args(["-smp", &format!("{}", smp)]);
        }

        self.arch.add_args(command);
    }
}

#[cfg(test)]
mod tests {
    use crate::{device::QemuSerialTarget, Qemu};

    #[test]
    fn empty() {
        let builder = Qemu::new_aarch64();
        let command = builder.into_command();
        assert_eq!(command.get_program(), "qemu-system-aarch64");
        assert_eq!(command.get_args().len(), 0);
    }

    #[test]
    fn qemu_override() {
        let mut qemu = Qemu::new_aarch64();
        qemu.override_qemu("test-qemu");
        let command = qemu.into_command();
        assert_eq!(command.get_program(), "test-qemu");
    }

    #[test]
    fn common_options() {
        let mut qemu = Qemu::new_aarch64();
        qemu.with_serial(QemuSerialTarget::MonStdio);
        let command = qemu.into_command();

        assert_eq!(
            command.get_args().collect::<Vec<_>>(),
            vec!["-serial", "mon:stdio"]
        );
    }
}
