#![allow(dead_code)]
#![deny(unused_must_use)]

use std::path::{Path, PathBuf};
use std::{env, format, fs};

use walkdir::WalkDir;
use xshell::{cmd, Cmd};
use yaml_rust::YamlLoader;

extern crate yaml_rust;

fn main() -> Result<(), anyhow::Error> {
    let args = env::args().skip(1).collect::<Vec<_>>();
    let args = args.iter().map(|s| &**s).collect::<Vec<_>>();

    match &args[..] {
        ["ci"] => task_ci()?,
        ["core"] => task_check(Realm::Core)?,
        ["metapac"] => task_metapac_gen()?,
        ["examples"] => task_check(Realm::Examples)?,
        ["fmt-check"] => task_cargo_fmt_check()?,
        ["fmt"] => task_cargo_fmt()?,
        _ => {
            println!("");
            println!("USAGE: cargo xtask [command]");
            println!("");
            println!("Commands:");
            println!("  ci        :: Runs entire CI");
            println!("  core      :: Builds the core");
            println!("  metapac   :: Builds the metapac");
            println!("  examples  :: Builds the examples");
            println!("  fmt-check :: Checks rustfmt");
            println!("  fmt       :: Performs rustfmt");
            println!("");
        }
    }
    Ok(())
}

fn task_ci() -> Result<(), anyhow::Error> {
    task_check(Realm::Core)?;
    task_check(Realm::Examples)?;
    task_metapac_gen()?;
    task_cargo_fmt_check()?;
    Ok(())
}

#[derive(Copy, Clone)]
enum Realm {
    All,
    Core,
    Examples,
}

impl Realm {
    fn accepts(&self, package: &str) -> bool {
        match self {
            Realm::All => true,
            Realm::Core => !package.contains("examples"),
            Realm::Examples => package.contains("examples"),
        }
    }
}

fn task_check(realm: Realm) -> Result<(), anyhow::Error> {
    let _e = xshell::pushenv("CI", "true");

    let matrix_yaml = root_dir().join(".github").join("workflows").join("rust.yml");

    let matrix = YamlLoader::load_from_str(&*fs::read_to_string(matrix_yaml).unwrap()).unwrap();

    let matrix = &matrix.get(0).unwrap()["jobs"]["ci"]["strategy"]["matrix"]["include"];

    let entries = matrix.as_vec().unwrap();

    for entry in entries {
        let package = entry["package"].as_str().unwrap();
        if !realm.accepts(package) {
            continue;
        }
        let target = entry["target"].as_str().unwrap();
        let features = entry["features"].as_str();
        let package_dir = root_dir().join(entry["package"].as_str().unwrap());
        let _p = xshell::pushd(package_dir)?;
        banner(&*format!(
            "Building {} [target={}] [features={}]",
            package,
            target,
            features.unwrap_or("default-features")
        ));

        let root_cargo_dir = root_dir().join(".cargo");
        fs::create_dir_all(root_cargo_dir.clone()).unwrap();
        fs::write(
            root_cargo_dir.join("config"),
            "[target.\"cfg(all())\"]\nrustflags = [\"-D\", \"warnings\"]",
        )
        .unwrap();

        let mut args = Vec::new();
        args.push("check");
        args.push("--target");
        args.push(target);

        if let Some(features) = features {
            args.push("--features");
            args.push(features);
        }

        let command = Cmd::new(PathBuf::from("cargo"));
        let command = command.args(args);
        let result = command.run();

        fs::remove_file(root_cargo_dir.join("config")).unwrap();

        result?;
    }

    Ok(())
}

fn task_metapac_gen() -> Result<(), anyhow::Error> {
    banner("Building metapac");
    let _p = xshell::pushd(root_dir().join("stm32-metapac-gen"));
    cmd!("cargo run").run()?;
    Ok(())
}

fn task_cargo_fmt() -> Result<(), anyhow::Error> {
    for entry in WalkDir::new(root_dir())
        .follow_links(false)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let f_name = entry.file_name().to_string_lossy();

        if f_name.ends_with(".rs") {
            if !is_primary_source(entry.path()) {
                continue;
            }
            let mut args = Vec::new();
            args.push("--skip-children");
            args.push("--unstable-features");
            args.push("--edition=2018");
            args.push(&*entry.path().to_str().unwrap());
            let command = Cmd::new("rustfmt");
            command.args(args).run()?;
        }
    }

    Ok(())
}

fn task_cargo_fmt_check() -> Result<(), anyhow::Error> {
    let mut actual_result = Ok(());
    for entry in WalkDir::new(root_dir())
        .follow_links(false)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let f_name = entry.file_name().to_string_lossy();

        if f_name.ends_with(".rs") {
            if !is_primary_source(entry.path()) {
                continue;
            }
            let mut args = Vec::new();
            args.push("--check");
            args.push("--skip-children");
            args.push("--unstable-features");
            args.push("--edition=2018");
            args.push(&*entry.path().to_str().unwrap());
            let command = Cmd::new("rustfmt");
            if let Err(result) = command.args(args).run() {
                actual_result = Err(result.into());
            }
        }
    }

    actual_result
}

fn root_dir() -> PathBuf {
    let mut xtask_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    xtask_dir.pop();
    xtask_dir
}

fn examples_dir() -> PathBuf {
    root_dir().join("examples")
}

fn is_primary_source(path: &Path) -> bool {
    let mut current = path;

    loop {
        let current_file_name = current.file_name().unwrap().to_str().unwrap();
        if current_file_name == "target"
            || current_file_name == "stm32-metapac-gen"
            || current_file_name == "stm32-data"
        {
            return false;
        }

        if let Some(path) = current.parent() {
            current = path.into();
            if current == root_dir() {
                return true;
            }
        } else {
            return false;
        }
    }
}

fn banner(text: &str) {
    println!("------------------------------------------------------------------------------------------------------------------------------------------------");
    println!("== {}", text);
    println!("------------------------------------------------------------------------------------------------------------------------------------------------");
}
