/*
Copyright (c) 2020, OpenEmu Team
Copyright (c) 2020-2022, 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/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_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>

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

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;

static jg_coreinfo_t coreinfo;
static jg_videoinfo_t vidinfo;
static jg_pathinfo_t pathinfo;
static jg_fileinfo_t gameinfo;
static jg_inputinfo_t inputinfo[MAXINPUTS];
static jg_inputstate_t *input_device[MAXINPUTS];

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

// Emulator settings
static jg_setting_t settings_mdfn[] = { // name, default, min, max
    { "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, 1
    },
    { "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, 1
    },
    { "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
    },
    { "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", "Sega 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, 1
    },
    { "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, 1
    },
    { "wswan_bmonth", "WonderSwan Birth Month",
      "N = Birth Month",
      "Set the WonderSwan Birth Month",
      6, 0, 12, 1
    },
    { "wswan_bday", "WonderSwan Birth Day",
      "N = Birth Day",
      "Set the WonderSwan Birth Day",
      23, 0, 31, 1
    },
    { "wswan_blood", "WonderSwan Blood Type",
      "0 = A, 1 = B, 2 = O, 3 = AB",
      "Set the WonderSwan Blood entry",
      2, 0, 3, 1
    },
    { "wswan_sex", "WonderSwan Sex",
      "0 = Female, 1 = Male",
      "Set the WonderSwan Sex entry",
      0, 0, 1, 1
    }
};

enum mdfnjg_settings {
    FIPCHECK,
    PCEFAST,
    PS1PORTSWAP,
    SNES_SPEX,
    SS3D_DISABLE,
    VBPALETTE,
    WSBYEAR,
    WSBMONTH,
    WSBDAY,
    WSBLOOD,
    WSSEX,
};

// 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 bool disc_inserted = false;
static uint8_t disc_index = 0; // 0-indexed disc index
static uint8_t disc_num = 1; // Default to single disc games
static uint8_t ss_reset = 0;
static std::string serial;

// Input Polling
static void mdfn_input_poll_null(int port) { } // 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() {
    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;
}

// 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() {
    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() {
    MDFNI_SetSetting("pce.cdbios",
        std::string(pathinfo.bios) + "/syscard3.pce");// PCE CD BIOS
    MDFNI_SetSetting("pce.slstart", "4"); // PCE: First rendered scanline
    MDFNI_SetSetting("pce.slend", "235"); // PCE: Last rendered scanline
}

static void mdfn_input_setup_pce() {

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

// 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])
        jg_cb_rumble(port, buf[0xb] / 510.0, 1000 / FRAMERATE); // Weak

    if (buf[0xc])
        jg_cb_rumble(port, buf[0xc] / 255.0, 1000 / FRAMERATE); // Strong
}

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() {
    // 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.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");

        disc_num = 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() {
    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_input_poll_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_snes() {
    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;
        }
    }

    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_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() {
    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

    // 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");

        disc_num = 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() {
    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() {
    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() {
    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() {
    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() {
    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;
}

// Jolly Good API Calls
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;
}

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

void jg_set_cb_rumble(jg_cb_rumble_t func) {
    jg_cb_rumble = func;
}

int jg_init() {
    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.untrusted_fip_check",
        settings_mdfn[FIPCHECK].val ? "1" : "0");

    return 1;
}

void jg_deinit() {
    free(surf);
}

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

    MDFNI_Reset();
}

void jg_exec_frame() {
    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);

    MDFNI_Emulate(&spec);

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

    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;

    jg_cb_frametime(_mednafenCoreTiming);
    jg_cb_audio(spec.SoundBufSize * audinfo.channels);
}

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

    // Do per-system preload setup
    if (!strcmp(coreinfo.sys, "pce")) {
        if (settings_mdfn[PCEFAST].val) {
            subsys = pcefast;
            vidinfo.wmax = vidinfo.p = 512;
            vidinfo.hmax = 242;
        }
        mdfn_setup_pce();
    }
    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
    disc_inserted = MDFNI_SetMedia(0, 2, disc_index, 0);
    jg_cb_log(JG_LOG_DBG, "Disc inserted: %d\n", disc_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);

    vidinfo.wmax = game->fb_width;
    vidinfo.hmax = game->fb_height;
    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, "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, "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() {
    MDFNI_CloseGame();

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

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

    lw.reset(nullptr);

    return 1;
}

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

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

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

void jg_media_select() {
    if (disc_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 {
        disc_index++;
        if (disc_index >= disc_num) disc_index = 0;
        jg_cb_log(JG_LOG_INF, "Disc %d Selected\n", disc_index + 1);
        jg_cb_log(JG_LOG_SCR, "Disc %d Selected.", disc_index + 1);
    }
}

void jg_media_insert() {
    if (disc_inserted) { // open drive/eject disc
        MDFNI_SetMedia(0, 0, 0, 0);
        disc_inserted = false;
        jg_cb_log(JG_LOG_INF, "Disc Removed\n");
        jg_cb_log(JG_LOG_SCR, "Disc Removed.");
    }
    else { // close drive/insert disc (2 = close)
        disc_inserted = MDFNI_SetMedia(0, 2, disc_index, 0);
        if (disc_inserted) {
            jg_cb_log(JG_LOG_INF, "Disc %d Inserted\n", disc_index + 1);
            jg_cb_log(JG_LOG_SCR, "Disc %d Inserted.", disc_index + 1);
        }
        else {
            jg_cb_log(JG_LOG_WRN, "Failed to Insert Disc %d\n", disc_index + 1);
            jg_cb_log(JG_LOG_SCR, "Failed to Insert Disc %d.", disc_index + 1);
        }
    }
}

void jg_cheat_clear() {
}

void jg_cheat_set(const char *code) {
    if (code) { }
}

void jg_rehash() {
    if (!strcmp(coreinfo.sys, "vb")) {
        mdfn_setup_vb();
    }
    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 type, int port, const void *ptr, size_t size) {
    if (type || port || ptr || size) { }
}

// JG Functions that return values to the frontend
jg_coreinfo_t* jg_get_coreinfo(const char *subsys) {
    if (!strcmp(subsys, "lynx")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "lynx", 1, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 160, 102, 160, 102, 0, 0, 160, 160.0/102.0, NULL
        };
    }
    else if (!strcmp(subsys, "ngp")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "ngp", 1, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 160, 152, 160, 152, 0, 0, 160, 160.0/152.0, NULL
        };
    }
    else if (!strcmp(subsys, "pce")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "pce", 5, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 1365, 270, 256, 232, 0, 0, 1365, 1.2413793, NULL
        };
    }
    else if (!strcmp(subsys, "psx")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "psx", 8, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 768, 480, 320, 240, 0, 0, 768, 4.0/3.0, NULL
        };
    }
    else if (!strcmp(subsys, "snes")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "snes", 8, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 512, 480, 256, 224, 0, 0, 512, 1.3061224, NULL
        };
    }
    else if (!strcmp(subsys, "ss")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "ss", 12, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 704, 480, 320, 240, 0, 0, 704, 4.0/3.0, NULL
        };
    }
    else if (!strcmp(subsys, "vb")) {
        coreinfo = { "mednafen", "Mednafen", MEDNAFEN_VERSION, "vb", 2, 0 };
        vidinfo = {
            JG_PIXFMT_XRGB8888, 384, 224, 384, 224, 0, 0, 384, 384.0/224.0, NULL
        };
        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 = {
            JG_PIXFMT_XRGB8888, 224, 144, 224, 144, 0, 0, 224, 224.0/144.0, NULL
        };
        audinfo = (jg_audioinfo_t){
            JG_SAMPFMT_INT16, SAMPLERATE, CHANNELS, (SAMPLERATE / 75) * CHANNELS
        };
    }
    else {
        jg_cb_log(JG_LOG_ERR, "Failed to select subsystem: %s", subsys);
    }
    return &coreinfo;
}

jg_videoinfo_t* jg_get_videoinfo() {
    return &vidinfo;
}

jg_audioinfo_t* jg_get_audioinfo() {
    return &audinfo;
}

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

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

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