//! Build system for llama-cpp-bindings-sys FFI bindings to llama.cpp.

mod android_ndk;
mod bindgen_config;
mod cmake_config;
mod cpp_wrapper;
mod cpp_wrapper_mtmd;
mod library_asset_extraction;
mod library_linking;
mod library_name_extraction;
mod rebuild_tracking;
mod shared_libs;
mod target_os;

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

use android_ndk::AndroidNdk;
use target_os::TargetOs;

#[macro_export]
macro_rules! debug_log {
    ($($arg:tt)*) => {
        if std::env::var("BUILD_DEBUG").is_ok() {
            println!("cargo:warning=[DEBUG] {}", format!($($arg)*));
        }
    };
}

/// Shared state passed between build phases.
#[derive(Debug)]
pub struct BuildContext {
    pub out_dir: PathBuf,
    pub target_dir: PathBuf,
    pub llama_src: PathBuf,
    pub target_os: TargetOs,
    pub target_triple: String,
    pub build_shared_libs: bool,
    pub profile: String,
    pub static_crt: bool,
    pub android_ndk: Option<AndroidNdk>,
}

impl BuildContext {
    fn detect() -> Self {
        let target_triple =
            env::var("TARGET").expect("TARGET env var is required in build scripts");
        let target_os = TargetOs::from_target_triple(&target_triple)
            .unwrap_or_else(|error| panic!("Failed to parse target OS: {error}"));
        let out_dir = PathBuf::from(
            env::var("OUT_DIR").expect("OUT_DIR env var is required in build scripts"),
        );
        let target_dir = cargo_target_dir(&out_dir);
        let manifest_dir = env::var("CARGO_MANIFEST_DIR")
            .expect("CARGO_MANIFEST_DIR env var is required in build scripts");
        let llama_src = Path::new(&manifest_dir).join("llama.cpp");

        let build_shared_libs = env::var("LLAMA_BUILD_SHARED_LIBS")
            .map_or_else(|_| cfg!(feature = "dynamic-link"), |value| value == "1");

        let profile = env::var("LLAMA_LIB_PROFILE").unwrap_or_else(|_| "Release".to_string());

        let static_crt = env::var("LLAMA_STATIC_CRT")
            .map(|value| value == "1")
            .unwrap_or(false);

        let android_ndk = if target_os.is_android() {
            Some(
                AndroidNdk::detect(&target_triple)
                    .unwrap_or_else(|error| panic!("Android NDK detection failed: {error}")),
            )
        } else {
            None
        };

        debug_log!("TARGET: {}", target_triple);
        debug_log!("CARGO_MANIFEST_DIR: {}", manifest_dir);
        debug_log!("TARGET_DIR: {}", target_dir.display());
        debug_log!("OUT_DIR: {}", out_dir.display());
        debug_log!("BUILD_SHARED: {}", build_shared_libs);

        Self {
            out_dir,
            target_dir,
            llama_src,
            target_os,
            target_triple,
            build_shared_libs,
            profile,
            static_crt,
            android_ndk,
        }
    }
}

fn cargo_target_dir(out_dir: &Path) -> PathBuf {
    out_dir
        .ancestors()
        .nth(3)
        .expect("OUT_DIR is not deep enough to determine target directory")
        .to_path_buf()
}

fn set_cmake_parallelism() {
    if let Ok(parallelism) = std::thread::available_parallelism() {
        // SAFETY: build scripts are single-threaded, so modifying env is safe.
        unsafe {
            env::set_var("CMAKE_BUILD_PARALLEL_LEVEL", parallelism.get().to_string());
        }
    }
}

/// Main entry point for the llama.cpp build system.
///
/// Call this from `build.rs` in `llama-cpp-bindings-sys`.
pub fn build() {
    let context = BuildContext::detect();

    rebuild_tracking::register_rebuild_triggers(&context.llama_src);

    set_cmake_parallelism();

    bindgen_config::generate_bindings(
        &context.llama_src,
        &context.out_dir,
        &context.target_os,
        &context.target_triple,
        context.android_ndk.as_ref(),
        cfg!(feature = "mtmd"),
    );

    cpp_wrapper::compile_cpp_wrappers(&context.llama_src, &context.target_os);

    let build_dir = cmake_config::configure_and_build(
        &context.llama_src,
        &context.target_os,
        &context.target_triple,
        context.build_shared_libs,
        &context.profile,
        context.static_crt,
        context.android_ndk.as_ref(),
    );

    if cfg!(feature = "mtmd") {
        cpp_wrapper_mtmd::compile_mtmd(&context.llama_src, &context.target_os);
    }

    library_linking::link_libraries(
        &context.out_dir,
        &build_dir,
        &context.target_os,
        &context.target_triple,
        context.build_shared_libs,
        &context.profile,
    );

    if context.build_shared_libs {
        shared_libs::copy_shared_libraries(&context.out_dir, &context.target_dir);
    }
}
