use core::error::Error;
use std::{
    env,
    fs::File,
    io::Write,
    path::{Path, PathBuf},
};

use cc::Build;
use system_deps::{Config, Dependencies};
#[cfg(target_env = "msvc")]
use tap::TapFallible;
use tap::TapOptional;

static DEFAULT_SOURCES: &[&str] = &[
    "address",
    "channel",
    "client",
    "clock",
    "ctx",
    "curve_client",
    "curve_mechanism_base",
    "curve_server",
    "dealer",
    "decoder_allocators",
    "devpoll",
    "dgram",
    "dish",
    "dist",
    "endpoint",
    "epoll",
    "err",
    "fq",
    "gather",
    "gssapi_client",
    "gssapi_mechanism_base",
    "gssapi_server",
    "io_object",
    "io_thread",
    "ip_resolver",
    "ip",
    "ipc_address",
    "ipc_connecter",
    "ipc_listener",
    "kqueue",
    "lb",
    "mailbox_safe",
    "mailbox",
    "mechanism_base",
    "mechanism",
    "metadata",
    "msg",
    "mtrie",
    "norm_engine",
    "null_mechanism",
    "object",
    "options",
    "own",
    "pair",
    "peer",
    "pgm_receiver",
    "pgm_sender",
    "pgm_socket",
    "pipe",
    "plain_client",
    "plain_server",
    "poll",
    "poller_base",
    "polling_util",
    "pollset",
    "precompiled",
    "proxy",
    "pub",
    "pull",
    "push",
    "radio",
    "radix_tree",
    "random",
    "raw_decoder",
    "raw_encoder",
    "raw_engine",
    "reaper",
    "rep",
    "req",
    "router",
    "scatter",
    "select",
    "server",
    "session_base",
    "signaler",
    "socket_base",
    "socket_poller",
    "socks_connecter",
    "socks",
    "stream_connecter_base",
    "stream_engine_base",
    "stream_listener_base",
    "stream",
    "sub",
    "tcp_address",
    "tcp_connecter",
    "tcp_listener",
    "tcp",
    "thread",
    "timers",
    "tipc_address",
    "tipc_connecter",
    "tipc_listener",
    "trie",
    "udp_address",
    "udp_engine",
    "v1_decoder",
    "v1_encoder",
    "v2_decoder",
    "v2_encoder",
    "v3_1_encoder",
    "vmci_address",
    "vmci_connecter",
    "vmci_listener",
    "vmci",
    "ws_address",
    "ws_connecter",
    "ws_decoder",
    "ws_encoder",
    "ws_engine",
    "ws_listener",
    "xpub",
    "xsub",
    "zap_client",
    "zmq_utils",
    "zmq",
    "zmtp_engine",
];

fn add_cpp_sources(build: &mut Build, root: impl AsRef<Path>, files: &[&str]) {
    build.cpp(true);
    let root = root.as_ref();
    build.files(files.iter().map(|src| {
        let mut p = root.join(src);
        p.set_extension("cpp");
        println!("cargo:rerun-if-changed={}", p.display());
        p
    }));
    build.include(root);
}

fn add_c_sources(build: &mut Build, root: impl AsRef<Path>, files: &[&str]) {
    let root = root.as_ref();
    // Temporarily use c instead of c++.
    build.cpp(false);
    build.files(files.iter().map(|src| {
        let mut p = root.join(src);
        p.set_extension("h");
        println!("cargo:rerun-if-changed={}", p.display());
        p.set_extension("c");
        println!("cargo:rerun-if-changed={}", p.display());
        p
    }));

    build.include(root);
}

fn emit_static_libs_in<D>(dir: D)
where
    D: AsRef<Path>,
{
    let dir = dir.as_ref();

    walkdir::WalkDir::new(dir)
        .into_iter()
        .filter_map(|entry| {
            entry.ok().filter(|dir_entry| {
                let path = dir_entry.path();
                path.is_file()
                    && path.extension().is_some_and(|ext| {
                        (cfg!(target_env = "msvc") && ext == "lib")
                            || (cfg!(not(target_env = "msvc")) && ext == "a")
                    })
            })
        })
        .for_each(|entry| {
            if let Some(lib_name) = entry.path().file_stem() {
                if let Some(parent) = entry.path().parent() {
                    println!("cargo:rustc-link-search=native={}", parent.display());
                }
                #[cfg(target_env = "msvc")]
                println!("cargo::rustc-link-lib={}", lib_name.display());
                #[cfg(not(target_env = "msvc"))]
                {
                    let lib = lib_name.to_string_lossy();
                    println!(
                        "cargo::rustc-link-lib=static={}",
                        lib.strip_prefix("lib").unwrap_or(&lib)
                    );
                }
            }
        });
}

fn check_low_level_compilation<S, F>(c_src: S, configure_build: F) -> Result<bool, Box<dyn Error>>
where
    S: AsRef<str>,
    F: FnOnce(&mut Build) -> &mut Build,
{
    let out_dir = env::var("OUT_DIR")?;
    let out_dir = Path::new(&out_dir);
    let check_compile = tempfile::Builder::new()
        .prefix("check_compile")
        .tempdir_in(out_dir)?;

    let src_path = check_compile.path().join("check_compile.c");
    {
        let mut src_file = File::create(&src_path)?;
        src_file.write_all(c_src.as_ref().as_bytes())?;
        src_file.flush()?;
    }

    let mut builder = Build::new();
    let mut compile_command = configure_build(&mut builder).get_compiler().to_command();

    compile_command.arg(src_path);

    #[cfg(not(target_env = "msvc"))]
    compile_command
        .arg("-o")
        .arg(check_compile.path().join("check_compile"));

    #[cfg(target_env = "msvc")]
    compile_command.arg("/c").arg(format!(
        "/Fo{}",
        check_compile.path().join("check_compile").display()
    ));

    Ok(compile_command.status().map(|status| status.success())?)
}

#[cfg(target_env = "gnu")]
fn check_strlcpy() -> Result<bool, Box<dyn Error>> {
    check_low_level_compilation(
        r#"
#include <string.h>

int main() {
    char buf[1];
    (void)strlcpy(buf, "a", 1);
    return 0;
}
"#,
        |build| build.warnings(false),
    )
}

#[cfg(all(target_os = "windows", not(target_vendor = "uwp")))]
fn check_ipc_headers() -> Result<bool, Box<dyn Error>> {
    check_low_level_compilation(
        r#"
#include <winsock2.h>
#include <afunix.h>

int main() {
    SOCKET sock = INVALID_SOCKET;
    int family = AF_UNIX;
    return 0;
}
"#,
        |build| build.warnings(false),
    )
}

#[cfg(not(target_env = "msvc"))]
fn check_cxx11() -> Result<bool, Box<dyn Error>> {
    check_low_level_compilation(
        r#"
int main(void) {
    return 0;
}
"#,
        |build| {
            build
                .cpp(true)
                .warnings(true)
                .warnings_into_errors(true)
                .std("c++11")
        },
    )
}

fn configure(build: &mut Build) -> Result<(), Box<dyn Error>> {
    let libraries = Config::new().probe()?;

    let vendor = Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor");

    #[cfg(not(target_env = "msvc"))]
    build.flags(&[
        "-Wno-unused-function",
        "-Wno-deprecated",
        "-Wno-unused-parameter",
        "-Wno-ignored-qualifiers",
        "-Wno-implicit-fallthrough",
        "-Wno-missing-field-initializers",
        "-Wno-missing-braces",
    ]);

    build
        .define("ZMQ_BUILD_TESTS", "OFF")
        .include(vendor.join("include"));

    add_cpp_sources(build, vendor.join("src"), DEFAULT_SOURCES);

    walkdir::WalkDir::new(vendor.join("src"))
        .into_iter()
        .filter(|entry| {
            entry.as_ref().is_ok_and(|dir_entry| {
                dir_entry.path().is_file()
                    && dir_entry
                        .path()
                        .extension()
                        .is_some_and(|dir_path| dir_path.to_string_lossy() == "hpp")
            })
        })
        .for_each(|entry| {
            if let Ok(dir_entry) = entry {
                println!("cargo:rerun-if-changed={}", dir_entry.path().display());
            }
        });

    libraries
        .iter()
        .iter()
        .filter(|(name, _lib)| *name == "gnutls")
        .for_each(|(_name, lib)| {
            add_cpp_sources(build, vendor.join("src"), &["wss_address", "wss_engine"]);
            build.includes(&lib.include_paths);
        });

    add_c_sources(build, vendor.join("external/sha1"), &["sha1.c"]);

    build.define("ZMQ_USE_CV_IMPL_STL11", "1");
    build.define("ZMQ_STATIC", "1");
    build.define("ZMQ_USE_BUILTIN_SHA1", "1");

    build.define("ZMQ_HAVE_WS", "1");

    #[cfg(not(windows))]
    let create_platform_hpp_shim = |build: &mut Build| {
        let out_includes = PathBuf::from(env::var("OUT_DIR").unwrap());

        let mut f = File::create(out_includes.join("platform.hpp")).unwrap();
        f.write_all(b"").unwrap();
        f.sync_all().unwrap();

        build.include(out_includes);
    };

    #[cfg(target_os = "windows")]
    {
        #[cfg(not(target_env = "gnu"))]
        add_c_sources(build, vendor.join("external/wepoll"), &["wepoll.c"]);

        build.define("ZMQ_HAVE_WINDOWS", "1");
        build.define("ZMQ_IOTHREAD_POLLER_USE_EPOLL", "1");
        build.define("ZMQ_POLL_BASED_ON_POLL", "1");
        build.define("_WIN32_WINNT", "0x0600"); // vista
        build.define("ZMQ_HAVE_STRUCT_SOCKADDR_UN", "1");

        println!("cargo::rustc-link-lib=Advapi32");
        println!("cargo::rustc-link-lib=wsock32");
        println!("cargo::rustc-link-lib=ws2_32");
        println!("cargo::rustc-link-lib=Iphlpapi");

        #[cfg(target_env = "msvc")]
        {
            build.include(vendor.join("builds/deprecated-msvc"));
            build.flag("/GL-");

            build.flag("/EHsc");
        }
        #[cfg(not(target_env = "msvc"))]
        {
            create_platform_hpp_shim(build);
            build.define("HAVE_STRNLEN", "1");
        }

        #[cfg(not(target_vendor = "uwp"))]
        if check_ipc_headers().unwrap_or(false) {
            build.define("ZMQ_HAVE_IPC", "1");
        }
    }

    #[cfg(target_os = "linux")]
    {
        create_platform_hpp_shim(build);
        build.define("ZMQ_HAVE_LINUX", "1");
        build.define("ZMQ_IOTHREAD_POLLER_USE_EPOLL", "1");
        build.define("ZMQ_POLL_BASED_ON_POLL", "1");
        build.define("ZMQ_HAVE_IPC", "1");

        build.define("HAVE_STRNLEN", "1");
        build.define("ZMQ_HAVE_UIO", "1");
        build.define("ZMQ_HAVE_STRUCT_SOCKADDR_UN", "1");

        #[cfg(any(target_os = "android", target_env = "musl"))]
        build.define("ZMQ_HAVE_STRLCPY", "1");
    }

    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
    {
        create_platform_hpp_shim(build);
        build.define("ZMQ_IOTHREAD_POLLER_USE_KQUEUE", "1");
        build.define("ZMQ_POLL_BASED_ON_POLL", "1");
        build.define("HAVE_STRNLEN", "1");
        build.define("ZMQ_HAVE_UIO", "1");
        build.define("ZMQ_HAVE_IPC", "1");
        build.define("ZMQ_HAVE_STRUCT_SOCKADDR_UN", "1");
        build.define("ZMQ_HAVE_STRLCPY", "1");
    }

    #[cfg(target_env = "gnu")]
    if check_strlcpy().unwrap_or(false) {
        build.define("ZMQ_HAVE_STRLCPY", "1");
    }

    #[cfg(not(target_env = "msvc"))]
    if check_cxx11().unwrap_or(false) {
        build.std("c++11");
    }

    #[cfg(feature = "draft-api")]
    build.define("ZMQ_BUILD_DRAFT_API", "1");

    check_curve_config(build, &libraries);
    check_gssapi_config(build, &libraries);
    check_pgm_config(build);
    check_norm_config(build);
    check_vmci_config(build)
}

fn check_curve_config(build: &mut Build, libraries: &Dependencies) {
    if cfg!(not(feature = "curve")) {
        return;
    }

    libraries.get_by_name("libsodium").tap_some(|lib| {
        build.define("ZMQ_USE_LIBSODIUM", "1");
        build.define("ZMQ_HAVE_CURVE", "1");

        build.includes(&lib.include_paths);
    });

    #[cfg(target_env = "msvc")]
    let _ = vcpkg::find_package("libsodium").tap_ok(|lib| {
        build.define("ZMQ_USE_LIBSODIUM", "1");
        build.define("ZMQ_HAVE_CURVE", "1");

        build.includes(&lib.include_paths);
    });
}

fn check_gssapi_config(build: &mut Build, libraries: &Dependencies) {
    if cfg!(not(feature = "gssapi")) {
        return;
    }

    libraries.get_by_name("gssapi").tap_some(|lib| {
        build.define("HAVE_LIBGSSAPI_KRB5", "1");
        build.includes(&lib.include_paths);
    });

    #[cfg(target_env = "msvc")]
    {
        let _ = vcpkg::Config::new().find_package("krb5").tap_ok(|lib| {
            build.define("HAVE_LIBGSSAPI_KRB5", "1");
            build.includes(&lib.include_paths);
        });
    }
}

fn check_pgm_config(build: &mut Build) {
    if cfg!(not(feature = "pgm")) {
        return;
    }

    let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    #[cfg(target_env = "msvc")]
    {
        let mut pgm_build = cmake::Config::new("openpgm/openpgm/pgm");

        pgm_build
            .no_build_target(true)
            .out_dir(out_dir.join("openpgm"))
            .profile("Release")
            .build_target("libpgm");

        let pgm_install_dir = pgm_build.build();

        let lib_dir = pgm_install_dir.join("build").join("lib");
        build.define("ZMQ_HAVE_OPENPGM", "1");
        build.include(
            base_dir
                .join("openpgm")
                .join("openpgm")
                .join("pgm")
                .join("include"),
        );

        emit_static_libs_in(&lib_dir);
    }
    #[cfg(not(target_env = "msvc"))]
    {
        let src_dir = base_dir.join("openpgm").join("openpgm").join("pgm");
        let pgm_out_base_dir = out_dir.join("openpgm");
        let pgm_src_dir = pgm_out_base_dir.join("pgm");

        dircpy::copy_dir(&src_dir, &pgm_src_dir).expect("unable to copy pgm sources dir");

        let mut pgm_build = autotools::Config::new(&pgm_src_dir);
        pgm_build
            .reconf("-ivf")
            .disable_shared()
            .enable_static()
            .out_dir(&pgm_out_base_dir);

        let pgm_install_dir = pgm_build.build();

        let lib_dir = pgm_install_dir.join("lib");
        build.define("ZMQ_HAVE_OPENPGM", "1");
        build.include(
            base_dir
                .join("openpgm")
                .join("openpgm")
                .join("pgm")
                .join("include"),
        );

        #[cfg(target_os = "macos")]
        build.define("restrict", "__restrict__");

        emit_static_libs_in(&lib_dir);
    }
}

fn check_norm_config(build: &mut Build) {
    if cfg!(not(feature = "norm")) {
        return;
    }

    let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    let src_dir = base_dir.join("libnorm");
    let norm_out_base_dir = out_dir.join("libnorm");
    let norm_src_dir = norm_out_base_dir.join("libnorm");

    let protolib_src_dir = base_dir.join("protolib");
    let protolib_out_src_dir = norm_src_dir.join("protolib");

    dircpy::copy_dir(&src_dir, &norm_src_dir).expect("unable to copy libnorm sources dir");
    dircpy::copy_dir(&protolib_src_dir, &protolib_out_src_dir)
        .expect("unable to copy protolib sources dir");

    let mut norm_build = cmake::Config::new(&norm_src_dir);

    norm_build
        .pic(true)
        .configure_arg("-Wno-dev")
        .define("NORM_CUSTOM_PROTOLIB_VERSION", "./protolib")
        .out_dir(out_dir.join("libnorm"))
        .profile("Release");

    #[cfg(target_env = "msvc")]
    norm_build.cxxflag("/DWIN32 /_WINDOWS");

    let norm_install_dir = norm_build.build();

    let lib_dir = norm_install_dir.join("lib");
    build.define("ZMQ_HAVE_NORM", "1");
    build.include(norm_install_dir.join("include"));

    emit_static_libs_in(lib_dir);

    #[cfg(target_os = "windows")]
    println!("cargo:rustc-link-lib=user32");
}

fn check_vmci_config(build: &mut Build) -> Result<(), Box<dyn Error>> {
    if cfg!(not(feature = "vmci")) {
        return Ok(());
    }

    let vmci = Path::new(env!("CARGO_MANIFEST_DIR")).join("vmci");

    build.define("ZMQ_HAVE_VMCI", "1");

    build.cpp(false);
    build.include(vmci);

    Ok(())
}

fn build_zmq() -> Result<(), Box<dyn Error>> {
    let vendor = Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor");

    let mut build = Build::new();
    configure(&mut build)?;

    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    let lib_dir = out_dir.join("lib");

    build.out_dir(&lib_dir).cpp(true);

    build.compile("zmq");

    emit_static_libs_in(&lib_dir);

    let source_dir = out_dir.join("source");
    let include_dir = source_dir.join("include");

    dircpy::copy_dir(vendor.join("include"), &include_dir).expect("unable to copy include dir");
    dircpy::copy_dir(vendor.join("src"), source_dir.join("src")).expect("unable to copy src dir");
    dircpy::copy_dir(vendor.join("external"), source_dir.join("external"))
        .expect("unable to copy external dir");

    println!("cargo:include={}", include_dir.display());
    println!("cargo:lib={}", lib_dir.display());
    println!("cargo:out={}", out_dir.display());

    Ok(())
}

fn generate_bindings() -> Result<(), Box<dyn Error>> {
    let vendor_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?).join("vendor");
    let include_dir = vendor_dir.join("include");

    let builder = bindgen::Builder::default()
        .header(include_dir.join("zmq.h").to_string_lossy())
        .size_t_is_usize(true)
        .derive_default(true)
        .derive_eq(true)
        .derive_partialeq(true)
        .derive_debug(true)
        .use_core()
        .allowlist_function("^zmq_.*")
        .allowlist_type("^zmq_.*")
        .allowlist_var("^ZMQ_.*")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));

    #[cfg(feature = "draft-api")]
    let builder = builder.clang_args(vec!["-DZMQ_BUILD_DRAFT_API=1"]);

    let bindings = builder.generate().expect("Unable to generate bindings");

    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    bindings
        .write_to_file(out_dir.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=PROFILE");
    println!("cargo:rerun-if-env-changed=CARGO_CFG_FEATURE");

    build_zmq()?;

    generate_bindings()
}
