use pkg_config::Config;
use std::env;
use std::fmt;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

/// # Link Type Enumeration
///
/// Holds the different types of linking we support in this
/// script. Used to keep track of what the default link type is and
/// what override has been specified, if any, in the environment.
#[derive(Eq, PartialEq)]
enum LinkType {
    /// Static linking. This corresponds to the `static` type in Cargo.
    Static,
    /// Dynamic linking. This corresponds to the `dylib` type in Cargo.
    Dynamic,
}

impl fmt::Display for LinkType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                &LinkType::Static => "static",
                &LinkType::Dynamic => "dylib",
            }
        )
    }
}

fn env_var_bool(name: &str) -> Option<bool> {
    if name.starts_with("RUSTONIG") {
        println!("cargo:rerun-if-env-changed={}", name);
    }
    env::var(name)
        .ok()
        .map(|s| match &s.to_string().to_lowercase()[..] {
            "0" | "no" | "false" => false,
            _ => true,
        })
}

/// # Link Type Override
///
/// Retuns the override from the environment, if any is set.
fn link_type_override() -> Option<LinkType> {
    let dynamic_env = env_var_bool("RUSTONIG_DYNAMIC_LIBONIG").map(|b| match b {
        true => LinkType::Dynamic,
        false => LinkType::Static,
    });
    let static_env = env_var_bool("RUSTONIG_STATIC_LIBONIG").map(|b| match b {
        true => LinkType::Static,
        false => LinkType::Dynamic,
    });

    dynamic_env.or(static_env)
}

fn compile() {
    bindgen_headers("oniguruma/src/oniguruma.h");

    let mut cc = cc::Build::new();
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
    let ref src = Path::new("oniguruma").join("src");
    let config_h = out_dir.join("config.h");

    if env_var_bool("CARGO_FEATURE_PRINT_DEBUG").unwrap_or(false) {
        cc.define("ONIG_DEBUG_PARSE", Some("1"));
        cc.define("ONIG_DEBUG_COMPILE", Some("1"));
        cc.define("ONIG_DEBUG_SEARCH", Some("1"));
        cc.define("ONIG_DEBUG_MATCH", Some("1"));
    }

    if !src.exists() {
        panic!(
            "Unable to find source files in {}. Is oniguruma submodule checked out?\n\
             Try git submodule init; git submodule update",
            src.display()
        );
    }

    let arch = env::var("CARGO_CFG_TARGET_ARCH");
    let os = env::var("CARGO_CFG_TARGET_OS");
    let bits = env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap();
    let target_env = env::var("CARGO_CFG_TARGET_ENV");
    if let Ok("windows") = os.as_ref().map(String::as_str) {
        fs::copy(src.join(format!("config.h.win{}", bits)), config_h)
            .expect("Can't copy config.h.win??");
    } else {
        let family = env::var("CARGO_CFG_TARGET_FAMILY");
        if let Ok("unix") = family.as_ref().map(String::as_str) {
            cc.define("HAVE_UNISTD_H", Some("1"));
            cc.define("HAVE_SYS_TYPES_H", Some("1"));
            cc.define("HAVE_SYS_TIME_H", Some("1"));
            // On musl and BSD targets, alloca is provided as a compiler
            // built-in and alloca.h may not be in the compiler's search path
            // in all configurations (e.g., when using musl-gcc with a custom
            // sysroot, or on NetBSD/OpenBSD/FreeBSD/DragonFly where alloca.h
            // is not available as a standard header).
            let is_bsd = matches!(
                os.as_deref(),
                Ok("netbsd") | Ok("openbsd") | Ok("freebsd") | Ok("dragonfly")
            );
            if target_env.as_deref() != Ok("musl") && !is_bsd {
                cc.define("HAVE_ALLOCA_H", Some("1"));
            }
        }

        // Can't use size_of::<c_long>(), because it'd refer to build arch, not target arch.
        // so instead assume it's a non-exotic target (LP32/LP64).
        fs::write(
            config_h,
            format!(
                "
            #define HAVE_PROTOTYPES 1
            #define STDC_HEADERS 1
            #define HAVE_STRING_H 1
            #define HAVE_STDARG_H 1
            #define HAVE_STDLIB_H 1
            #define HAVE_LIMITS_H 1
            #define HAVE_INTTYPES_H 1
            #define SIZEOF_INT 4
            #define SIZEOF_SHORT 2
            #define SIZEOF_LONG {0}
            #define SIZEOF_VOIDP {0}
            #define SIZEOF_LONG_LONG 8
        ",
                if bits == "64" { "8" } else { "4" }
            ),
        )
        .expect("Can't write config.h to OUT_DIR");
    }
    if let Ok("wasm32") = arch.as_ref().map(String::as_str) {
        cc.define("ONIG_DISABLE_DIRECT_THREADING", Some("1"));
        if let Ok("unknown") = os.as_ref().map(String::as_str) {
            cc.define(
                "ONIG_EXTERN",
                Some(r#"__attribute__((visibility("default")))"#),
            );
        }
    }

    // On native musl hosts (e.g., Alpine Linux), the musl-gcc wrapper or the
    // cross-compiler selected by the cc crate may use a sysroot (typically
    // /usr/local/musl) whose include directory does not exist on native musl
    // systems. On Alpine the C standard library headers live in /usr/include.
    // Explicitly add that directory so that stdlib.h / limits.h can be found.
    if let Ok("musl") = target_env.as_ref().map(String::as_str) {
        let host = env::var("HOST").unwrap_or_default();
        if host.contains("musl") && Path::new("/usr/include/stdlib.h").exists() {
            cc.flag("-isystem").flag("/usr/include");
        }
    }

    cc.include(out_dir); // Read config.h from there
    cc.include(src);

    let files = [
        "regexec.c",
        "regerror.c",
        "regparse.c",
        "regext.c",
        "regcomp.c",
        "reggnu.c",
        "regenc.c",
        "regsyntax.c",
        "regtrav.c",
        "regversion.c",
        "st.c",
        "onig_init.c",
        "unicode.c",
        "ascii.c",
        "utf8.c",
        "utf16_be.c",
        "utf16_le.c",
        "utf32_be.c",
        "utf32_le.c",
        "euc_jp.c",
        "sjis.c",
        "iso8859_1.c",
        "iso8859_2.c",
        "iso8859_3.c",
        "iso8859_4.c",
        "iso8859_5.c",
        "iso8859_6.c",
        "iso8859_7.c",
        "iso8859_8.c",
        "iso8859_9.c",
        "iso8859_10.c",
        "iso8859_11.c",
        "iso8859_13.c",
        "iso8859_14.c",
        "iso8859_15.c",
        "iso8859_16.c",
        "euc_tw.c",
        "euc_kr.c",
        "big5.c",
        "gb18030.c",
        "koi8_r.c",
        "cp1251.c",
        "euc_jp_prop.c",
        "sjis_prop.c",
        "unicode_unfold_key.c",
        "unicode_fold1_key.c",
        "unicode_fold2_key.c",
        "unicode_fold3_key.c",
    ];
    for file in files.iter() {
        cc.file(src.join(file));
    }

    if cfg!(feature = "posix-api") {
        cc.file(src.join("regposix.c"));
        cc.file(src.join("regposerr.c"));
    }

    cc.warnings(false); // not actionable by the end user
    cc.compile("onig");
}

#[cfg(not(feature = "generate"))]
fn bindgen_headers(_path: &str) {}

#[cfg(feature = "generate")]
fn bindgen_headers(path: &str) {
    let arch = env::var("CARGO_CFG_TARGET_ARCH");
    let mut bindgen = bindgen::Builder::default()
        .header(path)
        .derive_eq(true)
        .no_partialeq("OnigEncodingTypeST")
        .rust_target("1.70.0".parse().unwrap())
        .rust_edition("2021".parse().unwrap())
        .layout_tests(false);
    if let Ok("wasm32") = arch.as_ref().map(String::as_str) {
        bindgen = bindgen.clang_arg("-fvisibility=default");
    }
    let bindings = bindgen.generate().expect("bindgen");
    let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR");
    let out_path = Path::new(&out_dir);
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

pub fn main() {
    let link_type = link_type_override();
    let require_pkg_config = env_var_bool("RUSTONIG_SYSTEM_LIBONIG").unwrap_or(false);

    if require_pkg_config || link_type == Some(LinkType::Dynamic) {
        let mut conf = Config::new();
        // dynamically-generated headers can work with an older version
        // pre-generated headers are for the latest
        conf.atleast_version(if cfg!(feature = "generate") {
            "6.8.0"
        } else {
            "6.9.3"
        });
        if link_type == Some(LinkType::Static) {
            conf.statik(true);
        }
        match conf.probe("oniguruma") {
            Ok(lib) => {
                for path in &lib.include_paths {
                    let header = path.join("oniguruma.h");
                    if header.exists() {
                        bindgen_headers(&header.display().to_string());
                        return;
                    }
                }
                if require_pkg_config {
                    panic!(
                        "Unable to find oniguruma.h in include paths from pkg-config: {:?}",
                        lib.include_paths
                    );
                }
            }
            Err(ref err) if require_pkg_config => {
                panic!("Unable to find oniguruma in pkg-config, and RUSTONIG_SYSTEM_LIBONIG is set: {}", err);
            }
            _ => {}
        }
    }

    compile();
}
