/**
 * kusamoji mmap N-API addon.
 *
 * Exposes mmapFile(filePath) → ArrayBuffer backed by mmap'd file (read-only).
 *
 * The returned ArrayBuffer can be wrapped in any TypedArray (Int32Array,
 * Uint8Array, etc.) for zero-copy access to dictionary .dat files. The
 * OS manages page faults and eviction — only accessed pages become
 * resident in RAM.
 *
 * SAFETY: The external ArrayBuffer is NOT transferable/detachable. If
 * JS code calls .transfer() or structuredClone(), the buffer is copied
 * (not moved), preventing use-after-munmap in worker threads.
 *
 * Build: cd src/native && npx node-gyp rebuild
 * Requires: Node.js >= 14 (N-API 6+), POSIX (macOS/Linux)
 */

#include <node_api.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

/* ── Release callback: munmap when JS GC's the ArrayBuffer ──────────── */

typedef struct {
    void *addr;
    size_t len;
} mmap_info;

static void mmap_buffer_finalize(napi_env env, void *data, void *hint) {
    mmap_info *info = (mmap_info *)hint;
    if (info && info->addr && info->addr != MAP_FAILED) {
        munmap(info->addr, info->len);
    }
    free(info);
}

/* ── mmapFile(filePath: string) → ArrayBuffer ────────────────────────── */

static napi_value MmapFile(napi_env env, napi_callback_info cbinfo) {
    size_t argc = 1;
    napi_value argv[1];
    napi_get_cb_info(env, cbinfo, &argc, argv, NULL, NULL);

    if (argc < 1) {
        napi_throw_error(env, NULL, "mmapFile requires a file path argument");
        return NULL;
    }

    /* Extract file path string */
    char path[4096];
    size_t path_len;
    napi_status status = napi_get_value_string_utf8(env, argv[0], path, sizeof(path), &path_len);
    if (status != napi_ok) {
        napi_throw_error(env, NULL, "mmapFile: first argument must be a string");
        return NULL;
    }

    /* Open file */
    int fd = open(path, O_RDONLY);
    if (fd < 0) {
        char msg[4300];
        snprintf(msg, sizeof(msg), "mmapFile: cannot open '%s': %s", path, strerror(errno));
        napi_throw_error(env, NULL, msg);
        return NULL;
    }

    /* Get file size */
    struct stat st;
    if (fstat(fd, &st) < 0) {
        char msg[4300];
        snprintf(msg, sizeof(msg), "mmapFile: fstat failed for '%s': %s", path, strerror(errno));
        close(fd);
        napi_throw_error(env, NULL, msg);
        return NULL;
    }
    size_t file_size = (size_t)st.st_size;

    if (file_size == 0) {
        close(fd);
        napi_throw_error(env, NULL, "mmapFile: file is empty");
        return NULL;
    }

    /* mmap the file read-only, private */
    void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd); /* fd can be closed after mmap */

    if (addr == MAP_FAILED) {
        char msg[4300];
        snprintf(msg, sizeof(msg), "mmapFile: mmap failed for '%s': %s", path, strerror(errno));
        napi_throw_error(env, NULL, msg);
        return NULL;
    }

    /* Use MADV_RANDOM — the Viterbi does random-access lookups into the
       trie, not sequential scans. MADV_WILLNEED would prefetch the entire
       file, defeating the lazy-paging benefit of mmap. */
    madvise(addr, file_size, MADV_RANDOM);

    /* Allocate info struct for the release callback */
    mmap_info *info = (mmap_info *)malloc(sizeof(mmap_info));
    if (!info) {
        munmap(addr, file_size);
        napi_throw_error(env, NULL, "mmapFile: malloc failed");
        return NULL;
    }
    info->addr = addr;
    info->len = file_size;

    /* Create an external ArrayBuffer backed by the mmap'd region.
       When the JS ArrayBuffer is GC'd, mmap_buffer_finalize is called
       to munmap.

       IMPORTANT: External ArrayBuffers created via napi_create_external_arraybuffer
       are NOT transferable by default in Node.js — .transfer() and
       structuredClone() will copy the data, not move it. This is the
       correct behavior: the original mmap region stays valid until GC,
       and any copy made by transfer is an independent heap allocation.
       No use-after-munmap is possible. */
    napi_value result;
    status = napi_create_external_arraybuffer(
        env, addr, file_size, mmap_buffer_finalize, info, &result
    );
    if (status != napi_ok) {
        munmap(addr, file_size);
        free(info);
        napi_throw_error(env, NULL, "mmapFile: failed to create ArrayBuffer");
        return NULL;
    }

    return result;
}

/* ── Module init ─────────────────────────────────────────────────────── */

static napi_value Init(napi_env env, napi_value exports) {
    napi_value fn_mmap;
    napi_create_function(env, "mmapFile", NAPI_AUTO_LENGTH, MmapFile, NULL, &fn_mmap);
    napi_set_named_property(env, exports, "mmapFile", fn_mmap);
    return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
