/*
 * Nestopia JG
 *
 * Copyright (C) 2008-2018 Nestopia UE Contributors
 * Copyright (C) 2020-2025 Rupert Carmichael
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 */

#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring>
#include <cstdlib>
#include <stdint.h>

#include <jg/jg.h>
#include <jg/jg_nes.h>

#include "core/api/NstApiMachine.hpp"
#include "core/api/NstApiEmulator.hpp"
#include "core/api/NstApiVideo.hpp"
#include "core/api/NstApiCheats.hpp"
#include "core/api/NstApiSound.hpp"
#include "core/api/NstApiInput.hpp"
#include "core/api/NstApiCartridge.hpp"
#include "core/api/NstApiUser.hpp"
#include "core/api/NstApiFds.hpp"
#include "version.h"

#define AFILT_IMPL
#include "afilt.h"

#define SAMPLERATE 48000
#define FRAMERATE 60.098814
#define FRAMERATE_PAL 50.006978
#define CHANNELS 1
#define NUMINPUTS 5

#define ASPECT_NTSC 8.0 / 7.0
#define ASPECT_PAL 7375000.0 / 5320342.5
#define NTSC_FILTER_RATIO 2.3515625 // 602 / 256

using namespace Nes::Api;

static Emulator emulator;
static Video::Output *NstVideo;
static Sound::Output *NstSound;
static Input::Controllers *NstInput;

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 = {
    "nestopia", "Nestopia JG", JG_VERSION, "nes", NUMINPUTS, 0
};

static jg_fileinfo_t biosinfo;
static jg_fileinfo_t gameinfo;
static jg_pathinfo_t pathinfo;

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888,         // pixfmt
    Video::Output::NTSC_WIDTH,  // wmax
    Video::Output::HEIGHT,      // hmax
    Video::Output::WIDTH,       // w
    Video::Output::HEIGHT,      // h
    0,                          // x
    0,                          // y
    Video::Output::NTSC_WIDTH,  // p
    (Video::Output::WIDTH * ASPECT_NTSC) / Video::Output::HEIGHT, // aspect
    NULL
};

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

// Input Devices
static jg_inputinfo_t inputinfo[NUMINPUTS];
static jg_inputstate_t *input_device[NUMINPUTS];

// Emulator-specific settings
static jg_setting_t settings_nst[] = {
    { "port1", "Controller Port 1",
      "0 = Auto, 1 = Controller, 2 = Zapper, 3 = Arkanoid Paddle, "
      "4 = Power Pad, 5 = Power Glove",
      "Select the device plugged into controller port 1",
      0, 0, 5, JG_SETTING_INPUT
    },
    { "port2", "Controller Port 2",
      "0 = Auto, 1 = Controller, 2 = Zapper, 3 = Arkanoid Paddle, "
      "4 = Power Pad, 5 = Power Glove",
      "Select the device plugged into controller port 2",
      0, 0, 5, JG_SETTING_INPUT
    },
    { "port3", "Controller Port 3",
      "0 = Auto, 1 = Controller",
      "Select the device plugged into controller port 3",
      0, 0, 1, JG_SETTING_INPUT
    },
    { "port4", "Controller Port 4",
      "0 = Auto, 1 = Controller",
      "Select the device plugged into controller port 4",
      0, 0, 1, JG_SETTING_INPUT
    },
    { "portexp", "Expansion Port",
      "0 = Auto, 1 = Family Trainer, 2 = Pachinko, 3 = Oeka Kids Tablet, "
      "4 = Konami Hypershot, 5 = Bandai Hypershot, 6 = Crazy Climber, "
      "7 = Mahjong, 8 = Exciting Boxing, 9 = Top Rider, 10 = Pokkun Moguraa, "
      "11 = PartyTap",
      "Select the device plugged into the expansion port",
      0, 0, 11, JG_SETTING_INPUT
    },
    { "turbo_rate", "Turbo Pulse Rate",
      "N = Pulse every N frames",
      "Set the speed (in frames) at which the turbo buttons are pulsed",
      3, 2, 9, JG_SETTING_INPUT
    },
    { "palette", "Palette",
      "0 = Canonical, 1 = Consumer, 2 = Alternative, 3 = CXA2025AS (JP), "
      "4 = CXA2025AS (US), 5 = Royaltea, 6 = Nobilitea, "
      "7 = Digital Prime (FBX), 8 = Magnum (FBX), 9 = PVM Style D93 (FBX), "
      "10 = Smooth V2 (FBX), 11 = RGB, 12 = Custom",
      "Set the colour palette",
      Video::DECODER_CXA2025AS_US, 0, 12, 0
    },
    { "ntsc_filter", "NTSC Filter",
      "0 = Disable, 1 = Enable",
      "Use blargg's NTSC filter (required for Chroma Crosstalk emulation)",
      0, 0, 1, 0
    },
    { "ntsc_mode", "NTSC Filter Mode",
      "0 = Composite, 1 = S-Video, 2 = RGB, 3 = Monochrome",
      "Set the NTSC Filter Mode",
      0, 0, 3, 0
    },
    { "overscan_t", "Overscan Mask (Top)",
      "N = Hide N pixels of Overscan (Top)",
      "Hide N pixels of Overscan (Top)",
      8, 0, 12, 0
    },
    { "overscan_b", "Overscan Mask (Bottom)",
      "N = Hide N pixels of Overscan (Bottom)",
      "Hide N pixels of Overscan (Bottom)",
      8, 0, 12, 0
    },
    { "overscan_l", "Overscan Mask (Left)",
      "N = Hide N pixels of Overscan (Left)",
      "Hide N pixels of Overscan (Left)",
      0, 0, 12, 0
    },
    { "overscan_r", "Overscan Mask (Right)",
      "N = Hide N pixels of Overscan (Right)",
      "Hide N pixels of Overscan (Right)",
      0, 0, 12, 0
    },
    { "audio_filter", "Audio Output Filter",
      "0 = Disable, 1 = Enable",
      "Enable or Disable the audio output filter, which includes First Order "
      "High Pass and First Order Low Pass filtering similar to real hardware",
      0, 0, 1, 0
    },
    { "favored_system", "Favored/System",
      "0 = NTSC, 1 = PAL, 2 = Famicom, 3 = Dendy",
      "Set the desired system in cases where a database entry does not exist",
      0, 0, 3, JG_SETTING_RESTART
    },
    { "force_region", "Force Region",
      "0 = Auto, 1 = Force",
      "Force the system to be the Favored System",
      0, 0, 1, JG_SETTING_RESTART
    },
    { "ram_power_state", "RAM Power-on State",
      "0 = 0x00, 1 = 0xff, 2 = Random",
      "Set the initial values in System RAM when the system is powered on",
      2, 0, 2, JG_SETTING_RESTART
    },
    { "unlimited_sprites", "Unlimited Sprites",
      "0 = Disable, 1 = Enable",
      "Disables the 8 sprite per line limit - may cause glitches",
      0, 0, 1, 0
    },
    { "genie_distortion", "Game Genie Sound Distortion",
      "0 = Disable, 1 = Enable",
      "Some games do not clear APU registers at boot, causing distortion when "
      "a Game Genie is in use (Mega Man, Mega Man 2)",
      0, 0, 1, JG_SETTING_RESTART
    },
    { "softpatch", "Soft Patching",
      "0 = Disable, 1 = Enable",
      "Automatically patch games when a .ips or .ups patch file with the same "
      "name as the ROM is present",
      0, 0, 1, JG_SETTING_RESTART
    }
};

enum {
    PORT1,
    PORT2,
    PORT3,
    PORT4,
    PORTEXP,
    TURBORATE,
    PALETTE,
    NTSC,
    NTSCMODE,
    OVERSCAN_T,
    OVERSCAN_B,
    OVERSCAN_L,
    OVERSCAN_R,
    AUDIOFILTER,
    FAVSYSTEM,
    FORCEREGION,
    RAMPOWERSTATE,
    UNLIMITEDSPRITES,
    GENIEDISTORTION,
    SOFTPATCH
};

// Microphone Input
static unsigned micstate = 0;
static bool kstudio = false;

// Nestopia Helper Functions
static void nst_fds_info(void) {
    Fds fds(emulator);
    if (fds.IsAnyDiskInserted()) {
        jg_cb_log(JG_LOG_INF, "Disk %c Side %c\n",
            fds.GetCurrentDisk() == 0 ? '1' : '2',
            fds.GetCurrentDiskSide() == 0 ? 'A' : 'B');
        jg_cb_log(JG_LOG_SCR, "Disk %c Side %c.",
            fds.GetCurrentDisk() == 0 ? '1' : '2',
            fds.GetCurrentDiskSide() == 0 ? 'A' : 'B');
    }
    else {
        jg_cb_log(JG_LOG_INF, "Disk Drive Empty\n");
        jg_cb_log(JG_LOG_SCR, "Disk Drive Empty.");
    }
}

static int nst_db_load(void) {
    Cartridge::Database db(emulator);
    std::string dbpath = std::string(pathinfo.core) + "/NstDatabase.xml";
    std::ifstream dbfile(dbpath.c_str(),
        std::ifstream::in|std::ifstream::binary);

    if (dbfile.is_open()) {
        db.Load(dbfile);
        db.Enable(true);
        dbfile.close();
        return 1;
    }
    return 0;
}

static void nst_sample_load(User::File& file, const char *path, bool rel) {
    std::string filepath;
    if (rel) {
        filepath = std::string(path);
    }
    else {
        char wavfile[] = "xx.wav";
        unsigned id = file.GetId();
        if (id > 99) {
            return;
        }
        wavfile[0] = '0' + id / 10;
        wavfile[1] = '0' + id % 10;
        filepath = std::string(pathinfo.user) + "/" + path + "/" + wavfile;
    }

    std::ifstream is(filepath.c_str(), std::ifstream::binary);
    if (is) {
        is.seekg(0, is.end);
        int length = is.tellg();
        is.seekg(0, is.beg);

        uint8_t *buffer = new uint8_t[length];
        is.read((char*)buffer, length);
        is.close();

        uint32_t datasize = buffer[0x2b] << 24 | buffer[0x2a] << 16 |
            buffer[0x29] << 8 | buffer[0x28];
        uint16_t blockalign = buffer[0x21] << 8 | buffer[0x20];
        uint16_t numchannels = buffer[0x17] << 8 | buffer[0x16];
        uint16_t bitspersample = buffer[0x23] << 8 | buffer[0x22];
        uint32_t samplerate = buffer[0x1b] << 24 | buffer[0x1a] << 16 |
            buffer[0x19] << 8 | buffer[0x18];
        file.SetSampleContent(&buffer[0x2c], datasize / blockalign,
            numchannels == 2, bitspersample, samplerate);
        delete[] buffer;
    }
}

// Nestopia Callbacks
static void NST_CALLBACK nst_cb_event(void *userdata, User::Event event,
    const void *data) {
    // Handle special events
    switch (event) {
        case User::EVENT_CPU_JAM:
            jg_cb_log(JG_LOG_SCR, "Cpu: Jammed.");
            break;
        case User::EVENT_CPU_UNOFFICIAL_OPCODE:
            jg_cb_log(JG_LOG_DBG, "Cpu: Unofficial Opcode %s\n",
                (const char*)data);
            break;
        case User::EVENT_DISPLAY_TIMER:
            jg_cb_log(JG_LOG_SCR, (const char*)data);
            break;
        default: break;
    }
}

static void NST_CALLBACK nst_cb_file(void *userdata, User::File& file) {
    switch (file.GetAction()) {
        case User::File::LOAD_ROM: { // XML Romset loading
            std::wstring wfname = std::wstring(file.GetName());
            std::string fname(wfname.begin(), wfname.end());
            std::string basedir = std::string(gameinfo.path);
            basedir = basedir.substr(0, basedir.rfind("/"));
            fname = basedir + "/" + fname;
            std::ifstream romfile(fname.c_str(),
                std::ifstream::in|std::ifstream::binary);
            if (romfile.is_open()) {
                file.SetContent(romfile);
                romfile.close();
            }
            break;
        }
        case User::File::LOAD_SAMPLE: {
            std::wstring wfname = std::wstring(file.GetName());
            std::string fname(wfname.begin(), wfname.end());
            std::string basedir = std::string(gameinfo.path);
            basedir = basedir.substr(0, basedir.rfind("/"));
            fname = basedir + "/" + fname;
            nst_sample_load(file, fname.c_str(), true);
            break;
        }
        case User::File::LOAD_SAMPLE_MOERO_PRO_YAKYUU: {
            nst_sample_load(file, "moepro", false);
            break;
        }
        case User::File::LOAD_SAMPLE_MOERO_PRO_YAKYUU_88: {
            nst_sample_load(file, "moepro88", false);
            break;
        }
        case User::File::LOAD_SAMPLE_MOERO_PRO_TENNIS: {
            nst_sample_load(file, "mptennis", false);
            break;
        }
        case User::File::LOAD_SAMPLE_TERAO_NO_DOSUKOI_OOZUMOU: {
            nst_sample_load(file, "terao", false);
            break;
        }
        case User::File::LOAD_SAMPLE_AEROBICS_STUDIO: {
            nst_sample_load(file, "ftaerobi", false);
            break;
        }
        case User::File::LOAD_BATTERY:
        case User::File::LOAD_EEPROM:
        case User::File::LOAD_TAPE:
        case User::File::LOAD_TURBOFILE: {
            std::string savename = std::string(pathinfo.save) + "/" +
                std::string(gameinfo.name) + ".sav";
            std::ifstream savefile(savename.c_str(),
                std::ifstream::in|std::ifstream::binary);
            if (savefile.is_open()) {
                file.SetContent(savefile);
                savefile.close();
            }
            break;
        }
        case User::File::SAVE_BATTERY:
        case User::File::SAVE_EEPROM:
        case User::File::SAVE_TAPE:
        case User::File::SAVE_TURBOFILE: {
            std::string savename = std::string(pathinfo.save) + "/" +
                std::string(gameinfo.name) + ".sav";
            std::ofstream savefile(savename.c_str(),
                std::ifstream::out|std::ifstream::binary);
            const void *savedata;
            unsigned long datasize;
            file.GetContent(savedata, datasize);
            if (savefile.is_open()) {
                savefile.write((const char*)savedata, datasize);
                savefile.close();
            }
            break;
        }
        case User::File::LOAD_FDS: {
            std::string savename = std::string(pathinfo.save) + "/" +
                std::string(gameinfo.name) + ".ups";
            std::ifstream savefile(savename.c_str(),
                std::ifstream::in|std::ifstream::binary);
            if (savefile.is_open()) {
                file.SetPatchContent(savefile);
                savefile.close();
            }
            break;
        }
        case User::File::SAVE_FDS: {
            std::string savename = std::string(pathinfo.save) + "/" +
                std::string(gameinfo.name) + ".ups";
            std::ofstream savefile(savename.c_str(),
                std::ifstream::out|std::ifstream::binary);
            if (savefile.is_open()) {
                file.GetPatchContent(User::File::PATCH_UPS, savefile);
                savefile.close();
            }
            break;
        }
    }
}

static void NST_CALLBACK nst_cb_log(void *udata, const char *str,
    unsigned long int length) {

    jg_cb_log(JG_LOG_DBG, str);
}

static bool NST_CALLBACK nst_cb_videolock(void *udata, Video::Output& video) {
    video.pixels = vidinfo.buf;
    video.pitch = Video::Output::NTSC_WIDTH * sizeof(uint32_t);
    return true;
}

static void NST_CALLBACK nst_cb_videounlock(void *udata, Video::Output& video) {
}

static bool NST_CALLBACK nst_cb_soundlock(void* udata, Sound::Output& sound) {
    if (settings_nst[AUDIOFILTER].val) {
        afilt(&afilter[FILTER_FO_HPF], (int16_t*)audinfo.buf, audinfo.spf);
        afilt(&afilter[FILTER_FO_LPF], (int16_t*)audinfo.buf, audinfo.spf);
    }
    jg_cb_audio(audinfo.spf);
    return true;
}

static void NST_CALLBACK nst_cb_soundunlock(void* udata, Sound::Output& sound) {
}

// Input Poll Callbacks
static uint8_t NESMap[] = {
    Input::Controllers::Pad::UP, Input::Controllers::Pad::DOWN,
    Input::Controllers::Pad::LEFT, Input::Controllers::Pad::RIGHT,
    Input::Controllers::Pad::SELECT, Input::Controllers::Pad::START,
    Input::Controllers::Pad::A, Input::Controllers::Pad::B
};

static bool NST_CALLBACK nst_poll_pad(Input::UserData data,
    Input::Controllers::Pad& pad, unsigned port) {

    unsigned buttons = 0;

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

    // Turbo
    if (input_device[port]->button[8]) {
        if (input_device[port]->button[8] == settings_nst[TURBORATE].val) {
            buttons |= Input::Controllers::Pad::A;
            input_device[port]->button[8] = 1;
        }
        else {
            ++input_device[port]->button[8];
        }
    }
    if (input_device[port]->button[9]) {
        if (input_device[port]->button[9] == settings_nst[TURBORATE].val) {
            buttons |= Input::Controllers::Pad::B;
            input_device[port]->button[9] = 1;
        }
        else {
            ++input_device[port]->button[9];
        }
    }

    if (port == 1)
        pad.mic = micstate;

    pad.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_zapper(Input::UserData data,
    Input::Controllers::Zapper& zapper) {

    jg_inputstate_t *zdev = static_cast<jg_inputstate_t*>(data);
    zapper.fire = false;

    if (zdev->button[0]) {
        zapper.fire = true;

        if (settings_nst[NTSC].val)
            zapper.x = zdev->coord[0] / NTSC_FILTER_RATIO;
        else
            zapper.x = zdev->coord[0];

        zapper.y = zdev->coord[1];
    }
    else if (zdev->button[1]) {
        zapper.fire = true;
        zapper.x = ~0U;
        zapper.y = ~0U;
    }

    return true;
}

static bool NST_CALLBACK nst_poll_paddle(Input::UserData data,
    Input::Controllers::Paddle& paddle) {

    jg_inputstate_t *adev = static_cast<jg_inputstate_t*>(data);

    paddle.x = (adev->axis[0] / 546) + 106; // 46-166 - super magical numbers!
    paddle.button = adev->button[0];

    return true;
}

static bool NST_CALLBACK nst_poll_powerpad(Input::UserData data,
    Input::Controllers::PowerPad& powerPad) {

    jg_inputstate_t *ppdev = static_cast<jg_inputstate_t*>(data);

    for (int i = 0; i < 12; ++i)
        powerPad.sideA[i] = ppdev->button[i];

    for (int i = 0; i < 8; ++i)
        powerPad.sideB[i] = ppdev->button[i + 12];

    return true;
}

static bool NST_CALLBACK nst_poll_powerglove(Input::UserData data,
    Input::Controllers::PowerGlove& glove) {

    jg_inputstate_t *pgdev = static_cast<jg_inputstate_t*>(data);
    unsigned pad = 0;

    if (pgdev->button[0]) pad |= Input::Controllers::PowerGlove::SELECT;
    if (pgdev->button[1]) pad |= Input::Controllers::PowerGlove::START;
    glove.buttons = pad;

    if (pgdev->button[2])
        glove.distance = Input::Controllers::PowerGlove::DISTANCE_IN;
    else if (pgdev->button[3])
        glove.distance = Input::Controllers::PowerGlove::DISTANCE_OUT;
    else
        glove.distance = 0;

    if (pgdev->button[4])
        glove.wrist = Input::Controllers::PowerGlove::ROLL_LEFT;
    else if (pgdev->button[5])
        glove.wrist = Input::Controllers::PowerGlove::ROLL_RIGHT;
    else
        glove.wrist = 0;

    if (pgdev->button[6])
        glove.gesture = Input::Controllers::PowerGlove::GESTURE_FIST;
    else if (pgdev->button[7])
        glove.gesture = Input::Controllers::PowerGlove::GESTURE_FINGER;
    else
        glove.gesture = Input::Controllers::PowerGlove::GESTURE_OPEN;

    if (settings_nst[NTSC].val)
        glove.x = pgdev->coord[0] / NTSC_FILTER_RATIO;
    else
        glove.x = pgdev->coord[0];

    glove.y = pgdev->coord[1];

    return true;
}

static bool NST_CALLBACK nst_poll_familytrainer(Input::UserData data,
    Input::Controllers::FamilyTrainer& familyTrainer) {

    jg_inputstate_t *ftdev = static_cast<jg_inputstate_t*>(data);

    for (int i = 0; i < 12; ++i)
        familyTrainer.sideA[i] = ftdev->button[i];

    for (int i = 0; i < 8; ++i)
        familyTrainer.sideB[i] = ftdev->button[i + 12];

    return true;
}

static bool NST_CALLBACK nst_poll_pachinko(Input::UserData data,
    Input::Controllers::Pachinko& pachinko) {

    jg_inputstate_t *pchdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    for (int i = 0; i < 8; ++i)
        if (pchdev->button[i]) buttons |= NESMap[i];

    pachinko.buttons = buttons;

    pachinko.throttle = pchdev->axis[0] / 512;

    return true;
}

static bool NST_CALLBACK nst_poll_oekakids(Input::UserData data,
    Input::Controllers::OekaKidsTablet& tablet) {

    jg_inputstate_t *okdev = static_cast<jg_inputstate_t*>(data);

    if (settings_nst[NTSC].val)
        tablet.x = okdev->coord[0] / NTSC_FILTER_RATIO;
    else
        tablet.x = okdev->coord[0];

    tablet.y = okdev->coord[1];

    tablet.button = okdev->button[0];

    return true;
}

static bool NST_CALLBACK nst_poll_konamihypershot(Input::UserData data,
    Input::Controllers::KonamiHyperShot& konamiHyperShot) {

    jg_inputstate_t *khsdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    if (khsdev->button[0])
        buttons |= Input::Controllers::KonamiHyperShot::PLAYER1_BUTTON_1;

    if (khsdev->button[1])
        buttons |= Input::Controllers::KonamiHyperShot::PLAYER1_BUTTON_2;

    if (khsdev->button[2])
        buttons |= Input::Controllers::KonamiHyperShot::PLAYER2_BUTTON_1;

    if (khsdev->button[3])
        buttons |= Input::Controllers::KonamiHyperShot::PLAYER2_BUTTON_2;

    konamiHyperShot.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_bandaihypershot(Input::UserData data,
    Input::Controllers::BandaiHyperShot& bandaiHyperShot) {

    jg_inputstate_t *bhsdev = static_cast<jg_inputstate_t*>(data);
    bandaiHyperShot.fire = false;

    if (bhsdev->button[0]) {
        bandaiHyperShot.fire = true;

        if (settings_nst[NTSC].val)
            bandaiHyperShot.x = bhsdev->coord[0] / NTSC_FILTER_RATIO;
        else
            bandaiHyperShot.x = bhsdev->coord[0];

        bandaiHyperShot.y = bhsdev->coord[1];
    }
    else if (bhsdev->button[1]) {
        bandaiHyperShot.fire = true;
        bandaiHyperShot.x = ~0U;
        bandaiHyperShot.y = ~0U;
    }
    bandaiHyperShot.move = bhsdev->button[2];

    return true;
}

static bool NST_CALLBACK nst_poll_crazyclimber(Input::UserData data,
    Input::Controllers::CrazyClimber& crazyClimber) {

    jg_inputstate_t *ccdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    if (ccdev->button[0]) buttons |= Input::Controllers::CrazyClimber::UP;
    if (ccdev->button[1]) buttons |= Input::Controllers::CrazyClimber::DOWN;
    if (ccdev->button[2]) buttons |= Input::Controllers::CrazyClimber::LEFT;
    if (ccdev->button[3]) buttons |= Input::Controllers::CrazyClimber::RIGHT;
    crazyClimber.left = buttons;

    buttons = 0;

    if (ccdev->button[4]) buttons |= Input::Controllers::CrazyClimber::UP;
    if (ccdev->button[5]) buttons |= Input::Controllers::CrazyClimber::DOWN;
    if (ccdev->button[6]) buttons |= Input::Controllers::CrazyClimber::LEFT;
    if (ccdev->button[7]) buttons |= Input::Controllers::CrazyClimber::RIGHT;
    crazyClimber.right = buttons;

    return true;
}

static bool NST_CALLBACK nst_poll_mahjong(Input::UserData data,
    Input::Controllers::Mahjong& mahjong, unsigned part) {

    jg_inputstate_t *mjdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    switch (part) {
        case Input::Controllers::Mahjong::PART_1:
            if (mjdev->button[8])
                buttons |= Input::Controllers::Mahjong::PART_1_I;
            if (mjdev->button[9])
                buttons |= Input::Controllers::Mahjong::PART_1_J;
            if (mjdev->button[10])
                buttons |= Input::Controllers::Mahjong::PART_1_K;
            if (mjdev->button[11])
                buttons |= Input::Controllers::Mahjong::PART_1_L;
            if (mjdev->button[12])
                buttons |= Input::Controllers::Mahjong::PART_1_M;
            if (mjdev->button[13])
                buttons |= Input::Controllers::Mahjong::PART_1_N;
            break;
        case Input::Controllers::Mahjong::PART_2:
            if (mjdev->button[0])
                buttons |= Input::Controllers::Mahjong::PART_2_A;
            if (mjdev->button[1])
                buttons |= Input::Controllers::Mahjong::PART_2_B;
            if (mjdev->button[2])
                buttons |= Input::Controllers::Mahjong::PART_2_C;
            if (mjdev->button[3])
                buttons |= Input::Controllers::Mahjong::PART_2_D;
            if (mjdev->button[4])
                buttons |= Input::Controllers::Mahjong::PART_2_E;
            if (mjdev->button[5])
                buttons |= Input::Controllers::Mahjong::PART_2_F;
            if (mjdev->button[6])
                buttons |= Input::Controllers::Mahjong::PART_2_G;
            if (mjdev->button[7])
                buttons |= Input::Controllers::Mahjong::PART_2_H;
            break;
        case Input::Controllers::Mahjong::PART_3:
            if (mjdev->button[14])
                buttons |= Input::Controllers::Mahjong::PART_3_SELECT;
            if (mjdev->button[15])
                buttons |= Input::Controllers::Mahjong::PART_3_START;
            if (mjdev->button[16])
                buttons |= Input::Controllers::Mahjong::PART_3_KAN;
            if (mjdev->button[17])
                buttons |= Input::Controllers::Mahjong::PART_3_PON;
            if (mjdev->button[18])
                buttons |= Input::Controllers::Mahjong::PART_3_CHI;
            if (mjdev->button[19])
                buttons |= Input::Controllers::Mahjong::PART_3_REACH;
            if (mjdev->button[20])
                buttons |= Input::Controllers::Mahjong::PART_3_RON;
            break;
    }

    mahjong.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_excitingboxing(Input::UserData data,
    Input::Controllers::ExcitingBoxing& excitingBoxing, unsigned part) {

    jg_inputstate_t *ebdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    switch (part) {
        case Input::Controllers::ExcitingBoxing::PART_1:
            if (ebdev->button[5])
                buttons |=
                    Input::Controllers::ExcitingBoxing::PART_1_RIGHT_HOOK;
            if (ebdev->button[3])
                buttons |=
                    Input::Controllers::ExcitingBoxing::PART_1_RIGHT_MOVE;
            if (ebdev->button[4])
                buttons |= Input::Controllers::ExcitingBoxing::PART_1_LEFT_HOOK;
            if (ebdev->button[2])
                buttons |= Input::Controllers::ExcitingBoxing::PART_1_LEFT_MOVE;
            break;
        case Input::Controllers::ExcitingBoxing::PART_2:
            if (ebdev->button[0])
                buttons |= Input::Controllers::ExcitingBoxing::PART_2_STRAIGHT;
            if (ebdev->button[7])
                buttons |= Input::Controllers::ExcitingBoxing::PART_2_RIGHT_JAB;
            if (ebdev->button[1])
                buttons |= Input::Controllers::ExcitingBoxing::PART_2_BODY;
            if (ebdev->button[6])
                buttons |= Input::Controllers::ExcitingBoxing::PART_2_LEFT_JAB;
            break;
    }

    excitingBoxing.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_toprider(Input::UserData data,
    Input::Controllers::TopRider& topRider) {

    jg_inputstate_t *trdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    if (trdev->button[0]) buttons |= Input::Controllers::TopRider::SHIFT_GEAR;
    if (trdev->button[1]) buttons |= Input::Controllers::TopRider::REAR;
    if (trdev->button[2]) buttons |= Input::Controllers::TopRider::STEER_LEFT;
    if (trdev->button[3]) buttons |= Input::Controllers::TopRider::STEER_RIGHT;
    if (trdev->button[4]) buttons |= Input::Controllers::TopRider::SELECT;
    if (trdev->button[5]) buttons |= Input::Controllers::TopRider::START;
    if (trdev->button[6]) buttons |= Input::Controllers::TopRider::ACCEL;
    if (trdev->button[7]) buttons |= Input::Controllers::TopRider::BRAKE;

    topRider.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_pokkunmoguraa(Input::UserData data,
    Input::Controllers::PokkunMoguraa& pokkunMoguraa, unsigned row) {

    jg_inputstate_t *pmdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    if (row & Input::Controllers::PokkunMoguraa::ROW_1) {
        if (pmdev->button[0])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_1;
        if (pmdev->button[1])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_2;
        if (pmdev->button[2])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_3;
        if (pmdev->button[3])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_4;
    }
    if (row & Input::Controllers::PokkunMoguraa::ROW_2) {
        if (pmdev->button[4])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_1;
        if (pmdev->button[5])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_2;
        if (pmdev->button[6])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_3;
        if (pmdev->button[7])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_4;
    }
    if (row & Input::Controllers::PokkunMoguraa::ROW_3) {
        if (pmdev->button[8])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_1;
        if (pmdev->button[9])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_2;
        if (pmdev->button[10])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_3;
        if (pmdev->button[11])
            buttons |= Input::Controllers::PokkunMoguraa::BUTTON_4;
    }

    pokkunMoguraa.buttons = buttons;
    return true;
}

static bool NST_CALLBACK nst_poll_partytap(Input::UserData data,
    Input::Controllers::PartyTap& partyTap) {

    jg_inputstate_t *ptdev = static_cast<jg_inputstate_t*>(data);
    unsigned units = 0;

    if (ptdev->button[0]) units |= Input::Controllers::PartyTap::UNIT_1;
    if (ptdev->button[1]) units |= Input::Controllers::PartyTap::UNIT_2;
    if (ptdev->button[2]) units |= Input::Controllers::PartyTap::UNIT_3;
    if (ptdev->button[3]) units |= Input::Controllers::PartyTap::UNIT_4;
    if (ptdev->button[4]) units |= Input::Controllers::PartyTap::UNIT_5;
    if (ptdev->button[5]) units |= Input::Controllers::PartyTap::UNIT_6;

    partyTap.units = units;
    return true;
}

static bool NST_CALLBACK nst_poll_vssys(Input::UserData data,
    Input::Controllers::VsSystem& vsSystem) {

    jg_inputstate_t *vsdev = static_cast<jg_inputstate_t*>(data);
    unsigned slots = 0;

    if (vsdev->button[0]) slots |= Input::Controllers::VsSystem::COIN_1;
    if (vsdev->button[1]) slots |= Input::Controllers::VsSystem::COIN_2;

    vsSystem.insertCoin = slots;
    return true;
}

static bool NST_CALLBACK nst_poll_karaokestudio(Input::UserData data,
    Input::Controllers::KaraokeStudio& karaokeStudio) {

    jg_inputstate_t *ksdev = static_cast<jg_inputstate_t*>(data);
    unsigned buttons = 0;

    if (ksdev->button[0]) buttons |= Input::Controllers::KaraokeStudio::A;
    if (ksdev->button[1]) buttons |= Input::Controllers::KaraokeStudio::B;
    buttons |= micstate; // 2nd bit for mic, like Famicom built-in mic

    karaokeStudio.buttons = buttons;
    return true;
}

static void nst_params_input(void) {
    // Autoselect the adapter
    Input(emulator).AutoSelectAdapter();

    // Select input devices for controller ports
    for (int i = 0; i < 4; ++i) {
        switch (settings_nst[PORT1 + i].val) {
            case 0: default:
                Input(emulator).AutoSelectController(i); break;
            case 1:
                Input(emulator).ConnectController(i,
                    (Input::Type)(Input::PAD1 + i)); break;
            case 2:
                Input(emulator).ConnectController(i, Input::ZAPPER); break;
            case 3:
                Input(emulator).ConnectController(i, Input::PADDLE); break;
            case 4:
                Input(emulator).ConnectController(i, Input::POWERPAD); break;
            case 5:
                Input(emulator).ConnectController(i, Input::POWERGLOVE); break;
        }
    }

    // Select input device for expansion port
    switch (settings_nst[PORTEXP].val) {
        case 0: default:
            Input(emulator).AutoSelectController(4); break;
        case 1:
            Input(emulator).ConnectController(4, Input::FAMILYTRAINER); break;
        case 2:
            Input(emulator).ConnectController(4, Input::PACHINKO); break;
        case 3:
            Input(emulator).ConnectController(4, Input::OEKAKIDSTABLET); break;
        case 4:
            Input(emulator).ConnectController(4, Input::KONAMIHYPERSHOT); break;
        case 5:
            Input(emulator).ConnectController(4, Input::BANDAIHYPERSHOT); break;
        case 6:
            Input(emulator).ConnectController(4, Input::CRAZYCLIMBER); break;
        case 7:
            Input(emulator).ConnectController(4, Input::MAHJONG); break;
        case 8:
            Input(emulator).ConnectController(4, Input::EXCITINGBOXING); break;
        case 9:
            Input(emulator).ConnectController(4, Input::TOPRIDER); break;
        case 10:
            Input(emulator).ConnectController(4, Input::POKKUNMOGURAA); break;
        case 11:
            Input(emulator).ConnectController(4, Input::PARTYTAP); break;
    }

    for (int i = 0; i < NUMINPUTS; ++i) {
        int type = Input(emulator).GetConnectedController(i);
        i < 4 ? jg_cb_log(JG_LOG_DBG, "Controller Port %d: %s\n", i + 1,
            jg_nes_input_name[type]) :
        jg_cb_log(JG_LOG_DBG, "Expansion Port: %s\n", jg_nes_input_name[type]);

        // Assign input info for connected devices
        inputinfo[i] = jg_nes_inputinfo(i, type);

        if (kstudio)
            inputinfo[4] = jg_nes_inputinfo(4, JG_NES_KARAOKESTUDIO);

        switch (type) {
            case Input::ZAPPER: {
                Input::Controllers::Zapper::callback.Set(nst_poll_zapper,
                    input_device[i]);
                break;
            }
            case Input::PADDLE: {
                Input::Controllers::Paddle::callback.Set(nst_poll_paddle,
                    input_device[i]);
                break;
            }
            case Input::POWERPAD: {
                Input::Controllers::PowerPad::callback.Set(nst_poll_powerpad,
                    input_device[i]);
                break;
            }
            case Input::FAMILYTRAINER: {
                Input::Controllers::FamilyTrainer::callback.Set(
                    nst_poll_familytrainer, input_device[i]);
                break;
            }
            case Input::POWERGLOVE: {
                Input::Controllers::PowerGlove::callback.Set(
                    nst_poll_powerglove, input_device[i]);
                break;
            }
            case Input::PACHINKO: {
                Input::Controllers::Pachinko::callback.Set(
                    nst_poll_pachinko, input_device[i]);
                break;
            }
            case Input::OEKAKIDSTABLET: {
                Input::Controllers::OekaKidsTablet::callback.Set(
                    nst_poll_oekakids, input_device[i]);
                break;
            }
            case Input::KONAMIHYPERSHOT: {
                Input::Controllers::KonamiHyperShot::callback.Set(
                    nst_poll_konamihypershot, input_device[i]);
                break;
            }
            case Input::BANDAIHYPERSHOT: {
                Input::Controllers::BandaiHyperShot::callback.Set(
                    nst_poll_bandaihypershot, input_device[i]);
                break;
            }
            case Input::CRAZYCLIMBER: {
                Input::Controllers::CrazyClimber::callback.Set(
                    nst_poll_crazyclimber, input_device[i]);
                break;
            }
            case Input::MAHJONG: {
                Input::Controllers::Mahjong::callback.Set(nst_poll_mahjong,
                    input_device[i]);
                break;
            }
            case Input::EXCITINGBOXING: {
                Input::Controllers::ExcitingBoxing::callback.Set(
                    nst_poll_excitingboxing, input_device[i]);
                break;
            }
            case Input::TOPRIDER: {
                Input::Controllers::TopRider::callback.Set(nst_poll_toprider,
                    input_device[i]);
                break;
            }
            case Input::POKKUNMOGURAA: {
                Input::Controllers::PokkunMoguraa::callback.Set(
                    nst_poll_pokkunmoguraa, input_device[i]);
                break;
            }
            case Input::PARTYTAP: {
                Input::Controllers::PartyTap::callback.Set(nst_poll_partytap,
                    input_device[i]);
                break;
            }
            default: break;
        }
    }

    // Set Pad callback separately
    Input::Controllers::Pad::callback.Set(nst_poll_pad, 0);
}

static void nst_params_video(void) {
    // Set up video parameters
    int renderwidth = settings_nst[NTSC].val ?
        Video::Output::NTSC_WIDTH : Video::Output::WIDTH;

    unsigned aspect_w = Video::Output::WIDTH - (settings_nst[OVERSCAN_L].val +
        settings_nst[OVERSCAN_R].val);
    unsigned w = renderwidth - (settings_nst[OVERSCAN_L].val +
        settings_nst[OVERSCAN_R].val);
    unsigned h = Video::Output::HEIGHT - (settings_nst[OVERSCAN_T].val +
        settings_nst[OVERSCAN_B].val);

    vidinfo.w = w;
    vidinfo.h = h;
    vidinfo.x = settings_nst[OVERSCAN_L].val;
    vidinfo.y = settings_nst[OVERSCAN_T].val;

    bool pal = Machine(emulator).GetMode() == Machine::PAL;
    vidinfo.aspect = (aspect_w * (pal ? ASPECT_PAL : ASPECT_NTSC)) / (double)h;

    Video video(emulator);
    Video::RenderState renderState;

    renderState.bits.mask.r = 0x00ff0000;
    renderState.bits.mask.g = 0x0000ff00;
    renderState.bits.mask.b = 0x000000ff;

    renderState.filter = settings_nst[NTSC].val ?
        Video::RenderState::FILTER_NTSC : Video::RenderState::FILTER_NONE;
    renderState.width = renderwidth;
    renderState.height = Video::Output::HEIGHT;
    renderState.bits.count = 32;

    switch (settings_nst[NTSCMODE].val) {
        case 0: { // Composite
            video.SetSaturation(Video::DEFAULT_SATURATION_COMP);
            video.SetSharpness(Video::DEFAULT_SHARPNESS_COMP);
            video.SetColorResolution(Video::DEFAULT_COLOR_RESOLUTION_COMP);
            video.SetColorBleed(Video::DEFAULT_COLOR_BLEED_COMP);
            video.SetColorArtifacts(Video::DEFAULT_COLOR_ARTIFACTS_COMP);
            video.SetColorFringing(Video::DEFAULT_COLOR_FRINGING_COMP);
            break;
        }
        case 1: { // S-Video
            video.SetSaturation(Video::DEFAULT_SATURATION_SVIDEO);
            video.SetSharpness(Video::DEFAULT_SHARPNESS_SVIDEO);
            video.SetColorResolution(Video::DEFAULT_COLOR_RESOLUTION_SVIDEO);
            video.SetColorBleed(Video::DEFAULT_COLOR_BLEED_SVIDEO);
            video.SetColorArtifacts(Video::DEFAULT_COLOR_ARTIFACTS_SVIDEO);
            video.SetColorFringing(Video::DEFAULT_COLOR_FRINGING_SVIDEO);
            break;
        }
        case 2: { // RGB
            video.SetSaturation(Video::DEFAULT_SATURATION_RGB);
            video.SetSharpness(Video::DEFAULT_SHARPNESS_RGB);
            video.SetColorResolution(Video::DEFAULT_COLOR_RESOLUTION_RGB);
            video.SetColorBleed(Video::DEFAULT_COLOR_BLEED_RGB);
            video.SetColorArtifacts(Video::DEFAULT_COLOR_ARTIFACTS_RGB);
            video.SetColorFringing(Video::DEFAULT_COLOR_FRINGING_RGB);
            break;
        }
        case 3: { // Monochrome
            video.SetSaturation(Video::DEFAULT_SATURATION_MONO);
            video.SetSharpness(Video::DEFAULT_SHARPNESS_MONO);
            video.SetColorResolution(Video::DEFAULT_COLOR_RESOLUTION_MONO);
            video.SetColorBleed(Video::DEFAULT_COLOR_BLEED_MONO);
            video.SetColorArtifacts(Video::DEFAULT_COLOR_ARTIFACTS_MONO);
            video.SetColorFringing(Video::DEFAULT_COLOR_FRINGING_MONO);
            break;
        }
    }

    // Path to external palette file
    std::string palpath = std::string(pathinfo.core) + "/palettes/";

    switch (settings_nst[PALETTE].val) {
        case 0: case 1: case 2: case 3: case 4: // YUV Palettes
            video.GetPalette().SetMode(Video::Palette::MODE_YUV);
            video.SetDecoder((Video::DecoderPreset)settings_nst[PALETTE].val);
            break;
        case 5: { // Royaltea
            palpath += "Royaltea.pal";
            break;
        }
        case 6: { // Nobilitea
            palpath += "Nobilitea.pal";
            break;
        }
        case 7: { // Digital Prime (FBX)
            palpath += "Digital_Prime_FBX.pal";
            break;
        }
        case 8: { // Magnum (FBX)
            palpath += "Magnum_FBX.pal";
            break;
        }
        case 9: { // PVM Style D93 (FBX)
            palpath += "PVM_Style_D93_FBX.pal";
            break;
        }
        case 10: { // Smooth V2 (FBX)
            palpath += "Smooth_V2_FBX.pal";
            break;
        }
        case 11: // RGB Palette
            video.GetPalette().SetMode(Video::Palette::MODE_RGB);
            break;
        case 12: { // Custom
            palpath = std::string(pathinfo.user) + "/custom.pal";
            break;
        }
        default: break;
    }

    // Load the palette from an external file
    if (settings_nst[PALETTE].val > 4 && settings_nst[PALETTE].val != 11) {
        std::ifstream ifs(palpath.c_str(), std::ifstream::binary);
        if (ifs.is_open()) {
            std::filebuf *pbuf = ifs.rdbuf();
            std::size_t size = pbuf->pubseekoff(0, ifs.end, ifs.in);
            pbuf->pubseekpos(0, ifs.in);
            char *buffer = new char[size];
            pbuf->sgetn(buffer, size);
            video.GetPalette().SetMode(Video::Palette::MODE_CUSTOM);
            video.GetPalette().SetCustom((Video::Palette::Colors)buffer,
                Video::Palette::STD_PALETTE);
            ifs.close();
            delete[] buffer;
        }
    }

    if (!Machine(emulator).Is(Machine::DISK)) {
        switch (Cartridge(emulator).GetProfile()->system.type) {
            case Cartridge::Profile::System::VS_UNISYSTEM:
            case Cartridge::Profile::System::VS_DUALSYSTEM: {
                if (settings_nst[PALETTE].val >= 4) {
                    video.GetPalette().SetMode(Video::Palette::MODE_YUV);
                    video.SetDecoder(Video::DECODER_CONSUMER);
                }
            }
            default: break;
        }
    }

    video.EnableUnlimSprites(settings_nst[UNLIMITEDSPRITES].val);

    if (NES_FAILED(video.SetRenderState(renderState))) {
        jg_cb_log(JG_LOG_ERR, "Nestopia core rejected render state\n");
    }

    video.Blit(*NstVideo); // Make changes visible if emulation is paused
}

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(void) {
    if (!nst_db_load()) {
        jg_cb_log(JG_LOG_ERR, "Failed to load NstDatabase.xml\n");
        return 0;
    }

    NstVideo = new Video::Output;
    NstSound = new Sound::Output;
    NstInput = new Input::Controllers;

    // Set internal callbacks
    Video::Output::lockCallback.Set(nst_cb_videolock, 0);
    Video::Output::unlockCallback.Set(nst_cb_videounlock, 0);
    User::eventCallback.Set(nst_cb_event, 0);
    User::fileIoCallback.Set(nst_cb_file, 0);
    User::logCallback.Set(nst_cb_log, 0);

    // Initialize the audio filters
    afilter[FILTER_FO_LPF] = afilt_init(FILTER_FO_LPF, 14000, SAMPLERATE);
    afilter[FILTER_FO_HPF] = afilt_init(FILTER_FO_HPF, 220, SAMPLERATE);

    return 1;
}

void jg_deinit(void) {
    if (NstVideo)
        delete NstVideo;

    if (NstSound)
        delete NstSound;

    if (NstInput)
        delete NstInput;
}

void jg_reset(int hard) {
    if (hard)
        Machine(emulator).SetRamPowerState(settings_nst[RAMPOWERSTATE].val);

    Machine(emulator).Reset(hard);
    Fds(emulator).EjectDisk();
    Fds(emulator).InsertDisk(0, 0);
}

void jg_exec_frame(void) {
    emulator.Execute(NstVideo, NstSound, NstInput);
}

int jg_game_load(void) {
    Machine machine(emulator);
    Nes::Result result;

    // Load the game into memory
    std::string rombuf((const char*)gameinfo.data, gameinfo.size);
    std::istringstream file(rombuf);

    // Check if it's an FDS game
    if (strstr(gameinfo.fname, ".fds") || strstr(gameinfo.fname, ".FDS")) {
        Fds fds(emulator);

        // Load the FDS BIOS as an auxiliary file if one was specified
        if (biosinfo.size) {
            std::string biosbuf((const char*)biosinfo.data, biosinfo.size);
            std::istringstream fdsbios(biosbuf);
            fds.SetBIOS(&fdsbios);
        }
        else { // Load the BIOS from the default path
            // Set up the path to the FDS BIOS
            std::string fdspath = std::string(pathinfo.bios) + "/disksys.rom";
            jg_cb_log(JG_LOG_INF, "FDS BIOS Path: %s\n", fdspath.c_str());

            // Load the FDS BIOS
            std::ifstream *fdsbios = new std::ifstream(fdspath.c_str(),
                std::ifstream::in|std::ifstream::binary);
            if (fdsbios->is_open()) {
                fds.SetBIOS(fdsbios);
                delete fdsbios;
            }
            else {
                jg_cb_log(JG_LOG_INF, "Failed to load FDS BIOS\n");
            }
        }
    }

    if (settings_nst[SOFTPATCH].val) {
        // Attempt to open an IPS patch matching the filename of the loaded ROM
        std::string ppath(gameinfo.path);
        ppath = ppath.substr(0, ppath.find_last_of("."))  + ".ips";
        std::ifstream pfile(ppath.c_str(), std::ios::in|std::ios::binary);

        // If there is no IPS patch, try to find a UPS patch
        if (!pfile.is_open()) {
            ppath = ppath.substr(0, ppath.find_last_of("."))  + ".ups";
            std::ifstream pfile(ppath.c_str(), std::ios::in|std::ios::binary);
        }

        // If a patch was found, apply it
        if (pfile.is_open()) {
            Machine::Patch patch(pfile);
            result = machine.Load(file,
                (Machine::FavoredSystem)settings_nst[FAVSYSTEM].val, patch);
            jg_cb_log(JG_LOG_INF, "Patch Applied: %s\n", ppath.c_str());
        }
        else {
            result = machine.Load(file,
                (Machine::FavoredSystem)settings_nst[FAVSYSTEM].val);
        }
    }
    else {
        result = machine.Load(file,
            (Machine::FavoredSystem)settings_nst[FAVSYSTEM].val);
    }

    if (NES_FAILED(result)) {
        switch (result) {
            case Nes::RESULT_ERR_INVALID_FILE:
                jg_cb_log(JG_LOG_ERR, "Invalid file\n");
                break;
            case Nes::RESULT_ERR_OUT_OF_MEMORY:
                jg_cb_log(JG_LOG_ERR, "Out of Memory\n");
                break;
            case Nes::RESULT_ERR_CORRUPT_FILE:
                jg_cb_log(JG_LOG_ERR, "Corrupt or Missing File\n");
                break;
            case Nes::RESULT_ERR_UNSUPPORTED_MAPPER:
                jg_cb_log(JG_LOG_ERR, "Unsupported Mapper\n");
                break;
            case Nes::RESULT_ERR_MISSING_BIOS:
                jg_cb_log(JG_LOG_ERR, "Missing FDS BIOS\n");
                break;
            default:
                jg_cb_log(JG_LOG_ERR, "%d\n", result);
                break;
        }
        return 0;
    }

    // Force the region based on "Favored System", or use the automatic region
    if (settings_nst[FORCEREGION].val) {
        if (settings_nst[FAVSYSTEM].val & 0x1)
            machine.SetMode(Machine::PAL);
        else
            machine.SetMode(Machine::NTSC);
    }
    else {
        machine.SetMode(machine.GetDesiredMode());
    }

    // Adjustments for PAL mode
    if (machine.GetMode() == Machine::PAL) {
        audinfo.spf = (SAMPLERATE / (unsigned)FRAMERATE_PAL) * CHANNELS;
        jg_cb_frametime(FRAMERATE_PAL);
    }
    else {
        audinfo.spf = (SAMPLERATE / (unsigned)FRAMERATE) * CHANNELS;
        jg_cb_frametime(FRAMERATE);
    }

    nst_params_input();

    // Set up FDS and Vs. System
    if (machine.Is(Machine::DISK)) { // Auto-insert FDS Disk
        coreinfo.hints |= JG_HINT_INPUT_AUDIO;
        Fds fds(emulator);
        fds.InsertDisk(0, 0);
    }
    else { // Famicom and VS. System
        const Cartridge::Profile *profile = Cartridge(emulator).GetProfile();

        switch (profile->system.type) {
            case Cartridge::Profile::System::FAMICOM: {
                coreinfo.hints |= JG_HINT_INPUT_AUDIO;

                // Jump through flaming hoops to get the mapper number
                unsigned mapper = Cartridge::Database(emulator).FindEntry(
                    profile->hash, Machine::FAVORED_FAMICOM).GetMapper();

                // Check if it's Karaoke Studio
                if (mapper == 188) {
                    inputinfo[4] = jg_nes_inputinfo(4, JG_NES_KARAOKESTUDIO);
                    Input::Controllers::KaraokeStudio::callback.Set(
                        nst_poll_karaokestudio, input_device[4]);
                    kstudio = true;
                }
                break;
            }
            case Cartridge::Profile::System::VS_UNISYSTEM:
            case Cartridge::Profile::System::VS_DUALSYSTEM: {
                inputinfo[4] = jg_nes_inputinfo(4, JG_NES_VSSYS);
                Input::Controllers::VsSystem::callback.Set(nst_poll_vssys,
                    input_device[4]);
                break;
            }
            default: break;
        }
    }

    // Set the RAM Power State
    machine.SetRamPowerState(settings_nst[RAMPOWERSTATE].val);

    // Start the machine
    machine.Power(true);

    return 1;
}

int jg_game_unload(void) {
    Machine(emulator).Power(false);
    Machine(emulator).Unload();
    return 1;
}

int jg_state_load(const char *filename) {
    std::ifstream statefile(filename, std::ifstream::in|std::ifstream::binary);
    if (statefile.is_open()) {
        Machine(emulator).LoadState(statefile);
        statefile.close();
        return 1;
    }
    return 0;
}

void jg_state_load_raw(const void*) {
}

int jg_state_save(const char *filename) {
    std::ofstream statefile(filename, std::ifstream::out|std::ifstream::binary);
    if (statefile.is_open()) {
        Machine(emulator).SaveState(statefile,
            Nes::Api::Machine::NO_COMPRESSION);
        statefile.close();
        return 1;
    }
    return 0;
}

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

size_t jg_state_size(void) {
    return 0;
}

void jg_media_select(void) {
    if (Machine(emulator).Is(Machine::DISK)) {
        Fds fds(emulator);
        int disk = fds.GetCurrentDisk();
        int side = fds.GetCurrentDiskSide();

        // Switch disks if it's a multi-disk game on Side B
        if (fds.GetNumDisks() > 1 && side == 1) {
            fds.EjectDisk();
            fds.InsertDisk(!disk, 0);
        }
        else if (fds.CanChangeDiskSide()) {
            fds.ChangeSide();
        }

        nst_fds_info();
    }
}

void jg_media_insert(void) {
    if (Machine(emulator).Is(Machine::DISK)) {
        Fds fds(emulator);
        fds.IsAnyDiskInserted() ? fds.EjectDisk() : fds.InsertDisk(0, 0);
        nst_fds_info();
    }
}

void jg_cheat_clear(void) {
    Cheats cheats(emulator);
    cheats.ClearCodes();
}

void jg_cheat_set(const char *code) {
    Cheats cheats(emulator);
    Cheats::Code cheatcode;

    // Check if the cheat is Raw
    if (strstr(code, "0x")) {
        std::vector<std::string> tokens;
        std::stringstream rawcode(code);
        std::string token;

        while (std::getline(rawcode, token, ' '))
            tokens.push_back(token);

        if ((tokens.size() == 2) || (tokens.size() == 3)) {
            cheatcode.address = strtoul(tokens[0].c_str(), NULL, 16);
            cheatcode.value = strtoul(tokens[1].c_str(), NULL, 16);

            if (tokens.size() == 3) {
                cheatcode.compare = strtoul(tokens[2].c_str(), NULL, 16);
                cheatcode.useCompare = true;
            }

            cheats.SetCode(cheatcode);
        }
        else {
            jg_cb_log(JG_LOG_WRN, "Invalid cheat: %s\n", code);
        }
    }
    // Game Genie
    else if (cheats.GameGenieDecode(code, cheatcode) == Nes::RESULT_OK) {
        cheats.SetCode(cheatcode);
    }
    // Pro Action Rocky
    else if (cheats.ProActionRockyDecode(code, cheatcode) == Nes::RESULT_OK) {
        cheats.SetCode(cheatcode);
    }
    else {
        jg_cb_log(JG_LOG_WRN, "Invalid cheat: %s\n", code);
    }
}

void jg_rehash(void) {
    nst_params_video();
    nst_params_input();
}

void jg_data_push(uint32_t type, int, const void *ptr, size_t size) {
    if (type == JG_DATA_AUDIO) {
        int16_t *buf = (int16_t*)(((jg_audioinfo_t*)ptr)->buf);
        size_t total = 0;

        for (size_t i = 0; i < size; ++i)
            total += abs(buf[i]);

        if (total / size > 2500)
            micstate ^= 0x04;
    }
}

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

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

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

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

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

void jg_setup_video(void) {
    nst_params_video();
}

void jg_setup_audio(void) {
    NstSound->samples[0] = audinfo.buf;
    NstSound->length[0] = audinfo.spf;
    NstSound->samples[1] = NULL;
    NstSound->length[1] = 0;

    // Set up sound parameters
    Sound sound(emulator);
    sound.SetSampleRate(audinfo.rate);
    sound.SetSpeaker(Sound::SPEAKER_MONO);
    sound.SetSpeed(Sound::DEFAULT_SPEED);
    sound.SetVolume(Sound::ALL_CHANNELS, 100);
    sound.SetGenie(settings_nst[GENIEDISTORTION].val);

    Sound::Output::lockCallback.Set(nst_cb_soundlock, 0);
    Sound::Output::unlockCallback.Set(nst_cb_soundunlock, 0);
}

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

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

void jg_set_auxinfo(jg_fileinfo_t info, int index) {
    if (index)
        return;
    biosinfo = info;
}

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