use std::{env, error::Error, io};

#[cfg(feature = "bindgen")]
use std::{fs, path::PathBuf};

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

    if let Ok(lib_dir) = env::var("FREESWITCH_LIB_DIR") {
        println!("cargo:rustc-link-search=native={lib_dir}");
        println!("cargo:rustc-link-lib=freeswitch");
    } else if env::var_os("FREESWITCH_NO_PKG_CONFIG").is_none()
        && let Err(error) = pkg_config::Config::new().probe("freeswitch")
    {
        println!("cargo:warning=pkg-config could not find freeswitch: {error}");
    }

    generate_bindings()?;
    Ok(())
}

#[cfg(feature = "bindgen")]
fn generate_bindings() -> Result<(), Box<dyn Error>> {
    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    let wrapper = out_dir.join("wrapper.h");
    fs::write(&wrapper, "#include <switch.h>\n")?;
    let bundled_config_dir = out_dir.join("bundled-include");

    let mut builder = bindgen::Builder::default()
        .header(wrapper.display().to_string())
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .allowlist_type("switch_.*")
        .allowlist_function("switch_.*")
        .allowlist_var("SWITCH_.*")
        .rustified_enum("switch_status_t")
        .rustified_enum("switch_module_interface_name_t")
        .rustified_enum("switch_event_types_t")
        .rustified_enum("switch_stack_t")
        .layout_tests(false)
        .generate_comments(false)
        .derive_default(true);

    if cfg!(feature = "bundled") {
        write_bundled_config_header(&bundled_config_dir)?;
        builder = builder.clang_arg(format!("-I{}", bundled_config_dir.display()));
        builder = add_include_dirs(builder, bundled_include_dirs()?);
    } else if let Ok(include_dir) = env::var("FREESWITCH_INCLUDE_DIR") {
        let include_dir = PathBuf::from(include_dir);
        let config_header = include_dir.join("switch_am_config.h");
        if !config_header.exists() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!(
                    "FREESWITCH_INCLUDE_DIR must point at configured FreeSWITCH headers; missing {}",
                    config_header.display()
                ),
            )
            .into());
        }
        builder = builder.clang_arg(format!("-I{}", include_dir.display()));
    } else {
        builder = add_include_dirs(builder, local_workspace_include_dirs()?);
    }

    let bindings = builder.generate()?;
    bindings.write_to_file(out_dir.join("bindings.rs"))?;
    Ok(())
}

#[cfg(feature = "bindgen")]
fn add_include_dirs(mut builder: bindgen::Builder, include_dirs: Vec<PathBuf>) -> bindgen::Builder {
    for path in include_dirs {
        if path.exists() {
            println!("cargo:rerun-if-changed={}", path.display());
            builder = builder.clang_arg(format!("-I{}", path.display()));
        }
    }

    builder
}

#[cfg(all(feature = "bindgen", feature = "bundled"))]
fn bundled_include_dirs() -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let include_dirs = fswtch_src::include_dirs();
    let switch_header = fswtch_src::root().join("src/include/switch.h");
    if !switch_header.exists() {
        return Err(io::Error::new(
            io::ErrorKind::NotFound,
            format!(
                "the bundled feature requires fswtch-src to include FreeSWITCH headers; missing {}",
                switch_header.display()
            ),
        )
        .into());
    }

    Ok(include_dirs)
}

#[cfg(all(feature = "bindgen", not(feature = "bundled")))]
fn bundled_include_dirs() -> Result<Vec<PathBuf>, Box<dyn Error>> {
    Ok(Vec::new())
}

#[cfg(feature = "bindgen")]
fn local_workspace_include_dirs() -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
    let repo_root = manifest_dir
        .parent()
        .and_then(|p| p.parent())
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidInput,
                "crates/fswtch-sys must be nested under the workspace",
            )
        })?;

    Ok([
        "freeswitch/src/include",
        "freeswitch/libs/apr/include",
        "freeswitch/libs/apr-util/include",
        "freeswitch/libs/libteletone/src",
        "freeswitch/libs/sqlite",
        "freeswitch/libs/pcre2/src",
    ]
    .into_iter()
    .map(|include| repo_root.join(include))
    .collect())
}

#[cfg(feature = "bindgen")]
fn write_bundled_config_header(include_dir: &PathBuf) -> Result<(), Box<dyn Error>> {
    fs::create_dir_all(include_dir)?;
    fs::write(
        include_dir.join("switch_am_config.h"),
        r#"#ifndef SWITCH_AM_CONFIG_H
#define SWITCH_AM_CONFIG_H

#include <stdint.h>
#include <stddef.h>
#include <inttypes.h>
#include <sys/types.h>

#define SWITCH_INT_16 short
#define SWITCH_INT_32 int
#define SWITCH_INT_64 long
#define SWITCH_SIZE_T size_t
#define SWITCH_SSIZE_T ssize_t

#define SWITCH_SIZEOF_VOIDP __SIZEOF_POINTER__
#define SWITCH_PREFIX_DIR "/usr/local/freeswitch"

#define SWITCH_SIZE_T_FMT "zu"
#define SWITCH_SSIZE_T_FMT "zd"
#define SWITCH_INT64_T_FMT PRId64
#define SWITCH_UINT64_T_FMT PRIu64

#endif
"#,
    )?;
    Ok(())
}

#[cfg(not(feature = "bindgen"))]
fn generate_bindings() -> Result<(), Box<dyn Error>> {
    Ok(())
}
