/*
 * JNI bridge for VICE headless C64 emulation.
 *
 * Without VICE source (stub mode):
 *   initEmulator fills g_framebuf with a solid C64-blue — the app builds
 *   and runs normally; the display shows a placeholder.
 *
 * With VICE source (HAVE_VICE_SRC=1, set automatically by CMakeLists.txt
 * when vice-libs/<abi>/libvice.a exists):
 *   VICE runs in its own thread started by initEmulator().
 *   The custom video canvas callback writes each frame into g_framebuf.
 *   getVideoBuffer() snapshots g_framebuf into the Java ByteBuffer.
 *   injectKey() writes directly to VICE's keyboard matrix.
 */

#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <android/log.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,  "ViceJNI", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "ViceJNI", __VA_ARGS__)

#define FRAME_W 320
#define FRAME_H 200

static uint32_t        g_framebuf[FRAME_W * FRAME_H];
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static int             g_ready = 0;

/* Pending disk path — written by loadDisk (Android thread), consumed by
 * video_canvas_refresh (VICE thread).  autostart_disk must be called from
 * the VICE thread; calling it cross-thread corrupts VICE internal state. */
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
static char            g_pending_disk[512];

/* =========================================================================
 * VICE integration (compiled only when libvice.a is linked in)
 * ========================================================================= */
#ifdef HAVE_VICE_SRC
#include "vice.h"
#include "main.h"
#include "machine.h"
#include "maincpu.h"

/* Provided by the linker --wrap symbols */
extern void __real_maincpu_mainloop(void);
extern int  __real_init_main(void);
extern int  __real_machine_init(void);
extern int  __real_console_init(void);
#include "videoarch.h"
#include "video.h"
#include "palette.h"
#include "keyboard.h"
#include "autostart.h"

/* =========================================================================
 * Android OpenSL ES sound driver — registered with VICE before main_program()
 * so VICE can select it via -sounddev android.
 * ========================================================================= */
#include "sound.h"

/* Mute flag — 0 = silent (default), 1 = audio passes through. */
static volatile int g_sound_enabled = 0;

/* Lock-free-ish ring buffer (mutex-protected).
 * AUDIO_RING_SAMPLES must be a power of 2 for the masking trick.
 * 32768 × 2 bytes = 64 KB, about 0.34 s at 48 kHz mono. */
#define AUDIO_RING_SAMPLES (1u << 15)
static int16_t          g_audio_ring[AUDIO_RING_SAMPLES];
static unsigned int     g_ring_write = 0;
static unsigned int     g_ring_read  = 0;
static pthread_mutex_t  g_ring_mutex = PTHREAD_MUTEX_INITIALIZER;

/* OpenSL ES state */
static SLObjectItf g_sl_engine_obj  = NULL;
static SLEngineItf g_sl_engine      = NULL;
static SLObjectItf g_sl_out_mix_obj = NULL;
static SLObjectItf g_sl_player_obj  = NULL;
static SLPlayItf   g_sl_play        = NULL;
static SLAndroidSimpleBufferQueueItf g_sl_bq = NULL;

/* Four output buffers, each 2048 int16_t samples (~42 ms at 48 kHz) */
#define AUDIO_N_BUFS    4
#define AUDIO_BUF_SAMPS 2048
static int16_t g_sl_bufs[AUDIO_N_BUFS][AUDIO_BUF_SAMPS];
static int     g_sl_next_buf = 0;

static unsigned int ring_avail_locked(void) {
    return (g_ring_write - g_ring_read) & (AUDIO_RING_SAMPLES - 1);
}

/* OpenSL ES callback — called when a buffer finishes playing. */
static void sl_bufq_callback(SLAndroidSimpleBufferQueueItf bq, void *ctx) {
    (void)ctx;
    int buf = g_sl_next_buf;
    g_sl_next_buf = (g_sl_next_buf + 1) % AUDIO_N_BUFS;

    pthread_mutex_lock(&g_ring_mutex);
    unsigned int avail = ring_avail_locked();
    unsigned int fill  = (avail > AUDIO_BUF_SAMPS) ? AUDIO_BUF_SAMPS : avail;
    for (unsigned int i = 0; i < fill; i++)
        g_sl_bufs[buf][i] = g_audio_ring[(g_ring_read + i) & (AUDIO_RING_SAMPLES - 1)];
    g_ring_read = (g_ring_read + fill) & (AUDIO_RING_SAMPLES - 1);
    pthread_mutex_unlock(&g_ring_mutex);

    /* Silence for underrun */
    for (unsigned int i = fill; i < AUDIO_BUF_SAMPS; i++)
        g_sl_bufs[buf][i] = 0;

    (*bq)->Enqueue(bq, g_sl_bufs[buf], AUDIO_BUF_SAMPS * sizeof(int16_t));
}

static int android_sound_init(const char *param, int *speed,
                               int *fragsize, int *fragnr, int *channels) {
    (void)param;
    LOGI("android_sound_init: speed=%d frag=%d nr=%d ch=%d",
         *speed, *fragsize, *fragnr, *channels);
    if (*channels > 2) *channels = 2;
    if (*channels < 1) *channels = 1;

    SLresult r;
    r = slCreateEngine(&g_sl_engine_obj, 0, NULL, 0, NULL, NULL);
    if (r != SL_RESULT_SUCCESS) { LOGE("slCreateEngine: %d", (int)r); return -1; }
    (*g_sl_engine_obj)->Realize(g_sl_engine_obj, SL_BOOLEAN_FALSE);
    (*g_sl_engine_obj)->GetInterface(g_sl_engine_obj, SL_IID_ENGINE, &g_sl_engine);

    (*g_sl_engine)->CreateOutputMix(g_sl_engine, &g_sl_out_mix_obj, 0, NULL, NULL);
    (*g_sl_out_mix_obj)->Realize(g_sl_out_mix_obj, SL_BOOLEAN_FALSE);

    SLDataLocator_AndroidSimpleBufferQueue loc_bq = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, AUDIO_N_BUFS };
    SLDataFormat_PCM fmt = {
        SL_DATAFORMAT_PCM,
        (SLuint32)*channels,
        (SLuint32)*speed * 1000,  /* OpenSL ES uses milliHz */
        SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
        (*channels == 2) ? (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT)
                         : SL_SPEAKER_FRONT_CENTER,
        SL_BYTEORDER_LITTLEENDIAN };
    SLDataSource src = { &loc_bq, &fmt };
    SLDataLocator_OutputMix loc_out = { SL_DATALOCATOR_OUTPUTMIX, g_sl_out_mix_obj };
    SLDataSink   sink = { &loc_out, NULL };

    const SLInterfaceID iid[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
    const SLboolean     req[] = { SL_BOOLEAN_TRUE };
    r = (*g_sl_engine)->CreateAudioPlayer(g_sl_engine, &g_sl_player_obj,
                                           &src, &sink, 1, iid, req);
    if (r != SL_RESULT_SUCCESS) { LOGE("CreateAudioPlayer: %d", (int)r); return -1; }
    (*g_sl_player_obj)->Realize(g_sl_player_obj, SL_BOOLEAN_FALSE);
    (*g_sl_player_obj)->GetInterface(g_sl_player_obj, SL_IID_PLAY, &g_sl_play);
    (*g_sl_player_obj)->GetInterface(g_sl_player_obj,
                                      SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &g_sl_bq);
    (*g_sl_bq)->RegisterCallback(g_sl_bq, sl_bufq_callback, NULL);
    (*g_sl_play)->SetPlayState(g_sl_play, SL_PLAYSTATE_PLAYING);

    /* Prime the callback loop with AUDIO_N_BUFS silent buffers */
    for (int i = 0; i < AUDIO_N_BUFS; i++) {
        memset(g_sl_bufs[i], 0, sizeof(g_sl_bufs[i]));
        (*g_sl_bq)->Enqueue(g_sl_bq, g_sl_bufs[i], AUDIO_BUF_SAMPS * sizeof(int16_t));
    }
    g_sl_next_buf = 0;
    LOGI("android_sound_init: OpenSL ES ready");
    return 0;
}

static int android_sound_write(int16_t *pbuf, size_t nr) {
    if (!g_sound_enabled) return 0;
    pthread_mutex_lock(&g_ring_mutex);
    unsigned int free_space = (AUDIO_RING_SAMPLES - 1) - ring_avail_locked();
    unsigned int to_write   = ((unsigned int)nr < free_space) ? (unsigned int)nr : free_space;
    for (unsigned int i = 0; i < to_write; i++) {
        g_audio_ring[g_ring_write] = pbuf[i];
        g_ring_write = (g_ring_write + 1) & (AUDIO_RING_SAMPLES - 1);
    }
    pthread_mutex_unlock(&g_ring_mutex);
    return 0;
}

static int android_sound_bufferspace(void) {
    pthread_mutex_lock(&g_ring_mutex);
    int free_space = (int)((AUDIO_RING_SAMPLES - 1) - ring_avail_locked());
    pthread_mutex_unlock(&g_ring_mutex);
    return free_space;
}

static void android_sound_close(void) {
    if (g_sl_play)
        (*g_sl_play)->SetPlayState(g_sl_play, SL_PLAYSTATE_STOPPED);
    if (g_sl_player_obj) {
        (*g_sl_player_obj)->Destroy(g_sl_player_obj);
        g_sl_player_obj = NULL; g_sl_play = NULL; g_sl_bq = NULL;
    }
    if (g_sl_out_mix_obj) {
        (*g_sl_out_mix_obj)->Destroy(g_sl_out_mix_obj);
        g_sl_out_mix_obj = NULL;
    }
    if (g_sl_engine_obj) {
        (*g_sl_engine_obj)->Destroy(g_sl_engine_obj);
        g_sl_engine_obj = NULL; g_sl_engine = NULL;
    }
    LOGI("android_sound_close: done");
}

static const sound_device_t g_android_sound_device = {
    "android",
    android_sound_init,
    android_sound_write,
    NULL,   /* dump */
    NULL,   /* flush */
    android_sound_bufferspace,
    android_sound_close,
    NULL,   /* suspend */
    NULL,   /* resume */
    1,      /* need_attenuation */
    2,      /* max_channels */
    false   /* is_timing_source */
};

/* Stubs for symbols that live in arch-specific or excluded files. */
void main_exit(void) { LOGI("main_exit called — ignoring in JNI context"); }

/* All functions below replace arch/headless/video.c (excluded from libvice.a). */

int  video_arch_get_active_chip(void)              { return 0; /* VIDEO_CHIP_VICII */ }
void video_arch_canvas_init(video_canvas_t *c)     { (void)c; }
int  video_arch_cmdline_options_init(void)          { return 0; }
int  video_arch_resources_init(void)                { return 0; }
void video_arch_resources_shutdown(void)            {}
/* Return 1 so VICE uses visible_width/visible_height for canvas_width/canvas_height.
 * With return 0, VICE would use canvas_physical_width/scalex but physical is never
 * initialised, giving permanent 0x0 frames. */
char video_canvas_can_resize(video_canvas_t *c)    { (void)c; return 1; }
void video_canvas_destroy(video_canvas_t *c)       { (void)c; }
void video_canvas_resize(video_canvas_t *c, char r){ (void)c; (void)r; }
int  video_init(void)                               { return 0; }
void video_shutdown(void)                           {}

int video_canvas_set_palette(video_canvas_t *canvas, struct palette_s *palette) {
    canvas->palette = palette;
    if (!palette || !canvas->videoconfig) return 0;

    /* Force 1×1 pixel rendering.  VICE chip caps set rendermode=2 (PAL_NTSC_2X2)
     * by default for VICII, which doubles each pixel and row and only renders the
     * left half of the screen.  Override to 1 (PAL_NTSC_1X1) for correct
     * 320×200 output into our fixed-size framebuffer. */
    canvas->videoconfig->rendermode = VIDEO_RENDER_PAL_NTSC_1X1;

    /* Force no CRT filter.  VIDEO_FILTER_CRT (the default) routes to
     * render_32_1x1_pal which uses YUV lookup tables built by VICE's PAL color
     * science.  Those tables stay zero in the headless arch (no GPU/SDL setup),
     * so every output pixel is 0x00000000.  VIDEO_FILTER_NONE routes instead to
     * render_32_1x1_04, the simple renderer that reads from physical_colors[]
     * which we correctly populate below via video_render_setphysicalcolor. */
    canvas->videoconfig->filter = VIDEO_FILTER_NONE;

    /* Populate the physical color table so the render pipeline writes real pixels.
     * VICE never calls video_render_setphysicalcolor in the headless arch.
     * Bitmap.copyPixelsFromBuffer reads raw bytes as [R,G,B,A] per pixel.
     * On little-endian ARM a uint32_t 0xAABBGGRR has bytes [R,G,B,A] in memory,
     * so we pack the color as ABGR (not ARGB) to get the correct byte order. */
    for (unsigned int i = 0; i < palette->num_entries; i++) {
        uint32_t col = 0xFF000000u
                       | ((uint32_t)palette->entries[i].blue  << 16)
                       | ((uint32_t)palette->entries[i].green <<  8)
                       |  (uint32_t)palette->entries[i].red;
        video_render_setphysicalcolor(canvas->videoconfig, (int)i, col, 32);
    }
    return 0;
}

video_canvas_t *video_canvas_create(video_canvas_t *canvas,
                                    unsigned int *width, unsigned int *height,
                                    int mapped) {
    (void)mapped;
    canvas->created = 1;
    *width  = FRAME_W;
    *height = FRAME_H;
    return canvas;
}

/* Called by VICE each time a screen region needs updating.
 * The draw_buffer holds 8-bit palette indices (pitch = width in bytes).
 * video_canvas_render() converts them to 32bpp ARGB using the physical color
 * table we populated in video_canvas_set_palette(). */
void video_canvas_refresh(video_canvas_t *canvas,
                          unsigned int xs, unsigned int ys,
                          unsigned int xi, unsigned int yi,
                          unsigned int w,  unsigned int h) {
    /* Process any pending disk load on the VICE thread so autostart_disk is
     * never called cross-thread (it modifies complex internal VICE state). */
    pthread_mutex_lock(&g_pending_lock);
    if (g_pending_disk[0] != '\0') {
        char path[512];
        strncpy(path, g_pending_disk, sizeof(path) - 1);
        path[sizeof(path) - 1] = '\0';
        g_pending_disk[0] = '\0';
        pthread_mutex_unlock(&g_pending_lock);
        LOGI("autostart_disk: %s", path);
        int asd_ret = autostart_disk(8, 0, path, NULL, 0, AUTOSTART_MODE_RUN);
        LOGI("autostart_disk returned %d", asd_ret);
    } else {
        pthread_mutex_unlock(&g_pending_lock);
    }

    /* Clamp destination to our framebuffer bounds. */
    if (xi >= FRAME_W || yi >= FRAME_H) return;
    if (xi + w > FRAME_W) w = FRAME_W - xi;
    if (yi + h > FRAME_H) h = FRAME_H - yi;
    if (w == 0 || h == 0) return;

    pthread_mutex_lock(&g_lock);
    video_canvas_render(canvas, (uint8_t *)g_framebuf,
                        (int)w, (int)h,
                        (int)xs, (int)ys,
                        (int)xi, (int)yi,
                        FRAME_W * 4);
    pthread_mutex_unlock(&g_lock);
}

/* VICE main loop runs in this thread. */
static char  *g_vice_argv[20];
static int    g_vice_argc;

/* Read from fd line-by-line and forward each line to Android logcat.
 * Used to capture VICE stderr output which would otherwise be invisible. */
static void *stderr_reader_thread(void *arg) {
    int fd = *(int *)arg;
    free(arg);
    char line[512];
    int  pos = 0;
    char ch;
    while (read(fd, &ch, 1) == 1) {
        if (ch == '\n' || pos >= (int)sizeof(line) - 1) {
            line[pos] = '\0';
            if (pos > 0) LOGI("VICE: %s", line);
            pos = 0;
        } else {
            line[pos++] = ch;
        }
    }
    close(fd);
    return NULL;
}

static void *vice_thread(void *arg) {
    (void)arg;
    LOGI("vice_thread: starting main_program");
    int rc = main_program(g_vice_argc, g_vice_argv);
    LOGI("vice_thread: main_program returned %d", rc);
    return NULL;
}

/* archdep_vice_exit normally calls exit() and would kill the whole process.
 * --wrap=archdep_vice_exit redirects all calls here so only the VICE thread
 * terminates instead. */
void __wrap_archdep_vice_exit(int code) {
    LOGI("archdep_vice_exit(%d) — intercepted, terminating VICE thread", code);
    pthread_exit(NULL);
}

void machine_mainloop(void)       { __real_maincpu_mainloop(); }
void __wrap_maincpu_mainloop(void) { __real_maincpu_mainloop(); }
int  __wrap_init_main(void)        { return __real_init_main(); }
int  __wrap_machine_init(void)     { return __real_machine_init(); }
int  __wrap_console_init(void)     { return __real_console_init(); }
#endif /* HAVE_VICE_SRC */

/* =========================================================================
 * JNI entry points
 * ========================================================================= */
#define JNI_FN(ret, name) \
    JNIEXPORT ret JNICALL \
    Java_de_ladkau_schwertundmagieonpebblecompanionapp_C64Engine_##name

JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
#ifdef HAVE_VICE_SRC
    const char *dir = (*env)->GetStringUTFChars(env, romDir, NULL);
    LOGI("initEmulator: romDir=%s", dir);

    /* Prepare argv for VICE: x64 -directory <romDir> -headless */
    static char dir_copy[512];
    strncpy(dir_copy, dir, sizeof(dir_copy) - 1);
    (*env)->ReleaseStringUTFChars(env, romDir, dir);

    /* archdep_create_user_cache_dir/config_dir (called from archdep_init) need a
     * writable HOME to create ~/.cache/vice and ~/.config/vice.  On Android,
     * HOME is unset or points to a read-only root, so redirect it to romDir. */
    setenv("HOME", dir_copy, 1);

    /* sysfile_load() searches <Directory>/<machine_name>/<rom_name>.
     * We pass -directory <romDir> so VICE searches romDir/C64/.
     * The ROM files the user provides are named "kernal"/"basic"/"chargen";
     * VICE's compiled-in defaults are "kernal-901227-03.bin" etc., so we
     * also pass -kernal/-basic/-chargen to match the actual file names.
     * Android FUSE rejects symlinks, so we copy the files into romDir/C64/. */
    /* Copy C64 ROMs into romDir/C64/ (sysfile_load searches <dir>/<machine>/). */
    char vice_c64_dir[512];
    snprintf(vice_c64_dir, sizeof(vice_c64_dir), "%s/C64", dir_copy);
    mkdir(vice_c64_dir, 0755);
    const char *c64_roms[] = {"kernal", "basic", "chargen", NULL};
    for (int i = 0; c64_roms[i]; i++) {
        char src[512], dst[512];
        snprintf(src, sizeof(src), "%s/%s", dir_copy,     c64_roms[i]);
        snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, c64_roms[i]);
        FILE *fsrc = fopen(src, "rb");
        if (!fsrc) { LOGI("ROM not found: %s", src); continue; }
        FILE *fdst = fopen(dst, "wb");
        if (!fdst) { fclose(fsrc); LOGI("ROM copy failed: %s", dst); continue; }
        char buf[4096]; size_t n;
        while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
        fclose(fsrc); fclose(fdst);
        LOGI("ROM copied: %s", c64_roms[i]);
    }

    /* Copy 1541 drive ROM into romDir/DRIVES/1541 (sysfile_load path for drives).
     * Required for true 1541 emulation so the game's custom fast loader works.
     * User places the file named "1541" alongside kernal/basic/chargen. */
    char vice_drives_dir[512];
    snprintf(vice_drives_dir, sizeof(vice_drives_dir), "%s/DRIVES", dir_copy);
    mkdir(vice_drives_dir, 0755);
    {
        char src[512], dst[512];
        snprintf(src, sizeof(src), "%s/1541",  dir_copy);
        snprintf(dst, sizeof(dst), "%s/DRIVES/1541", dir_copy);
        FILE *fsrc = fopen(src, "rb");
        if (fsrc) {
            FILE *fdst = fopen(dst, "wb");
            if (fdst) {
                char buf[4096]; size_t n;
                while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
                fclose(fdst);
                LOGI("ROM copied: 1541");
            }
            fclose(fsrc);
        } else {
            LOGI("1541 ROM not found — drive may not work with fast loaders");
        }
    }

    static char dir_arg[512];
    snprintf(dir_arg, sizeof(dir_arg), "%s", dir_copy);

    /* -VICIIborders none  → 320×200 canvas, no borders.
     * -dos1541 1541       → drive ROM filename (overrides VICE's long default).
     * -iecdevice8         → enable IEC device emulation on unit 8 (sets the ID
     *                       bit in iecbus_device_index).  This is the flag that
     *                       actually routes IEC bus traffic for unit 8 to the
     *                       virtual device layer.  Without it the index resolves
     *                       to IECBUS_DEVICE_NONE → "DEVICE NOT PRESENT".
     * -virtualdev8        → enable KERNAL trap-based emulation on unit 8 (VD
     *                       bit).  Together with -iecdevice8 this gives index 3
     *                       → IECBUS_DEVICE_IECDEVICE and fast KERNAL traps.
     * -sounddev android   → select our custom OpenSL ES driver registered above
     *                       instead of VICE's built-in dummy (silent) driver. */
    g_vice_argc     = 17;
    g_vice_argv[0]  = "x64";
    g_vice_argv[1]  = "-directory";
    g_vice_argv[2]  = dir_arg;
    g_vice_argv[3]  = "-kernal";
    g_vice_argv[4]  = "kernal";
    g_vice_argv[5]  = "-basic";
    g_vice_argv[6]  = "basic";
    g_vice_argv[7]  = "-chargen";
    g_vice_argv[8]  = "chargen";
    g_vice_argv[9]  = "-VICIIborders";
    g_vice_argv[10] = "none";
    g_vice_argv[11] = "-dos1541";
    g_vice_argv[12] = "1541";
    g_vice_argv[13] = "-iecdevice8";
    g_vice_argv[14] = "-virtualdev8";
    g_vice_argv[15] = "-sounddev";
    g_vice_argv[16] = "android";
    g_vice_argv[17] = NULL;

    /* Redirect stdout and stderr to Android logcat.
     * VICE's archdep_open_default_log_file() detects a FIFO on stdout and
     * writes all log output there instead of opening a log file, so piping
     * stdout captures trap installation messages, autostart progress, etc.
     * Stderr captures VICE command-line parse errors. */
    int stdout_fds[2], stderr_fds[2];
    if (pipe(stdout_fds) == 0) {
        dup2(stdout_fds[1], STDOUT_FILENO);
        close(stdout_fds[1]);
        int *rfd = malloc(sizeof(int));
        *rfd = stdout_fds[0];
        pthread_t stdout_tid;
        pthread_create(&stdout_tid, NULL, stderr_reader_thread, rfd);
        pthread_detach(stdout_tid);
    }
    if (pipe(stderr_fds) == 0) {
        dup2(stderr_fds[1], STDERR_FILENO);
        close(stderr_fds[1]);
        int *rfd = malloc(sizeof(int));
        *rfd = stderr_fds[0];
        pthread_t stderr_tid;
        pthread_create(&stderr_tid, NULL, stderr_reader_thread, rfd);
        pthread_detach(stderr_tid);
    }

    /* Register the android sound driver before VICE's sound_open() runs.
     * pthread_create provides a memory barrier so the VICE thread sees our write. */
    sound_register_device(&g_android_sound_device);

    pthread_t tid;
    int rc = pthread_create(&tid, NULL, vice_thread, NULL);
    pthread_detach(tid);
    g_ready = (rc == 0);
    return (jboolean)g_ready;
#else
    (void)env; (void)obj; (void)romDir;
    /* Fill with C64-blue placeholder so the UI works without VICE. */
    for (int i = 0; i < FRAME_W * FRAME_H; i++)
        g_framebuf[i] = 0xFF6464C0u;
    g_ready = 1;
    return JNI_TRUE;
#endif
}

JNI_FN(jboolean, loadDisk)(JNIEnv *env, jobject obj, jstring path) {
    (void)obj;
#ifdef HAVE_VICE_SRC
    const char *p = (*env)->GetStringUTFChars(env, path, NULL);
    /* Queue the disk path for autostart on the VICE thread.
     * autostart_disk must not be called cross-thread — video_canvas_refresh
     * picks this up on the next frame and calls it from the VICE thread. */
    pthread_mutex_lock(&g_pending_lock);
    strncpy(g_pending_disk, p, sizeof(g_pending_disk) - 1);
    g_pending_disk[sizeof(g_pending_disk) - 1] = '\0';
    pthread_mutex_unlock(&g_pending_lock);
    (*env)->ReleaseStringUTFChars(env, path, p);
    return JNI_TRUE;
#else
    (void)env; (void)path;
    return JNI_FALSE;
#endif
}

/* In thread mode runFrame() is a no-op: VICE drives its own loop. */
JNI_FN(void, runFrame)(JNIEnv *env, jobject obj) {
    (void)env; (void)obj;
}

JNI_FN(void, getVideoBuffer)(JNIEnv *env, jobject obj, jobject byteBuffer) {
    (void)obj;
    uint8_t *dst = (uint8_t *)(*env)->GetDirectBufferAddress(env, byteBuffer);
    if (!dst) return;
    pthread_mutex_lock(&g_lock);
    memcpy(dst, g_framebuf, FRAME_W * FRAME_H * 4);
    pthread_mutex_unlock(&g_lock);
}

JNI_FN(void, injectKey)(JNIEnv *env, jobject obj, jint keyCode, jboolean pressed) {
    (void)env; (void)obj;
#ifdef HAVE_VICE_SRC
    int row = (keyCode >> 8) & 0xFF;
    int col =  keyCode       & 0xFF;
    keyboard_set_keyarr(row, col, pressed ? 1 : 0);
#else
    (void)keyCode; (void)pressed;
#endif
}

JNI_FN(void, setSoundEnabled)(JNIEnv *env, jobject obj, jboolean enabled) {
    (void)env; (void)obj;
    g_sound_enabled = enabled ? 1 : 0;
}
