/*
 Copyright (c) 2016, Jeffrey Pfau
 Copyright (c) 2020-2026, Rupert Carmichael

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:
     * Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
     * Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ''AS IS''
 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include "mgba-util/common.h"

#include "mgba/core/blip_buf.h"
#include "mgba/core/core.h"
#include "mgba/core/cheats.h"
#include "mgba/core/serialize.h"
#include "mgba/gba/core.h"
#include "mgba/internal/gba/cheats.h"
#include "mgba/internal/gba/input.h"
#include "mgba-util/circle-buffer.h"
#include "mgba-util/memory.h"
#include "mgba-util/vfs.h"
#include "version.h"

#include <jg/jg.h>
#include <jg/jg_gba.h>
#if JG_VERSION_NUMBER < 10100
#include "jg_compat.h"
#endif

#define SAMPLERATE 48000
#define FRAMERATE 60
#define CHANNELS 2
#define NUMINPUTS 1
#define TOTALINPUTS 1

static jg_cb_audio_t jg_cb_audio;
static jg_cb_frametime_t jg_cb_frametime;
static jg_cb_log_t jg_cb_log;
static jg_cb_rumble_t jg_cb_rumble;

#if JG_VERSION_NUMBER >= 10100
static void *udata_audio;
static void *udata_frametime;
static void *udata_rumble;
#endif

static jg_coreinfo_t coreinfo = {
    "mgba", "mGBA", JG_VERSION, "gba", NUMINPUTS, 0
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XBGR8888, 240, 160, 240, 160, 0, 0, 240, 240.0/160.0, NULL
};

static jg_audioinfo_t audinfo = {
    JG_SAMPFMT_INT16,
    SAMPLERATE,
    CHANNELS,
    (SAMPLERATE / FRAMERATE) * CHANNELS,
    NULL
};

static jg_pathinfo_t pathinfo;
static jg_fileinfo_t gameinfo;
static jg_inputinfo_t inputinfo[NUMINPUTS];
static jg_inputinfo_t inputlist[TOTALINPUTS];
static jg_inputstate_t *input_device[NUMINPUTS];

static int inputlist_built = 0;

static jg_systeminfo_t systemlist[] = {
    { "gba", "Game Boy Advance", "gba" },
};

#ifdef _WIN32
void *FOLDERID_RoamingAppData = NULL;
#endif

const char* const projectName = "mGBA";
const char* const projectVersion = JG_VERSION;
const char* const binaryName = "mGBA";
static struct mCore *core = NULL;
static struct mLogger logger;
static struct mCheatDevice *cheatDevice = NULL;
static struct mCheatSet *cheatSet = NULL;

void mgba_log(struct mLogger* logger, int category, enum mLogLevel level,
    const char* format, va_list args) {
    // Do nothing for now, this just suppresses ugly log data
}

static const int GBAMap[] = {
    GBA_KEY_UP, GBA_KEY_DOWN, GBA_KEY_LEFT, GBA_KEY_RIGHT,
    GBA_KEY_SELECT, GBA_KEY_START, GBA_KEY_A, GBA_KEY_B, GBA_KEY_L, GBA_KEY_R
};

static void mgba_input_poll(void) {
    uint16_t keys = 0;

    for (int i = 0; i < NDEFS_GBA; i++)
        if (input_device[0]->button[i]) keys |= 1 << GBAMap[i];

    core->setKeys(core, keys);
}

#if JG_VERSION_NUMBER >= 10100
void jg_set_cb_audio(jg_cb_audio_t func, void *ud) {
    jg_cb_audio = func;
    udata_audio = ud;
}

void jg_set_cb_frametime(jg_cb_frametime_t func, void *ud) {
    jg_cb_frametime = func;
    udata_frametime = ud;
}
#else
void jg_set_cb_audio(jg_cb_audio_t func) {
    jg_cb_audio = func;
}

void jg_set_cb_frametime(jg_cb_frametime_t func) {
    jg_cb_frametime = func;
}
#endif

void jg_set_cb_log(jg_cb_log_t func) {
    jg_cb_log = func;
}

#if JG_VERSION_NUMBER >= 10100
void jg_set_cb_rumble(jg_cb_rumble_t func, void *ud) {
    jg_cb_rumble = func;
    udata_rumble = ud;
}
#else
void jg_set_cb_rumble(jg_cb_rumble_t func) {
    jg_cb_rumble = func;
}
#endif

int jg_init(void) {
    logger.log = mgba_log; // Set a log callback to avoid ugly debug output
    mLogSetDefaultLogger(&logger);

    core = GBACoreCreate();
    mCoreInitConfig(core, NULL);

    struct mCoreOptions opts = {
        .useBios = true,
    };

    mCoreConfigLoadDefaults(&core->config, &opts);
    core->init(core);

    core->desiredVideoDimensions(core, &vidinfo.w, &vidinfo.h);
    core->setAudioBufferSize(core, audinfo.spf);

    blip_set_rates(core->getAudioChannel(core, 0),
        core->frequency(core), SAMPLERATE);
    blip_set_rates(core->getAudioChannel(core, 1),
        core->frequency(core), SAMPLERATE);

#if JG_VERSION_NUMBER >= 10100
    jg_cb_frametime(udata_frametime,
        core->frequency(core) / (double) core->frameCycles(core));
#else
    jg_cb_frametime(core->frequency(core) / (double) core->frameCycles(core));
#endif

    return 1;
}

void jg_deinit(void) {
    mCoreConfigDeinit(&core->config);
    core->deinit(core);
}

void jg_reset(int hard) {
    core->reset(core);
}

void jg_exec_frame(void) {
    mgba_input_poll();
    core->runFrame(core);

    int16_t *abuf = audinfo.buf;
    size_t nsamps = 0;
    nsamps = blip_samples_avail(core->getAudioChannel(core, 0));
    blip_read_samples(core->getAudioChannel(core, 0), abuf, nsamps, true);
    blip_read_samples(core->getAudioChannel(core, 1), abuf + 1, nsamps, true);
#if JG_VERSION_NUMBER >= 10100
    jg_cb_audio(udata_audio, nsamps * CHANNELS);
#else
    jg_cb_audio(nsamps * CHANNELS);
#endif
}

int jg_game_load(void) {
    if (core->dirs.save) {
        core->dirs.save->close(core->dirs.save);
    }
    core->dirs.save = VDirOpen(pathinfo.save);

    struct VFile *rom;
    rom = VFileFromMemory(gameinfo.data, gameinfo.size);
    core->loadROM(core, rom);

    char sav[256];
    snprintf(sav, sizeof(sav), "%s/%s.sav", pathinfo.save, gameinfo.name);
    struct VFile *savfile = VFileOpen(sav, O_CREAT | O_RDWR);
    core->loadSave(core, savfile);

    inputinfo[0] = jg_gba_inputinfo(0, 0);

    return 1;
}

int jg_game_unload(void) {
    return 1;
}

int jg_state_load(const char *filename) {
    struct VFile* vf = VFileOpen(filename, O_RDONLY);
    int ret = 0;
    if (vf) {
        ret = mCoreLoadStateNamed(core, vf, 0);
        vf->close(vf);
    }
    return ret;
}

void jg_state_load_raw(const void *data) {
    if (data) { }
}

int jg_state_save(const char *filename) {
    struct VFile* vf = VFileOpen(filename, O_CREAT | O_TRUNC | O_RDWR);
    int ret = 0;
    if (vf) {
        ret = mCoreSaveStateNamed(core, vf, 0);
        vf->close(vf);
    }
    return ret;
}

const void* jg_state_save_raw(void) {
    return NULL;
}

size_t jg_state_size(void) {
    return 0;
}

void jg_media_select(void) {
}

void jg_media_insert(void) {
}

void jg_media_mount(unsigned mount) {
    (void)mount;
}

void jg_media_set(unsigned id) {
    (void)id;
}

void jg_cheat_clear(void) {
    if (cheatSet) {
        cheatSet->enabled = false;
        cheatSet->refresh(cheatSet, cheatDevice);
        mCheatRemoveSet(cheatDevice, cheatSet);
        cheatSet->deinit(cheatSet);
        cheatSet = NULL;
    }
}

void jg_cheat_set(const char *code) {
    if (!cheatDevice)
        cheatDevice = core->cheatDevice(core);

    if (!cheatSet) {
        cheatSet = cheatDevice->createSet(cheatDevice, NULL);
        mCheatAddSet(cheatDevice, cheatSet);
    }

    mCheatAddLine(cheatSet, code, 0);
    cheatSet->refresh(cheatSet, cheatDevice);
}

void jg_rehash(void) {
}

void jg_data_push(uint32_t type, int port, const void *ptr, size_t size) {
    if (type || port || ptr || size) { }
}

jg_systeminfo_t* jg_get_systemlist(size_t *num) {
    *num = sizeof(systemlist) / sizeof(jg_systeminfo_t);
    return systemlist;
}

jg_coreinfo_t* jg_get_coreinfo(const char *sys) {
    return &coreinfo;
}

jg_videoinfo_t* jg_get_videoinfo(void) {
    return &vidinfo;
}

jg_audioinfo_t* jg_get_audioinfo(void) {
    return &audinfo;
}

jg_inputinfo_t* jg_get_inputinfo(int port) {
    return &inputinfo[port];
}

jg_inputinfo_t* jg_get_inputlist(size_t *num) {
    if (!inputlist_built) {
        inputlist[0] = jg_gba_inputinfo(0, 0);
        inputlist_built = 1;
    }
    *num = TOTALINPUTS;
    return inputlist;
}

jg_setting_t* jg_get_settings(size_t *numsettings) {
    *numsettings = 0;
    return NULL;
}

jg_setting_t* jg_get_dips(size_t *num) {
    *num = 0;
    return NULL;
}

jg_mediainfo_t* jg_get_mediainfo(void) {
    return NULL;
}

jg_biosinfo_t* jg_get_bioslist(size_t *num) {
    *num = 0;
    return NULL;
}

void jg_setup_video(void) {
    core->setVideoBuffer(core, vidinfo.buf, vidinfo.wmax);
}

void jg_setup_audio(void) {
}

void jg_set_inputstate(jg_inputstate_t *ptr, int port) {
    input_device[port] = ptr;
}

void jg_set_gameinfo(jg_fileinfo_t info) {
    gameinfo = info;
}

void jg_set_auxinfo(jg_fileinfo_t info, int index) {
    if (info.size || index) { }
}

void jg_set_paths(jg_pathinfo_t paths) {
    pathinfo = paths;
}

unsigned jg_api_version(void) {
    return JG_VERSION_NUMBER;
}
