Skip to main content

priv/native/jni/mob_bluetooth_nif.zig

//! mob_bluetooth_nif — Bluetooth Classic (BR/EDR) tier-1 ZIG plugin NIF.
//!
//! Extracted wholesale from mob-core's `mob_nif.zig` (the bt suite): the 16
//! `nif_bt_*` wrappers, the ~33 `mob_deliver_bt_*` delivery exports, the BT
//! atom cache, the term constructors, and the paired-list streaming
//! accumulator. The Kotlin side is the plugin-owned bridge class
//! `io.mob.bluetooth.MobBluetoothBridge`; the JNI delivery thunks live in the
//! sibling `mob_bluetooth_jni.c` (copied verbatim from beam_jni.c with the
//! Java_ symbol prefix renamed).
//!
//! Build path: compiled via `addZigObject` from `-Dplugin_zig_nifs`, reaching
//! mob-core ERTS / JNI bindings through the named imports `@import("erts")`
//! (→ mob_erts.zig) and `@import("jni")` (→ mob_zig.zig) that build.zig wires
//! for plugin zig objects. `get_jenv` + `g_jvm` are mob-core exports linked
//! into the same `.so` (extern-declared below, NOT duplicated).
//!
//! Bridge-class registration: the JVM calls
//! `Java_io_mob_bluetooth_MobBluetoothBridge_nativeRegister(jenv, cls)` at
//! startup (via the generated MobPluginBootstrap.registerAll → register()).
//! That thunk caches a global ref to the bridge jclass (`g_bt_cls`) + the 16
//! `bt_*` static method IDs (`g_bt`), with no FindClass / classloader problem.
//! The NIF's outbound `CallStaticVoidMethod` uses that cache; inbound
//! deliveries are reached by the C thunks calling the `mob_deliver_bt_*`
//! exports below.
const std = @import("std");
const erts = @import("erts");
const jni = @import("jni");

// mob-core exports (linked into the same .so). NOT duplicated.
extern fn get_jenv(attached: *c_int) ?*jni.JNIEnv;
extern var g_jvm: ?*jni.JavaVM;

// ── Plugin-owned bridge-class method-id cache ────────────────────────────
// Cached by the nativeRegister thunk. This is the plugin's OWN cache — it does
// NOT touch mob-core's exported `Bridge`. Method signatures match the
// cacheOptional block the bt suite used in mob-core's nif_load.
const BtMethods = struct {
    list_paired: jni.JMethodID = null,
    start_discovery: jni.JMethodID = null,
    cancel_discovery: jni.JMethodID = null,
    make_discoverable: jni.JMethodID = null,
    pair: jni.JMethodID = null,
    unpair: jni.JMethodID = null,
    disconnect: jni.JMethodID = null,
    hfp_connect: jni.JMethodID = null,
    hfp_subscribe_vendor_at: jni.JMethodID = null,
    hfp_send_vendor_at: jni.JMethodID = null,
    hfp_start_sco: jni.JMethodID = null,
    hfp_stop_sco: jni.JMethodID = null,
    spp_connect: jni.JMethodID = null,
    spp_write: jni.JMethodID = null,
    // BLE (Low Energy) — GATT peripheral.
    ble_start_advertising: jni.JMethodID = null,
    ble_stop_advertising: jni.JMethodID = null,
    ble_notify: jni.JMethodID = null,
};

var g_bt: BtMethods = .{};
var g_bt_cls: jni.JClass = null;

// ── nativeRegister thunk — cache the bridge jclass + method ids ───────────
// The JVM passes the declaring class as `cls` for a static-method thunk, so we
// cache a global ref to it directly — no FindClass, no classloader issue.
// Resolved by name from the loaded .so (zig `export fn` emits the C-ABI
// symbol). Signatures come from the bt cacheOptional block in mob-core's
// nif_load.
export fn Java_io_mob_bluetooth_MobBluetoothBridge_nativeRegister(jenv: *jni.JNIEnv, cls: jni.JClass) callconv(.c) void {
    g_bt_cls = jni.newGlobalRef(jenv, cls);
    if (g_bt_cls == null) return;
    g_bt.list_paired = jni.getStaticMethodID(jenv, cls, "bt_list_paired", "(J)V");
    g_bt.start_discovery = jni.getStaticMethodID(jenv, cls, "bt_start_discovery", "(J)V");
    g_bt.cancel_discovery = jni.getStaticMethodID(jenv, cls, "bt_cancel_discovery", "(J)V");
    g_bt.make_discoverable = jni.getStaticMethodID(jenv, cls, "bt_make_discoverable", "(JI)V");
    g_bt.pair = jni.getStaticMethodID(jenv, cls, "bt_pair", "(JLjava/lang/String;)V");
    g_bt.unpair = jni.getStaticMethodID(jenv, cls, "bt_unpair", "(JLjava/lang/String;)V");
    g_bt.disconnect = jni.getStaticMethodID(jenv, cls, "bt_disconnect", "(JI)V");
    g_bt.hfp_connect = jni.getStaticMethodID(jenv, cls, "bt_hfp_connect", "(JLjava/lang/String;)V");
    g_bt.hfp_subscribe_vendor_at = jni.getStaticMethodID(jenv, cls, "bt_hfp_subscribe_vendor_at", "(JILjava/lang/String;)V");
    g_bt.hfp_send_vendor_at = jni.getStaticMethodID(jenv, cls, "bt_hfp_send_vendor_at", "(JILjava/lang/String;Ljava/lang/String;)V");
    g_bt.hfp_start_sco = jni.getStaticMethodID(jenv, cls, "bt_hfp_start_sco", "(JI)V");
    g_bt.hfp_stop_sco = jni.getStaticMethodID(jenv, cls, "bt_hfp_stop_sco", "(JI)V");
    g_bt.spp_connect = jni.getStaticMethodID(jenv, cls, "bt_spp_connect", "(JLjava/lang/String;)V");
    g_bt.spp_write = jni.getStaticMethodID(jenv, cls, "bt_spp_write", "(JI[B)V");
    g_bt.ble_start_advertising = jni.getStaticMethodID(jenv, cls, "ble_start_advertising", "(JLjava/lang/String;)V");
    g_bt.ble_stop_advertising = jni.getStaticMethodID(jenv, cls, "ble_stop_advertising", "(J)V");
    g_bt.ble_notify = jni.getStaticMethodID(jenv, cls, "ble_notify", "(JLjava/lang/String;[B)V");
}

// ── Thread-attach helpers ────────────────────────────────────────────────

/// Detach when get_jenv set *attached = 1. Mirrors mob-core's helper.
inline fn detachIfAttached(attached: c_int) void {
    if (attached != 0) {
        if (g_jvm) |jvm| jni.detachCurrentThread(jvm);
    }
}

// ── Shared helpers (DUPLICATED from mob-core; mob-core keeps its own) ─────

/// Accept either a plain binary or an iolist (deep-flatten to binary).
fn getBinOrIolist(env: ?*erts.ErlNifEnv, term: erts.ERL_NIF_TERM) ?erts.ErlNifBinary {
    var bin: erts.ErlNifBinary = undefined;
    if (erts.enif_inspect_binary(env, term, &bin) != 0) return bin;
    if (erts.enif_inspect_iolist_as_binary(env, term, &bin) != 0) return bin;
    return null;
}

/// Heap-allocate a NUL-terminated copy of an `ErlNifBinary` for NewStringUTF.
fn binToCString(bin: erts.ErlNifBinary) ?[*:0]u8 {
    const buf_ptr = jni.malloc(bin.size + 1) orelse return null;
    const dst: [*]u8 = @ptrCast(buf_ptr);
    @memcpy(dst[0..bin.size], bin.data[0..bin.size]);
    dst[bin.size] = 0;
    return @ptrCast(buf_ptr);
}

inline fn freeCString(p: ?[*:0]u8) void {
    if (p) |ptr| jni.free(@as(?*anyopaque, @ptrCast(ptr)));
}

// ── pid <-> jlong round-trip (bt-only in mob-core; moved wholesale) ───────
//
// Pack an ErlNifPid into a jlong for the JNI-side delivery handle. Kotlin
// hands it back unchanged when it calls one of the mob_deliver_bt_* hooks; we
// round-trip via pidFromLong. On aarch64 ERL_NIF_TERM == jlong width so the
// bitcast is a true reinterpret; on 32-bit ARM we zero-extend / truncate.
inline fn pidToJlong(pid: erts.ErlNifPid) jni.JLong {
    if (@sizeOf(erts.ERL_NIF_TERM) == @sizeOf(jni.JLong)) {
        return @bitCast(pid.pid);
    }
    return @intCast(pid.pid);
}

inline fn pidFromLong(jpid: jni.JLong) erts.ErlNifPid {
    if (@sizeOf(erts.ERL_NIF_TERM) == @sizeOf(jni.JLong)) {
        return .{ .pid = @bitCast(jpid) };
    }
    const low: u32 = @truncate(@as(u64, @bitCast(jpid)));
    return .{ .pid = low };
}

/// Call `MobBluetoothBridge.<method>(pid_long, arg)` — the standard async
/// shape. Returns `:ok` unconditionally; results land later via a
/// mob_deliver_bt_* hook. Mirrors mob-core's callBridgePidStr, retargeted at
/// the plugin's own cached class.
fn callBridgePidStr(env: ?*erts.ErlNifEnv, method: jni.JMethodID, pid: erts.ErlNifPid, arg: ?[*:0]const u8) erts.ERL_NIF_TERM {
    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    const jarg: jni.JString = if (arg) |a| jni.newStringUTF(jenv, a) else null;
    jenv.*.CallStaticVoidMethod.?(jenv, g_bt_cls, method, pidToJlong(pid), jarg);
    if (jarg != null) jni.deleteLocalRef(jenv, jarg);
    detachIfAttached(attached);
    return erts.ok(env);
}

// ═════════════════════════════════════════════════════════════════════════
// BT atom cache
// ═════════════════════════════════════════════════════════════════════════

const MobBtAtoms = struct {
    // Channel atoms
    bt: erts.ERL_NIF_TERM = 0,
    bt_hfp: erts.ERL_NIF_TERM = 0,
    bt_spp: erts.ERL_NIF_TERM = 0,

    // Discovery / pairing tags
    discovery_started: erts.ERL_NIF_TERM = 0,
    discovery_finished: erts.ERL_NIF_TERM = 0,
    discovery_cancelled: erts.ERL_NIF_TERM = 0,
    discovered: erts.ERL_NIF_TERM = 0,
    paired_list: erts.ERL_NIF_TERM = 0,
    paired: erts.ERL_NIF_TERM = 0,
    pair_failed: erts.ERL_NIF_TERM = 0,
    unpaired: erts.ERL_NIF_TERM = 0,
    err: erts.ERL_NIF_TERM = 0,

    // Profile lifecycle tags
    connecting: erts.ERL_NIF_TERM = 0,
    connected: erts.ERL_NIF_TERM = 0,
    connect_failed: erts.ERL_NIF_TERM = 0,
    disconnected: erts.ERL_NIF_TERM = 0,

    // HFP-specific
    vendor_subscribed: erts.ERL_NIF_TERM = 0,
    vendor_at: erts.ERL_NIF_TERM = 0,
    sco_started: erts.ERL_NIF_TERM = 0,
    sco_stopped: erts.ERL_NIF_TERM = 0,

    // SPP-specific
    data: erts.ERL_NIF_TERM = 0,
    written: erts.ERL_NIF_TERM = 0,

    // BLE (Low Energy) channel + tags
    bt_le: erts.ERL_NIF_TERM = 0,
    advertising_started: erts.ERL_NIF_TERM = 0,
    advertising_failed: erts.ERL_NIF_TERM = 0,
    central_connected: erts.ERL_NIF_TERM = 0,
    central_disconnected: erts.ERL_NIF_TERM = 0,
    subscribed: erts.ERL_NIF_TERM = 0,
    unsubscribed: erts.ERL_NIF_TERM = 0,
    write: erts.ERL_NIF_TERM = 0,

    // Map keys
    k_address: erts.ERL_NIF_TERM = 0,
    k_name: erts.ERL_NIF_TERM = 0,
    k_bonded: erts.ERL_NIF_TERM = 0,
    k_reason: erts.ERL_NIF_TERM = 0,
    k_cmd: erts.ERL_NIF_TERM = 0,
    k_cmd_type: erts.ERL_NIF_TERM = 0,
    k_args: erts.ERL_NIF_TERM = 0,
    k_size: erts.ERL_NIF_TERM = 0,
    k_central: erts.ERL_NIF_TERM = 0,
    k_characteristic: erts.ERL_NIF_TERM = 0,
    k_bytes: erts.ERL_NIF_TERM = 0,

    // Constants
    nil_atom: erts.ERL_NIF_TERM = 0,
    true_atom: erts.ERL_NIF_TERM = 0,
    false_atom: erts.ERL_NIF_TERM = 0,
};

var mob_bt_atoms: MobBtAtoms = .{};

/// Initialise the BT atom cache. Called from the ErlNifEntry `.load`.
pub fn mobBtAtomsInit(env: ?*erts.ErlNifEnv) void {
    mob_bt_atoms.bt = erts.atom(env, "bt");
    mob_bt_atoms.bt_hfp = erts.atom(env, "bt_hfp");
    mob_bt_atoms.bt_spp = erts.atom(env, "bt_spp");

    mob_bt_atoms.discovery_started = erts.atom(env, "discovery_started");
    mob_bt_atoms.discovery_finished = erts.atom(env, "discovery_finished");
    mob_bt_atoms.discovery_cancelled = erts.atom(env, "discovery_cancelled");
    mob_bt_atoms.discovered = erts.atom(env, "discovered");
    mob_bt_atoms.paired_list = erts.atom(env, "paired_list");
    mob_bt_atoms.paired = erts.atom(env, "paired");
    mob_bt_atoms.pair_failed = erts.atom(env, "pair_failed");
    mob_bt_atoms.unpaired = erts.atom(env, "unpaired");
    mob_bt_atoms.err = erts.atom(env, "error");

    mob_bt_atoms.connecting = erts.atom(env, "connecting");
    mob_bt_atoms.connected = erts.atom(env, "connected");
    mob_bt_atoms.connect_failed = erts.atom(env, "connect_failed");
    mob_bt_atoms.disconnected = erts.atom(env, "disconnected");

    mob_bt_atoms.vendor_subscribed = erts.atom(env, "vendor_subscribed");
    mob_bt_atoms.vendor_at = erts.atom(env, "vendor_at");
    mob_bt_atoms.sco_started = erts.atom(env, "sco_started");
    mob_bt_atoms.sco_stopped = erts.atom(env, "sco_stopped");

    mob_bt_atoms.data = erts.atom(env, "data");
    mob_bt_atoms.written = erts.atom(env, "written");

    mob_bt_atoms.bt_le = erts.atom(env, "bt_le");
    mob_bt_atoms.advertising_started = erts.atom(env, "advertising_started");
    mob_bt_atoms.advertising_failed = erts.atom(env, "advertising_failed");
    mob_bt_atoms.central_connected = erts.atom(env, "central_connected");
    mob_bt_atoms.central_disconnected = erts.atom(env, "central_disconnected");
    mob_bt_atoms.subscribed = erts.atom(env, "subscribed");
    mob_bt_atoms.unsubscribed = erts.atom(env, "unsubscribed");
    mob_bt_atoms.write = erts.atom(env, "write");

    mob_bt_atoms.k_address = erts.atom(env, "address");
    mob_bt_atoms.k_name = erts.atom(env, "name");
    mob_bt_atoms.k_bonded = erts.atom(env, "bonded");
    mob_bt_atoms.k_reason = erts.atom(env, "reason");
    mob_bt_atoms.k_cmd = erts.atom(env, "cmd");
    mob_bt_atoms.k_cmd_type = erts.atom(env, "cmd_type");
    mob_bt_atoms.k_args = erts.atom(env, "args");
    mob_bt_atoms.k_size = erts.atom(env, "size");
    mob_bt_atoms.k_central = erts.atom(env, "central");
    mob_bt_atoms.k_characteristic = erts.atom(env, "characteristic");
    mob_bt_atoms.k_bytes = erts.atom(env, "bytes");

    mob_bt_atoms.nil_atom = erts.atom(env, "nil");
    mob_bt_atoms.true_atom = erts.atom(env, "true");
    mob_bt_atoms.false_atom = erts.atom(env, "false");
}

// ═════════════════════════════════════════════════════════════════════════
// Term constructors — mirror the C mob_bt_make_* helpers
// ═════════════════════════════════════════════════════════════════════════

/// Convert a C string into an Erlang binary term. Empty input → empty binary.
fn mobBtMakeBinaryStr(env: ?*erts.ErlNifEnv, s: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const len: usize = if (s) |p| jni.strlen(p) else 0;
    var bin: erts.ErlNifBinary = undefined;
    _ = erts.enif_alloc_binary(len, &bin);
    if (len > 0) {
        if (s) |p| @memcpy(bin.data[0..len], p[0..len]);
    }
    return erts.enif_make_binary(env, &bin);
}

/// Build a binary term from arbitrary bytes (SPP byte streams, etc).
fn mobBtMakeBinaryBytes(env: ?*erts.ErlNifEnv, bytes: ?[*]const u8, len: usize) erts.ERL_NIF_TERM {
    var bin: erts.ErlNifBinary = undefined;
    _ = erts.enif_alloc_binary(len, &bin);
    if (len > 0) {
        if (bytes) |p| @memcpy(bin.data[0..len], p[0..len]);
    }
    return erts.enif_make_binary(env, &bin);
}

/// Build `%{address: <<...>>, name: <<...>>, bonded: bool}`.
fn mobBtMakeDeviceMap(env: ?*erts.ErlNifEnv, address: ?[*:0]const u8, name: ?[*:0]const u8, bonded: c_int) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{
        mob_bt_atoms.k_address,
        mob_bt_atoms.k_name,
        mob_bt_atoms.k_bonded,
    };
    const vals = [_]erts.ERL_NIF_TERM{
        mobBtMakeBinaryStr(env, address),
        mobBtMakeBinaryStr(env, name),
        if (bonded != 0) mob_bt_atoms.true_atom else mob_bt_atoms.false_atom,
    };
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{address: <<...>>}`.
fn mobBtMakeAddressOnly(env: ?*erts.ErlNifEnv, address: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{mob_bt_atoms.k_address};
    const vals = [_]erts.ERL_NIF_TERM{mobBtMakeBinaryStr(env, address)};
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{address: <<...>>, name: <<...>>}`.
fn mobBtMakeAddressName(env: ?*erts.ErlNifEnv, address: ?[*:0]const u8, name: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{ mob_bt_atoms.k_address, mob_bt_atoms.k_name };
    const vals = [_]erts.ERL_NIF_TERM{
        mobBtMakeBinaryStr(env, address),
        mobBtMakeBinaryStr(env, name),
    };
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{address: <<...>>, reason: :reason_atom}`. Reason defaults to
/// `:unknown` for null Kotlin strings — never explodes on missing data.
fn mobBtMakeAddressReason(env: ?*erts.ErlNifEnv, address: ?[*:0]const u8, reason: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const reason_atom = if (reason) |r| erts.enif_make_atom(env, r) else erts.atom(env, "unknown");
    const keys = [_]erts.ERL_NIF_TERM{ mob_bt_atoms.k_address, mob_bt_atoms.k_reason };
    const vals = [_]erts.ERL_NIF_TERM{
        mobBtMakeBinaryStr(env, address),
        reason_atom,
    };
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{reason: :reason_atom}`.
fn mobBtMakeReasonOnly(env: ?*erts.ErlNifEnv, reason: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const reason_atom = if (reason) |r| erts.enif_make_atom(env, r) else erts.atom(env, "unknown");
    const keys = [_]erts.ERL_NIF_TERM{mob_bt_atoms.k_reason};
    const vals = [_]erts.ERL_NIF_TERM{reason_atom};
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{cmd: <<...>>, cmd_type: int, args: <<...>>, address: <<...>>}` for
/// vendor AT events. cmd_type is the HFP AT command type code.
fn mobBtMakeVendorAtMap(env: ?*erts.ErlNifEnv, cmd: ?[*:0]const u8, cmd_type: c_int, args: ?[*:0]const u8, address: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{
        mob_bt_atoms.k_cmd,
        mob_bt_atoms.k_cmd_type,
        mob_bt_atoms.k_args,
        mob_bt_atoms.k_address,
    };
    const vals = [_]erts.ERL_NIF_TERM{
        mobBtMakeBinaryStr(env, cmd),
        erts.enif_make_int(env, cmd_type),
        mobBtMakeBinaryStr(env, args),
        mobBtMakeBinaryStr(env, address),
    };
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{size: int}` for write-completion events.
fn mobBtMakeSizeMap(env: ?*erts.ErlNifEnv, size: c_int) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{mob_bt_atoms.k_size};
    const vals = [_]erts.ERL_NIF_TERM{erts.enif_make_int(env, size)};
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{central: int}` for LE connection events.
fn mobBleMakeCentralMap(env: ?*erts.ErlNifEnv, central: c_int) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{mob_bt_atoms.k_central};
    const vals = [_]erts.ERL_NIF_TERM{erts.enif_make_int(env, central)};
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{characteristic: <<...>>}` for subscribe/unsubscribe events.
fn mobBleMakeCharMap(env: ?*erts.ErlNifEnv, char_uuid: ?[*:0]const u8) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{mob_bt_atoms.k_characteristic};
    const vals = [_]erts.ERL_NIF_TERM{mobBtMakeBinaryStr(env, char_uuid)};
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}

/// Build `%{characteristic: <<...>>, bytes: <<...>>}` for an incoming write.
fn mobBleMakeWriteMap(env: ?*erts.ErlNifEnv, char_uuid: ?[*:0]const u8, bytes: ?[*]const u8, len: usize) erts.ERL_NIF_TERM {
    const keys = [_]erts.ERL_NIF_TERM{ mob_bt_atoms.k_characteristic, mob_bt_atoms.k_bytes };
    const vals = [_]erts.ERL_NIF_TERM{
        mobBtMakeBinaryStr(env, char_uuid),
        mobBtMakeBinaryBytes(env, bytes, len),
    };
    return erts.makeMap(env, &keys, &vals) orelse mob_bt_atoms.err;
}


// ═════════════════════════════════════════════════════════════════════════
// Paired-list streaming accumulator
// ═════════════════════════════════════════════════════════════════════════
//
// Kotlin streams paired devices one at a time (begin → 0..N entries → finish).
// A stable per-pid buffer accumulates entries, since Kotlin can interleave
// streams from concurrent callers. 16 buckets × 128 max entries each. Slot
// lookup/insert/remove holds the table mutex; enif_send happens AFTER the
// mutex is released so a slow consumer doesn't block other accumulators.

const MOB_BT_PAIRED_BUCKETS: usize = 16;
const MOB_BT_PAIRED_MAX_ENTRIES: usize = 128;
const MOB_BT_ADDR_MAX: usize = 24; // "00:11:22:33:44:55" + null + slop
const MOB_BT_NAME_MAX: usize = 248; // BT spec max friendly name + null

const MobBtPairedEntry = extern struct {
    address: [MOB_BT_ADDR_MAX]u8,
    name: [MOB_BT_NAME_MAX]u8,
    bonded: c_int,
};

const MobBtPairedSlot = extern struct {
    pid_long: jni.JLong,
    in_use: c_int,
    count: usize,
    entries: [MOB_BT_PAIRED_MAX_ENTRIES]MobBtPairedEntry,
};

var mob_bt_paired_slots: [MOB_BT_PAIRED_BUCKETS]MobBtPairedSlot = blk: {
    var buf: [MOB_BT_PAIRED_BUCKETS]MobBtPairedSlot = undefined;
    for (&buf) |*s| s.* = std.mem.zeroes(MobBtPairedSlot);
    break :blk buf;
};
var mob_bt_paired_mutex: ?*erts.ErlNifMutex = null;

/// Initialise the paired-list accumulator. Returns 0 on success, -1 on
/// mutex-create failure. Called from the ErlNifEntry `.load`.
pub fn mobBtPairedInit() c_int {
    mob_bt_paired_mutex = erts.enif_mutex_create("mob_bt_paired_mutex") orelse return -1;
    return 0;
}

/// Find an in-use slot matching `pid_long`. Must hold the mutex.
fn mobBtPairedFindLocked(pid_long: jni.JLong) ?*MobBtPairedSlot {
    for (&mob_bt_paired_slots) |*s| {
        if (s.in_use != 0 and s.pid_long == pid_long) return s;
    }
    return null;
}

/// Claim the first free slot for `pid_long`, OR — if `pid_long` already has a
/// slot — reset it for a new accumulation cycle. Must hold mutex.
fn mobBtPairedClaimLocked(pid_long: jni.JLong) ?*MobBtPairedSlot {
    if (mobBtPairedFindLocked(pid_long)) |existing| {
        existing.count = 0;
        return existing;
    }
    for (&mob_bt_paired_slots) |*s| {
        if (s.in_use == 0) {
            s.in_use = 1;
            s.pid_long = pid_long;
            s.count = 0;
            return s;
        }
    }
    return null; // all slots in use — drop this accumulation
}

/// Mark a slot free. Must hold mutex.
fn mobBtPairedReleaseLocked(slot: *MobBtPairedSlot) void {
    slot.in_use = 0;
    slot.pid_long = 0;
    slot.count = 0;
}

// ═════════════════════════════════════════════════════════════════════════
// BT envelope helper — 4-tuple {:bt, tag, session_or_nil, payload}
// ═════════════════════════════════════════════════════════════════════════

/// Return :nil or an integer for the session slot.
inline fn btSessionTerm(env: ?*erts.ErlNifEnv, session: c_int) erts.ERL_NIF_TERM {
    return if (session < 0) mob_bt_atoms.nil_atom else erts.enif_make_int(env, session);
}

// ═════════════════════════════════════════════════════════════════════════
// Delivery functions — `mob_deliver_bt_*` exports
// ═════════════════════════════════════════════════════════════════════════
//
// Called from mob_bluetooth_jni.c's Java_io_mob_bluetooth_MobBluetoothBridge_
// nativeDeliverBt* thunks when Kotlin emits BT events. Each builds the typed
// envelope tuple and posts it to `pid_long` (an ErlNifPid round-tripped
// through Kotlin as a jlong).

// ── Discovery (2-tuples — no payload) ──────────────────────────────────

pub export fn mob_deliver_bt_discovery_started(pid_long: jni.JLong) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.discovery_started,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_discovery_finished(pid_long: jni.JLong) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.discovery_finished,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_discovery_cancelled(pid_long: jni.JLong) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.discovery_cancelled,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── Discovery / pairing (3-tuples — no session) ────────────────────────

pub export fn mob_deliver_bt_discovered(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    name: ?[*:0]const u8,
    bonded: c_int,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.discovered,
        mobBtMakeDeviceMap(env, address, name, bonded),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_paired(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    name: ?[*:0]const u8,
    bonded: c_int,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.paired,
        mobBtMakeDeviceMap(env, address, name, bonded),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_pair_failed(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    reason: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.pair_failed,
        mobBtMakeAddressReason(env, address, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_unpaired(pid_long: jni.JLong, address: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.unpaired,
        mobBtMakeAddressOnly(env, address),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_error(pid_long: jni.JLong, reason: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.err,
        mobBtMakeReasonOnly(env, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── Legacy JSON paired-devices (unused by current Elixir API but kept for
//    compat with Kotlin templates that pre-date the streamed paired list
//    accumulator). No JNI thunk calls this; kept as an export for parity.
pub export fn mob_deliver_bt_paired_devices(pid_long: jni.JLong, json: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        erts.atom(env, "paired_devices_json"),
        mob_bt_atoms.nil_atom,
        mobBtMakeBinaryStr(env, json),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── Paired-list streaming (begin / entry / finish) ─────────────────────

pub export fn mob_deliver_bt_paired_list_begin(pid_long: jni.JLong) callconv(.c) void {
    erts.enif_mutex_lock(mob_bt_paired_mutex);
    _ = mobBtPairedClaimLocked(pid_long);
    erts.enif_mutex_unlock(mob_bt_paired_mutex);
}

pub export fn mob_deliver_bt_paired_list_entry(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    name: ?[*:0]const u8,
    bonded: c_int,
) callconv(.c) void {
    erts.enif_mutex_lock(mob_bt_paired_mutex);
    defer erts.enif_mutex_unlock(mob_bt_paired_mutex);

    const slot = mobBtPairedFindLocked(pid_long) orelse return;
    if (slot.count >= MOB_BT_PAIRED_MAX_ENTRIES) return;

    const entry = &slot.entries[slot.count];
    if (address) |a| {
        const a_len = jni.strlen(a);
        const a_copy = @min(a_len, entry.address.len - 1);
        @memcpy(entry.address[0..a_copy], a[0..a_copy]);
        entry.address[a_copy] = 0;
    } else {
        entry.address[0] = 0;
    }
    if (name) |n| {
        const n_len = jni.strlen(n);
        const n_copy = @min(n_len, entry.name.len - 1);
        @memcpy(entry.name[0..n_copy], n[0..n_copy]);
        entry.name[n_copy] = 0;
    } else {
        entry.name[0] = 0;
    }
    entry.bonded = if (bonded != 0) 1 else 0;
    slot.count += 1;
}

pub export fn mob_deliver_bt_paired_list_finish(pid_long: jni.JLong) callconv(.c) void {
    // Snapshot under lock, then release before any term allocation.
    var snapshot: MobBtPairedSlot = undefined;

    erts.enif_mutex_lock(mob_bt_paired_mutex);
    const slot = mobBtPairedFindLocked(pid_long);
    if (slot == null) {
        erts.enif_mutex_unlock(mob_bt_paired_mutex);
        return;
    }
    snapshot = slot.?.*;
    mobBtPairedReleaseLocked(slot.?);
    erts.enif_mutex_unlock(mob_bt_paired_mutex);

    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);

    // Empty list as the cons-cdr seed. enif_make_list is variadic in C and
    // intentionally not exposed in mob_erts.zig; enif_make_list_from_array
    // with count=0 returns the same empty-list term via the non-variadic ABI.
    const empty: [0]erts.ERL_NIF_TERM = .{};
    var list = erts.enif_make_list_from_array(env, &empty, 0);
    var i: usize = snapshot.count;
    while (i > 0) {
        i -= 1;
        const entry = &snapshot.entries[i];
        const addr_ptr: [*:0]const u8 = @ptrCast(&entry.address);
        const name_ptr: [*:0]const u8 = @ptrCast(&entry.name);
        const dev = mobBtMakeDeviceMap(env, addr_ptr, name_ptr, entry.bonded);
        list = erts.enif_make_list_cell(env, dev, list);
    }

    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt,
        mob_bt_atoms.paired_list,
        list,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── HFP profile deliveries ─────────────────────────────────────────────

pub export fn mob_deliver_bt_hfp_connecting(
    pid_long: jni.JLong,
    session: c_int,
    address: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.connecting,
        erts.enif_make_int(env, session),
        mobBtMakeAddressOnly(env, address),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_connected(
    pid_long: jni.JLong,
    session: c_int,
    address: ?[*:0]const u8,
    name: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.connected,
        erts.enif_make_int(env, session),
        mobBtMakeAddressName(env, address, name),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_connect_failed(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    reason: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.connect_failed,
        mobBtMakeAddressReason(env, address, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_disconnected(
    pid_long: jni.JLong,
    session: c_int,
    reason_atom: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const reason_term = if (reason_atom) |r| erts.enif_make_atom(env, r) else erts.atom(env, "unknown");
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.disconnected,
        erts.enif_make_int(env, session),
        reason_term,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_vendor_subscribed(pid_long: jni.JLong, session: c_int) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.vendor_subscribed,
        erts.enif_make_int(env, session),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_vendor_at(
    pid_long: jni.JLong,
    session: c_int,
    cmd: ?[*:0]const u8,
    cmd_type: c_int,
    args: ?[*:0]const u8,
    address: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.vendor_at,
        erts.enif_make_int(env, session),
        mobBtMakeVendorAtMap(env, cmd, cmd_type, args, address),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_sco_started(
    pid_long: jni.JLong,
    session: c_int,
    address: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.sco_started,
        erts.enif_make_int(env, session),
        mobBtMakeAddressOnly(env, address),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_sco_stopped(pid_long: jni.JLong, session: c_int) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.sco_stopped,
        erts.enif_make_int(env, session),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_hfp_error(
    pid_long: jni.JLong,
    session: c_int,
    reason: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_hfp,
        mob_bt_atoms.err,
        erts.enif_make_int(env, session),
        mobBtMakeReasonOnly(env, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── SPP profile deliveries ─────────────────────────────────────────────

pub export fn mob_deliver_bt_spp_connected(
    pid_long: jni.JLong,
    session: c_int,
    address: ?[*:0]const u8,
    name: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.connected,
        erts.enif_make_int(env, session),
        mobBtMakeAddressName(env, address, name),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_spp_connect_failed(
    pid_long: jni.JLong,
    address: ?[*:0]const u8,
    reason: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.connect_failed,
        mobBtMakeAddressReason(env, address, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_spp_disconnected(
    pid_long: jni.JLong,
    session: c_int,
    reason_atom: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const reason_term = if (reason_atom) |r| erts.enif_make_atom(env, r) else erts.atom(env, "unknown");
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.disconnected,
        erts.enif_make_int(env, session),
        reason_term,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_spp_data(
    pid_long: jni.JLong,
    session: c_int,
    bytes: ?[*]const u8,
    len: usize,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.data,
        erts.enif_make_int(env, session),
        mobBtMakeBinaryBytes(env, bytes, len),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_spp_written(pid_long: jni.JLong, session: c_int, size: c_int) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.written,
        erts.enif_make_int(env, session),
        mobBtMakeSizeMap(env, size),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_bt_spp_error(
    pid_long: jni.JLong,
    session: c_int,
    reason: ?[*:0]const u8,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_spp,
        mob_bt_atoms.err,
        erts.enif_make_int(env, session),
        mobBtMakeReasonOnly(env, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ── BLE (Low Energy) peripheral deliveries ─────────────────────────────
// All tagged `:bt_le`. Connection events carry an opaque integer `central`
// handle; subscribe/write carry the characteristic UUID string.

pub export fn mob_deliver_ble_advertising_started(pid_long: jni.JLong) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.advertising_started,
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_advertising_failed(pid_long: jni.JLong, reason: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.advertising_failed,
        mobBtMakeReasonOnly(env, reason),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_central_connected(pid_long: jni.JLong, central: c_int) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.central_connected,
        mobBleMakeCentralMap(env, central),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_central_disconnected(pid_long: jni.JLong, central: c_int) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.central_disconnected,
        mobBleMakeCentralMap(env, central),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_subscribed(pid_long: jni.JLong, char_uuid: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.subscribed,
        mobBleMakeCharMap(env, char_uuid),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_unsubscribed(pid_long: jni.JLong, char_uuid: ?[*:0]const u8) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.unsubscribed,
        mobBleMakeCharMap(env, char_uuid),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

pub export fn mob_deliver_ble_write(
    pid_long: jni.JLong,
    char_uuid: ?[*:0]const u8,
    bytes: ?[*]const u8,
    len: usize,
) callconv(.c) void {
    var pid = pidFromLong(pid_long);
    const env = erts.enif_alloc_env() orelse return;
    defer erts.enif_free_env(env);
    const msg = erts.makeTuple(env, .{
        mob_bt_atoms.bt_le,
        mob_bt_atoms.write,
        mobBleMakeWriteMap(env, char_uuid, bytes, len),
    });
    _ = erts.enif_send(null, &pid, env, msg);
}

// ═════════════════════════════════════════════════════════════════════════
// NIF wrappers — `nif_bt_*`
// ═════════════════════════════════════════════════════════════════════════
//
// Pull caller pid via enif_self, attach JNIEnv, dispatch via the cached
// jmethodID on the plugin's own g_bt_cls, return :ok. All responses come back
// asynchronously through the mob_deliver_bt_* hooks.
//
// `unsupported` short-circuit: if the bridge method id is null (bridge not
// registered, or an older bridge class), emit a single
// `{:bt, :error, nil, %{reason: :unsupported}}` and return :ok.

fn btUnsupported(env: ?*erts.ErlNifEnv) erts.ERL_NIF_TERM {
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    const msg_env = erts.enif_alloc_env() orelse return erts.ok(env);
    defer erts.enif_free_env(msg_env);
    const unsupported = erts.atom(msg_env, "unsupported");
    const keys = [_]erts.ERL_NIF_TERM{erts.atom(msg_env, "reason")};
    const vals = [_]erts.ERL_NIF_TERM{unsupported};
    const map = erts.makeMap(msg_env, &keys, &vals) orelse unsupported;
    const msg = erts.makeTuple(msg_env, .{
        erts.atom(msg_env, "bt"),
        erts.atom(msg_env, "error"),
        erts.atom(msg_env, "nil"),
        map,
    });
    _ = erts.enif_send(null, &pid, msg_env, msg);
    return erts.ok(env);
}

// ── No-arg discovery / paired-list NIFs ──

export fn nif_bt_list_paired(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    _ = argv;
    if (g_bt.list_paired == null) return btUnsupported(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(jenv, g_bt_cls, g_bt.list_paired, pidToJlong(pid));
    return erts.ok(env);
}

export fn nif_bt_start_discovery(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    _ = argv;
    if (g_bt.start_discovery == null) return btUnsupported(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(jenv, g_bt_cls, g_bt.start_discovery, pidToJlong(pid));
    return erts.ok(env);
}

export fn nif_bt_cancel_discovery(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    _ = argv;
    if (g_bt.cancel_discovery == null) return btUnsupported(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(jenv, g_bt_cls, g_bt.cancel_discovery, pidToJlong(pid));
    return erts.ok(env);
}

export fn nif_bt_make_discoverable(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.make_discoverable == null) return btUnsupported(env);
    var duration: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &duration) == 0) return erts.badarg(env);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.make_discoverable,
        pidToJlong(pid),
        @as(jni.JInt, duration),
    );
    return erts.ok(env);
}

// ── JSON-arg NIFs (pair / unpair / hfp_connect / spp_connect) ──

export fn nif_bt_pair(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.pair == null) return btUnsupported(env);
    const bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_bt.pair, pid, json);
}

export fn nif_bt_unpair(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.unpair == null) return btUnsupported(env);
    const bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_bt.unpair, pid, json);
}

export fn nif_bt_hfp_connect(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.hfp_connect == null) return btUnsupported(env);
    const bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_bt.hfp_connect, pid, json);
}

export fn nif_bt_spp_connect(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.spp_connect == null) return btUnsupported(env);
    const bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_bt.spp_connect, pid, json);
}

// ── Session-only NIFs ──

export fn nif_bt_disconnect(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.disconnect == null) return btUnsupported(env);
    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.disconnect,
        pidToJlong(pid),
        @as(jni.JInt, session),
    );
    return erts.ok(env);
}

export fn nif_bt_hfp_subscribe_vendor_at(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.hfp_subscribe_vendor_at == null) return btUnsupported(env);
    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);
    const bin = getBinOrIolist(env, argv[1]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    const jjson = jni.newStringUTF(jenv, json);
    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.hfp_subscribe_vendor_at,
        pidToJlong(pid),
        @as(jni.JInt, session),
        jjson,
    );
    if (jjson != null) jni.deleteLocalRef(jenv, jjson);
    return erts.ok(env);
}

export fn nif_bt_hfp_start_sco(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.hfp_start_sco == null) return btUnsupported(env);
    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.hfp_start_sco,
        pidToJlong(pid),
        @as(jni.JInt, session),
    );
    return erts.ok(env);
}

export fn nif_bt_hfp_stop_sco(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.hfp_stop_sco == null) return btUnsupported(env);
    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.hfp_stop_sco,
        pidToJlong(pid),
        @as(jni.JInt, session),
    );
    return erts.ok(env);
}

// ── Two-string + session NIF (hfp_send_vendor_at/3) ──

export fn nif_bt_hfp_send_vendor_at(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.hfp_send_vendor_at == null) return btUnsupported(env);

    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);

    const cmd_bin = getBinOrIolist(env, argv[1]) orelse return erts.badarg(env);
    const cmd = binToCString(cmd_bin) orelse return erts.atom(env, "error");
    defer freeCString(cmd);

    const args_bin = getBinOrIolist(env, argv[2]) orelse return erts.badarg(env);
    const args = binToCString(args_bin) orelse return erts.atom(env, "error");
    defer freeCString(args);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    const jcmd = jni.newStringUTF(jenv, cmd);
    const jargs = jni.newStringUTF(jenv, args);
    jenv.*.CallStaticVoidMethod.?(
        jenv,
        g_bt_cls,
        g_bt.hfp_send_vendor_at,
        pidToJlong(pid),
        @as(jni.JInt, session),
        jcmd,
        jargs,
    );
    if (jcmd != null) jni.deleteLocalRef(jenv, jcmd);
    if (jargs != null) jni.deleteLocalRef(jenv, jargs);
    return erts.ok(env);
}

// ── Byte-array NIF (spp_write/2) — dirty IO ──

export fn nif_bt_spp_write(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.spp_write == null) return btUnsupported(env);

    var session: c_int = 0;
    if (erts.enif_get_int(env, argv[0], &session) == 0) return erts.badarg(env);
    const bin = getBinOrIolist(env, argv[1]) orelse return erts.badarg(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    const size: jni.JSize = @intCast(bin.size);
    const jbytes = jni.newByteArray(jenv, size);
    if (jbytes != null) {
        jni.setByteArrayRegion(jenv, jbytes, 0, size, @ptrCast(bin.data));
        jenv.*.CallStaticVoidMethod.?(
            jenv,
            g_bt_cls,
            g_bt.spp_write,
            pidToJlong(pid),
            @as(jni.JInt, session),
            jbytes,
        );
        jni.deleteLocalRef(jenv, jbytes);
    }
    return erts.ok(env);
}

// ── BLE (Low Energy) peripheral NIFs ──

export fn nif_ble_start_advertising(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.ble_start_advertising == null) return btUnsupported(env);
    const bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const json = binToCString(bin) orelse return erts.atom(env, "error");
    defer freeCString(json);
    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);
    return callBridgePidStr(env, g_bt.ble_start_advertising, pid, json);
}

export fn nif_ble_stop_advertising(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    _ = argv;
    if (g_bt.ble_stop_advertising == null) return btUnsupported(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    jenv.*.CallStaticVoidMethod.?(jenv, g_bt_cls, g_bt.ble_stop_advertising, pidToJlong(pid));
    return erts.ok(env);
}

// ble_notify(char_uuid_string, bytes) — outbound notification on a
// characteristic. String + byte[], like a hybrid of pair (string) and
// spp_write (bytes).
export fn nif_ble_notify(
    env: ?*erts.ErlNifEnv,
    argc: c_int,
    argv: [*]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = argc;
    if (g_bt.ble_notify == null) return btUnsupported(env);

    const uuid_bin = getBinOrIolist(env, argv[0]) orelse return erts.badarg(env);
    const uuid = binToCString(uuid_bin) orelse return erts.atom(env, "error");
    defer freeCString(uuid);
    const bytes_bin = getBinOrIolist(env, argv[1]) orelse return erts.badarg(env);

    var pid: erts.ErlNifPid = undefined;
    _ = erts.enif_self(env, &pid);

    var attached: c_int = 0;
    const jenv = get_jenv(&attached) orelse return erts.atom(env, "error");
    defer detachIfAttached(attached);

    const juuid = jni.newStringUTF(jenv, uuid);
    const size: jni.JSize = @intCast(bytes_bin.size);
    const jbytes = jni.newByteArray(jenv, size);
    if (jbytes != null) {
        jni.setByteArrayRegion(jenv, jbytes, 0, size, @ptrCast(bytes_bin.data));
        jenv.*.CallStaticVoidMethod.?(
            jenv,
            g_bt_cls,
            g_bt.ble_notify,
            pidToJlong(pid),
            juuid,
            jbytes,
        );
        jni.deleteLocalRef(jenv, jbytes);
    }
    if (juuid != null) jni.deleteLocalRef(jenv, juuid);
    return erts.ok(env);
}

// ── ErlNifEntry `.load` — init atom cache + paired-list accumulator ───────

fn nifLoad(env: ?*erts.ErlNifEnv, priv: *?*anyopaque, info: erts.ERL_NIF_TERM) callconv(.c) c_int {
    _ = priv;
    _ = info;
    mobBtAtomsInit(env);
    if (mobBtPairedInit() != 0) return -1;
    return 0;
}

// ── NIF table + init entry point ─────────────────────────────────────────

const nif_funcs = [_]erts.ErlNifFunc{
    .{ .name = "bt_list_paired", .arity = 0, .fptr = nif_bt_list_paired, .flags = 0 },
    .{ .name = "bt_start_discovery", .arity = 0, .fptr = nif_bt_start_discovery, .flags = 0 },
    .{ .name = "bt_cancel_discovery", .arity = 0, .fptr = nif_bt_cancel_discovery, .flags = 0 },
    .{ .name = "bt_make_discoverable", .arity = 1, .fptr = nif_bt_make_discoverable, .flags = 0 },
    .{ .name = "bt_pair", .arity = 1, .fptr = nif_bt_pair, .flags = 0 },
    .{ .name = "bt_unpair", .arity = 1, .fptr = nif_bt_unpair, .flags = 0 },
    .{ .name = "bt_disconnect", .arity = 1, .fptr = nif_bt_disconnect, .flags = 0 },
    .{ .name = "bt_hfp_connect", .arity = 1, .fptr = nif_bt_hfp_connect, .flags = 0 },
    .{ .name = "bt_hfp_subscribe_vendor_at", .arity = 2, .fptr = nif_bt_hfp_subscribe_vendor_at, .flags = 0 },
    .{ .name = "bt_hfp_send_vendor_at", .arity = 3, .fptr = nif_bt_hfp_send_vendor_at, .flags = 0 },
    .{ .name = "bt_hfp_start_sco", .arity = 1, .fptr = nif_bt_hfp_start_sco, .flags = 0 },
    .{ .name = "bt_hfp_stop_sco", .arity = 1, .fptr = nif_bt_hfp_stop_sco, .flags = 0 },
    .{ .name = "bt_spp_connect", .arity = 1, .fptr = nif_bt_spp_connect, .flags = 0 },
    .{ .name = "bt_spp_write", .arity = 2, .fptr = nif_bt_spp_write, .flags = erts.ERL_NIF_DIRTY_JOB_IO_BOUND },
    .{ .name = "ble_start_advertising", .arity = 1, .fptr = nif_ble_start_advertising, .flags = 0 },
    .{ .name = "ble_stop_advertising", .arity = 0, .fptr = nif_ble_stop_advertising, .flags = 0 },
    .{ .name = "ble_notify", .arity = 2, .fptr = nif_ble_notify, .flags = 0 },
};

var nif_entry: erts.ErlNifEntry = .{
    .major = erts.ERL_NIF_MAJOR_VERSION,
    .minor = erts.ERL_NIF_MINOR_VERSION,
    .name = "mob_bluetooth_nif",
    .num_of_funcs = nif_funcs.len,
    .funcs = &nif_funcs,
    .load = nifLoad,
    .reload = null,
    .upgrade = null,
    .unload = null,
    .vm_variant = erts.ERL_NIF_VM_VARIANT,
    .options = 1, // enable dirty-NIF support (spp_write is dirty IO).
    .sizeof_ErlNifResourceTypeInit = erts.SIZEOF_ErlNifResourceTypeInit,
    .min_erts = erts.ERL_NIF_MIN_ERTS_VERSION,
};

/// The symbol the BEAM looks up via the static NIF table (driver_tab) to find
/// this NIF's `ErlNifEntry`. RegenDriverTab extern-declares it as
/// `mob_bluetooth_nif_nif_init` over C ABI.
pub export fn mob_bluetooth_nif_nif_init() callconv(.c) *erts.ErlNifEntry {
    return &nif_entry;
}