//! Stub object generation for unresolved imports — LLVM port.
//!
//! This is the LLVM equivalent of `crates/perry-codegen/src/stubs.rs`.
//! It produces a `.o` containing dummy globals/functions for symbols that
//! the linker would otherwise fail on (missing imports from native modules
//! that the user doesn't actually use, identity pass-throughs for runtime
//! helpers absent in standalone mode, etc.).
//!
//! The strategy: emit a tiny LLVM IR text snippet and feed it to the
//! existing `compile_ll_to_object` shellout. No special-case in the rest
//! of the pipeline — the result is a normal object file.

use anyhow::Result;

use crate::linker::compile_ll_to_object;
use crate::nanbox::TAG_UNDEFINED;

/// Generate a stub object file for missing symbols from unresolved imports.
///
/// Four kinds of stubs are produced:
///
/// 1. **Data symbols** — exported `i64` globals initialized to NaN-boxed
///    `TAG_UNDEFINED`. Used when an `extern` data slot is referenced but
///    the providing module isn't linked.
/// 2. **Function symbols** — `double()` functions that return NaN-boxed
///    `TAG_UNDEFINED`. Used for nullary helpers in standalone mode.
/// 3. **Identity functions** — `double(double)` functions that pass their
///    argument through unchanged. Used for `js_await_any_promise` and
///    similar pass-through points where the V8 runtime is not present.
/// 4. **Closure-wrapper symbols** — `double(i64, double, double, double,
///    double, double, double)` functions returning NaN-boxed `TAG_UNDEFINED`.
///    Match the closure-call ABI emitted by `expr.rs:~11783` for cross-
///    module function-VALUE references (`__perry_wrap_perry_fn_<src>__<name>`).
///    Used when a non-entry module fails to compile under `--allow-unimplemented`
///    so the link still succeeds; consumers that read the import as a closure
///    handle observe a singleton wrapping the undefined-return stub.
///
/// `target` is a Perry short target name (`macos`, `ios`, `android`,
/// `linux`, `windows`, …) and is forwarded to clang via `-target` so the
/// emitted object matches the rest of the link.
pub fn generate_stub_object(
    missing_data_symbols: &[String],
    missing_func_symbols: &[String],
    identity_func_symbols: &[String],
    target: Option<&str>,
) -> Result<Vec<u8>> {
    generate_stub_object_full(
        missing_data_symbols,
        missing_func_symbols,
        identity_func_symbols,
        &[],
        target,
    )
}

/// Same as [`generate_stub_object`], plus a fourth bucket for closure-
/// wrapper symbols. Split out so the existing 3-arg callers don't break.
pub fn generate_stub_object_full(
    missing_data_symbols: &[String],
    missing_func_symbols: &[String],
    identity_func_symbols: &[String],
    wrapper_func_symbols: &[String],
    target: Option<&str>,
) -> Result<Vec<u8>> {
    let mut ll = String::new();

    // Module header. We do not emit a target triple here — clang infers it
    // from `-target` (set below) and from the host otherwise. Leaving it
    // unspecified avoids a "warning: overriding the module target triple"
    // when the caller passes a non-host target.
    ll.push_str("; Perry stub object — generated by perry-codegen::stubs\n");
    ll.push_str("; All stubs use NaN-boxed TAG_UNDEFINED as their value.\n\n");

    // LLVM IR double constants are hex bit patterns: `0x` followed by 16
    // hex digits, interpreted as the f64's IEEE 754 bit pattern. NaN-boxed
    // undefined is u64 0x7FFC000000000001.
    let undef_hex = format!("0x{:016X}", TAG_UNDEFINED);

    // 1. Data symbols — i64 globals (the runtime treats the slot as a raw
    //    bit pattern and re-interprets via NaN-box accessors).
    for name in missing_data_symbols {
        // global i64 with TAG_UNDEFINED bit pattern
        ll.push_str(&format!(
            "@{} = global i64 {}, align 8\n",
            name, TAG_UNDEFINED as i64
        ));
    }
    if !missing_data_symbols.is_empty() {
        ll.push('\n');
    }

    // 2. Nullary function stubs — `double name()` returning TAG_UNDEFINED.
    for name in missing_func_symbols {
        ll.push_str(&format!(
            "define double @{}() {{\n  ret double {}\n}}\n\n",
            name, undef_hex
        ));
    }

    // 3. Identity functions — `double name(double %0)` returning %0 as-is.
    for name in identity_func_symbols {
        ll.push_str(&format!(
            "define double @{}(double %0) {{\n  ret double %0\n}}\n\n",
            name
        ));
    }

    // 4. Closure-wrapper stubs — match the closure-call ABI emitted at
    //    `crates/perry-codegen/src/expr.rs:~11783` for cross-module
    //    function-VALUE references: `double name(i64 closure, double a0,
    //    double a1, double a2, double a3, double a4)`. Return NaN-boxed
    //    TAG_UNDEFINED. The 5-double arg list covers the max-arity 5
    //    that `js_closure_call0..5` dispatches through; signature shape
    //    matches the no-op variable/class branch at codegen.rs:~2750.
    //
    //    Used by the failed-module stub block at
    //    `crates/perry/src/commands/compile.rs:~5518` so consumers that
    //    reference `__perry_wrap_perry_fn_<src>__<name>` for a module
    //    whose codegen failed under `--allow-unimplemented` still link.
    //    Without this the link error pre-#903 was masked by the
    //    same-file-default-import collision pointing the consumer at
    //    a SIBLING module's wrapper (e.g. uuid v5.js's `import sha1
    //    from './sha1.js'` plus `import v35 from './v35.js'` both
    //    resolved to v35.js's wrapper); post-#903 the collision is
    //    gone and the missing wrapper surfaces as an undefined symbol.
    //    Refs #903 / uuid `__perry_wrap_perry_fn_...sha1_js__default`.
    for name in wrapper_func_symbols {
        ll.push_str(&format!(
            "define double @{}(i64 %0, double %1, double %2, double %3, double %4, double %5) {{\n  ret double {}\n}}\n\n",
            name, undef_hex
        ));
    }

    // If absolutely nothing was requested, emit a single dummy symbol so
    // the resulting object isn't empty (some linkers complain about empty
    // objects).
    if missing_data_symbols.is_empty()
        && missing_func_symbols.is_empty()
        && identity_func_symbols.is_empty()
        && wrapper_func_symbols.is_empty()
    {
        ll.push_str("@__perry_stubs_placeholder = global i64 0, align 8\n");
    }

    // Resolve the target triple via the same map the rest of the LLVM
    // backend uses, so a Perry short name like "macos" / "ios" turns into
    // the right LLVM triple for clang's `-target` flag.
    let triple = target.and_then(crate::resolve_target_triple);

    compile_ll_to_object(&ll, triple.as_deref())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_stubs_produce_object() {
        let bytes = generate_stub_object(&[], &[], &[], None).unwrap();
        // Object files are not empty and start with a recognizable magic.
        assert!(
            bytes.len() > 16,
            "stub object too small: {} bytes",
            bytes.len()
        );
    }

    #[test]
    fn data_func_and_identity_stubs() {
        let data = vec!["__missing_data".to_string()];
        let funcs = vec!["js_missing_helper".to_string()];
        let id = vec!["js_await_any_promise".to_string()];
        let bytes = generate_stub_object(&data, &funcs, &id, None).unwrap();
        assert!(bytes.len() > 64);
    }

    #[test]
    fn closure_wrapper_stubs() {
        // Regression for the uuid `__perry_wrap_perry_fn_<src>__default`
        // link error masked pre-#903 by the same-file default-import
        // collision: the wrapper signature must match the closure-call
        // ABI (i64 closure + up to 5 doubles), and the body returns
        // NaN-boxed undefined.
        let wrappers =
            vec!["__perry_wrap_perry_fn_node_modules_uuid_dist_sha1_js__default".to_string()];
        let bytes = generate_stub_object_full(&[], &[], &[], &wrappers, None).unwrap();
        assert!(bytes.len() > 64);
    }
}
