use std::env;
use std::path::PathBuf;

#[cfg(feature = "download-lib")]
#[path = "build-utils/downloader.rs"]
mod downloader;

#[path = "build-utils/runtime_linking.rs"]
mod runtime_linking;

#[path = "build-utils/linking.rs"]
mod linking;

fn main() {
    // Rerun the build script if the header file changes
    println!("cargo:rerun-if-changed=include/aic.h");

    // Rerun the build script if the AIC_LIB_PATH environment variable changes
    println!("cargo:rerun-if-env-changed=AIC_LIB_PATH");

    // Bindings need to be generated before early return on docs.rs
    generate_bindings();

    if env::var("DOCS_RS").is_ok() {
        // On docs.rs we don't need to link and we don't have network,
        // so we couldn't download anything if we wanted to
        return;
    }

    let runtime_linking = env::var("CARGO_FEATURE_RUNTIME_LINKING").is_ok();
    let dynamic_linking = env::var("CARGO_FEATURE_DYNAMIC_LINKING").is_ok();

    // `dynamic-linking` and `runtime-linking` select alternative linking strategies. Cargo
    // features are additive, so enabling both (e.g. via `--all-features`) is possible; in that
    // case runtime linking wins. Warn so the choice is not silently surprising.
    if runtime_linking && dynamic_linking {
        println!(
            "cargo:warning=Both `dynamic-linking` and `runtime-linking` are enabled; using \
             runtime linking. These features select alternative linking strategies and are not \
             meant to be combined."
        );
    }

    if runtime_linking {
        // Runtime linking resolves symbols from a user-provided dynamic library path,
        // so there is intentionally no build-time link step. If `dynamic-linking`
        // is also enabled through additive Cargo features, runtime linking wins.
        return;
    }

    let lib_path = if let Ok(path) = env::var("AIC_LIB_PATH") {
        PathBuf::from(path)
    } else {
        #[cfg(feature = "download-lib")]
        {
            let downloaded_path = download_lib();
            downloaded_path.join("lib")
        }
        #[cfg(not(feature = "download-lib"))]
        {
            panic!(
                "Enable feature `download-lib` or use a local library by setting the environment variable `AIC_LIB_PATH`"
            );
        }
    };

    let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();

    // On Windows MSVC the SDK ships the *static* library twice, once per C runtime flavour, under
    // `static-crt/` and `dynamic-crt/`. Pick the directory matching the CRT the Rust toolchain
    // links against (controlled by the `crt-static` target feature), otherwise the CRT symbols
    // clash at link time. `AIC_LIB_PATH` and the downloader both point at the SDK `lib` root, so
    // the CRT subdirectory is appended here.
    //
    // The dynamic (DLL) import library `aic.dll.lib` is CRT-agnostic: `aic.dll` bundles its own
    // CRT, so it stays at the `lib` root and the subdirectory is not applied when dynamic linking.
    let lib_path = if target_env == "msvc" && !dynamic_linking {
        let target_features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
        lib_path.join(linking::msvc_static_crt_subdir(&target_features))
    } else {
        lib_path
    };

    // Link with the curated library
    println!("cargo:rustc-link-search=native={}", lib_path.display());
    if dynamic_linking && target_env == "msvc" {
        // The MSVC SDK package ships the DLL import library as `aic.dll.lib` next to the static
        // `aic.lib`. A plain `dylib=aic` resolves to `aic.lib` (the static archive), which would
        // be linked statically and miss its system dependencies. Name the import library
        // verbatim so the linker binds against `aic.dll` instead.
        println!("cargo:rustc-link-lib=dylib:+verbatim=aic.dll.lib");
    } else {
        // Every other target uses the standard `-laic` naming, so no verbatim trick is needed:
        //   * static: `libaic.a` (Unix and the Windows `gnullvm` target).
        //   * dynamic on Unix: `libaic.so` / `libaic.dylib`.
        //   * dynamic on `gnullvm` (target_env == "gnu"): the MinGW/LLD driver searches the GNU
        //     import library `libaic.dll.a` before the static `libaic.a`, so `dylib=aic` binds
        //     against `aic.dll` correctly. This is the gnu equivalent of the MSVC case above,
        //     which only needs special handling because its import library (`aic.dll.lib`) and
        //     static archive (`aic.lib`) collide under the plain `dylib=aic` name.
        let link_kind = if dynamic_linking { "dylib" } else { "static" };
        println!("cargo:rustc-link-lib={link_kind}=aic");
    }

    // The platform system libraries below are transitive dependencies of the *static* AIC
    // library and must be linked into the final binary. A shared `libaic` already records its
    // own dependencies, so when linking dynamically we leave them out.
    if !dynamic_linking {
        add_platform_specific_libs();
    }
}

fn add_platform_specific_libs() {
    // Select libraries by the *target* OS via `CARGO_CFG_TARGET_OS`, not `cfg!(target_os = ...)`.
    // A build script is compiled for the host, so `cfg!` reports the host platform and would link
    // the wrong system libraries when cross-compiling (e.g. host Linux -> target windows-gnullvm
    // would pull in `dl`/`rt` and miss the Windows libs). For a native build the two agree.
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    match target_os.as_str() {
        "macos" => {
            // macOS requires CoreFoundation framework for time zone operations
            // This is needed by chrono and other crates that interact with system time
            println!("cargo:rustc-link-lib=framework=CoreFoundation");

            // Security framework might also be needed for some operations
            println!("cargo:rustc-link-lib=framework=Security");
        }
        "windows" => {
            // Windows system libraries that might be needed
            println!("cargo:rustc-link-lib=advapi32");
            println!("cargo:rustc-link-lib=bcrypt");
            println!("cargo:rustc-link-lib=kernel32");
            println!("cargo:rustc-link-lib=ws2_32");
            println!("cargo:rustc-link-lib=oleaut32");
            println!("cargo:rustc-link-lib=crypt32");
        }
        "linux" => {
            // Linux system libraries
            println!("cargo:rustc-link-lib=pthread");
            println!("cargo:rustc-link-lib=dl");
            println!("cargo:rustc-link-lib=rt");
        }
        _ => {}
    }
}

#[cfg(feature = "download-lib")]
fn download_lib() -> PathBuf {
    use downloader::Downloader;

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    let downloader = Downloader::new(&out_dir);
    downloader.download()
}

/// Maps a Cargo target triple to the triple clang should use when parsing the C header.
///
/// Returns `Some` only for the Rust-only `*-pc-windows-gnullvm` targets, which clang rejects
/// (it parses the `gnullvm` environment as an invalid OS version). Their ABI-compatible clang
/// triple is `*-pc-windows-gnu`. All other targets are valid clang triples, so this returns
/// `None` and bindgen keeps using the `TARGET` value unchanged.
fn clang_target_for(target: &str) -> Option<String> {
    target
        .strip_suffix("-pc-windows-gnullvm")
        .map(|arch| format!("{arch}-pc-windows-gnu"))
}

fn generate_bindings() {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let header_path = manifest_dir.join("include").join("aic.h");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

    // Generate bindings using bindgen
    let mut builder = bindgen::Builder::default()
        // The input header we would like to generate bindings for.
        .header(header_path.to_str().unwrap())
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // Generate constified enums to avoid name repetition
        .constified_enum_module("AicErrorCode")
        .constified_enum_module("AicProcessorParameter")
        .constified_enum_module("AicVadParameter");

    // bindgen feeds the Cargo `TARGET` triple to libclang, but clang does not understand the
    // Rust-only `gnullvm` environment: it reads `windows-gnullvm` as the `windows-gnu` triple
    // with an invalid OS version (`llvm`) and aborts before it can parse the header. Map such
    // triples to their clang equivalent (`*-pc-windows-gnu`, ABI-compatible) and pass it
    // explicitly. bindgen detects the user-provided `--target=` and does not add its own.
    let target = env::var("TARGET").unwrap_or_default();
    if let Some(clang_target) = clang_target_for(&target) {
        builder = builder.clang_arg(format!("--target={clang_target}"));
    }

    if env::var("CARGO_FEATURE_RUNTIME_LINKING").is_ok() {
        let runtime_bindings = builder
            .clone()
            .generate()
            .expect("Unable to generate runtime-linking symbols");
        runtime_linking::generate(&runtime_bindings, &out_path.join("runtime_symbols.rs"));

        // The runtime-linking module provides Rust functions with these names
        // that dispatch through libloading. Keep types/constants from bindgen,
        // but omit build-linked extern function declarations to avoid conflicts.
        builder = builder.blocklist_function("aic_.*");
    }

    let bindings = builder
        // 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.
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    // Tell cargo to rerun the build script if the library or header changes.
    println!("cargo:rerun-if-changed={}", header_path.display());
}
