use std::env;
use std::fs;
use std::path::Path;

fn main() {
    println!(
        "Running build.rs with working directory {:?}",
        std::env::current_dir()
    );
    println!("cargo:rerun-if-env-changed=R_CARGO_RUN_COUNTER");
    let src_path = Path::new("roxido.txt");
    let snippet = match env::var("R_CARGO_RUN_COUNTER") {
        Ok(x) if x == "1" => None,
        Ok(x) if x == "2" => make_registration_code(src_path),
        Ok(_) => {
            println!("Environment variable 'R_CARGO_RUN_COUNTER' has an unexpected value.");
            None
        }
        Err(_) => {
            println!("Environment variable 'R_CARGO_RUN_COUNTER' is not set.");
            None
        }
    };
    let out_dir = env::var_os("OUT_DIR").expect(
        "Environment variable 'OUT_DIR' is not set.  Are you running as part of 'cargo build'?",
    );
    let dest_path = Path::new(&out_dir).join("registration.rs");
    let snippet = snippet.unwrap_or_else(|| {
        let _ = fs::remove_file(src_path);
        String::new()
    });
    fs::write(dest_path, snippet).expect("Could not write file: {dest_path}");
}

fn make_registration_code(src_path: &Path) -> Option<String> {
    match env::var("R_PACKAGE_NAME") {
        Err(_) => None,
        Ok(package_name) => match fs::read_to_string(src_path) {
            Err(_) => None,
            Ok(functions_info) => {
                let mut buffer = String::new();
                let mut functions_info: Vec<_> = functions_info.lines().collect();
                functions_info.sort_unstable();
                functions_info.dedup();
                buffer.clear();
                buffer.push_str("# Automatically regenerated. Do not edit.\n\n");
                for &line in functions_info.iter() {
                    let line_for_r = line.replace(':', "_");
                    buffer.push_str("# .Call(.");
                    buffer.push_str(&line_for_r);
                    buffer.push_str(")\n");
                }
                // Note: salso emits a `_PACKAGE` roxygen block here.
                // We skip it because R/mx.crypto-package.R already owns
                // the package-level docs, and NAMESPACE has useDynLib
                // statically. Two _PACKAGE blocks confuse tinyrox.
                let _ = fs::write("../../R/roxido.R", &buffer);
                let mut snippet = String::new();
                // R substitutes '_' for '.' in init symbol names, so a
                // package called "mx.crypto" looks for R_init_mx_crypto.
                let init_symbol = package_name.replace('.', "_");
                snippet.push_str(&format!(
                    r#"use roxido::*;

#[unsafe(no_mangle)]
extern "C" fn R_init_{}_rust(info: *mut rbindings::DllInfo) {{
    let mut call_routines = Vec::with_capacity({});
    let mut _names: Vec<std::ffi::CString> = Vec::with_capacity({});"#,
                    init_symbol,
                    functions_info.len(),
                    functions_info.len()
                ));
                for line in functions_info {
                    let tidbits: Vec<_> = line.split_whitespace().collect();
                    let func_name_with_comma = tidbits[0];
                    let (func_name, should_be_comma) =
                        func_name_with_comma.split_at(func_name_with_comma.len() - 1);
                    let func_name = if should_be_comma.is_empty() || should_be_comma != "," {
                        func_name_with_comma
                    } else {
                        func_name
                    };
                    let func_name_for_r = func_name.replace(':', "_");
                    let n_args = tidbits.len() - 1;
                    snippet.push_str(&format!(
                        r#"
    _names.push(std::ffi::CString::new(".{}").unwrap());
    call_routines.push(rbindings::R_CallMethodDef {{
        name: _names.last().unwrap().as_ptr(),
        fun: unsafe {{ std::mem::transmute(crate::{} as *const u8) }},
        numArgs: {},
    }});"#,
                        func_name_for_r, func_name, n_args
                    ));
                }
                snippet.push_str(
                    r#"
    call_routines.push(rbindings::R_CallMethodDef {
        name: std::ptr::null(),
        fun: None,
        numArgs: 0,
    });
    unsafe {
        rbindings::R_registerRoutines(
            info,
            std::ptr::null(),
            call_routines.as_ptr(),
            std::ptr::null(),
            std::ptr::null(),
        );
        rbindings::R_useDynamicSymbols(info, 0);
        rbindings::R_forceSymbols(info, 1);
    }
    roxido::__private_set_custom_panic_hook();
}"#,
                );
                Some(snippet)
            }
        },
    }
}
