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;

pub struct BuildContext {
    pub project_dir: PathBuf,
    pub build_dir: PathBuf,
    pub artifact_dir: PathBuf,
}

// ----------  build ----------------------------------------------------------
pub fn build_project(ctx: BuildContext) -> Result<(), BuildError> {
    // variables
    let binary_path = ctx.artifact_dir.join("out.dsb");
    let main_path = ctx.build_dir.join("main.dsa");
    let config_path = ctx.project_dir.join("Dsx.toml");

    let src_dir = ctx.project_dir.join("src");

    let config: DsxConfig =
        toml::from_str(&fs::read_to_string(&config_path)?).map_err(|deser_err| {
            io::Error::new(io::ErrorKind::InvalidData, deser_err.to_string())
        })?;

    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
    fs::create_dir_all(&ctx.build_dir)?;
    copy_recursively(&src_dir, &ctx.build_dir)?;

    if has_dsc {
        build_all_dsc(&ctx.build_dir)?;
    }

    // Replace .dsc with .dsa only in include statements, recursively for each file.

    let status = Command::new("bash").args([
        "-c",
        &format!(
            "find \"{}\" -type f -name '*.dsa' -exec sed -i '/^include/ s/\\.dsc/.dsa/g' {{}} +",
            ctx.build_dir.display()
        ),
    ]).status()?;

    if !status.success() {
        return Err(BuildError::IoError(String::from(
            "Failed to execute build command command",
        )));
    }

    // assemble result
    {
        fs::create_dir_all(&ctx.artifact_dir)?;
        let mut asm = Assembler::new(&main_path);
        asm.start(());
        asm.write_result(&binary_path)?;
    }

    println!("Build finished. Binary at {}", binary_path.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(())
}
