extern crate bindgen;

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

use std::fmt::Debug;

#[cfg(not(feature = "dynamic"))]
use bindgen::callbacks::ItemInfo;

#[cfg(not(feature = "dynamic"))]
use std::collections::HashMap;
use std::fmt::{Display, Formatter};

fn find_static_lib(dir: &Path, base_name: &str, target_is_windows: bool) -> Option<PathBuf> {
    if !dir.is_dir() {
        return None;
    }
    let prefixes = ["", "lib"];
    let extensions = if target_is_windows {
        &[".lib", ".a"][..]
    } else {
        &[".a", ".lib"][..]
    };
    for prefix in prefixes {
        for ext in extensions {
            let name = format!("{}{}{}", prefix, base_name, ext);
            let path = dir.join(&name);
            if path.exists() {
                return Some(path);
            }
        }
    }
    None
}

fn lib_name_from_path(
    lib_path: &Path,
    target_is_windows: bool,
) -> Result<String, Box<dyn std::error::Error>> {
    let file_name = lib_path
        .file_stem()
        .ok_or(Error::msg("cannot get file stem"))?
        .to_str()
        .ok_or(Error::msg("cannot into string"))?;
    // On Unix, `cargo::rustc-link-lib=static=NAME` makes the linker search for libNAME.a,
    // so strip any leading "lib" prefix to avoid liblibNAME.a.
    // On MSVC, the linker searches for NAME.lib directly, so keep the full name
    // (e.g. libCZIAPIStatic.lib needs link-lib=static=libCZIAPIStatic).
    if target_is_windows {
        Ok(file_name.to_string())
    } else {
        Ok(file_name.strip_prefix("lib").unwrap_or(file_name).to_string())
    }
}

struct Error(String);

impl Debug for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl std::error::Error for Error {}

impl Error {
    fn msg<S: ToString>(msg: S) -> Box<dyn std::error::Error> {
        Box::new(Error(msg.to_string()))
    }
}

fn fix_xwin_lib_case() -> Result<(), Box<dyn std::error::Error>> {
    let toolchain_key = format!(
        "CMAKE_TOOLCHAIN_FILE_{}",
        env::var("TARGET")?.replace('-', "_")
    );
    let toolchain_file = env::var(&toolchain_key)
        .or_else(|_| env::var("TARGET_CMAKE_TOOLCHAIN_FILE"))
        .or_else(|_| env::var("CMAKE_TOOLCHAIN_FILE"))?;

    let toolchain_path = PathBuf::from(&toolchain_file);
    let cargo_xwin_base = toolchain_path
        .parent()
        .and_then(|p| p.parent())
        .and_then(|p| p.parent())
        .ok_or(Error::msg("cannot determine cargo-xwin base dir"))?;

    let xwin_sdk_lib = cargo_xwin_base.join("xwin/sdk/lib/um/x86_64");
    if !xwin_sdk_lib.exists() {
        return Ok(());
    }

    #[cfg(unix)]
    {
        use std::os::unix::fs::symlink;
        let fixes = [
            ("Windowscodecs.lib", "WINDOWSCODECS.lib"),
            ("Windowscodecs.lib", "windowscodecs.lib"),
        ];
        for (link_name, target_name) in &fixes {
            let target = xwin_sdk_lib.join(target_name);
            let link = xwin_sdk_lib.join(link_name);
            if target.exists() && !link.exists() {
                let _ = symlink(&target, &link);
            }
        }
    }

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    if env::var("DOCS_RS").is_err() {
        let host = env::var("HOST")?;
        let target = env::var("TARGET")?;
        let is_cross = host != target;
        let is_windows = target.contains("windows");
        let _is_macos = target.contains("apple");

        let libczi_dir = PathBuf::from("libczi");
        let libczi_src = libczi_dir.join("Src/libCZI");
        let libcziapi_inc = libczi_dir.join("Src/libCZIAPI/inc");
        let libcziapi_src = libczi_dir.join("Src/libCZIAPI/src");
        let libcziapi_h = libcziapi_inc.join("libCZIApi.h");

        let mut cmake_config = cmake::Config::new(&libczi_dir);
        cmake_config
            .cxxflag("-fms-extensions")
            .define(
                "ADDITIONAL_LIBS_REQUIRED_FOR_ATOMIC",
                "",
            )
            .define("LIBCZI_BUILD_UNITTESTS", "OFF")
            .define("LIBCZI_BUILD_CZICMD", "OFF")
            .define("LIBCZI_BUILD_DYNLIB", "OFF")
            .define("LIBCZI_BUILD_PREFER_EXTERNALPACKAGE_EIGEN3", "OFF")
            .define("LIBCZI_BUILD_PREFER_EXTERNALPACKAGE_ZSTD", "OFF")
            .define("LIBCZI_BUILD_CURL_BASED_STREAM", "OFF")
            .define("LIBCZI_BUILD_PREFER_EXTERNALPACKAGE_LIBCURL", "OFF")
            .define("LIBCZI_BUILD_AZURESDK_BASED_STREAM", "OFF")
            .define("LIBCZI_BUILD_PREFER_EXTERNALPACKAGE_RAPIDJSON", "OFF")
            .define("LIBCZI_BUILD_LIBCZIAPI", "ON");

        if is_cross {
            if is_windows {
                cmake_config
                    .define("CMAKE_SYSTEM_NAME", "Windows")
                    .define("CRASH_ON_UNALIGNED_ACCESS", "FALSE");

                let has_toolchain_file = env::var("TARGET_CMAKE_TOOLCHAIN_FILE").is_ok()
                    || env::var(format!(
                        "CMAKE_TOOLCHAIN_FILE_{}",
                        target.replace('-', "_")
                    ))
                    .is_ok()
                    || env::var("CMAKE_TOOLCHAIN_FILE").is_ok();

                if !has_toolchain_file {
                    cmake_config
                        .define("CMAKE_C_COMPILER", "clang")
                        .define("CMAKE_CXX_COMPILER", "clang++")
                        .define("CMAKE_RC_COMPILER", "llvm-rc");
                }

                fix_xwin_lib_case()?;
            } else if target.contains("apple") {
                cmake_config
                    .define("CMAKE_SYSTEM_NAME", "Darwin")
                    .define("CRASH_ON_UNALIGNED_ACCESS", "OFF");
                if target.contains("aarch64") {
                    cmake_config.define("NEON_INTRINSICS_CAN_BE_USED", "ON");
                }
            }
        }

        if is_cross && is_windows {
            cmake_config.build_target("libCZIAPIStatic");
        }

        let dst = cmake_config.build();

        #[cfg(not(feature = "dynamic"))]
        let bindings = {
            let libcziapi_search_dirs = [
                dst.join("build/Src/libCZIAPI"),
                dst.join("Src/libCZIAPI"),
                dst.join("lib"),
                dst.join("lib64"),
            ];
            let libcziapi_static = libcziapi_search_dirs
                .iter()
                .find_map(|dir| find_static_lib(dir, "libCZIAPIStatic", is_windows))
                .ok_or(Error::msg("cannot find libCZIAPIStatic library"))?;
            bindgen::Builder::default().parse_callbacks(Box::new(DeMangler::new(
                libcziapi_static,
                is_cross && is_windows,
            )?))
        };

        #[cfg(feature = "dynamic")]
        let bindings = bindgen::Builder::default();

        let bindings = bindings
            .merge_extern_blocks(true)
            .clang_args([
                "-fms-extensions",
                "-x",
                "c++",
                "-std=c++14",
                "-I",
                libcziapi_inc
                    .to_str()
                    .ok_or(Error::msg("cannot into string"))?,
                "-I",
                libcziapi_src
                    .to_str()
                    .ok_or(Error::msg("cannot into string"))?,
                "-I",
                libczi_src
                    .to_str()
                    .ok_or(Error::msg("cannot into string"))?,
            ])
            .header(
                libcziapi_h
                    .to_str()
                    .ok_or(Error::msg("cannot into string"))?,
            )
            .generate()?;

        bindings.write_to_file(dst.join("lib_czi_api.rs"))?;

        #[cfg(not(feature = "dynamic"))]
        {
            let libcziapi_dir = dst.join("build/Src/libCZIAPI");
            let libcziapi_dir2 = dst.join("Src/libCZIAPI");
            let libczi_dir = dst.join("build/Src/libCZI");
            let libczi_dir2 = dst.join("Src/libCZI");

            let libcziapi_lib = find_static_lib(&libcziapi_dir, "libCZIAPIStatic", is_windows)
                .or_else(|| find_static_lib(&libcziapi_dir2, "libCZIAPIStatic", is_windows))
                .ok_or(Error::msg("cannot find libCZIAPIStatic for linking"))?;
            let libcziapi_name = lib_name_from_path(&libcziapi_lib, is_windows)?;

            let profile = env::var("PROFILE")?;
            let libczi_base = match profile.as_str() {
                "debug" => "libCZIStaticd",
                "release" => "libCZIStatic",
                _ => return Err(Error::msg(format!("unsupported profile: {}", profile))),
            };
            let libczi_lib = find_static_lib(&libczi_dir, libczi_base, is_windows)
                .or_else(|| find_static_lib(&libczi_dir2, libczi_base, is_windows))
                .ok_or(Error::msg(format!(
                    "cannot find {} for linking",
                    libczi_base
                )))?;
            let libczi_name = lib_name_from_path(&libczi_lib, is_windows)?;

            if libcziapi_dir.exists() {
                println!(
                    "cargo::rustc-link-search=native={}",
                    libcziapi_dir.display()
                );
            }
            if libcziapi_dir2.exists() {
                println!(
                    "cargo::rustc-link-search=native={}",
                    libcziapi_dir2.display()
                );
            }
            println!("cargo::rustc-link-lib=static={}", libcziapi_name);

            if libczi_dir.exists() {
                println!(
                    "cargo::rustc-link-search=native={}",
                    libczi_dir.display()
                );
            }
            if libczi_dir2.exists() {
                println!(
                    "cargo::rustc-link-search=native={}",
                    libczi_dir2.display()
                );
            }
            println!("cargo::rustc-link-lib=static={}", libczi_name);

            println!(
                "cargo::rustc-link-search=native={}",
                dst.join("lib").display()
            );
            println!(
                "cargo::rustc-link-search=native={}",
                dst.join("lib64").display()
            );
            if is_windows && is_cross {
                let zstd_dir = dst.join("build/_deps/zstd-build/lib");
                let zstd_dir2 = dst.join("_deps/zstd-build/lib");
                if zstd_dir.exists() {
                    println!(
                        "cargo::rustc-link-search=native={}",
                        zstd_dir.display()
                    );
                }
                if zstd_dir2.exists() {
                    println!(
                        "cargo::rustc-link-search=native={}",
                        zstd_dir2.display()
                    );
                }
                let zstd_name = find_static_lib(&zstd_dir, "zstd_static", is_windows)
                    .or_else(|| find_static_lib(&zstd_dir2, "zstd_static", is_windows))
                    .map(|p| lib_name_from_path(&p, is_windows).unwrap_or_else(|_| "zstd_static".to_string()))
                    .unwrap_or_else(|| "zstd_static".to_string());
                println!("cargo::rustc-link-lib=static={}", zstd_name);
            } else {
                println!("cargo::rustc-link-lib=static=zstd");
            }

            if is_windows {
                println!("cargo::rustc-link-lib=kernel32");
                println!("cargo::rustc-link-lib=advapi32");
                println!("cargo::rustc-link-lib=bcrypt");
                println!("cargo::rustc-link-lib=ntdll");
                println!("cargo::rustc-link-lib=user32");
                println!("cargo::rustc-link-lib=ole32");
                println!("cargo::rustc-link-lib=shell32");
                println!("cargo::rustc-link-lib=Windowscodecs");
            }
        }

        #[cfg(feature = "dynamic")]
        {
            println!(
                "cargo::rustc-link-search=native={}",
                dst.join("build/Src/libCZIAPI").display()
            );
            println!("cargo::rustc-link-lib=libCZIAPI");
        }
    }
    println!("cargo::rerun-if-changed=build.rs");
    Ok(())
}

#[cfg(not(feature = "dynamic"))]
#[derive(Debug)]
struct DeMangler {
    map: HashMap<String, String>,
}

#[cfg(not(feature = "dynamic"))]
impl DeMangler {
    fn new(a_file: PathBuf, use_llvm_nm: bool) -> Result<Self, Box<dyn std::error::Error>> {
        let nm_cmd = if use_llvm_nm { "llvm-nm" } else { "nm" };
        let cmd = std::process::Command::new(nm_cmd)
            .arg("--defined-only")
            .arg(&a_file)
            .output()?;
        let mut map = HashMap::new();
        for line in std::str::from_utf8(&cmd.stdout)?.lines() {
            let line = line.trim();
            let mangled = if let Some(sym) = line.split_whitespace().last() {
                sym
            } else {
                continue;
            };
            let plain_name = Self::demangle(mangled);
            if let Some(name) = plain_name {
                if name.starts_with("libCZI_") {
                    Self::insert_mapping(&mut map, &name, mangled, &a_file)?;
                }
            }
        }
        Ok(Self { map })
    }

    fn demangle(symbol: &str) -> Option<String> {
        // Only demangle actual function symbols, not catch/dtor/cppxdata metadata
        if let Some(rest) = symbol.strip_prefix('?') {
            // MSVC: real functions start with "libCZI_", not "catch$", "dtor$", etc.
            if !rest.starts_with("libCZI_") {
                return None;
            }
            let flags = msvc_demangler::DemangleFlags::llvm();
            let demangled = msvc_demangler::demangle(symbol, flags).ok()?;
            Self::extract_msvc_name(&demangled)
        } else if symbol.starts_with("_Z") {
            // Itanium: demangle and extract the function name
            let sym = cpp_demangle::Symbol::new(symbol.as_bytes()).ok()?;
            let demangled = sym.demangle().ok()?;
            // e.g. "libCZI_Foo(int, bool)" -> "libCZI_Foo"
            // e.g. "ns::libCZI_Foo(int)" -> "libCZI_Foo"
            demangled.split('(').next().and_then(|s| {
                let s = s.trim();
                if let Some(name) = s.rsplit("::").next() {
                    Some(name.to_string())
                } else {
                    Some(s.to_string())
                }
            })
        } else {
            None
        }
    }

    fn extract_msvc_name(demangled: &str) -> Option<String> {
        // Find the identifier that looks like a function name.
        // MSVC demangled output: "return_type calling_conv name(params)"
        for word in demangled.split_whitespace() {
            if let Some(rest) = word.strip_prefix("libCZI_") {
                let name = format!("libCZI_{}", rest.split('(').next().unwrap_or(rest));
                return Some(name);
            }
        }
        None
    }

    fn insert_mapping(
        map: &mut HashMap<String, String>,
        demangled: &str,
        mangled: &str,
        a_file: &Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if let Some(existing_mangled) = map.get(demangled) {
            if existing_mangled != mangled {
                return Err(Error::msg(format!(
                    "conflicting mangled symbols for {} in {}: {}, {}",
                    demangled,
                    a_file.to_string_lossy(),
                    existing_mangled,
                    mangled
                )));
            }
        } else {
            map.insert(demangled.to_string(), mangled.to_string());
        }
        Ok(())
    }
}

#[cfg(not(feature = "dynamic"))]
impl bindgen::callbacks::ParseCallbacks for DeMangler {
    fn generated_link_name_override(&self, item_info: ItemInfo<'_>) -> Option<String> {
        self.map.get(item_info.name).cloned()
    }
}
