/*******************************************************************************
The Jolly Good Genesis Plus GX Port

Copyright Eke-Eke (2007-2018)
Copyright OpenEmu Team (2018)
Copyright Rupert Carmichael (2020-2022)

Redistribution and use of this code or any derivative works are permitted
provided that the following conditions are met:

- Redistributions may not be sold, nor may they be used in a commercial
  product or activity.

- Redistributions that are modified from the original source must include the
  complete source code, including the source code for all components used by a
  binary built from the modified sources. However, as a special exception, the
  source code distributed need not include anything that is normally distributed
  (in either source or binary form) with the major components (compiler, kernel,
  and so on) of the operating system on which the executable runs, unless that
  component itself accompanies the executable.

- Redistributions 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 OWNER 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 "shared.h"
#include "inputdb.h"

#include "cheats.h"
#include "version.h"

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

#define SAMPLERATE 48000
#define FRAMERATE 60
#define CHANNELS 2
#define NUMINPUTS MAX_INPUTS
#define TOTALINPUTS 18 // 18 MD

#define ASPECT_PAL 1.4257812
#define FRAMERATE_PAL 50

#define VIDEO_WIDTH_GG 160
#define VIDEO_HEIGHT_GG 144
#define VIDEO_ASPECT_GG (160.0/144.0)

void (*gp_input_poll[NUMINPUTS])(int);

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 = {
    "genplus", "Genesis Plus GX", JG_VERSION, "md", NUMINPUTS, JG_HINT_CHEATS
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888, // pixfmt
    720,                // wmax
    576,                // hmax
    320,                // w
    224,                // h
    0,                  // x
    0,                  // y
    720,                // p
    292.0/224.0,        // aspect
    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];

// Emulator settings
static jg_setting_t settings_gp[] = {
    { "menacer_crosshair", "Menacer Crosshair",
      "0 = Disable, 1 = Enable",
      "Show a crosshair on screen when the Menacer is enabled",
      0, 0, 1, 1
    },
    { "region", "Region",
      "0 = Auto, 1 = USA, 2 = EUROPE, 3 = JAPAN/NTSC, 4 = JAPAN/PAL",
      "Set the region to use",
      0, 0, 4, 1
    },
    { "ym2413_enable", "SMS YM2413 FM Audio",
      "0 = Disable, 1 = Enable, 2 = Auto",
      "Use the YM2413 FM Synthesizer in supported games",
      1, 0, 2, 1
    },
    { "ym2413_mode", "YM2413 Mode",
      "0 = Nuked, 1 = MAME",
      "",
      0, 0, 1, 1
    },
    { "ym2612_mode", "YM2612 Mode",
      "0 = YM2612-Nuked, 1 = YM3438-Nuked, 2 = YM2612-MAME,"
      "3 = YM3438-MAME, 4 = YM3438-Enhanced-MAME",
      "Choose the emulation core and revision for YM2612 audio",
      0, 0, 4, 1
    }
};

enum {
    MCROSSHAIR,
    REGION,
    YM2413ENABLE,
    YM2413MODE,
    YM2612MODE
};

static jg_biosinfo_t bioslist[] = {
    { "bios_CD_U.bin", "Sega CD 2 (USA) (v2.00W)",
      "854b9150240a198070150e4566ae1290", 0, 0 },
    { "bios_CD_J.bin", "Mega-CD 2 (Japan) (v2.00C)",
      "683a8a9e273662561172468dfa2858eb", 0, 0 },
    { "bios_CD_E.bin", "Mega-CD 2 (Europe) (v2.00W)",
      "b10c0a97abc57b758497d3fae6ab35a4", 0, 0 },
};

static jg_systeminfo_t systemlist[] = {
    { "sms", "Sega Master System", "sms" },
    { "gg", "Sega Game Gear", "gg" },
    { "md", "Sega Mega Drive/Genesis", "md,gen" },
    { "md", "Mega CD/Sega CD", "cue" },
};

static uint8_t brm_format[0x40] = {
    0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
    0x5f, 0x5f, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x40,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x53, 0x45, 0x47, 0x41, 0x5f, 0x43, 0x44, 0x5f,
    0x52, 0x4f, 0x4d, 0x00, 0x01, 0x00, 0x00, 0x00,
    0x52, 0x41, 0x4d, 0x5f, 0x43, 0x41, 0x52, 0x54,
    0x52, 0x49, 0x44, 0x47, 0x45, 0x5f, 0x5f, 0x5f
};

t_config config;

char GG_ROM[256];
char AR_ROM[256];
char SK_ROM[256];
char SK_UPMEM[256];
char GG_BIOS[256];
char MD_BIOS[256];
char CD_BIOS_EU[256];
char CD_BIOS_US[256];
char CD_BIOS_JP[256];
char MS_BIOS_US[256];
char MS_BIOS_EU[256];
char MS_BIOS_JP[256];

static int inputlist_built = 0;

void gp_input_poll_null(int port) { } // Called if nothing is plugged in

static const unsigned MDMap[] = {
    INPUT_UP, INPUT_DOWN, INPUT_LEFT, INPUT_RIGHT, INPUT_MODE, INPUT_START,
    INPUT_A, INPUT_B, INPUT_C, INPUT_X, INPUT_Y, INPUT_Z
};

void gp_input_poll_pad(int port) {
    unsigned newinput = 0;

    // Offset the port number if necessary
    unsigned offset = (port && input.system[0] != SYSTEM_TEAMPLAYER
        && (input.system[1] == SYSTEM_TEAMPLAYER
        || input.system[1] == SYSTEM_GAMEPAD)) ? 3 : 0;

    for (int i = 0; i < NDEFS_MDPAD6B; i++)
        if (input_device[port]->button[i]) newinput |= MDMap[i];

    input.pad[port + offset] = newinput;
}

void gp_input_poll_justifier(int port) {
    input.analog[4][0] = input_device[port]->coord[0];
    input.analog[4][1] = input_device[port]->coord[1];

    unsigned newinput = 0;
    if (input_device[port]->button[0]) newinput |= INPUT_A;
    if (input_device[port]->button[1]) {
        newinput |= INPUT_A;
        input.analog[4][0] = 800;
        input.analog[4][1] = 800;
    }
    if (input_device[port]->button[2]) newinput |= INPUT_START;
    input.pad[4] = newinput;
}

void gp_input_poll_lightphaser(int port) {
    unsigned offset = port ? 3 : 0;
    input.analog[port + offset][0] = input_device[port]->coord[0];
    input.analog[port + offset][1] = input_device[port]->coord[1];

    unsigned newinput = 0;
    if (input_device[port + offset]->button[0]) newinput |= INPUT_A;
    if (input_device[port + offset]->button[1]) {
        newinput |= INPUT_A;
        input.analog[port + offset][0] = 800;
        input.analog[port + offset][1] = 800;
    }
    if (input_device[port]->button[2]) newinput |= INPUT_START;
    input.pad[port + offset] = newinput;
}

void gp_input_poll_menacer(int port) {
    input.analog[4][0] = input_device[port]->coord[0];
    input.analog[4][1] = input_device[port]->coord[1];
    unsigned newinput = 0;
    if (input_device[port]->button[0]) newinput |= INPUT_A;
    if (input_device[port]->button[1]) {
        newinput |= INPUT_A;
        input.analog[4][0] = 800;
        input.analog[4][1] = 800;
    }
    if (input_device[port]->button[2]) newinput |= INPUT_B;
    if (input_device[port]->button[3]) newinput |= INPUT_C;
    if (input_device[port]->button[4]) newinput |= INPUT_START;
    input.pad[4] = newinput;
}

void gp_input_setup(void) {
    // Autodetect the input type - some games may need overrides
    uint8_t padtype = DEVICE_PAD2B | DEVICE_PAD3B | DEVICE_PAD6B;
    uint8_t itype = 0; // Input Type

    for (int i = 0; i < NUMINPUTS; i++) { // Set default input state poll
        gp_input_poll[i] = &gp_input_poll_null;
    }

    // Check for overrides - Pre-sort and do binary search in the future?
    for (int i = 0; i < sizeof(idb_list) / sizeof(idb_entry_t); i++) {
        if (!strcmp(gameinfo.md5, idb_list[i].md5)) {
            itype = idb_list[i].type;
            break;
        }
    }

    // Override to force 6-button controller in games missing '6' in the header
    if (itype & INPUT_DB_6B) { padtype = DEVICE_PAD6B; }

    // 1-4 players: TeamPlayer in Port 1
    if (itype & INPUT_DB_TP1) {
        input.system[0] = SYSTEM_TEAMPLAYER;
        for (int i = 0; i < 4; i++) {
            config.input[i].padtype = padtype;
            gp_input_poll[i] = &gp_input_poll_pad;
            inputinfo[i] = jg_md_inputinfo(i, JG_MD_PAD6B);
        }
    }
    // 1-4/5 players: Gamepad Port 1, TeamPlayer Port 2
    else if (itype & INPUT_DB_TP2) {
        input.system[0] = SYSTEM_GAMEPAD;
        input.system[1] = SYSTEM_TEAMPLAYER;
        config.input[0].padtype = padtype;
        gp_input_poll[0] = &gp_input_poll_pad;
        for (int i = 0; i < 4; i++) {
            config.input[4 + i].padtype = padtype;
            gp_input_poll[1 + i] = &gp_input_poll_pad;
        }
        inputinfo[0] = jg_md_inputinfo(0, JG_MD_PAD6B);
        inputinfo[1] = jg_md_inputinfo(1, JG_MD_PAD6B);
        inputinfo[2] = jg_md_inputinfo(2, JG_MD_PAD6B);
        inputinfo[3] = jg_md_inputinfo(3, JG_MD_PAD6B);
        inputinfo[4] = jg_md_inputinfo(4, JG_MD_PAD6B);
    }
    // 1-8 players: TeamPlayer in Port 1, TeamPlayer Port 2
    else if (itype & INPUT_DB_TPBOTH) {
        input.system[0] = SYSTEM_TEAMPLAYER;
        input.system[1] = SYSTEM_TEAMPLAYER;
        for (int i = 0; i < 8; i++) {
            config.input[i].padtype = padtype;
            gp_input_poll[i] = &gp_input_poll_pad;
            inputinfo[i] = jg_md_inputinfo(i, JG_MD_PAD6B);
        }
    }
    // 1-4 players: EA 4-Way Play
    else if (itype & INPUT_DB_EA4WAY) {
        input.system[0] = input.system[1] = SYSTEM_WAYPLAY;
        for (int i = 0; i < 4; i++) {
            config.input[i].padtype = padtype;
            gp_input_poll[i] = &gp_input_poll_pad;
            inputinfo[i] = jg_md_inputinfo(i, JG_MD_PAD6B);
        }
    }
    else if (input.system[1] == SYSTEM_JUSTIFIER) {
        config.input[0].padtype = padtype;
        gp_input_poll[0] = &gp_input_poll_pad;
        gp_input_poll[1] = &gp_input_poll_justifier;
        inputinfo[0] = jg_md_inputinfo(0, JG_MD_PAD6B);
        inputinfo[1] = jg_md_inputinfo(1, JG_MD_JUSTIFIER);
    }
    else if (input.system[1] == SYSTEM_MENACER) {
        config.input[0].padtype = padtype;
        gp_input_poll[0] = &gp_input_poll_pad;
        gp_input_poll[1] = &gp_input_poll_menacer;
        inputinfo[0] = jg_md_inputinfo(0, JG_MD_PAD6B);
        inputinfo[1] = jg_md_inputinfo(1, JG_MD_MENACER |
            (settings_gp[MCROSSHAIR].val ? 0x80 : 0x00));
    }
    else { // Default
        input.system[0] = input.system[1] = SYSTEM_GAMEPAD;
        config.input[0].padtype = config.input[4].padtype = padtype;
        gp_input_poll[0] = gp_input_poll[1] = &gp_input_poll_pad;
        inputinfo[0] = jg_md_inputinfo(0, JG_MD_PAD6B);
        inputinfo[1] = jg_md_inputinfo(1, JG_MD_PAD6B);
    }
}

void gp_input_setup_sms(void) {
    if (input.system[0] == SYSTEM_LIGHTPHASER) {
        gp_input_poll[0] = &gp_input_poll_lightphaser;
        inputinfo[0] = jg_sms_inputinfo(0, JG_SMS_LIGHTPHASER);
    }
}

static inline void gp_checkext(char *ext) {
    // File extension is used to determine system type
    if (ext) {
        memcpy(ext, &gameinfo.fname[strlen(gameinfo.fname) - 3], 3);
        ext[3] = 0;
    }
}

int load_archive(char *filename, unsigned char *buf, int maxsize, char *ext) {
    // Extension is .cue
    if (strstr(gameinfo.path, ".cue") || strstr(gameinfo.path, ".CUE")) {
        FILE *file = fopen(filename, "rb");
        if (!file) {
            jg_cb_log(JG_LOG_ERR, "Failed to load file: %s\n", filename);
            return 0;
        }

        fseek(file, 0, SEEK_END);
        int size = ftell(file);
        rewind(file);

        if (!fread(buf, size, 1, file)) {
            fclose(file);
            return 0;
        }

        fclose(file);

        gp_checkext(ext);
        return size;
    }
    else {
        gp_checkext(ext);
        memcpy(buf, gameinfo.data, gameinfo.size);
        return gameinfo.size;
    }
}

void osd_input_update(void) {
    for (int i = 0; i < NUMINPUTS; i++) gp_input_poll[i](i);

    /* Update RAM patches */
    RAMCheatUpdate();
}

static void gp_audio_push(void) {
#if JG_VERSION_NUMBER >= 10100
    jg_cb_audio(udata_audio, audio_update(audinfo.buf) * audinfo.channels);
#else
    jg_cb_audio(audio_update(audinfo.buf) * audinfo.channels);
#endif
}

#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) {
    // sound options
    config.psg_preamp     = 150;
    config.fm_preamp      = 100;
    config.cdda_volume    = 100;
    config.pcm_volume     = 100;
    config.hq_fm          = 1;
    config.hq_psg         = 1;
    config.filter         = 1;
    config.low_freq       = 200;
    config.high_freq      = 8000;
    config.lg             = 100;
    config.mg             = 100;
    config.hg             = 100;
    config.lp_range       = 0x9999; // 0.6 in 0.16 fixed point
    config.mono           = 0;
    config.ym3438         = 0; // Default 0, turn on with options
    config.cd_latency     = 1;

    // YM2413 Options
    config.ym2413 = settings_gp[YM2413ENABLE].val;
    config.opll = settings_gp[YM2413MODE].val ? 0 : 1;

    // YM2612 Options
    switch (settings_gp[YM2612MODE].val) {
        case 0: // YM2612-Nuked
            config.ym3438 = 1;
            OPN2_SetChipType(ym3438_mode_ym2612);
            break;
        case 1: // YM3438-Nuked
            config.ym3438 = 2;
            OPN2_SetChipType(ym3438_mode_readmode);
            break;
        case 2: // YM2612-MAME
            config.ym2612 = YM2612_DISCRETE;
            YM2612Config(YM2612_DISCRETE);
            break;
        case 3: // YM3438-MAME
            config.ym2612 = YM2612_INTEGRATED;
            YM2612Config(YM2612_INTEGRATED);
            break;
        case 4: // YM3438-Enhanced-MAME
            config.ym2612 = YM2612_ENHANCED;
            YM2612Config(YM2612_ENHANCED);
            break;
        default: break;
    }

    // system options
    // 0 = AUTO (or SYSTEM_SG, SYSTEM_MARKIII, SYSTEM_SMS, SYSTEM_SMS2,
    //  SYSTEM_GG, SYSTEM_MD)
    config.system = 0;

    // 0 = AUTO (1 = USA, 2 = EUROPE, 3 = JAPAN/NTSC, 4 = JAPAN/PAL)
    config.region_detect = settings_gp[REGION].val;

    config.vdp_mode = 0; // = AUTO (1 = NTSC, 2 = PAL)
    config.master_clock = 0; // = AUTO (1 = NTSC, 2 = PAL)
    config.force_dtack = 0;
    config.addr_error = 1;
    config.bios = 0;
    config.lock_on = 0; // 0 = OFF (can be TYPE_SK, TYPE_GG & TYPE_AR)
    // 0 = HW_ADDON_AUTO (or HW_ADDON_MEGACD, HW_ADDON_MEGASD & HW_ADDON_NONE)
    config.add_on = 0;
    config.ntsc = 0;
    config.lcd = 0; // 0.8 fixed point
    //config.lcd = (uint8)(0.80 * 256);

    // display options
    config.overscan = 0;
    config.gg_extra = 0; // 1 = show extended Game Gear screen (256x192)
    config.render = 1; // 1 = double resolution output (interlaced mode 2)
    config.enhanced_vscroll = 0;
    config.enhanced_vscroll_limit = 8;

    // Default to no BIOS loaded
    system_bios = 0;

    // Initialize the paths to BIOS files
    snprintf(GG_ROM, sizeof(GG_ROM), "%s/ggenie.bin", pathinfo.bios);
    snprintf(AR_ROM, sizeof(AR_ROM), "%s/areplay.bin", pathinfo.bios);
    snprintf(SK_ROM, sizeof(SK_ROM), "%s/sk.bin", pathinfo.bios);
    snprintf(SK_UPMEM, sizeof(SK_UPMEM), "%s/sk2chip.bin", pathinfo.bios);
    snprintf(CD_BIOS_US, sizeof(CD_BIOS_US), "%s/bios_CD_U.bin", pathinfo.bios);
    snprintf(CD_BIOS_EU, sizeof(CD_BIOS_EU), "%s/bios_CD_E.bin", pathinfo.bios);
    snprintf(CD_BIOS_JP, sizeof(CD_BIOS_JP), "%s/bios_CD_J.bin", pathinfo.bios);
    snprintf(MD_BIOS, sizeof(MD_BIOS), "%s/bios_MD.bin", pathinfo.bios);
    snprintf(MS_BIOS_US, sizeof(MS_BIOS_US), "%s/bios_U.sms", pathinfo.bios);
    snprintf(MS_BIOS_EU, sizeof(MS_BIOS_EU), "%s/bios_E.sms", pathinfo.bios);
    snprintf(MS_BIOS_JP, sizeof(MS_BIOS_JP), "%s/bios_J.sms", pathinfo.bios);
    snprintf(GG_BIOS, sizeof(GG_BIOS), "%s/bios.gg", pathinfo.bios);

    // Clear all bits in the bitmap
    memset(&bitmap, 0, sizeof(t_bitmap));

    //set initial dimensions: 720x576
    bitmap.width = vidinfo.wmax;
    bitmap.height = vidinfo.hmax;
    bitmap.pitch = (bitmap.width * sizeof(uint32_t));
    bitmap.viewport.changed = 3;
    return 1;
}

void jg_deinit(void) { }

void jg_reset(int hard) { gen_reset(hard); }

void jg_exec_frame(void) {
    // Execute frame
    if (system_hw == SYSTEM_MCD)
        system_frame_scd(0);
    else if ((system_hw & SYSTEM_PBC) == SYSTEM_MD)
        system_frame_gen(0);
    else
        system_frame_sms(0);

    // viewport size change
    if (bitmap.viewport.changed & 1) {
        bitmap.viewport.changed &= ~1;

        if (system_hw == SYSTEM_GG) {
            bitmap.width = VIDEO_WIDTH_GG;
            bitmap.height = VIDEO_HEIGHT_GG;
            vidinfo.aspect = VIDEO_ASPECT_GG;
        }
        else {
            bitmap.width = bitmap.viewport.w;
            bitmap.height = bitmap.viewport.h;
        }

        vidinfo.w = bitmap.width;
        vidinfo.h = interlaced ? bitmap.height * 2 : bitmap.height;
    }

    // Push audio frames to the frontend
    gp_audio_push();

    // Refresh video data
    bitmap.data = vidinfo.buf;
}

int jg_game_load(void) {
    // Initialize all systems and power on
    load_rom((char*)gameinfo.path);
    gp_input_setup();
    audio_init(audinfo.rate, 0);
    system_init();
    gp_input_setup_sms(); // SMS special inputs are handled later than MD inputs

    // Make adjustments for PAL
    if (vdp_pal) {
        vidinfo.aspect =  ASPECT_PAL;
        audinfo.spf = (SAMPLERATE / FRAMERATE_PAL) * CHANNELS;
#if JG_VERSION_NUMBER >= 10100
        jg_cb_frametime(udata_frametime, FRAMERATE_PAL);
#else
        jg_cb_frametime(FRAMERATE_PAL);
#endif
    }
    else {
#if JG_VERSION_NUMBER >= 10100
        jg_cb_frametime(udata_frametime, FRAMERATE);
#else
        jg_cb_frametime(FRAMERATE);
#endif
    }

    // Check what system it is
    switch (system_hw) {
        case SYSTEM_MCD: { // Sega CD/Mega CD
            // load internal backup RAM
            char brmfile[192];
            snprintf(brmfile, sizeof(brmfile), "%s/%s.brm",
                pathinfo.save, gameinfo.name);

            FILE *fp = fopen(brmfile, "rb");
            if (fp) {
                if (!fread(scd.bram, 0x2000, 1, fp)) {
                    jg_cb_log(JG_LOG_WRN, "Could not read BRAM\n");
                }
                fclose(fp);
            }

            // check if internal backup RAM is formatted
            if (memcmp(scd.bram + 0x2000 - 0x20, brm_format + 0x20, 0x20)) {
                // clear internal backup RAM
                memset(scd.bram, 0x00, 0x200);

                // Internal Backup RAM size fields
                brm_format[0x10] = brm_format[0x12] =
                    brm_format[0x14] = brm_format[0x16] = 0x00;
                brm_format[0x11] = brm_format[0x13] = brm_format[0x15] =
                    brm_format[0x17] = (sizeof(scd.bram) / 64) - 3;

                // format internal backup RAM
                memcpy(scd.bram + 0x2000 - 0x40, brm_format, 0x40);
            }

            // load cartridge backup RAM
            if (scd.cartridge.id) {
                snprintf(brmfile, sizeof(brmfile),
                    "%s/cart.brm", pathinfo.save);
                FILE *fp = fopen(brmfile, "rb");
                if (fp) {
                    if (!fread(scd.cartridge.area, scd.cartridge.mask + 1,
                        1, fp)) {
                            jg_cb_log(JG_LOG_WRN, "Could not read Cart BRAM\n");
                    }
                    fclose(fp);
                }

                // check if cartridge backup RAM is formatted
                if (memcmp(scd.cartridge.area + scd.cartridge.mask + 1 - 0x20,
                    brm_format + 0x20, 0x20)) {
                    // clear cartridge backup RAM
                    memset(scd.cartridge.area, 0x00, scd.cartridge.mask + 1);

                    /* Cartridge Backup RAM size fields */
                    brm_format[0x10] = brm_format[0x12] =
                        brm_format[0x14] = brm_format[0x16] =
                        (((scd.cartridge.mask + 1) / 64) - 3) >> 8;
                    brm_format[0x11] = brm_format[0x13] =
                        brm_format[0x15] = brm_format[0x17] =
                        (((scd.cartridge.mask + 1) / 64) - 3) & 0xff;

                    /* format cartridge backup RAM */
                    memcpy(scd.cartridge.area + scd.cartridge.mask + 1 -
                        sizeof(brm_format), brm_format, sizeof(brm_format));
                }
            }
            break;
        }

        case SYSTEM_GG:
            vidinfo.w = VIDEO_WIDTH_GG;
            vidinfo.h = VIDEO_HEIGHT_GG;
            vidinfo.aspect = VIDEO_ASPECT_GG;
            break;

        case SYSTEM_SMS2:
        case SYSTEM_SMS:
        default: // Genesis/Mega Drive
            // Load SRAM
            if (sram.on) {
                char savename[292];
                snprintf(savename, sizeof(savename),
                    "%s/%s.srm", pathinfo.save, gameinfo.name);

                FILE *fp = fopen(savename, "rb");
                if (fp) {
                    if (!fread(sram.sram, 0x10000, 1, fp)) {
                        jg_cb_log(JG_LOG_WRN, "Could not read SRAM\n");
                    }
                    fclose(fp);
                    jg_cb_log(JG_LOG_DBG, "Read SRAM: %s\n", savename);
                }
            }
            break;
    }

    return 1;
}

int jg_game_unload(void) {
    // Shut down
    if (system_hw == SYSTEM_MCD) {
        // save internal backup RAM (if formatted)
        char brmfile[192];
        if (!memcmp(scd.bram + 0x2000 - 0x20, brm_format + 0x20, 0x20)) {
            snprintf(brmfile, sizeof(brmfile), "%s/%s.brm",
                pathinfo.save, gameinfo.name);

            FILE *fp = fopen(brmfile, "wb");
            if (fp) {
                fwrite(scd.bram, 0x2000, 1, fp);
                fclose(fp);
            }
        }

        // save cartridge backup RAM (if formatted)
        if (scd.cartridge.id) {
            if (!memcmp(scd.cartridge.area + scd.cartridge.mask + 1 - 0x20,
                brm_format + 0x20, 0x20)) {
                snprintf(brmfile, sizeof(brmfile),
                    "%s/cart.brm", pathinfo.save);

                FILE *fp = fopen(brmfile, "wb");
                if (fp) {
                    fwrite(scd.cartridge.area, scd.cartridge.mask + 1, 1, fp);
                    fclose(fp);
                }
            }
        }
    }
    else if (sram.on) {
        char savename[292];
        snprintf(savename, sizeof(savename), "%s/%s.srm",
            pathinfo.save, gameinfo.name);

        FILE *fp = fopen(savename, "wb");
        if (fp) {
            fwrite(sram.sram, 0x10000, 1, fp);
            fclose(fp);
            jg_cb_log(JG_LOG_DBG, "Write SRAM: %s\n", savename);
        }
    }
    return 1;
}

int jg_state_load(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp) {
        uint8_t buf[STATE_SIZE];
        if (!fread(&buf, 1, STATE_SIZE, fp)) {
            fclose(fp);
            return 0;
        }
        state_load(buf);
        fclose(fp);
        return 1;
    }
    return 0;
}

void jg_state_load_raw(const void *data) {
}

int jg_state_save(const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (fp) {
        uint8_t buf[STATE_SIZE];
        int len = state_save(buf);
        fwrite(&buf, len, 1, fp);
        fclose(fp);
        return 1;
    }
    return 0;
}

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) {
    /* clear existing ROM patches */
    clear_cheats();

    /* delete all cheats */
    maxcheats = maxROMcheats = maxRAMcheats = 0;
    memset(cheatlist, 0, sizeof(cheatlist));
}

void jg_cheat_set(const char *code) {
    if (code) {
        /* clear existing ROM patches */
        clear_cheats();

        /* interpret code and give it an index */
        decode_cheat((char*)code, maxcheats);

        // Enable the cheat by default
        cheatlist[maxcheats].enable = 1;

        /* increment cheat count */
        maxcheats++;

        /* apply ROM patches */
        apply_cheats();
    }
}

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 Functions that return values to the frontend
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) {
        unsigned listnum = 0;

        // Mega Drive
        for (size_t i = 0; i < 8; ++i)
            inputlist[listnum++] = jg_md_inputinfo(i, JG_MD_PAD6B);

        // For now everything just uses the 6 button control pad
        /*for (size_t i = 0; i < 8; ++i)
            inputlist[listnum++] = jg_md_inputinfo(i, JG_MD_PAD3B);

        inputlist[listnum++] = jg_md_inputinfo(0, JG_MD_JUSTIFIER);
        inputlist[listnum++] = jg_md_inputinfo(0, JG_MD_MENACER);

        // SG-1000/SMS
        for (size_t i = 0; i < 2; ++i)
            inputlist[listnum++] = jg_sms_inputinfo(i, JG_SMS_PAD);

        inputlist[listnum++] = jg_sms_inputinfo(0, JG_SMS_SYS);

        for (size_t i = 0; i < 2; ++i)
            inputlist[listnum++] = jg_sms_inputinfo(i, JG_SMS_PADDLE);

        inputlist[listnum++] = jg_sms_inputinfo(0, JG_SMS_LIGHTPHASER);
        inputlist[listnum++] = jg_sms_inputinfo(0, JG_SMS_TEREBIOEKAKI);*/

        inputlist_built = 1;
    }
    *num = TOTALINPUTS;
    return inputlist;
}

jg_setting_t* jg_get_settings(size_t *numsettings) {
    *numsettings = sizeof(settings_gp) / sizeof(jg_setting_t);
    return settings_gp;
}

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

jg_mediainfo_t* jg_get_mediainfo(void) {
    // TODO: Probably should hook up CDs to this
    return NULL;
}

jg_biosinfo_t* jg_get_bioslist(size_t *num) {
    *num = sizeof(bioslist) / sizeof(jg_biosinfo_t);
    return bioslist;
}

// JG Functions that set values in the emulator core
void jg_setup_video(void) {
    bitmap.data = vidinfo.buf;
    system_reset();
}

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) {
}

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

unsigned jg_api_version(void) {
    return JG_VERSION_NUMBER;
}
