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

use crate::config::DsxConfig;

use assembler::prelude::Assembler;
use common::build::{BuildError, Builder};
use compiler::Compiler;

// ----------  build ----------------------------------------------------------
pub fn build_project(cwd: &Path) -> Result<(), BuildError> {
    let config: DsxConfig = toml::from_str(&fs::read_to_string(cwd.join("Dsx.toml"))?)
        .map_err(|deser_err| {
            io::Error::new(io::ErrorKind::InvalidData, deser_err.to_string())
        })?;

    let src_dir = cwd.join("src");
    if !src_dir.exists() {
        return Err(BuildError::Generic(String::from(
            "Source Directory does not exist",
        )));
    }

    // make sure there's a main file to assemble later.
    if !main_exists(&src_dir)? {
        return Err(BuildError::Generic(String::from(
            "No main.dsa or main.dsc file found in top level of src directory.",
        )));
    }

    // detect src.
    let (has_dsa, has_dsc) = detect_source_language(&src_dir);

    // create a build dir and copy all files across
    let build_dir = cwd.join("build");
    fs::create_dir_all(&build_dir)?;
    env::set_current_dir(&build_dir)?;

    copy_recursively(&src_dir, &build_dir)?;

    if has_dsc {
        build_all_dsc(&build_dir)?;
    }

    // Replace .dsc with .dsa only in include statements, recursively for each file.
    let mut sed_cmd = Command::new("bash");
    sed_cmd.args([
        "-c",
        &format!(
            "find \"{}\" -type f -name '*.dsa' -exec sed -i '/^include/ s/\\.dsc/.dsa/g' {{}} +",
            build_dir.display()
        ),
    ]);
    run(&mut sed_cmd);

    // assemble result
    {
        fs::create_dir_all(cwd.join("artifacts"))?;
        let mut asm = Assembler::new("./main.dsa");
        asm.start(());
        asm.write_result("../artifacts/out.dsb")?;
    }

    println!("Build finished. Binary at {}/main.dsb", build_dir.display());
    Ok(())
}

// ----- Helpers -------------------------------

struct BuildStep;
impl BuildStep {
    pub fn compiling(path: &Path) {
        println!("Compiling {}", path.display());
    }

    pub fn assembling(path: &Path) {
        println!("Assembling {}", path.display());
    }
}

/// Checks what source languages are used in the project.
fn detect_source_language(src_dir: &Path) -> (bool, bool) {
    let mut contains_dsc = false;
    let mut contains_dsa = false;

    for entry in walkdir::WalkDir::new(src_dir).into_iter().flatten() {
        match entry.path().extension().and_then(|s| s.to_str()) {
            Some("dsc") => contains_dsc = true,
            Some("dsa") => contains_dsa = true,
            _ => {}
        }
    }
    (contains_dsa, contains_dsc)
}

// Checks if either main.dsa or main.dsc exist in the source directory
fn main_exists(src_dir: &Path) -> Result<bool, std::io::Error> {
    for entry in fs::read_dir(src_dir).into_iter().flatten() {
        match entry?.path().file_name().and_then(|s| s.to_str()) {
            Some("main.dsc") => return Ok(true),
            Some("main.dsa") => return Ok(true),
            _ => {}
        }
    }

    Ok(false)
}

// Copy contents of one directory to another
fn copy_recursively(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
    if src.is_file() {
        fs::create_dir_all(dst.parent().unwrap())?;
        fs::copy(src, dst)?;
        return Ok(());
    }

    if src.is_dir() {
        for entry in fs::read_dir(src)? {
            let entry = entry?;
            let child_src = entry.path();
            let child_dst = dst.join(entry.file_name());
            copy_recursively(&child_src, &child_dst)?;
        }
    }

    Ok(())
}

fn build_all_dsc(path: &Path) -> Result<(), BuildError> {
    if path.is_dir() {
        for entry in fs::read_dir(path)? {
            let entry = entry?;
            build_all_dsc(&entry.path())?;
        }

        return Ok(());
    }

    if path.extension().and_then(|s| s.to_str()) == Some("dsc") {
        let input_path = path;
        let output_path = path.with_extension("dsa");

        let is_lib = !(input_path.file_stem().unwrap().to_str().unwrap() == "main");

        let mut compiler = Compiler::new(input_path);
        compiler.start(is_lib);
        compiler.write_result(output_path.clone())?;
    }

    Ok(())
}

/// Run a command and exit on failure.
fn run(cmd: &mut Command) {
    let status = cmd.status().expect("failed to execute command");
    if !status.success() {
        std::process::exit(1);
    }
}
