use pkg_config;
use std::env;
use std::path::PathBuf;
use std::process::Command;

fn ninja_available() -> bool {
    Command::new("ninja").arg("--version").spawn().is_ok()
}

fn msvc_cmake_tools_available() -> bool {
    Command::new("cmake").arg("--version").spawn().is_ok() && ninja_available()
}

fn setup_env_msvc(compiler: &cc::Tool) {
    // If PATH already contains what we need, skip this
    if msvc_cmake_tools_available() {
        return;
    }

    let target = env::var("TARGET").unwrap();
    let devenv = cc::windows_registry::find_tool(target.as_str(), "devenv");
    let tool_root: PathBuf = match devenv {
        Some(devenv_tool) => devenv_tool.path().parent().unwrap().to_path_buf(),
        None => {
            // if devenv (i.e. Visual Studio) was not found, assume compiler is
            // from standalone Build Tools and look there instead.
            // this should be done properly in cc crate, but for now it's not.
            let tools_name = std::ffi::OsStr::new("BuildTools");
            let compiler_path = compiler.path().to_path_buf();
            compiler_path
                .iter()
                .find(|x| *x == tools_name)
                .expect("Failed to find devenv or Build Tools");
            compiler_path
                .iter()
                .take_while(|x| *x != tools_name)
                .collect::<PathBuf>()
                .join(tools_name)
                .join(r"Common7\IDE")
        }
    };
    let cmake_pkg_dir = tool_root.join(r"CommonExtensions\Microsoft\CMake");
    let cmake_path = cmake_pkg_dir.join(r"CMake\bin\cmake.exe");
    let ninja_path = cmake_pkg_dir.join(r"Ninja\ninja.exe");
    if !cmake_path.is_file() {
        panic!("missing cmake");
    }
    if !ninja_path.is_file() {
        panic!("missing ninja");
    }

    // append cmake and ninja location to PATH
    if let Some(path) = env::var_os("PATH") {
        let mut paths = env::split_paths(&path).collect::<Vec<_>>();
        for tool_path in [cmake_path, ninja_path] {
            paths.push(tool_path.parent().unwrap().to_path_buf());
        }
        let new_path = env::join_paths(paths).unwrap();
        env::set_var("PATH", &new_path);
    }
}

fn build_with_cmake() {
    let uc_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let compiler = cc::Build::new().get_compiler();
    let has_ninja = if compiler.is_like_msvc() {
        setup_env_msvc(&compiler);
        // this is a BIG HACK that should be fixed in unicorn's cmake!!
        // but for now, tell link.exe to ignore multiply defined symbol names
        println!("cargo:rustc-link-arg=/FORCE:MULTIPLE");
        true
    } else {
        ninja_available()
    };

    // cc crate (as of 1.0.73) misdetects clang as gnu on apple
    if compiler.is_like_gnu() && env::consts::OS != "macos" {
        // see comment on /FORCE:MULTIPLE
        println!("cargo:rustc-link-arg=-Wl,-allow-multiple-definition");
    }

    let mut config = cmake::Config::new(&uc_dir);
    if has_ninja {
        config.generator("Ninja");
    }

    let mut archs = String::new();

    if std::env::var("CARGO_FEATURE_ARCH_X86").is_ok() {
        archs.push_str("x86;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_ARM").is_ok() {
        archs.push_str("arm;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_AARCH64").is_ok() {
        archs.push_str("aarch64;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_RISCV").is_ok() {
        archs.push_str("riscv;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_MIPS").is_ok() {
        archs.push_str("mips;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_SPARC").is_ok() {
        archs.push_str("sparc;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_M68K").is_ok() {
        archs.push_str("m68k;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_PPC").is_ok() {
        archs.push_str("ppc;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_S390X").is_ok() {
        archs.push_str("s390x;");
    }
    if std::env::var("CARGO_FEATURE_ARCH_TRICORE").is_ok() {
        archs.push_str("tricore;");
    }

    if !archs.is_empty() {
        archs.pop();
    }

    // need to clear build target and append "build" to the path because
    // unicorn's CMakeLists.txt doesn't properly support 'install', so we use
    // the build artifacts from the build directory, which cmake crate sets
    // to "<out_dir>/build/"
    let dst = config
        .define("UNICORN_BUILD_TESTS", "OFF")
        .define("UNICORN_INSTALL", "OFF")
        .define("UNICORN_ARCH", archs)
        .no_build_target(true)
        .build();
    println!(
        "cargo:rustc-link-search=native={}",
        dst.join("build").display()
    );

    // Lazymio(@wtdcode): Dynamic link may break. See: https://github.com/rust-lang/cargo/issues/5077
    if cfg!(feature = "dynamic_linkage") {
        if compiler.is_like_msvc() {
            println!("cargo:rustc-link-lib=dylib=unicorn-import");
        } else {
            println!("cargo:rustc-link-lib=dylib=unicorn");
        }
    } else {
        println!("cargo:rustc-link-lib=static=unicorn");
    }
    if !compiler.is_like_msvc() {
        println!("cargo:rustc-link-lib=pthread");
        println!("cargo:rustc-link-lib=m");
    }
}

fn main() {
    match pkg_config::Config::new()
        .atleast_version("2")
        .cargo_metadata(false)
        .probe("unicorn")
    {
        Ok(lib) => {
            for dir in lib.link_paths {
                println!("cargo:rustc-link-search=native={}", dir.to_str().unwrap());
            }
            if cfg!(feature = "dynamic_linkage") {
                if cc::Build::new().get_compiler().is_like_msvc() {
                    println!("cargo:rustc-link-lib=dylib=unicorn-import");
                } else {
                    println!("cargo:rustc-link-lib=dylib=unicorn");
                }
            } else {
                println!("cargo:rustc-link-arg=-Wl,-allow-multiple-definition");
                println!("cargo:rustc-link-lib=static=unicorn");
                println!("cargo:rustc-link-lib=pthread");
                println!("cargo:rustc-link-lib=m");
            }
        }
        Err(_) => {
            build_with_cmake();
        }
    };
}
