use std::borrow::Cow;
use std::env;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};

fn main() {
    let target =
        env::var("TARGET").expect("selinux-sys: Environment variable 'TARGET' was not defined.");

    match get_target_os(&target).as_deref() {
        None => return, // Do not build anything for bare metal architectures.

        Some("linux") | Some("android") | Some("androideabi") | Some("dragonfly")
        | Some("freebsd") | Some("netbsd") | Some("openbsd") | Some("hurd") => {
            // Continue, probably supported.
        }

        _ => return, // Do not build anything, probably unsupported.
    }

    let out_dir = env::var_os("OUT_DIR")
        .map(PathBuf::from)
        .expect("fts-sys: Environment variable 'OUT_DIR' was not defined.");

    println!("cargo:root={}", out_dir.to_str().unwrap());

    let sysroot = target_env_var_os("SYSROOT", &target).map(PathBuf::from);

    generate_bindings(&target, sysroot.as_deref(), &out_dir)
}

fn target_env_var_os(name: &str, target: &str) -> Option<OsString> {
    rerun_if_env_changed(name, target);

    let target_underscores = target.replace('-', "_");

    env::var_os(format!("{name}_{target}"))
        .or_else(|| env::var_os(format!("{name}_{target_underscores}")))
        .or_else(|| env::var_os(format!("TARGET_{name}")))
        .or_else(|| env::var_os(name))
}

fn rerun_if_env_changed(name: &str, target: &str) {
    let target_underscores = target.replace('-', "_");

    println!("cargo:rerun-if-env-changed={name}_{target}");
    println!("cargo:rerun-if-env-changed={name}_{target_underscores}");
    println!("cargo:rerun-if-env-changed=TARGET_{name}");
    println!("cargo:rerun-if-env-changed={name}");
}

fn get_target_os(target: &str) -> Option<String> {
    let components: Vec<_> = target.split('-').collect();
    let os_index = match components.len() {
        2 => {
            // e.g., aarch64-fuchsia, wasm32-wasi, x86_64-fuchsia
            if components[1] == "none" {
                return None; // Bare metal target.
            }
            1
        }

        3 | 4 => {
            // e.g., aarch64-unknown-freebsd, aarch64-unknown-linux-gnu
            if components[1] == "none" || components[2] == "none" {
                return None; // Bare metal target.
            }
            2
        }

        _ => panic!("Unrecognized target triplet '{target}'"),
    };

    Some(String::from(components[os_index]))
}

// See: https://github.com/rust-lang/rust-bindgen/issues/2136
fn translate_rustc_target_to_clang(rustc_target: &str) -> Cow<str> {
    if let Some(suffix) = rustc_target.strip_prefix("riscv32") {
        let suffix = suffix.trim_start_matches(|c| c != '-');
        Cow::Owned(format!("riscv32{suffix}"))
    } else if let Some(suffix) = rustc_target.strip_prefix("riscv64") {
        let suffix = suffix.trim_start_matches(|c| c != '-');
        Cow::Owned(format!("riscv64{suffix}"))
    } else if let Some(suffix) = rustc_target.strip_prefix("aarch64-apple-") {
        Cow::Owned(format!("arm64-apple-{suffix}"))
    } else if let Some(prefix) = rustc_target.strip_suffix("-espidf") {
        Cow::Owned(format!("{prefix}-elf"))
    } else {
        Cow::Borrowed(rustc_target)
    }
}

fn generate_bindings(target: &str, sysroot: Option<&Path>, out_dir: &Path) {
    let clang_target = translate_rustc_target_to_clang(target);

    let mut builder = bindgen::Builder::default()
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .default_enum_style(bindgen::EnumVariation::ModuleConsts)
        .default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
        .size_t_is_usize(true)
        .derive_debug(true)
        .derive_copy(true)
        .impl_debug(true)
        //.clang_arg("-D_FILE_OFFSET_BITS=64")
        .clang_arg(format!("--target={clang_target}"));

    if let Some(sysroot) = sysroot.map(Path::as_os_str).map(OsStr::to_str) {
        let sysroot = sysroot.expect("SYSROOT is not encoded in UTF-8");
        builder = builder.clang_arg(format!("--sysroot={sysroot}"));
    }

    builder = builder
        .allowlist_function("fts_(open|read|children|set|close)")
        .allowlist_type("FTSENT")
        .blocklist_type("(FTS|stat|timespec|dev_t|ino_t|nlink_t)")
        .allowlist_var("FTS_.+")
        .header("src/fts-sys.h");

    let bindings = builder
        .generate()
        .expect("fts-sys: Failed to generate Rust bindings for 'fts.h'.");

    bindings
        .write_to_file(out_dir.join("fts-sys.rs"))
        .expect("fts-sys: Failed to write 'fts-sys.rs'.")
}
