use std::{fs, path::PathBuf, process::Command};

use crate::{
    build::cargo::CargoBuilder,
    check::{self, AllOk},
    env::{Arch, BuildEnv},
    error::Error,
};

pub mod x86_64;

pub mod c;
mod cargo;
pub mod ports;
pub mod ygglibc;
// mod module;
pub mod toolchain;
pub mod toolchain_c;
mod userspace;

#[derive(Debug)]
pub enum CheckAction {
    Check,
    Clippy,
}

impl CheckAction {
    pub fn command(&self) -> &str {
        match self {
            Self::Clippy => "clippy",
            Self::Check => "check",
        }
    }
}

pub struct ToolsBuilt(pub PathBuf);
pub struct KernelBuilt(pub PathBuf);
pub struct KernelBin(pub PathBuf);
pub struct InitrdGenerated(pub PathBuf);
pub struct ImageBuilt(pub PathBuf);
pub enum AllBuilt {
    Riscv64(KernelBin, InitrdGenerated),
    AArch64(KernelBin, InitrdGenerated),
    X86_64(ImageBuilt),
}

pub fn build_kernel(env: &BuildEnv, _: AllOk) -> Result<KernelBuilt, Error> {
    log::info!("Building kernel");
    CargoBuilder::Kernel(env).build("kernel")?;
    Ok(KernelBuilt(env.kernel_output_dir.join("yggdrasil-kernel")))
}

pub fn make_kernel_bin(env: &BuildEnv, kernel: KernelBuilt, _: AllOk) -> Result<KernelBin, Error> {
    log::info!("Building yggdrasil-kernel.bin");
    let kernel_bin = env.kernel_output_dir.join("yggdrasil-kernel.bin");

    let status = Command::new("llvm-objcopy")
        .args(["-O", "binary"])
        .arg(kernel.0)
        .arg(&kernel_bin)
        .status()?;
    if !status.success() {
        return Err(Error::ExternalCommandFailed);
    }

    Ok(KernelBin(kernel_bin))
}

pub fn build_all(env: &BuildEnv) -> Result<AllBuilt, Error> {
    log::debug!("Environment: {:#?}", env);

    let check = check::check_build_env(env.arch)?;

    // Kernel stuff
    let kernel = build_kernel(env, check)?;
    // Userspace stuff
    let initrd = userspace::build_initrd(env, check)?;

    // Build target-specific image
    let image = match env.arch {
        Arch::riscv64 => AllBuilt::Riscv64(make_kernel_bin(env, kernel, check)?, initrd),
        Arch::aarch64 => AllBuilt::AArch64(make_kernel_bin(env, kernel, check)?, initrd),
        Arch::x86_64 => AllBuilt::X86_64(x86_64::build_image(env, kernel, initrd)?),
    };

    Ok(image)
}

pub fn check_all(env: BuildEnv, action: CheckAction) -> Result<(), Error> {
    let action = action.command();

    if env.arch == Arch::x86_64 {
        CargoBuilder::Host(env.verbose).run(env.workspace_root.join("boot/yboot-proto"), action)?;
        CargoBuilder::Host(env.verbose).run(env.workspace_root.join("boot/yboot"), action)?;
    }

    CargoBuilder::Userspace(&env).run(env.workspace_root.join("userspace"), action)?;
    CargoBuilder::Ygglibc(&env).run(env.workspace_root.join("userspace/lib/ygglibc"), action)?;

    CargoBuilder::Kernel(&env).run(env.workspace_root.join("kernel"), action)?;

    Ok(())
}

pub fn test_all(env: BuildEnv) -> Result<(), Error> {
    for path in [
        "kernel/driver/fs/memfs",
        "kernel/driver/fs/fat32",
        "lib/abi",
        "kernel/libk",
        "kernel/libk/libk-util",
        "tool/abi-generator",
    ] {
        log::info!("Run tests: {:?}", path);
        CargoBuilder::Host(env.verbose).run(env.workspace_root.join(path), "test")?;
    }
    Ok(())
}

pub fn clean_userspace(env: &BuildEnv) -> Result<(), Error> {
    CargoBuilder::Host(env.verbose).run(env.workspace_root.join("userspace"), "clean")?;
    CargoBuilder::Host(env.verbose).run(
        env.workspace_root.join("userspace/dynload-program"),
        "clean",
    )?;
    Ok(())
}

pub fn clean_all(env: &BuildEnv, clean_toolchain: bool, repos: bool) -> Result<(), Error> {
    log::info!("Cleaning");

    CargoBuilder::Host(env.verbose).run(&env.workspace_root, "clean")?;
    clean_userspace(env)?;
    fs::remove_dir_all(env.workspace_root.join("toolchain-c/sysroot"))?;
    // Clean libm
    for target in Arch::all() {
        Command::new("make")
            .current_dir(
                env.workspace_root
                    .join(format!("toolchain-c/libs/openlibm-{}", target.name())),
            )
            .arg("clean")
            .status()
            .ok();
    }

    if repos {
        fs::remove_dir_all(env.workspace_root.join("toolchain")).ok();
        fs::remove_dir_all(env.workspace_root.join("toolchain-c/llvm-project")).ok();
        fs::remove_dir_all(env.workspace_root.join("toolchain-c/libs")).ok();
        fs::remove_dir_all(env.workspace_root.join("toolchain-c/prefix")).ok();
    } else if clean_toolchain {
        // clean repos also cleans toolchain, so no need for this
        Command::new("./x")
            .current_dir(env.workspace_root.join("toolchain"))
            .arg("clean")
            .status()?;

        // Clean host toolchain
        fs::remove_dir_all(env.workspace_root.join("toolchain-c/llvm-project/build")).ok();
        fs::remove_dir_all(env.workspace_root.join("toolchain-c/prefix")).ok();

        for target in Arch::all() {
            fs::remove_dir_all(
                env.workspace_root
                    .join(format!("toolchain-c/llvm-project/build-{}", target.name())),
            )
            .ok();
        }
    }

    Ok(())
}

pub fn build_toolchain(env: &BuildEnv, branch: &str) -> Result<(), Error> {
    toolchain::fetch(env, branch)?;
    toolchain::build(env)
}
