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

fn target_has_feature(feature: &str) -> bool {
    env::var("CARGO_CFG_TARGET_FEATURE")
        .unwrap_or_default()
        .split(',')
        .any(|enabled_feature| enabled_feature == feature)
}

/// Used to exclude autogenerated files in the output dir from `cargo:rerun-if-changed` directives.
#[derive(Debug)]
pub struct CustomCargoCallbacks {
    exclude_dir: Option<PathBuf>,
}

impl bindgen::callbacks::ParseCallbacks for CustomCargoCallbacks {
    fn header_file(&self, filename: &str) {
        if matches!(&self.exclude_dir, Some(exclude_dir) if Path::new(filename).starts_with(exclude_dir))
        {
            return;
        }
        println!("cargo:rerun-if-changed={filename}");
    }

    fn include_file(&self, filename: &str) {
        if matches!(&self.exclude_dir, Some(exclude_dir) if Path::new(filename).starts_with(exclude_dir))
        {
            return;
        }
        println!("cargo:rerun-if-changed={filename}");
    }

    fn read_env_var(&self, key: &str) {
        println!("cargo:rerun-if-env-changed={key}");
    }
}

fn generate_bindings<'a>(
    exclude_dir: Option<PathBuf>,
    include_paths: impl IntoIterator<Item = &'a Path>,
) {
    let target = env::var("TARGET").unwrap();
    let emscripten = target.contains("emscripten");

    // The bindgen::Builder is the main entry point
    // to bindgen, and lets you build up options for
    // the resulting bindings.
    let builder = include_paths
        .into_iter()
        .fold(bindgen::Builder::default(), |builder, path| {
            builder.clang_arg(format!("-I{}", path.to_string_lossy()))
        });

    // Work around bindgen/libclang missing `Highs_*` function declarations for
    // `wasm32-unknown-emscripten` when parsing with emscripten's sysroot.
    // The C API signatures are target-independent, so parse as host and skip
    // cross-target layout assertions.
    let builder = if emscripten {
        println!("cargo:rerun-if-env-changed=HOST");
        let host = env::var("HOST")
            .expect("TARGET is emscripten but HOST is not set; needed for bindgen host parsing");
        builder
            .clang_arg(format!("--target={host}"))
            .layout_tests(false)
    } else {
        builder
    };

    let c_bindings = builder
        // The input header we would like to generate bindings for.
        // This is a trivial wrapper header so that the HiGHS headers
        // can be discovered from the include path.
        .header("wrapper.h")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(CustomCargoCallbacks { exclude_dir }))
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    // Write the bindings to the $OUT_DIR/bindings.rs file.
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    c_bindings
        .write_to_file(out_path.join("c_bindings.rs"))
        .expect("Couldn't write bindings!");
}

#[cfg(feature = "build")]
fn build() -> bool {
    use cmake::Config;
    let target = env::var("TARGET").unwrap();
    let emscripten = target.contains("emscripten");
    let mut dst = Config::new("HiGHS");
    let crt_static = target_has_feature("crt-static");

    if cfg!(feature = "ninja") {
        dst.generator("Ninja");
    }

    dst.define("BUILD_CXX_EXE", "OFF");
    dst.define("BUILD_EXAMPLES", "OFF");

    // `cmake` crate default C++ flags inject `-fno-exceptions` for this target,
    // but HiGHS requires C++ exceptions. Use explicit flags for emscripten.
    if emscripten {
        dst.no_default_flags(true);
        dst.cxxflag("-fexceptions");
    }

    // Avoid using downstream project's profile setting for HiGHS build.
    if cfg!(feature = "highs_release") {
        dst.profile("Release");
    }

    let dst = dst
        .define("FAST_BUILD", "ON")
        .define("BUILD_SHARED_LIBS", "OFF")
        .define(
            "CMAKE_MSVC_RUNTIME_LIBRARY",
            if crt_static {
                "MultiThreaded"
            } else {
                "MultiThreadedDLL"
            },
        )
        .define("CMAKE_INTERPROCEDURAL_OPTIMIZATION", "FALSE")
        .define("ZLIB", if cfg!(feature = "libz") { "ON" } else { "OFF" })
        .build();

    let include_path = dst.join("include").join("highs");
    generate_bindings(Some(dst.clone()), [include_path.as_path()]);

    println!("cargo:rustc-link-search=native={}/lib", dst.display());
    println!("cargo:rustc-link-search=native={}/lib64", dst.display());
    println!("cargo:rustc-link-lib=static=highs");

    if cfg!(feature = "libz") {
        println!("cargo:rustc-link-lib=z");
    }

    let apple = target.contains("apple");
    let linux = target.contains("linux");
    let mingw = target.contains("pc-windows-gnu");
    if apple || emscripten {
        println!("cargo:rustc-link-lib=c++");
    } else if linux || mingw {
        println!("cargo:rustc-link-lib=stdc++");
    }
    println!("cargo:rerun-if-changed=HiGHS/highs/interfaces/highs_c_api.h");

    true
}

#[cfg(not(feature = "build"))]
fn build() -> bool {
    false
}

#[cfg(feature = "discover")]
fn discover() -> bool {
    let lib = match pkg_config::Config::new()
        .atleast_version("1.5.0")
        .probe("highs")
    {
        Ok(lib) => lib,
        Err(_e) => return false,
    };

    generate_bindings(None, lib.include_paths.iter().map(|p| p.as_path()));

    true
}

#[cfg(not(feature = "discover"))]
fn discover() -> bool {
    false
}

#[cfg(feature = "build")]
fn update_submodules<P: AsRef<Path>>(work_dir: P) {
    use std::process::Command;

    let program = "git";
    let args = ["submodule", "update", "--init"];
    println!(
        "Running command: \"{} {}\" in dir: {}",
        program,
        args.join(" "),
        work_dir.as_ref().display()
    );
    let ret = Command::new(program)
        .current_dir(work_dir.as_ref())
        .args(args)
        .status();

    match ret.map(|status| (status.success(), status.code())) {
        Ok((true, _)) => (),
        Ok((false, Some(c))) => panic!("Command failed with error code {}", c),
        Ok((false, None)) => panic!("Command got killed"),
        Err(e) => panic!("Command failed with error: {}", e),
    }
}

fn main() {
    println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_FEATURE");

    let crt_static = target_has_feature("crt-static");

    if cfg!(all(
        any(
            feature = "highs_release",
            feature = "libz",
            feature = "ninja"
        ),
        not(feature = "build")
    )) {
        panic!(
            "You have enabled features that control how HiGHS is built, but have not enabled the 'build' feature.
\
               Thus, your features will never have any effect. Please enable the 'build' feature on highs-sys if you want to build HiGHS or disable the 'libz', 'ninja' and 'highs_release' features."
        );
    }

    #[cfg(feature = "build")]
    if !Path::new("HiGHS/highs").exists() {
        update_submodules(env::var("CARGO_MANIFEST_DIR").unwrap());
    }

    if cfg!(feature = "discover") && crt_static {
        println!(
            "cargo::warning=You have enabled Rust's 'crt-static' target feature, but also enabled the 'discover' feature. Discovering a system-installed HiGHS bypasses the bundled build, so highs-sys cannot ensure that HiGHS uses the same MSVC runtime. Please disable 'discover' when using '-C target-feature=+crt-static'."
        );
    }

    if !discover() && !build() {
        panic!("Could neither discover nor build HiGHS");
    }
}
