use bindgen::CargoCallbacks;
use std::path::Path;
use std::path::PathBuf;
use std::{env, process};

fn get_cargo_target_dir() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
    let skip_triple = std::env::var("TARGET")? == std::env::var("HOST")?;
    let skip_parent_dirs = if skip_triple { 4 } else { 5 };

    let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
    let mut current = out_dir.as_path();
    for _ in 0..skip_parent_dirs {
        current = current.parent().ok_or("not found")?;
    }

    Ok(std::path::PathBuf::from(current))
}

fn main() {
    // Find compiler:
    // 1. GOC
    // 2. /usr/local/go/bin/go
    // 3. system "go"

    let compiler = match env::var("GOC") {
        Ok(c) => c,
        Err(_) => {
            if Path::new("/usr/local/go/bin/go").exists() {
                "/usr/local/go/bin/go".to_string()
            } else {
                "go".to_string()
            }
        }
    };

    println!("using go compiler {}", compiler);

    let c_compiler = cc::Build::new().try_get_compiler().unwrap();

    let compile_config = get_compile_config();

    let out_dir = env::var("OUT_DIR").unwrap();
    let out_path = PathBuf::from(out_dir);
    let out_file = compile_config.lib_filename.clone();
    let out = out_path.join(out_file);

    let mut command = process::Command::new(compiler);
    command.args([
        "build",
        "-buildmode",
        compile_config.link_type.as_str(),
        "-o",
        out.display().to_string().as_str(),
        "main.go",
    ]);
    command.env("CGO_ENABLED", "1");
    command.env("CC", c_compiler.path());
    command.env("GOARCH", compile_config.goarch.clone());
    command.env("GOOS", compile_config.goos.clone());
    println!("running go compile command: {:?}", command);

    let mut child = command.spawn().unwrap();
    let status = child.wait().unwrap();
    println!("{}", status);

    if !status.success() {
        panic!("`{:?}` exited with status code {}", command, status);
    }

    println!("Go compile success");

    println!("cargo:rustc-link-search={}", env::var("OUT_DIR").unwrap());

    if compile_config.link_type == "c-shared" {
        copy_shared_lib(&compile_config);
        println!("cargo:rustc-link-lib=dylib=nebula");
    } else {
        println!("cargo:rustc-link-lib=static=nebula");
    }

    //let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

    //println!("cargo:rustc-link-search={}", out_path.display());
    println!("cargo:rerun-if-changed=go.mod");
    println!("cargo:rerun-if-changed=go.sum");
    println!("cargo:rerun-if-changed=main.go");

    println!("Generating bindings");

    let bindings = bindgen::Builder::default()
        .header(
            out_path
                .join(compile_config.header_filename)
                .display()
                .to_string(),
        )
        .parse_callbacks(Box::new(CargoCallbacks::new()))
        .generate()
        .expect("Error generating CFFI bindings");

    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

fn copy_shared_lib(go_compile_config: &GoCompileConfig) {
    let target_dir = get_cargo_target_dir().unwrap();
    let target_file = target_dir.join(go_compile_config.lib_filename.clone());
    let out_dir = env::var("OUT_DIR").unwrap();
    let out_path = PathBuf::from(out_dir);
    let out_file = go_compile_config.lib_filename.clone();
    let out = out_path.join(out_file);
    std::fs::copy(out, target_file).unwrap();
}

// Go build supported modes.
// Pulled from golang/go/src/internal/platform/supported.go
// c-archive:
// aix, darwin, ios, windows
// linux/386, linux/amd64, linux/arm, linux/armbe, linux/arm64, linux/arm64be, linux/loong64, linux/ppc64le, linux/riscv64, linux/s390x
// freebsd/amd64

// c-shared:
// linux/amd64, linux/arm, linux/arm64, linux/loong64, linux/386, linux/ppc64le, linux/riscv64, linux/s390x
// android/amd64, android/arm, android/arm64, android/386
// freebsd/amd64
// darwin/amd64, darwin/arm64
// windows/amd64, windows/386, windows/arm64

struct GoCompileConfig {
    goarch: String,
    goos: String,
    link_type: String,
    lib_filename: String,
    header_filename: String,
}

fn get_compile_config() -> GoCompileConfig {
    let goarch = goarch();
    let goos = goos();
    let platform_value = format!("{}/{}", goos, goarch);
    let (preferred_link_type, lib_filename, header_filename) =
        match (goos.as_str(), goarch.as_str()) {
            ("darwin", _) => ("c-archive", "libnebula.a", "libnebula.h"),
            ("windows", _) => ("c-archive", "libnebula.a", "libnebula.h"),
            ("linux", "386")
            | ("linux", "amd64")
            | ("linux", "arm")
            | ("linux", "armbe")
            | ("linux", "arm64")
            | ("linux", "arm64be")
            | ("linux", "loong64")
            | ("linux", "ppc64le")
            | ("linux", "riscv64")
            | ("linux", "s390x") => ("c-archive", "libnebula.a", "libnebula.h"),
            ("freebsd", "amd64") => ("c-archive", "libnebula.a", "libnebula.h"),
            _ => panic!(
                "unsupported platform {} / {}",
                env::var("TARGET").unwrap(),
                platform_value
            ),
        };

    GoCompileConfig {
        goarch,
        goos,
        link_type: preferred_link_type.to_string(),
        lib_filename: lib_filename.to_string(),
        header_filename: header_filename.to_string(),
    }
}

fn goarch() -> String {
    match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() {
        "x86" => "386",
        "x86_64" => "amd64",
        "mips" => "mips",
        "powerpc" => "ppc",
        "powerpc64" => "ppc64",
        "arm" => "arm",
        "aarch64" => "arm64",
        arch => panic!("unsupported architecture {arch}"),
    }
    .to_string()
}
fn goos() -> String {
    match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() {
        "windows" => "windows",
        "macos" => "darwin",
        "ios" => "darwin",
        "linux" => "linux",
        "android" => "android",
        "freebsd" => "freebsd",
        "dragonfly" => "dragonfly",
        "openbsd" => "openbsd",
        "netbsd" => "netbsd",
        os => panic!("unsupported operating system {os}"),
    }
    .to_string()
}