use std::{
    fs::{File, OpenOptions},
    io::{self, BufReader, BufWriter},
    path::{Path, PathBuf},
};

use fscommon::BufStream;

use crate::{
    build::{cargo::CargoBuilder, KernelBuilt},
    env::BuildEnv,
    error::Error,
    util,
};

use super::{ImageBuilt, InitrdGenerated};

pub struct YbootBuilt(PathBuf);

fn build_yboot(env: &BuildEnv) -> Result<YbootBuilt, Error> {
    log::info!("Building yboot");
    CargoBuilder::Host(env.verbose).build("boot/yboot")?;
    let binary = env.workspace_root.join(format!(
        "boot/yboot/target/x86_64-unknown-uefi/debug/yboot.efi"
    ));
    Ok(YbootBuilt(binary))
}

fn copy_into_fat(
    dir: &fatfs::Dir<'_, BufStream<File>>,
    src: impl AsRef<Path>,
    dst: impl AsRef<str>,
) -> Result<(), Error> {
    let src = src.as_ref();
    let dst = dst.as_ref();

    log::debug!("{} -> {}", src.display(), dst);
    let mut dst = dir.create_file(dst)?;
    let mut src = BufReader::new(File::open(src)?);

    dst.truncate()?;
    let mut dst = BufWriter::new(dst);

    io::copy(&mut src, &mut dst)?;

    Ok(())
}

fn build_uefi_image(
    env: &BuildEnv,
    yboot: YbootBuilt,
    kernel: KernelBuilt,
    initrd: InitrdGenerated,
) -> Result<ImageBuilt, Error> {
    log::info!("Building x86-64 UEFI image");
    let image_path = env.kernel_output_dir.join("image.fat32");
    let yboot_cfg = env.workspace_root.join("etc/boot/yboot.cfg");

    if !image_path.exists() {
        util::run_external_command(
            "dd",
            [
                "if=/dev/zero",
                &format!("of={}", image_path.display()),
                "bs=1M",
                "count=512",
            ],
            false,
        )?;
        util::run_external_command(
            "mkfs.vfat",
            ["-F32".as_ref(), image_path.as_os_str()],
            false,
        )?;
    }

    let image_file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(&image_path)?;
    let buf_stream = fscommon::BufStream::new(image_file);
    let fs = fatfs::FileSystem::new(buf_stream, fatfs::FsOptions::new())?;

    let root_dir = fs.root_dir();

    let boot_dir = root_dir.create_dir("EFI")?.create_dir("Boot")?;
    copy_into_fat(&boot_dir, &yboot.0, "BootX64.efi")?;
    copy_into_fat(&root_dir, &kernel.0, "kernel.elf")?;
    copy_into_fat(&root_dir, &initrd.0, "initrd.img")?;
    if yboot_cfg.exists() {
        copy_into_fat(&root_dir, &yboot_cfg, "yboot.cfg")?;
    } else {
        root_dir.remove("yboot.cfg").ok();
    }

    Ok(ImageBuilt(image_path))
}

pub fn build_image(
    env: &BuildEnv,
    kernel: KernelBuilt,
    initrd: InitrdGenerated,
) -> Result<ImageBuilt, Error> {
    let yboot = build_yboot(env)?;
    let image = build_uefi_image(env, yboot, kernel, initrd)?;
    Ok(image)
}
