/*
Copyright (c) 2020, OpenEmu Team
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.
* Neither the names of the copyright holders nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

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 "mednafen/mednafen.h"
#include "mednafen/state-driver.h"
#include "mednafen/mednafen-driver.h"
#include "mednafen/mempatcher.h"
#include "mednafen/MemoryStream.h"

#include <unordered_map>
#include "gamedb_pce.h"
#include "gamedb_psx.h"
#include "gamedb_snes.h"
#include "gamedb_ss.h"
#include "serial.h"

#include <jg/jg.h>
#include <jg/jg_lynx.h>
#include <jg/jg_md.h>
#include <jg/jg_ngp.h>
#include <jg/jg_pce.h>
#include <jg/jg_psx.h>
#include <jg/jg_ss.h>
#include <jg/jg_snes.h>
#include <jg/jg_vb.h>
#include <jg/jg_wswan.h>

#if JG_VERSION_NUMBER > 10000
#include <jg/jg_pcfx.h>
#else
#include "jg_pcfx_mdfn.h"
#include "jg_compat.h"
#endif

#define SAMPLERATE 48000
#define FRAMERATE 60
#define CHANNELS 2
#define MAXINPUTS 12
#define TOTALINPUTS 57

void (*mdfn_input_poll[MAXINPUTS])(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;

/* wmax/hmax are the upper bounds across every system this multi-emu
   core supports, picked once here so the frontend can size a single
   video buffer at startup that fits any subsequent system without
   reallocation. Per-system dispatch in jg_get_coreinfo() sets only
   the current w/h/p/aspect/pixfmt. wmax/hmax must not be touched
   after this point.

   Current ceiling: PCE's 1365 horizontal blanket-render width and
   the 480-line interlaced fields used by md/pcfx/psx/snes/ss. Adjust
   here if a future system raises either bound.
*/
#define VIDEO_WMAX 1365
#define VIDEO_HMAX 576

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888,
    VIDEO_WMAX, VIDEO_HMAX,     // wmax, hmax: cross-system maximum
    VIDEO_WMAX, VIDEO_HMAX,     // w, h: replaced per-system in jg_get_coreinfo
    0, 0,                       // x, y
    VIDEO_WMAX,                 // p
    1.0,                        // aspect: replaced per-system
    NULL                        // buf: filled in by frontend
};

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

static int inputlist_built = 0;

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

// Emulator settings
static jg_setting_t settings_mdfn[] = {
    { "fip_check", "File Include Path Check",
      "0 = Disable File Include Path Check, 1 = Enable File Include Path Check",
      "Select whether to use the File Include Path Check when loading .cue or "
      ".m3u files containing absolute paths",
      1, 0, 1, JG_SETTING_RESTART
    },
    { "pce_fast", "PC Engine (Fast) Module",
      "0 = Disable (Accurate), 1 = Enable (Fast)",
      "Select between the Accurate or Fast PC Engine Emulator module - states "
      "are not compatible between Accurate and Fast modules",
      0, 0, 1, JG_SETTING_RESTART
    },
    { "pce_slstart", "PC Engine First Scanline",
      "N = First Line",
      "First displayed scanline",
      4, 0, 16, JG_SETTING_RESTART
    },
    { "pce_slend", "PC Engine Last Scanline",
      "N = Last Line",
      "Last displayed scanline",
      235, 223, 239, JG_SETTING_RESTART
    },
    { "pcfx_mouse", "PC-FX Mouse",
      "0 = Disable, 1 = Enable",
      "Use a PC-FX Mouse instead of a Gamepad in Port 1",
      0, 0, 1, JG_SETTING_INPUT
    },
    { "pcfx_slstart", "PC-FX First Scanline",
      "N = First Line",
      "First displayed scanline",
      4, 0, 16, JG_SETTING_RESTART
    },
    { "pcfx_slend", "PC-FX Last Scanline",
      "N = Last Line",
      "Last displayed scanline",
      235, 223, 239, JG_SETTING_RESTART
    },
    { "ps1_portswap", "PS1 Controller Port Swap",
      "0 = Disable, 1 = Enable",
      "Swap the Player 1 and Player 2 input ports in PS1 games for special "
      "occasions...",
      0, 0, 1, 0
    },
    { "ps1_slstart", "PS1 First Scanline",
      "N = First Line",
      "First displayed scanline in NTSC mode",
      0, 0, 16, JG_SETTING_RESTART
    },
    { "ps1_slend", "PS1 Last Scanline",
      "N = Last Line",
      "Last displayed scanline in NTSC mode",
      239, 223, 239, JG_SETTING_RESTART
    },
    { "ps1_slstartp", "PS1 First Scanline (PAL)",
      "N = First Line",
      "First displayed scanline in PAL mode",
      0, 0, 16, JG_SETTING_RESTART
    },
    { "ps1_slendp", "PS1 Last Scanline (PAL)",
      "N = Last Line",
      "Last displayed scanline in PAL mode",
      287, 271, 287, JG_SETTING_RESTART
    },
    { "snes_slstart", "SNES First Scanline",
      "N = First Line",
      "First displayed scanline in NTSC mode",
      0, 0, 16, JG_SETTING_RESTART
    },
    { "snes_slend", "SNES Last Scanline",
      "N = Last Line",
      "Last displayed scanline in NTSC mode",
      223, 207, 223, JG_SETTING_RESTART
    },
    { "snes_slstartp", "SNES First Scanline (PAL)",
      "N = First Line",
      "First displayed scanline in PAL mode",
      0, 0, 16, JG_SETTING_RESTART
    },
    { "snes_slendp", "SNES Last Scanline (PAL)",
      "N = Last Line",
      "Last displayed scanline in PAL mode",
      238, 222, 238, JG_SETTING_RESTART
    },
    { "snes_spex", "SNES Speculative Execution",
      "0 = Disable, 1 = Enable",
      "Allow speculative execution for SNES games (also known as runahead) "
      " to reduce input latency - may cause glitches",
      0, 0, 1, 0
    },
    { "ss3dpad_disable", "Saturn 3D Pad Disable",
      "0 = Enable 3D Pad, 1 = Disable 3D Pad",
      "Enable or Disable the Saturn 3D Pad for games supporting it",
      0, 0, 1, JG_SETTING_RESTART
    },
    { "ss_slstart", "Saturn First Scanline",
      "N = First Line",
      "First displayed scanline in NTSC mode",
      0, 0, 16, JG_SETTING_RESTART
    },
    { "ss_slend", "Saturn Last Scanline",
      "N = Last Line",
      "Last displayed scanline in NTSC mode",
      239, 223, 239, JG_SETTING_RESTART
    },
    { "ss_slstartp", "Saturn First Scanline (PAL)",
      "N = First Line",
      "First displayed scanline in PAL mode",
      0, -16, 16, JG_SETTING_RESTART
    },
    { "ss_slendp", "Saturn Last Scanline (PAL)",
      "N = Last Line",
      "Last displayed scanline in PAL mode",
      255, 239, 271, JG_SETTING_RESTART
    },
    { "vb_palette", "Virtual Boy Palette",
      "0 = Red/Black (2D), 1 = White/Black (2D), 2 = Magenta/Black (2D),"
      "3 = Red/Blue, 4 = Red/Cyan, 5 = Red/Electric Cyan,"
      "6 = Red/Green, 7 = Green/Magenta, 8 = Yellow/Blue",
      "Select the colour palette for Virtual Boy",
      0, 0, 8, 0
    },
    { "wswan_byear", "WonderSwan Birth Year",
      "N = Birth Year",
      "Set the WonderSwan Birth Year",
      1989, 1900, 2100, JG_SETTING_RESTART
    },
    { "wswan_bmonth", "WonderSwan Birth Month",
      "N = Birth Month",
      "Set the WonderSwan Birth Month",
      6, 0, 12, JG_SETTING_RESTART
    },
    { "wswan_bday", "WonderSwan Birth Day",
      "N = Birth Day",
      "Set the WonderSwan Birth Day",
      23, 0, 31, JG_SETTING_RESTART
    },
    { "wswan_blood", "WonderSwan Blood Type",
      "0 = A, 1 = B, 2 = O, 3 = AB",
      "Set the WonderSwan Blood entry",
      2, 0, 3, JG_SETTING_RESTART
    },
    { "wswan_sex", "WonderSwan Sex",
      "0 = Female, 1 = Male",
      "Set the WonderSwan Sex entry",
      0, 0, 1, JG_SETTING_RESTART
    }
};

enum mdfnjg_settings {
    FIPCHECK,
    PCEFAST,
    PCESLSTART,
    PCESLEND,
    PCFXMOUSE,
    PCFXSLSTART,
    PCFXSLEND,
    PS1PORTSWAP,
    PS1SLSTART,
    PS1SLEND,
    PS1SLSTARTP,
    PS1SLENDP,
    SNESSLSTART,
    SNESSLEND,
    SNESSLSTARTP,
    SNESSLENDP,
    SNES_SPEX,
    SS3D_DISABLE,
    SSSLSTART,
    SSSLEND,
    SSSLSTARTP,
    SSSLENDP,
    VBPALETTE,
    WSBYEAR,
    WSBMONTH,
    WSBDAY,
    WSBLOOD,
    WSSEX,
};

static jg_biosinfo_t bioslist[] = {
    { "lynxboot.img", "Atari Lynx BIOS",
      "fcd403db69f54290b51035d82f835e7b", 1, 0 },

    { "syscard3.pce", "PCE Super CD-ROM2 System V3 Card",
      "38179df8f4ac870017db21ebcbf53114", 1, 0 },

    { "pcfx.rom", "PC-FX BIOS",
      "08e36edbea28a017f79f8d4f7ff9b6d7", 1, 0 },

    { "scph5500.bin", "PlayStation JP SCPH-5500 BIOS",
      "8dd7d5296a650fac7319bce665a6a53c", 1, 0 },
    { "scph5501.bin", "PlayStation NA SCPH-5501 BIOS",
      "490f666e1afb15b7362b406ed1cea246", 1, 0 },
    { "scph5502.bin", "PlayStation EU SCPH-5502 BIOS",
      "32736f17079d0b2b7024407c39bd3050", 1, 0 },

    { "sega_101.bin", "Sega Saturn JP BIOS",
      "85ec9ca47d8f6807718151cbcca8b964", 1, 0 },
    { "mpr-17933.bin", "Sega Saturn NA/EU BIOS",
      "3240872c70984b6cbfda1586cab68dbe", 1, 0 },
};

static jg_systeminfo_t systemlist[] = {
    { "lynx", "Atari Lynx", "lnx" },
    { "md", "Mega Drive/Genesis", "md,gen" },
    { "ngp", "Neo Geo Pocket", "ngc,ngp" },
    { "pce", "PC Engine", "pce,sgx,cue" },
    { "pcfx", "PC-FX", "cue" },
    { "psx", "PlayStation", "cue,m3u" },
    { "ss", "Sega Saturn", "cue,m3u" },
    { "snes", "Super Nintendo", "sfc,smc" },
    { "vb", "Virtual Boy", "vb" },
    { "wswan", "WonderSwan", "ws,wsc" },
};

// Mednafen-specific stuff
using namespace Mednafen;

static MDFNGI *game;
static MDFN_Surface *surf;
static std::unique_ptr<int32[]> lw; // Line Widths
static double _masterClock = 0;
static double _mednafenCoreTiming = 60.0;
static uint32_t *_inputBuffer[13];

static int inputs_plugged = 0; // Number of input devices actually plugged in
static jg_mediainfo_t mediainfo = { "Disc", 1, 0, 0 };
static int media_delay = 0;
static uint8_t ss_reset = 0;
static std::string serial;

// Cheat state
static std::vector<std::string> cheat_codes;
static bool cheats_changed = false;
static unsigned cheat_count = 0;

// Input Polling
static void mdfn_input_poll_null(int) { } // Nothing plugged in

// Atari Lynx
static const int LynxMap[] = { 6, 7, 4, 5, 8, 0, 1, 3, 2 };

static void mdfn_input_poll_lynx(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << LynxMap[i];

    *_inputBuffer[port] = b;
}

static void mdfn_input_setup_lynx(void) {
    inputs_plugged = 1;
    inputinfo[0] = jg_lynx_inputinfo(0, 0);
    game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);

    mdfn_input_poll[0] = &mdfn_input_poll_lynx;
}

// Mega Drive
static const int MDMap6[] {
    // Up, Down, Left, Right, Mode, Start, A, B, C, X, Y, Z
    0, 1, 2, 3, 11, 7, 6, 4, 5, 10, 9, 8
};

static void mdfn_input_poll_md6(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << MDMap6[i];

    *_inputBuffer[port] = b;
}

/*static const int MDMap3[] {
    // Up, Down, Left, Right, Start, A, B, C
    0, 1, 2, 3, 7, 6, 4, 5
};

static void mdfn_input_poll_md3(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << MDMap3[i];

    *_inputBuffer[port] = b;
}*/

static void mdfn_input_setup_md(void) {
    for (int i = 0; i < inputs_plugged; ++i) {
        inputinfo[i] = jg_md_inputinfo(i, JG_MD_PAD6B);
        game->SetInput(i, "gamepad6", (uint8_t*)_inputBuffer[i]);
        mdfn_input_poll[i] = &mdfn_input_poll_md6;
        /*inputinfo[i] = jg_md_inputinfo(i, JG_MD_PAD3B);
        game->SetInput(i, "gamepad", (uint8_t*)_inputBuffer[i]);
        mdfn_input_poll[i] = &mdfn_input_poll_md3;*/
    }
}

static void mdfn_setup_md(void) {
    inputs_plugged = 2;
}

// Neo Geo Pocket Color
static void mdfn_input_poll_ngp(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << i;

    *_inputBuffer[port] = b;
}

static void mdfn_input_setup_ngp(void) {
    inputs_plugged = 1;
    game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);
    inputinfo[0] = jg_ngp_inputinfo(0, 0);
    mdfn_input_poll[0] = &mdfn_input_poll_ngp;
}

// PC Engine/PC Engine CD
// Up, Down, Left, Right, Select, Run, I, II, III, IV, V, VI, ModeSelect
static const int PCEMap[] = { 4, 6, 7, 5, 2, 3, 0, 1, 8, 9, 10, 11, 12 };

static void mdfn_input_poll_pce(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons - 2; ++i) // -2 for non-turbo
        if (input_device[port]->button[i]) b |= 1 << PCEMap[i];

    // Turbo
    if (input_device[port]->button[8]) {
        if (input_device[port]->button[8] == 3) {
            input_device[port]->button[8] = 1;
            b |= 0x1;
        }
        else {
            ++input_device[port]->button[8];
        }
    }
    if (input_device[port]->button[9]) {
        if (input_device[port]->button[9] == 3) {
            input_device[port]->button[9] = 1;
            b |= 0x2;
        }
        else {
            ++input_device[port]->button[9];
        }
    }

    *_inputBuffer[port] = b;
}

static void mdfn_input_poll_pceap6(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << PCEMap[i];

    *_inputBuffer[port] = b | 1 << 12;
}

static void mdfn_setup_pce(void) {
    MDFNI_SetSetting("pce.cdbios",
        std::string(pathinfo.bios) + "/syscard3.pce"); // PCE CD BIOS
    MDFNI_SetSetting("pce.slstart",
        std::to_string(settings_mdfn[PCESLSTART].val));
    MDFNI_SetSetting("pce.slend",
        std::to_string(settings_mdfn[PCESLEND].val));
}

static void mdfn_input_setup_pce(void) {
    SerialReader sr;
    serial = std::string(gameinfo.md5);

    if (sr.check_vector(serial, pce_6b_games)) {
        inputs_plugged = 2;

        game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);
        inputinfo[0] = jg_pce_inputinfo(0, JG_PCE_AP6);

        game->SetInput(1, "gamepad", (uint8_t*)_inputBuffer[1]);
        inputinfo[1] = jg_pce_inputinfo(1, JG_PCE_AP6);

        mdfn_input_poll[0] = &mdfn_input_poll_pceap6;
        mdfn_input_poll[1] = &mdfn_input_poll_pceap6;
        return;
    }

    inputs_plugged = 5;

    for (int i = 0; i < coreinfo.numinputs; ++i) {
        game->SetInput(i, "gamepad", (uint8_t*)_inputBuffer[i]);
        inputinfo[i] = jg_pce_inputinfo(i, JG_PCE_PAD);
        mdfn_input_poll[i] = &mdfn_input_poll_pce;
    }
}

// PC-FX
// Up, Down, Left, Right, Select, Run, I, II, III, IV, V, VI, Mode1, Mode2
static const int PCFXMap[] = { 8, 10, 11, 9, 6, 7, 0, 1, 2, 3, 4, 5, 12, 13 };
static void mdfn_input_poll_pcfx(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons - 2; ++i)
        if (input_device[port]->button[i]) b |= 1 << PCFXMap[i];

    *_inputBuffer[port] = b;
}

// The PC-FX and SNES Mouse work the same, so use the same callback for both
static void mdfn_input_poll_pcfx_snes_mouse(int port) {
    int16_t coordx = input_device[port]->rel[0] / 2;
    int16_t coordy = input_device[port]->rel[1] / 2;
    input_device[port]->rel[0] -= coordx;
    input_device[port]->rel[1] -= coordy;

    uint8_t *buf = (uint8_t *)_inputBuffer[port];
    MDFN_en16lsb(&buf[0], coordx); // X coord
    MDFN_en16lsb(&buf[2], coordy); // Y coord

    uint8_t buttons = 0;
    if (input_device[port]->button[0]) buttons |= (1 << 0);
    if (input_device[port]->button[1]) buttons |= (1 << 1);

    MDFN_enlsb<uint8_t>(&buf[4], buttons);
}

static void mdfn_setup_pcfx(void) {
    inputs_plugged = 2;
    MDFNI_SetSetting("pcfx.bios",
        std::string(pathinfo.bios) + "/pcfx.rom"); // PC-FX BIOS
    MDFNI_SetSetting("pcfx.slstart",
        std::to_string(settings_mdfn[PCFXSLSTART].val));
    MDFNI_SetSetting("pcfx.slend",
        std::to_string(settings_mdfn[PCFXSLEND].val));
}

static void mdfn_input_setup_pcfx() {
    for (int i = 0; i < coreinfo.numinputs; ++i) {
        game->SetInput(i, "gamepad", (uint8_t*)_inputBuffer[i]);
        inputinfo[i] = jg_pcfx_inputinfo(i, JG_PCFX_PAD);
        mdfn_input_poll[i] = &mdfn_input_poll_pcfx;
    }

    if (settings_mdfn[PCFXMOUSE].val) {
        game->SetInput(0, "mouse", (uint8_t*)_inputBuffer[0]);
        inputinfo[0] = jg_pcfx_inputinfo(0, JG_PCFX_MOUSE);
        mdfn_input_poll[0] = &mdfn_input_poll_pcfx_snes_mouse;
    }
}

// PSX - axis indices are independent of button indices
static const int PSXMap[] = {
    7, 9, 3, 5, // LeftStickX, LeftStickY, RightStickX, RightStickY
    4, 6, 7, 5, // Up, Down, Left, Right
    0, 3, // Select, Start
    12, 14, 15, 13, 16, // Triangle, Cross, Square, Circle, Analog
    10, 8, 1, 11, 9, 2, // L1, L2, L3, R1, R2, R3
};

static void mdfn_input_poll_psx(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i])
            b |= 1 << PSXMap[i + inputinfo[port].numaxes];

    uint8_t *buf = (uint8_t *)_inputBuffer[port];

    if (settings_mdfn[PS1PORTSWAP].val && port < 2)
        port ^= 1;

    *_inputBuffer[port] = b;

    // https://mednafen.github.io/documentation/psx.html#Section_analog_range
    // 30712 / cos(2*pi/8) / 32767 = 1.33
    for (int i = 0; i < inputinfo[port].numaxes; ++i) {
        /*double scaled = input_device[port]->axis[i] >= 0 ?
            std::min(floor(0.5 + input_device[port]->axis[i] * 1.33), 32767.0) :
            std::max(floor(0.5 + input_device[port]->axis[i] * 1.33), -32767.0);
        MDFN_en16lsb(&buf[PSXMap[i]], (uint16_t)scaled + 32767);//*/

        // This works better
        uint16_t aval = 0;
        if (input_device[port]->axis[i] > -32768)
            aval = input_device[port]->axis[i] + 32767;

        MDFN_en16lsb(&buf[PSXMap[i]], aval);
    }

    if (buf[0xb])
#if JG_VERSION_NUMBER >= 10100
        jg_cb_rumble(udata_rumble, port, buf[0xb] / 510.0, 1000 / FRAMERATE); // Weak
#else
        jg_cb_rumble(port, buf[0xb] / 510.0, 1000 / FRAMERATE); // Weak
#endif

    if (buf[0xc])
#if JG_VERSION_NUMBER >= 10100
        jg_cb_rumble(udata_rumble, port, buf[0xc] / 255.0, 1000 / FRAMERATE); // Strong
#else
        jg_cb_rumble(port, buf[0xc] / 255.0, 1000 / FRAMERATE); // Strong
#endif
}

static void mdfn_input_poll_psx_guncon(int port) {
    int scaled_x_coord = (input_device[port]->coord[0] + game->mouse_offs_x) *
        (game->mouse_scale_x / vidinfo.w);
    int scaled_y_coord = (input_device[port]->coord[1] +
        game->mouse_offs_y * (vidinfo.h / game->nominal_height)) *
        (game->mouse_scale_y / vidinfo.h);

    uint8_t *buf = (uint8_t *)_inputBuffer[port];
    MDFN_en16lsb(&buf[0], scaled_x_coord); // X coord
    MDFN_en16lsb(&buf[2], scaled_y_coord); // Y coord

    uint8_t gunbuttons = 0;
    if (input_device[port]->button[0]) gunbuttons |= 1 << 0; // Trigger
    if (input_device[port]->button[1]) gunbuttons |= 1 << 3; // Offscreen
    if (input_device[port]->button[2]) gunbuttons |= 1 << 1; // A
    if (input_device[port]->button[3]) gunbuttons |= 1 << 2; // B
    MDFN_enlsb<uint8_t>(&buf[4], gunbuttons);
}

static void mdfn_input_poll_psx_justifier(int port) {
    int scaled_x_coord = (input_device[port]->coord[0] + game->mouse_offs_x) *
        (game->mouse_scale_x / vidinfo.w);
    int scaled_y_coord = (input_device[port]->coord[1] +
        game->mouse_offs_y * (vidinfo.h / game->nominal_height)) *
        (game->mouse_scale_y / vidinfo.h);

    uint8_t *buf = (uint8_t *)_inputBuffer[port];
    MDFN_en16lsb(&buf[0], scaled_x_coord); // X coord
    MDFN_en16lsb(&buf[2], scaled_y_coord); // Y coord

    uint8_t gunbuttons = 0;
    if (input_device[port]->button[0]) gunbuttons |= 1 << 0; // Trigger
    if (input_device[port]->button[1]) gunbuttons |= 1 << 3; // Offscreen
    if (input_device[port]->button[2]) gunbuttons |= 1 << 1; // O
    if (input_device[port]->button[3]) gunbuttons |= 1 << 2; // Start
    MDFN_enlsb<uint8_t>(&buf[4], gunbuttons);
}

static void mdfn_setup_psx(void) {
    // PSX game operations that must be done before the game is loaded
    MDFNI_SetSetting("psx.bios_jp",
        std::string(pathinfo.bios) + "/scph5500.bin"); // JP SCPH-5500 BIOS
    MDFNI_SetSetting("psx.bios_na",
        std::string(pathinfo.bios) + "/scph5501.bin"); // NA SCPH-5501 BIOS
    MDFNI_SetSetting("psx.bios_eu",
        std::string(pathinfo.bios) + "/scph5502.bin"); // EU SCPH-5502 BIOS
    MDFNI_SetSetting("psx.h_overscan", "0"); // Remove PSX overscan
    MDFNI_SetSetting("psx.slstart",
        std::to_string(settings_mdfn[PS1SLSTART].val));
    MDFNI_SetSetting("psx.slend",
        std::to_string(settings_mdfn[PS1SLEND].val));
    MDFNI_SetSetting("psx.slstartp",
        std::to_string(settings_mdfn[PS1SLSTARTP].val));
    MDFNI_SetSetting("psx.slendp",
        std::to_string(settings_mdfn[PS1SLENDP].val));
    MDFNI_SetSetting("psx.input.port1.gun_chairs",
        "0x1000000"); // Disable crosshair

    // Find the serial number of the disc
    SerialReaderPSX sr;
    sr.read(serial, gameinfo.path);
    jg_cb_log(JG_LOG_DBG, "Serial: %s\n", serial.c_str());

    // Check if the game needs an SBI file, and make sure it's accessible if so
    if (sr.check_vector(serial, psx_sbi_games)) {
        if (!sr.sbi_exists(gameinfo.path))
            jg_cb_log(JG_LOG_ERR, "SBI (LibCrypt) file missing\n");
    }

    // Check if the game is multi-disc
    std::unordered_map<std::string, int>::iterator iter
        = psx_md_games.find(serial);

    if (iter != psx_md_games.end()) {
        // Check if the file extension is .m3u
        std::string ext = gameinfo.path;
        std::transform(ext.begin(), ext.end(), ext.begin(), tolower);
        ext = ext.substr(ext.find_last_of(".") + 1);
        if (ext != "m3u")
            jg_cb_log(JG_LOG_ERR, "Multi-disc game, m3u playlist required\n");

        mediainfo.count = iter->second; // Adjust the number of discs
    }

    // Plug multi-taps in if necessary
    inputs_plugged = 2; // Default to 2 input devices

    iter = psx_mtap_games.find(serial);
    if (iter != psx_mtap_games.end()) {
        inputs_plugged = iter->second; // Set the correct number of inputs

        // 5 or less player games requiring the multi-tap in port 2 only
        if (sr.check_vector(serial, psx_mtap5port2_games)) {
            inputs_plugged = 5;
            MDFNI_SetSetting("psx.input.pport2.multitap", "1");
        }
        else { // Enable multitap on PSX port 1
            MDFNI_SetSetting("psx.input.pport1.multitap", "1");
            if (inputs_plugged > 5) // Enable multitap on PSX port 2
                MDFNI_SetSetting("psx.input.pport2.multitap", "1");
        }
    }

    // Tenka is a badly programmed game
    if (sr.check_vector(serial, psx_single_memcard_games))
        MDFNI_SetSetting("psx.input.port2.memcard", "0");
}

static void mdfn_input_setup_psx(void) {
    SerialReaderPSX sr;

    if (sr.check_vector(serial, psx_guncon_games)) {
        game->SetInput(0, "guncon", (uint8_t *)_inputBuffer[0]);
        inputinfo[0] = jg_psx_inputinfo(0, JG_PSX_GUNCON);
        mdfn_input_poll[0] = &mdfn_input_poll_psx_guncon;

        // Clear mouse inputs
        uint8_t *buf = (uint8_t *)_inputBuffer[0];
        MDFN_enlsb<uint8_t>(&buf[4], 0);
    }
    else if (sr.check_vector(serial, psx_justifier_games)) {
        game->SetInput(0, "justifier", (uint8_t *)_inputBuffer[0]);
        inputinfo[0] = jg_psx_inputinfo(0, JG_PSX_JUSTIFIER);
        mdfn_input_poll[0] = &mdfn_input_poll_psx_justifier;

        // Clear mouse inputs
        uint8_t *buf = (uint8_t *)_inputBuffer[0];
        MDFN_enlsb<uint8_t>(&buf[4], 0);
    }
    else {
        for (int i = 0; i < inputs_plugged; ++i) {
            inputinfo[i] = jg_psx_inputinfo(i, JG_PSX_DUALSHOCK);

            game->SetInput(i, "dualshock", (uint8_t*)_inputBuffer[i]);

            // Center analog inputs
            uint8_t *buf = (uint8_t *)_inputBuffer[i];
            MDFN_en16lsb(&buf[3], 32767);
            MDFN_en16lsb(&buf[5], 32767);
            MDFN_en16lsb(&buf[7], 32767);
            MDFN_en16lsb(&buf[9], 32767);

            mdfn_input_poll[i] = &mdfn_input_poll_psx;
        }
    }
}

// SNES
static const int SNESMap[] = { 4, 5, 6, 7, 2, 3, 8, 0, 9, 1, 10, 11 };

static void mdfn_input_poll_snes(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << SNESMap[i];

    *_inputBuffer[port] = b;
}

static void mdfn_setup_snes(void) {
    inputs_plugged = 2;

    // Detect multitap games
    for (size_t i = 0; i < snes_multitap_games.size(); ++i) {
        if ((std::string)gameinfo.md5 == snes_multitap_games[i].md5) {
            inputs_plugged = snes_multitap_games[i].players;
            if (inputs_plugged > 5)
                MDFNI_SetSetting("snes_faust.input.sport1.multitap", "1");
            MDFNI_SetSetting("snes_faust.input.sport2.multitap", "1");
            break;
        }
    }

    MDFNI_SetSetting("snes_faust.slstart",
        std::to_string(settings_mdfn[SNESSLSTART].val));
    MDFNI_SetSetting("snes_faust.slend",
        std::to_string(settings_mdfn[SNESSLEND].val));
    MDFNI_SetSetting("snes_faust.slstartp",
        std::to_string(settings_mdfn[SNESSLSTARTP].val));
    MDFNI_SetSetting("snes_faust.slendp",
        std::to_string(settings_mdfn[SNESSLENDP].val));

    if (settings_mdfn[SNES_SPEX].val) {
        MDFNI_SetSetting("snes_faust.spex", "1");
        MDFNI_SetSetting("snes_faust.spex.sound", "1");
    }

    MDFNI_SetSetting("snes_faust.h_filter", "phr256blend_auto512");
}

static void mdfn_input_setup_snes() {
    // Default input devices are SNES Controllers
    for (int i = 0; i < inputs_plugged; ++i) {
        inputinfo[i] = jg_snes_inputinfo(i, JG_SNES_PAD);
        game->SetInput(i, "gamepad", (uint8_t*)_inputBuffer[i]);
        mdfn_input_poll[i] = &mdfn_input_poll_snes;
    }

    // Connect the SNES Mouse for games which support it
    for (size_t i = 0; i < snes_mouse_games.size(); ++i) {
        if ((std::string)gameinfo.md5 == snes_mouse_games[i]) {
            inputinfo[0] = jg_snes_inputinfo(0, JG_SNES_MOUSE);
            game->SetInput(0, "mouse", (uint8_t*)_inputBuffer[0]);
            mdfn_input_poll[0] = &mdfn_input_poll_pcfx_snes_mouse;
            break;
        }
    }
}

// Saturn
static const int SSMap[] {
    // Up, Down, Left, Right, Start, A, B, C, X, Y, Z, L, R
    4, 5, 6, 7, 11, 10, 8, 9, 2, 1, 0, 15, 3
};

static void mdfn_input_poll_ss(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << SSMap[i];

    *_inputBuffer[port] = b;
}

static const int SS3DMap[] { // axis indices are independent of button indices
    // StickX, StickY, L, R,
    // Up, Down, Left, Right, Start, A, B, C, X, Y, Z, Analog
    2, 4, 8, 6, 0, 1, 2, 3, 7, 6, 4, 5, 10, 9, 8, 12
};

static uint8_t SS3DAnalogSwitch[MAXINPUTS] = { 0 };

static void mdfn_input_poll_ss3d(int port) {
    for (int i = 0; i < inputinfo[port].numbuttons - 1; ++i)
        if (input_device[port]->button[i])
            *_inputBuffer[port] |= 1 << SS3DMap[i + inputinfo[port].numaxes];
        else
            *_inputBuffer[port] &=
                ~(1 << SS3DMap[i + inputinfo[port].numaxes]);

    // Handle analog switch separately (as a toggle)
    if (input_device[port]->button[11]) { // It has been pressed or is held
        SS3DAnalogSwitch[port] = 1; // Mark the button as being held
    }
    else if (SS3DAnalogSwitch[port]) { // It has been released
        *_inputBuffer[port] ^= 0x1000;
        jg_cb_log(JG_LOG_DBG, "3D Control Pad Port %d Analog Mode: %s\n",
            port, _inputBuffer[port][0] & 0x1000 ? "On" : "Off");
        SS3DAnalogSwitch[port] = 0; // Release the "held" bit
    }

    uint8_t *buf = (uint8_t *)_inputBuffer[port];

    for (int i = 0; i < inputinfo[port].numaxes; ++i) {
        uint16_t aval = 0;
        if (input_device[port]->axis[i] > -32768)
            aval = input_device[port]->axis[i] + 32767;

        MDFN_en16lsb(&buf[SS3DMap[i]], aval);
    }
}

static void mdfn_input_poll_ssvirtuagun(int port) {
    int scaled_x_coord = input_device[port]->coord[0] *
        (game->mouse_scale_x / vidinfo.w);
    int scaled_y_coord = input_device[port]->coord[1] *
        (game->mouse_scale_y / vidinfo.h);

    uint8_t *buf = (uint8_t *)_inputBuffer[port];
    MDFN_en16lsb(&buf[0], scaled_x_coord); // X coord
    MDFN_en16lsb(&buf[2], scaled_y_coord); // Y coord

    uint8_t gunbuttons = 0;
    if (input_device[port]->button[0]) gunbuttons |= 1 << 0; // Trigger
    if (input_device[port]->button[1]) gunbuttons |= 1 << 2; // Offscreen
    if (input_device[port]->button[2]) gunbuttons |= 1 << 1; // Start
    MDFN_enlsb<uint8_t>(&buf[4], gunbuttons);
}

static void mdfn_setup_ss(void) {
    MDFNI_SetSetting("ss.bios_jp",
        std::string(pathinfo.bios) + "/sega_101.bin"); // JP SS BIOS
    MDFNI_SetSetting("ss.bios_na_eu",
        std::string(pathinfo.bios) + "/mpr-17933.bin"); // NA/EU SS BIOS
    MDFNI_SetSetting("ss.input.port1.gun_chairs",
        "0x1000000"); // Disable crosshair
    MDFNI_SetSetting("ss.slstart",
        std::to_string(settings_mdfn[SSSLSTART].val));
    MDFNI_SetSetting("ss.slend",
        std::to_string(settings_mdfn[SSSLEND].val));
    MDFNI_SetSetting("ss.slstartp",
        std::to_string(settings_mdfn[SSSLSTARTP].val));
    MDFNI_SetSetting("ss.slendp",
        std::to_string(settings_mdfn[SSSLENDP].val));

    // Find the serial number of the disc
    SerialReaderSS sr;
    sr.read(serial, gameinfo.path);
    jg_cb_log(JG_LOG_DBG, "Serial: %s\n", serial.c_str());

    // Check if the game is multi-disc
    std::unordered_map<std::string, int>::iterator iter
        = ss_md_games.find(serial);

    if (iter != ss_md_games.end()) {
        // Check if the file extension is .m3u
        std::string ext = gameinfo.path;
        std::transform(ext.begin(), ext.end(), ext.begin(), tolower);
        ext = ext.substr(ext.find_last_of(".") + 1);
        if (ext != "m3u")
            jg_cb_log(JG_LOG_ERR, "Multi-disc game, m3u playlist required\n");

        mediainfo.count = iter->second; // Adjust the number of discs
    }

    // Plug multi-taps in if necessary
    inputs_plugged = 2; // Default to 2 input devices

    iter = ss_mtap_games.find(serial);
    if (iter != psx_mtap_games.end()) {
        inputs_plugged = iter->second; // Set the correct number of inputs
        // From the Sega Saturn 6 Player Multi-Player Adaptor manual:
        // 3-7 player games - multi-tap on port 2
        MDFNI_SetSetting("ss.input.sport2.multitap", "1");

        // 2 Saturn multitaps, 12 Saturn controllers, 6 24-packs of beer,
        // 4 couches, and one 36" CRT TV, all in one room.
        if(inputs_plugged > 7) // 8-12 player games - multi-taps in both ports
            MDFNI_SetSetting("ss.input.sport1.multitap", "1");
    }


}

static void mdfn_input_setup_ss(void) {
    SerialReaderSS sr;

    // Check if the game supports the 3D Control Pad
    if (!settings_mdfn[SS3D_DISABLE].val &&
        sr.check_vector(serial, ss_3dpad_games)) {

        for (int i = 0; i < inputs_plugged; ++i) {
            inputinfo[i] = jg_ss_inputinfo(i, JG_SS_3DPAD);

            game->SetInput(i, "3dpad", (uint8_t *)_inputBuffer[i]);

            // Center analog stick
            uint8_t *buf = (uint8_t *)_inputBuffer[i];
            MDFN_en16lsb(&buf[2], 32767);
            MDFN_en16lsb(&buf[4], 32767);

            // Set analog triggers
            MDFN_en16lsb(&buf[6], 0);
            MDFN_en16lsb(&buf[8], 0);
            _inputBuffer[i][0] |= 1 << 12; // Start in analog mode

            mdfn_input_poll[i] = &mdfn_input_poll_ss3d;
        }
    }
    // Check if the game supports the Saturn Stunner/Virtua Gun
    else if (sr.check_vector(serial, ss_virtuagun_games)) {
        inputinfo[0] = jg_ss_inputinfo(0, JG_SS_VIRTUAGUN);
        inputinfo[1] = jg_ss_inputinfo(1, JG_SS_PAD);

        game->SetInput(0, "gun", (uint8_t *)_inputBuffer[0]);
        game->SetInput(1, "gamepad", (uint8_t *)_inputBuffer[1]);

        mdfn_input_poll[0] = &mdfn_input_poll_ssvirtuagun;
        mdfn_input_poll[1] = &mdfn_input_poll_ss;
    }
    // Regular Saturn Control Pads
    else {
        for (int i = 0; i < inputs_plugged; ++i) {
            inputinfo[i] = jg_ss_inputinfo(i, JG_SS_PAD);
            game->SetInput(i, "gamepad", (uint8_t *)_inputBuffer[i]);
            mdfn_input_poll[i] = &mdfn_input_poll_ss;
        }
    }

    // System Reset button status
    _inputBuffer[12] = (uint32_t*)calloc(9, sizeof(uint32_t));
    game->SetInput(12, "builtin", (uint8_t *)_inputBuffer[12]);
}

// Virtual Boy
static int VBMap[] = { 9, 8, 7, 6, 4, 13, 12, 5, 11, 10, 0, 1, 3, 2 };

static void mdfn_input_poll_vb(int port) {
    uint32_t b = 0;

    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << VBMap[i];

    *_inputBuffer[port] = b;
}

static void mdfn_setup_vb(void) {
    if (settings_mdfn[VBPALETTE].val < 3) {
        MDFNI_SetSetting("vb.disable_parallax", "1");
        MDFNI_SetSetting("vb.anaglyph.preset", "disabled");
    }

    switch (settings_mdfn[VBPALETTE].val) {
        default: case 0: // Red/Black (2D)
            MDFNI_SetSetting("vb.anaglyph.lcolor", "0xFF0000");
            MDFNI_SetSetting("vb.anaglyph.rcolor", "0x000000");
            break;
        case 1: // White/Black (2D)
            MDFNI_SetSetting("vb.anaglyph.lcolor", "0xFFFFFF");
            MDFNI_SetSetting("vb.anaglyph.rcolor", "0x000000");
            break;
        case 2: // Magenta/Black (2D)
            MDFNI_SetSetting("vb.anaglyph.lcolor", "0xFF00FF");
            MDFNI_SetSetting("vb.anaglyph.rcolor", "0x000000");
            break;
        case 3: // Red/Blue
            MDFNI_SetSetting("vb.anaglyph.preset", "red_blue");
            break;
        case 4: // Red/Cyan
            MDFNI_SetSetting("vb.anaglyph.preset", "red_cyan");
            break;
        case 5: // Red/Electric Cyan
            MDFNI_SetSetting("vb.anaglyph.preset", "red_electriccyan");
            break;
        case 6: // Red/Green
            MDFNI_SetSetting("vb.anaglyph.preset", "red_green");
            break;
        case 7: // Green/Magenta
            MDFNI_SetSetting("vb.anaglyph.preset", "green_magenta");
            break;
        case 8: // Yellow/Blue
            MDFNI_SetSetting("vb.anaglyph.preset", "yellow_blue");
            break;
    }
}

static void mdfn_input_setup_vb(void) {
    inputs_plugged = 1;
    inputinfo[0] = jg_vb_inputinfo(0, 0);
    game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);

    // Low Battery Sensor bit, unused currently
    game->SetInput(1, "misc", (uint8_t *)_inputBuffer[1]);

    mdfn_input_poll[0] = &mdfn_input_poll_vb;
}

// WonderSwan
static void mdfn_input_poll_wswan(int port) {
    uint32_t b = 0;
    // X1, X2, X3, X4, Y1, Y2, Y3, Y4, Start, A, B
    for (int i = 0; i < inputinfo[port].numbuttons; ++i)
        if (input_device[port]->button[i]) b |= 1 << i;

    *_inputBuffer[port] = b;
}

static void mdfn_setup_wswan(void) {
    MDFNI_SetSetting("wswan.name", "Mednafen");
    MDFNI_SetSetting("wswan.language", "english");

    MDFNI_SetSetting("wswan.byear",
        std::to_string(settings_mdfn[WSBYEAR].val));
    MDFNI_SetSetting("wswan.bmonth",
        std::to_string(settings_mdfn[WSBMONTH].val));
    MDFNI_SetSetting("wswan.bday",
        std::to_string(settings_mdfn[WSBDAY].val));

    switch (settings_mdfn[WSBLOOD].val) {
        default: break;
        case 0: // A
            MDFNI_SetSetting("wswan.blood", "A"); break;
        case 1: // B
            MDFNI_SetSetting("wswan.blood", "B"); break;
        case 2:
            MDFNI_SetSetting("wswan.blood", "O"); break;
        case 3: // AB
            MDFNI_SetSetting("wswan.blood", "AB"); break;
    }

    MDFNI_SetSetting("wswan.sex", settings_mdfn[WSSEX].val ? "M" : "F");
}

static void mdfn_input_setup_wswan(void) {
    inputs_plugged = 1;
    inputinfo[0] = jg_wswan_inputinfo(0, 0);
    game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);

    mdfn_input_poll[0] = &mdfn_input_poll_wswan;
}

static void mdfn_cheat_refresh(void) {
    while (cheat_count > 0) {
        MDFNI_DelCheat(0);
        --cheat_count;
    }

    MemoryPatch mp;
    for (size_t i = 0; i < cheat_codes.size(); ++i) {
        for (const auto &fmt : game->CheatInfo.CheatFormatInfo) {
            try {
                if (!fmt.DecodeCheat(cheat_codes[i], &mp)) {
                    mp.status = true;
                    MDFNI_AddCheat(mp);
                    ++cheat_count;
                    mp = MemoryPatch();
                }
            }
            catch (std::exception &e) {
                jg_cb_log(JG_LOG_WRN,
                    "Cheat decode failed: %s\n", e.what());
                mp = MemoryPatch();
            }
            break;
        }
    }
    cheats_changed = false;
}

// Jolly Good API Calls
#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) {
    MDFNI_Init();
    MDFNI_InitFinalize(pathinfo.base);

    MDFNI_SetSetting("filesys.path_firmware", pathinfo.bios); // BIOS
    MDFNI_SetSetting("filesys.path_sav", pathinfo.save); // Memcards/Saves
    MDFNI_SetSetting("filesys.path_savbackup",
        std::string(pathinfo.save) + "/b"); // Save Backup
    MDFNI_SetSetting("filesys.path_movie", pathinfo.save); // Movies - not used
    MDFNI_SetSetting("filesys.path_state", pathinfo.save); // State - not used
    MDFNI_SetSetting("filesys.path_cheat", "DISABLE");

    MDFNI_SetSetting("filesys.untrusted_fip_check",
        settings_mdfn[FIPCHECK].val ? "1" : "0");

    return 1;
}

void jg_deinit(void) {
    delete surf;
    surf = nullptr;
    MDFNI_Kill();
}

void jg_reset(int hard) {
    if (!strcmp(coreinfo.sys, "ss")) {
        ss_reset = (uint8_t)_mednafenCoreTiming;
        *_inputBuffer[12] = 1;
    }

    MDFNI_Reset();
}

void jg_exec_frame(void) {
    EmulateSpecStruct spec;
    lw[0] = ~0;
    spec.surface = surf;
    spec.LineWidths = lw.get();

    spec.SoundRate = audinfo.rate;
    spec.SoundBuf = (int16_t*)audinfo.buf;
    spec.SoundBufMaxSize = audinfo.spf << 2;
    spec.SoundVolume = 1.0;
    spec.soundmultiplier = 1.0;

    // Poll for input
    for (int i = 0; i < inputs_plugged; ++i)
        mdfn_input_poll[i](i);

    // Deferred cheat rebuild - only when codes have changed
    if (MDFN_UNLIKELY(cheats_changed))
        mdfn_cheat_refresh();

    MDFNMP_ApplyPeriodicCheats();

    MDFNI_Emulate(&spec);

    // Handle the strange case of the Sega Saturn reset button
    if (ss_reset) {
        --ss_reset;
        if (!ss_reset) *_inputBuffer[12] = 0;
    }

    // Handle delayed media insertion
    if (media_delay && !--media_delay) {
        mediainfo.inserted = MDFNI_SetMedia(0, 2, mediainfo.index, 0);
        if (mediainfo.inserted) {
            jg_cb_log(JG_LOG_INF, "Disc %u Inserted\n", mediainfo.index + 1);
            jg_cb_log(JG_LOG_SCR, "Disc %u Inserted.", mediainfo.index + 1);
        }
        else {
            jg_cb_log(JG_LOG_WRN, "Failed to Insert Disc %u\n",
                mediainfo.index + 1);
            jg_cb_log(JG_LOG_SCR, "Failed to Insert Disc %u.",
                mediainfo.index + 1);
        }
    }

    vidinfo.w = (lw[0] == ~0) ? spec.DisplayRect.w : lw[spec.DisplayRect.y];
    vidinfo.h = spec.DisplayRect.h;
    vidinfo.x = spec.DisplayRect.x;
    vidinfo.y = spec.DisplayRect.y;

    _mednafenCoreTiming = _masterClock / spec.MasterCycles;

#if JG_VERSION_NUMBER >= 10100
    jg_cb_frametime(udata_frametime, _mednafenCoreTiming);
    jg_cb_audio(udata_audio, spec.SoundBufSize * audinfo.channels);
#else
    jg_cb_frametime(_mednafenCoreTiming);
    jg_cb_audio(spec.SoundBufSize * audinfo.channels);
#endif
}

int jg_game_load(void) {
    const char *subsys = coreinfo.sys;
    const char *faust = "snes_faust";
    const char *pcefast = "pce_fast";

    // Do per-system preload setup
    if (!strcmp(coreinfo.sys, "md")) {
        mdfn_setup_md();
    }
    else if (!strcmp(coreinfo.sys, "pce")) {
        if (settings_mdfn[PCEFAST].val) {
            subsys = pcefast;
            /* pce_fast emits a smaller intermediate framebuffer than
               full pce (512 vs 1365 pitch). wmax/hmax stay at the
               cross-system maximum so the frontend's pre-allocated
               buffer remains valid; only pitch and current dimensions
               change. */
            vidinfo.p = 512;
        }
        mdfn_setup_pce();
    }
    else if (!strcmp(coreinfo.sys, "pcfx")) {
        mdfn_setup_pcfx();
    }
    else if (!strcmp(coreinfo.sys, "psx")) {
        mdfn_setup_psx();
    }
    else if (!strcmp(coreinfo.sys, "snes")) {
        subsys = faust;
        mdfn_setup_snes();
    }
    else if (!strcmp(coreinfo.sys, "ss")) {
        mdfn_setup_ss();
    }
    else if (!strcmp(coreinfo.sys, "vb")) {
        mdfn_setup_vb();
    }
    else if (!strcmp(coreinfo.sys, "wswan")) {
        mdfn_setup_wswan();
    }

    // Load the game
    game = MDFNI_LoadGame(subsys, &::Mednafen::NVFS, gameinfo.path);

    if (!game) {
        jg_cb_log(JG_LOG_ERR, "Failed to load game. Ensure BIOS is correctly"
            " installed and/or dump/cue/m3u is clean.\n");
        return 0;
    }

    // Insert a CD if there is one
    mediainfo.inserted = MDFNI_SetMedia(0, 2, mediainfo.index, 0);
    jg_cb_log(JG_LOG_DBG, "Disc inserted: %u\n", mediainfo.inserted);

    jg_cb_log(JG_LOG_DBG, "Framebuffer width: %d, Framebuffer height: %d\n",
        game->fb_width, game->fb_height);
    jg_cb_log(JG_LOG_DBG, "Nominal width: %d, Nominal height: %d\n",
        game->nominal_width, game->nominal_height);

    /* Update current display dimensions to match this specific game.
       wmax/hmax stay at the cross-system maximum chosen at static init
       so the frontend's video buffer, sized once at startup, fits this
       and every subsequent game without reallocation. game->fb_width/
       fb_height must always be <= VIDEO_WMAX/VIDEO_HMAX — bump those
       constants if a future Mednafen game raises either bound.
    */
    vidinfo.w = game->nominal_width;
    vidinfo.h = game->nominal_height;
    vidinfo.aspect = (double)game->nominal_width / game->nominal_height;

    lw.reset(new int32[game->fb_height]);
    memset(lw.get(), 0, sizeof(int32) * game->fb_height);

    audinfo.channels = game->soundchan;

    _masterClock = game->MasterClock >> 32;

    for (int i = 0; i < coreinfo.numinputs; ++i)
        _inputBuffer[i] = (uint32_t*)calloc(9, sizeof(uint32_t));

    for (int i = 0; i < MAXINPUTS; ++i)
        mdfn_input_poll[i] = &mdfn_input_poll_null;

    // Set up input
    if (!strcmp(coreinfo.sys, "md"))
        mdfn_input_setup_md();
    else if (!strcmp(coreinfo.sys, "lynx"))
        mdfn_input_setup_lynx();
    else if (!strcmp(coreinfo.sys, "ngp"))
        mdfn_input_setup_ngp();
    else if (!strcmp(coreinfo.sys, "pce"))
        mdfn_input_setup_pce();
    else if (!strcmp(coreinfo.sys, "pcfx"))
        mdfn_input_setup_pcfx();
    else if (!strcmp(coreinfo.sys, "psx"))
        mdfn_input_setup_psx();
    else if (!strcmp(coreinfo.sys, "snes"))
        mdfn_input_setup_snes();
    else if (!strcmp(coreinfo.sys, "ss"))
        mdfn_input_setup_ss();
    else if (!strcmp(coreinfo.sys, "vb"))
        mdfn_input_setup_vb();
    else if (!strcmp(coreinfo.sys, "wswan"))
        mdfn_input_setup_wswan();
    else
        game->SetInput(0, "gamepad", (uint8_t*)_inputBuffer[0]);

    return 1;
}

int jg_game_unload(void) {
    // Clear cheats
    while (cheat_count > 0) {
        MDFNI_DelCheat(0);
        --cheat_count;
    }
    cheat_codes.clear();
    cheats_changed = false;

    // Unload the game
    MDFNI_CloseGame();

    for (int i = 0; i < coreinfo.numinputs; ++i) {
        free(_inputBuffer[i]);
        _inputBuffer[i] = nullptr;
    }

    if (!strcmp(coreinfo.sys, "ss")) {
        free(_inputBuffer[12]);
        _inputBuffer[12] = nullptr;
    }

    lw.reset(nullptr);

    game = nullptr; // freed by MDFNI_CloseGame
    inputs_plugged = 0;
    mediainfo = { "Disc", 1, 0, 0 };
    media_delay = 0;
    ss_reset = 0;
    serial.clear();

    return 1;
}

int jg_state_load(const char *filename) {
    return MDFNI_LoadState(filename, NULL);
}

void jg_state_load_raw(const void*) {
}

int jg_state_save(const char *filename) {
    return MDFNI_SaveState(filename, NULL, NULL, NULL, NULL);
}

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

size_t jg_state_size(void) {
    return 0;
}

// Deprecated
void jg_media_select(void) {
    if (mediainfo.inserted) {
        jg_cb_log(JG_LOG_WRN, "Eject current disc before inserting new disc\n");
        jg_cb_log(JG_LOG_SCR, "Eject current disc first.");
    }
    else {
        mediainfo.index++;
        if (mediainfo.index >= mediainfo.count) mediainfo.index = 0;
        jg_cb_log(JG_LOG_INF, "Disc %u Selected\n", mediainfo.index + 1);
        jg_cb_log(JG_LOG_SCR, "Disc %u Selected.", mediainfo.index + 1);
    }
}

// Deprecated
void jg_media_insert(void) {
    if (mediainfo.inserted) { // open drive/eject disc
        media_delay = 0;
        MDFNI_SetMedia(0, 0, 0, 0);
        mediainfo.inserted = 0;
        jg_cb_log(JG_LOG_INF, "Disc Removed\n");
        jg_cb_log(JG_LOG_SCR, "Disc Removed.");
    }
    else { // schedule close drive/insert disc
        media_delay = 30;
    }
}

void jg_media_mount(unsigned state) {
    if (state) { // schedule mount
        media_delay = 30;
    }
    else { // unmount
        media_delay = 0;
        MDFNI_SetMedia(0, 0, 0, 0);
        mediainfo.inserted = 0;
        jg_cb_log(JG_LOG_INF, "Disc Removed\n");
        jg_cb_log(JG_LOG_SCR, "Disc Removed.");
    }
}

void jg_media_set(unsigned index) {
    if (index >= mediainfo.count) return;
    mediainfo.index = index;
}

void jg_cheat_clear(void) {
    cheat_codes.clear();
    cheats_changed = true;
}

void jg_cheat_set(const char *code) {
    if (code && *code) {
        cheat_codes.push_back(code);
        cheats_changed = true;
    }
}

void jg_rehash(void) {
    if (!strcmp(coreinfo.sys, "vb")) {
        mdfn_setup_vb();
    }
    else if (!strcmp(coreinfo.sys, "pcfx")) {
        mdfn_input_setup_pcfx();
    }
    else if (!strcmp(coreinfo.sys, "snes")) {
        MDFNI_SetSetting("snes_faust.spex",
            settings_mdfn[SNES_SPEX].val ? "1" : "0");
        MDFNI_SetSetting("snes_faust.spex.sound",
            settings_mdfn[SNES_SPEX].val ? "1" : "0");
    }
}

void jg_data_push(uint32_t, int, const void*, size_t) {
}

// 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 *subsys) {
    if (!strcmp(subsys, "lynx")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "lynx", 1, 0 };
        vidinfo.w = vidinfo.p = 160;
        vidinfo.h = 102;
        vidinfo.aspect = 160.0/102.0;
    }
    else if (!strcmp(subsys, "md")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "md", 2, 0 };
        vidinfo.w = 320;
        vidinfo.p = 1024;
        vidinfo.h = 224;
        vidinfo.aspect = 1.3061224;
    }
    else if (!strcmp(subsys, "ngp")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "ngp", 1, 0 };
        vidinfo.w = vidinfo.p = 160;
        vidinfo.h = 152;
        vidinfo.aspect = 160.0/152.0;
    }
    else if (!strcmp(subsys, "pce")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "pce", 5, 0 };
        vidinfo.w = 256;
        vidinfo.p = 1365;
        vidinfo.h = 232;
        vidinfo.aspect = 1.2413793;
    }
    else if (!strcmp(subsys, "pcfx")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "pcfx", 2, 0 };
        vidinfo.w = 256;
        vidinfo.p = 1024;
        vidinfo.h = 232;
        vidinfo.aspect = 1.3061224;
    }
    else if (!strcmp(subsys, "psx")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "psx", 8, 0 };
        vidinfo.w = 320;
        vidinfo.p = 768;
        vidinfo.h = 240;
        vidinfo.aspect = 4.0/3.0;
    }
    else if (!strcmp(subsys, "snes")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "snes", 8, 0 };
        vidinfo.w = 256;
        vidinfo.p = 512;
        vidinfo.h = 224;
        vidinfo.aspect = 1.3061224;
    }
    else if (!strcmp(subsys, "ss")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "ss", 12, 0 };
        vidinfo.w = 320;
        vidinfo.p = 704;
        vidinfo.h = 240;
        vidinfo.aspect = 4.0/3.0;
    }
    else if (!strcmp(subsys, "vb")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "vb", 2, 0 };
        vidinfo.w = vidinfo.p = 384;
        vidinfo.h = 224;
        vidinfo.aspect = 384.0/224.0;
        audinfo = (jg_audioinfo_t){
            JG_SAMPFMT_INT16, SAMPLERATE, CHANNELS, (SAMPLERATE / 50) * CHANNELS
        };
    }
    else if (!strcmp(subsys, "wswan")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "wswan", 1, 0 };
        vidinfo.w = vidinfo.p = 224;
        vidinfo.h = 144;
        vidinfo.aspect = 224.0/144.0;
        audinfo = (jg_audioinfo_t){
            JG_SAMPFMT_INT16, SAMPLERATE, CHANNELS, (SAMPLERATE / 75) * CHANNELS
        };
    }
    else if (!strcmp(subsys, "")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "", 0, 0 };
        vidinfo.w = 320;
        vidinfo.p = 320;
        vidinfo.h = 240;
        vidinfo.aspect = 4.0/3.0;
    }
    else {
        jg_cb_log(JG_LOG_ERR, "Failed to select subsystem: %s", subsys);
    }

    // All emulated systems support cheats in some form
    coreinfo.hints |= JG_HINT_CHEATS;
    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;

        // PCE
        for (size_t i = 0; i < 5; ++i)
            inputlist[listnum++] = jg_pce_inputinfo(i, JG_PCE_PAD);

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

        // PC-FX
        for (size_t i = 0; i < 2; ++i)
            inputlist[listnum++] = jg_pcfx_inputinfo(i, JG_PCFX_PAD);

        inputlist[listnum++] = jg_pcfx_inputinfo(0, JG_PCFX_MOUSE); // 10

        // PlayStation
        for (size_t i = 0; i < 8; ++i)
            inputlist[listnum++] = jg_psx_inputinfo(i, JG_PSX_DUALSHOCK); // 18

        inputlist[listnum++] = jg_psx_inputinfo(0, JG_PSX_GUNCON);
        inputlist[listnum++] = jg_psx_inputinfo(0, JG_PSX_JUSTIFIER); // 20

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

        // Saturn
        for (size_t i = 0; i < 12; ++i)
            inputlist[listnum++] = jg_ss_inputinfo(i, JG_SS_PAD); // 34

        for (size_t i = 0; i < 12; ++i)
            inputlist[listnum++] = jg_ss_inputinfo(i, JG_SS_3DPAD); // 46

        inputlist[listnum++] = jg_ss_inputinfo(0, JG_SS_VIRTUAGUN); // 47

        // SNES
        for (size_t i = 0; i < 5; ++i)
            inputlist[listnum++] = jg_snes_inputinfo(i, JG_SNES_PAD); // 52

        inputlist[listnum++] = jg_snes_inputinfo(0, JG_SNES_MOUSE); // 53

        // Lynx
        inputlist[listnum++] = jg_lynx_inputinfo(0, 0); // 54

        // Virtual Boy
        inputlist[listnum++] = jg_vb_inputinfo(0, 0); // 55

        // NGP
        inputlist[listnum++] = jg_ngp_inputinfo(0, 0); // 56

        // WonderSwan
        inputlist[listnum++] = jg_wswan_inputinfo(0, 0); // 57

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

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

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

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

jg_mediainfo_t* jg_get_mediainfo() {
    return mediainfo.count > 1 ? &mediainfo : NULL;
}

void jg_setup_video() {
    if (!surf) {
        // BGRA pixel format
        MDFN_PixelFormat pix_fmt(MDFN_COLORSPACE_RGB, 4, 16, 8, 0, 24);
        surf = new MDFN_Surface((uint32_t*)vidinfo.buf, game->fb_width,
            game->fb_height, game->fb_width, pix_fmt);
    }
}

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;
}
