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

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum WasmMode {
    CompileOnly,
    Source,
    Provider,
}

#[derive(Debug)]
struct BuildConfig {
    manifest_dir: PathBuf,
    #[cfg_attr(not(feature = "bindgen"), allow(dead_code))]
    out_dir: PathBuf,
    target_env: String,
    target_os: String,
    profile: String,
    is_docsrs: bool,
    skip_cc: bool,
    force_bindgen: bool,
    wasm_mode: Option<WasmMode>,
}

impl BuildConfig {
    fn from_env() -> Self {
        let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
        let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
        let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
        let is_docsrs = env::var("DOCS_RS").is_ok() || env::var("CARGO_CFG_DOCSRS").is_ok();
        let skip_cc = parse_bool_env("BOXDDD_SYS_SKIP_CC");
        let force_bindgen = parse_bool_env("BOXDDD_SYS_FORCE_BINDGEN");
        let wasm_mode = (target_arch == "wasm32").then(|| {
            env::var("BOXDDD_SYS_WASM_MODE")
                .ok()
                .map(|value| parse_wasm_mode(&value))
                .unwrap_or_else(|| default_wasm_mode(&target_os))
        });

        Self {
            manifest_dir: PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()),
            out_dir: PathBuf::from(env::var("OUT_DIR").unwrap()),
            target_env,
            target_os,
            profile: env::var("PROFILE").unwrap_or_else(|_| "release".into()),
            is_docsrs,
            skip_cc,
            force_bindgen,
            wasm_mode,
        }
    }

    fn is_debug(&self) -> bool {
        self.profile == "debug"
    }

    fn pregenerated_bindings(&self) -> PathBuf {
        if cfg!(feature = "double-precision") {
            self.manifest_dir
                .join("src")
                .join("bindings_pregenerated_double.rs")
        } else {
            self.manifest_dir
                .join("src")
                .join("bindings_pregenerated.rs")
        }
    }
}

fn parse_bool_env(key: &str) -> bool {
    match env::var(key) {
        Ok(v) => matches!(
            v.as_str(),
            "1" | "true" | "yes" | "on" | "TRUE" | "YES" | "ON"
        ),
        Err(_) => false,
    }
}

fn parse_wasm_mode(value: &str) -> WasmMode {
    match value {
        "compile-only" | "compile_only" | "check" => WasmMode::CompileOnly,
        "source" | "c-backed" | "c_backed" | "wasi" => WasmMode::Source,
        "provider" | "import-provider" | "import_provider" => WasmMode::Provider,
        other => panic!(
            "unsupported BOXDDD_SYS_WASM_MODE={other:?}; expected compile-only, source, or provider"
        ),
    }
}

fn default_wasm_mode(target_os: &str) -> WasmMode {
    if target_os == "wasi" {
        WasmMode::Source
    } else {
        WasmMode::CompileOnly
    }
}

fn main() {
    println!("cargo:rustc-check-cfg=cfg(has_pregenerated)");
    println!("cargo:rustc-check-cfg=cfg(force_bindgen)");
    println!("cargo:rustc-check-cfg=cfg(boxddd_sys_wasm_provider)");
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-changed=third-party/box3d/include/box3d/box3d.h");
    println!("cargo:rerun-if-changed=third-party/box3d");
    println!("cargo:rerun-if-env-changed=BOXDDD_SYS_SKIP_CC");
    println!("cargo:rerun-if-env-changed=BOXDDD_SYS_FORCE_BINDGEN");
    println!("cargo:rerun-if-env-changed=BOXDDD_SYS_WASM_MODE");
    println!("cargo:rerun-if-env-changed=BOXDDD_SYS_LINK_LIB");
    println!("cargo:rerun-if-env-changed=BOXDDD_SYS_LINK_SEARCH");
    println!("cargo:rerun-if-env-changed=WASI_SYSROOT");
    println!("cargo:rerun-if-env-changed=WASI_SDK_PATH");
    println!("cargo:rerun-if-env-changed=DOCS_RS");
    println!("cargo:rerun-if-env-changed=CARGO_CFG_DOCSRS");

    let config = BuildConfig::from_env();
    let pregenerated = config.pregenerated_bindings();
    let has_pregenerated = pregenerated.exists();
    validate_build_config(&config);
    if config.force_bindgen {
        println!("cargo:rustc-cfg=force_bindgen");
    } else if has_pregenerated {
        println!("cargo:rustc-cfg=has_pregenerated");
    }
    if config.wasm_mode == Some(WasmMode::Provider) {
        println!("cargo:rustc-cfg=boxddd_sys_wasm_provider");
        if config.force_bindgen {
            panic!(
                "BOXDDD_SYS_WASM_MODE=provider cannot be combined with BOXDDD_SYS_FORCE_BINDGEN=1 yet"
            );
        }
        if !has_pregenerated {
            panic!("BOXDDD_SYS_WASM_MODE=provider requires checked-in pregenerated bindings");
        }
        generate_wasm_provider_bindings(&pregenerated, &config.out_dir);
    }

    if config.force_bindgen || (!has_pregenerated && !config.is_docsrs) {
        #[cfg(feature = "bindgen")]
        generate_bindings(&config.manifest_dir, &config.out_dir);
        #[cfg(not(feature = "bindgen"))]
        {
            if config.force_bindgen {
                panic!("BOXDDD_SYS_FORCE_BINDGEN=1 requires the `bindgen` feature");
            }
            panic!(
                "pregenerated Box3D bindings are missing for the selected ABI mode; enable `bindgen` or refresh checked-in bindings"
            );
        }
    }

    if config.is_docsrs || config.skip_cc {
        if config.is_docsrs {
            println!("cargo:warning=DOCS_RS detected: skipping native Box3D C build");
        } else {
            println!("cargo:warning=Skipping native Box3D C build due to BOXDDD_SYS_SKIP_CC");
        }
        return;
    }

    if handle_wasm_build(&config) {
        return;
    }

    if !cfg!(feature = "build-from-source") {
        emit_external_link_directives();
        println!(
            "cargo:warning=build-from-source disabled: not compiling vendored Box3D C sources"
        );
        return;
    }

    build_box3d_from_source(&config);
}

fn validate_build_config(config: &BuildConfig) {
    if config.skip_cc && config.wasm_mode == Some(WasmMode::Source) {
        panic!(
            "BOXDDD_SYS_SKIP_CC=1 cannot be combined with BOXDDD_SYS_WASM_MODE=source; \
             source mode is the runtime-capable WASM path and must compile Box3D C sources"
        );
    }
}

fn handle_wasm_build(config: &BuildConfig) -> bool {
    let Some(mode) = config.wasm_mode else {
        return false;
    };

    match mode {
        WasmMode::CompileOnly => {
            println!(
                "cargo:warning=boxddd-sys is using compile-only WASM mode; Box3D C sources are not linked"
            );
            true
        }
        WasmMode::Provider => {
            println!(
                "cargo:warning=boxddd-sys WASM provider mode is opt-in scaffold only; Box3D symbols are expected from the provider module"
            );
            true
        }
        WasmMode::Source => {
            if config.target_os != "wasi" {
                panic!(
                    "BOXDDD_SYS_WASM_MODE=source currently supports wasm32-wasip1 only; use provider mode for wasm32-unknown-unknown browser builds"
                );
            }
            if !cfg!(feature = "build-from-source") {
                panic!(
                    "BOXDDD_SYS_WASM_MODE=source requires the default `build-from-source` feature"
                );
            }
            build_box3d_from_source(config);
            true
        }
    }
}

fn emit_external_link_directives() {
    if let Ok(path) = env::var("BOXDDD_SYS_LINK_SEARCH") {
        if !path.is_empty() {
            println!("cargo:rustc-link-search=native={path}");
        }
    }

    let lib = env::var("BOXDDD_SYS_LINK_LIB").unwrap_or_else(|_| "box3d".into());
    if !lib.is_empty() {
        println!("cargo:rustc-link-lib={lib}");
    }
}

fn generate_wasm_provider_bindings(pregenerated: &Path, out_dir: &Path) {
    const IMPORT_MODULE: &str = "box3d-sys-v0";
    let source = fs::read_to_string(pregenerated).unwrap_or_else(|err| {
        panic!(
            "failed to read pregenerated bindings at {}: {err}",
            pregenerated.display()
        )
    });
    let rewritten = source.replace(
        "unsafe extern \"C\" {",
        &format!("#[link(wasm_import_module = \"{IMPORT_MODULE}\")]\nunsafe extern \"C\" {{"),
    );
    if rewritten == source {
        panic!(
            "failed to generate WASM provider bindings from {}; no extern blocks were found",
            pregenerated.display()
        );
    }
    fs::write(out_dir.join("wasm_provider_bindings.rs"), rewritten)
        .expect("failed to write WASM provider bindings");
}

#[cfg(feature = "bindgen")]
fn generate_bindings(manifest_dir: &Path, out_dir: &Path) {
    let include_root = manifest_dir
        .join("third-party")
        .join("box3d")
        .join("include");
    let header = include_root.join("box3d").join("box3d.h");
    let bindings = bindgen::Builder::default()
        .header(header.to_string_lossy())
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .clang_args(["-x", "c", "-std=c17"])
        .clang_arg(format!("-I{}", include_root.display()))
        .clang_args(double_precision_clang_args())
        .allowlist_function("b3.*")
        .allowlist_type("b3.*")
        .allowlist_var("B3_.*")
        .layout_tests(false)
        .generate()
        .expect("failed to generate Box3D bindings");

    bindings
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("failed to write Box3D bindings");
}

#[cfg(feature = "bindgen")]
fn double_precision_clang_args() -> Vec<&'static str> {
    if cfg!(feature = "double-precision") {
        vec!["-DBOX3D_DOUBLE_PRECISION"]
    } else {
        Vec::new()
    }
}

#[cfg(not(feature = "bindgen"))]
#[allow(dead_code)]
fn generate_bindings(_manifest_dir: &Path, _out_dir: &Path) {
    unreachable!("generate_bindings is only available with the `bindgen` feature enabled");
}

fn add_msvc_c_standard_flag(build: &mut cc::Build) {
    match build.is_flag_supported("/std:c17") {
        Ok(true) => {
            build.flag("/std:c17");
        }
        Ok(false) | Err(_) => {
            build.flag_if_supported("/std:c11");
        }
    }
}

fn build_box3d_from_source(config: &BuildConfig) {
    let box3d_root = config.manifest_dir.join("third-party").join("box3d");
    let box3d_include = box3d_root.join("include");
    let box3d_src = box3d_root.join("src");

    let mut build = cc::Build::new();
    build.include(&box3d_include);
    build.include(&box3d_src);

    let mut files = Vec::new();
    collect_c_files(&box3d_src, &mut files);
    for file in files {
        build.file(file);
    }

    if config.target_env == "msvc" {
        let use_static_crt = env::var("CARGO_CFG_TARGET_FEATURE")
            .unwrap_or_default()
            .split(',')
            .any(|feature| feature == "crt-static");
        build.static_crt(use_static_crt);
        build.debug(config.is_debug());
        build.opt_level(if config.is_debug() { 0 } else { 2 });
        add_msvc_c_standard_flag(&mut build);
    } else {
        build.flag_if_supported("-std=c17");
        build.flag_if_supported("-ffp-contract=off");
        build.debug(config.is_debug());
        build.opt_level(if config.is_debug() { 0 } else { 2 });
        if config.target_os == "linux" {
            build.define("_POSIX_C_SOURCE", Some("199309L"));
            build.flag_if_supported("-pthread");
            println!("cargo:rustc-link-lib=pthread");
            println!("cargo:rustc-link-lib=m");
        }
        if config.wasm_mode == Some(WasmMode::Source) {
            build.define("BOX3D_WASM_SINGLE_THREADED", None);
            configure_wasi_sysroot(&mut build);
        }
    }

    if cfg!(feature = "disable-simd") {
        build.define("BOX3D_DISABLE_SIMD", None);
    }
    if cfg!(feature = "validate") {
        build.define("BOX3D_VALIDATE", None);
    }
    if cfg!(feature = "double-precision") {
        build.define("BOX3D_DOUBLE_PRECISION", None);
    }

    build.compile("box3d");
}

fn configure_wasi_sysroot(build: &mut cc::Build) {
    let sysroot = env::var_os("WASI_SYSROOT")
        .map(PathBuf::from)
        .or_else(|| env::var_os("WASI_SDK_PATH").map(|path| PathBuf::from(path).join("share/wasi-sysroot")))
        .unwrap_or_else(|| {
            panic!(
                "wasm32-wasip1 source builds require WASI_SYSROOT or WASI_SDK_PATH so clang can find WASI libc headers"
            )
        });

    let has_libc_headers = sysroot.join("include").join("math.h").exists()
        || sysroot
            .join("include")
            .join("wasm32-wasi")
            .join("math.h")
            .exists();
    if !has_libc_headers {
        panic!(
            "WASI sysroot at {} does not contain WASI libc headers",
            sysroot.display()
        );
    }

    build.flag(format!("--sysroot={}", sysroot.display()));
}

fn collect_c_files(dir: &Path, out: &mut Vec<PathBuf>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                collect_c_files(&path, out);
            } else if path.extension().is_some_and(|ext| ext == "c") {
                out.push(path);
            }
        }
    }
}
