/*
 * melonDS - Jolly Good API Port
 *
 * Copyright (C) 2022-2026 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, specifically version 3 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, see <https://www.gnu.org/licenses/>.
 */

#include <condition_variable>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <thread>
#include <vector>
#include <cstdarg>
#include <ctime>

#include <jg/jg.h>
#if JG_VERSION_NUMBER > 10000
#include <jg/jg_nds.h>
#else
#include "jg_nds_mds.h"
#include "jg_compat.h"
#endif

#include "NDS.h"

#include "version.h"

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

#define SCREEN_W 256
#define SCREEN_H 192
#define SCREEN_W_FIB (SCREEN_W * 3)

using namespace melonDS;

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

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

static jg_coreinfo_t coreinfo = {
    "melonds", "melonDS",
    JG_VERSION,
    "nds",
    NUMINPUTS,
    JG_HINT_INPUT_AUDIO | JG_HINT_INPUT_VIDEO
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888,     // pixfmt
    SCREEN_W_FIB,           // wmax
    SCREEN_H << 1,          // hmax
    SCREEN_W_FIB,           // w
    SCREEN_H << 1,          // h
    0,                      // x
    0,                      // y
    SCREEN_W_FIB,           // p
    2.0,                    // aspect
    NULL                    // buf
};

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

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

static int inputlist_built = 0;

// Emulator settings
static jg_setting_t settings_mds[] = {
    { "screen_layout", "Screen Layout",
      "0 = Vertical, 1 = Horizontal, 2 = One Screen, 3 = Fibonacci (Left), "
      "4 = Fibonacci (Right)",
      "Select the screen layout. Vertical is similar to the physical console. "
      "Horizontal places the screens beside each other. One Screen shows only "
      "one screen at a time. Fibonacci places the original screens in vertical "
      "orientation on the left or right of a single double sized screen.",
      0, 0, 4, JG_SETTING_RESTART
    },
    { "screen_swap", "Screen Swap",
      "0 = Disable, 1 = Enable",
      "Swap the screens. Top becomes bottom, bottom becomes top.",
      0, 0, 1, 0
    },
    { "threaded_rendering", "Threaded Rendering",
      "0 = Disable, 1 = Enable",
      "Enabling this option allows software video rendering to be performed on "
      "a separate thread. This greatly increases performance and should only "
      "be disabled if you have a special edge case.",
      1, 0, 1, JG_SETTING_RESTART
    },
    { "external_firmware", "External Firmware",
      "0 = Disable, 1 = Enable",
      "Enable or Disable the use of external firmware. This requires a dump "
      "of the NDS firmware, and the ARM7/ARM9 BIOS images.",
      0, 0, 1, JG_SETTING_RESTART
    },
};

enum {
    SCREEN_LAYOUT,
    SCREEN_SWAP,
    THREADED_RENDERING,
    EXTERNAL_FIRMWARE
};

static jg_biosinfo_t bioslist[] = {
    { "firmware.bin", "NDS Firmware",
      "3c704824663ce26b6a1ed4d85238ae5b", 0, 0 },
    { "bios7.bin", "NDS ARM7 BIOS",
      "df692a80a5b1bc90728bc3dfc76cd948", 0, 0 },
    { "bios9.bin", "NDS ARM9 BIOS",
      "a392174eb3e572fed6447e956bde4b25", 0, 0 },
};

static jg_systeminfo_t systemlist[] = {
    { "nds", "Nintendo DS", "nds,dsi" },
};

// Screen Layout
static void (*mds_video_render)(void);
static unsigned mainscr = 0;
static int bound_x0, bound_x1, bound_y0, bound_y1 = 0;

// Emulator Core
static NDS *nds;

/* The JGSemaphore class implements just enough features for this emulator core
   to function. Since C++ did not get semaphores until C++20, and melonDS is
   a C++17 codebase, this implementation is required. POSIX semaphores are
   entirely broken on macOS, and cannot be used here.
*/
class JGSemaphore {
private:
    std::condition_variable cv;
    std::mutex mtx;
    int count;

public:
    JGSemaphore(int c = 0) : count(c) { }

    void acquire(int n = 1) {
        std::unique_lock<std::mutex> lck(mtx);
        while(count < n)
            cv.wait(lck);
        count -= n;
    }

    int available() const { return count; }

    void release(int n = 1) {
        std::lock_guard<std::mutex> lck(mtx);
        count += n;
        cv.notify_all();
    }
};

namespace melonDS::Platform {

void Init(int argc, char** argv) {
}

void DeInit() {
}

void StopEmu() {
}

int InstanceID() {
    return 0;
}

std::string InstanceFileSuffix() {
    return "";
}

bool FileFlush(FileHandle* file) {
    return fflush(reinterpret_cast<FILE *>(file)) == 0;
}

u64 FileLength(FileHandle* file) {
    FILE* stdfile = reinterpret_cast<FILE *>(file);
    long pos = ftell(stdfile);
    fseek(stdfile, 0, SEEK_END);
    long len = ftell(stdfile);
    fseek(stdfile, pos, SEEK_SET);
    return len;
}

u64 FileRead(void* data, u64 size, u64 count, FileHandle* file) {
    return fread(data, size, count, reinterpret_cast<FILE *>(file));
}

bool FileReadLine(char* str, int count, FileHandle* file) {
    return fgets(str, count, reinterpret_cast<FILE *>(file)) != nullptr;
}

void FileRewind(FileHandle* file) {
    rewind(reinterpret_cast<FILE *>(file));
}

bool FileSeek(FileHandle* file, s64 offset, FileSeekOrigin origin) {
    int stdorigin;
    switch (origin)
    {
        case FileSeekOrigin::Start: stdorigin = SEEK_SET; break;
        case FileSeekOrigin::Current: stdorigin = SEEK_CUR; break;
        case FileSeekOrigin::End: stdorigin = SEEK_END; break;
    }

    return fseek(reinterpret_cast<FILE *>(file), offset, stdorigin) == 0;
}

u64 FileWrite(const void* data, u64 size, u64 count, FileHandle* file) {
    return fwrite(data, size, count, reinterpret_cast<FILE *>(file));
}

u64 FileWriteFormatted(FileHandle* file, const char* fmt, ...) {
    if (fmt == nullptr)
        return 0;

    va_list args;
    va_start(args, fmt);
    u64 ret = vfprintf(reinterpret_cast<FILE *>(file), fmt, args);
    va_end(args);
    return ret;
}

constexpr char AccessMode(FileMode mode, bool file_exists) {
    if (!(mode & FileMode::Write))
        return 'r';

    if (mode & (FileMode::NoCreate))
        return 'r';

    if ((mode & FileMode::Preserve) && file_exists)
        return 'r';

    return 'w';
}

bool IsEndOfFile(FileHandle* file) {
    return feof(reinterpret_cast<FILE *>(file)) != 0;
}

constexpr bool IsExtended(FileMode mode) {
    // fopen's "+" flag always opens the file for read/write
    return (mode & FileMode::ReadWrite) == FileMode::ReadWrite;
}

static std::string GetModeString(FileMode mode, bool file_exists) {
    std::string modeString;

    modeString += AccessMode(mode, file_exists);

    if (IsExtended(mode))
        modeString += '+';

    if (!(mode & FileMode::Text))
        modeString += 'b';

    return modeString;
}

FileHandle* OpenFile(const std::string& path, FileMode mode) {
    std::string modeString = GetModeString(mode, true);
    FILE* file = fopen(path.c_str(), modeString.c_str());
    if (file) {
        Log(LogLevel::Debug,
            "Opened \"%s\" with FileMode 0x%x (effective mode \"%s\")\n",
            path.c_str(), mode, modeString.c_str());
        return reinterpret_cast<FileHandle *>(file);
    }
    else {
        Log(LogLevel::Warn,
            "Failed to open \"%s\" with FileMode 0x%x (effective mode \"%s\")"
            "\n", path.c_str(), mode, modeString.c_str());
        return nullptr;
    }
}

FileHandle* OpenLocalFile(const std::string& path, FileMode mode) {
    return OpenFile(path, mode);
}

bool CloseFile(FileHandle* file) {
    return fclose(reinterpret_cast<FILE *>(file)) == 0;
}

bool LocalFileExists(const std::string& name) {
    FileHandle* f = OpenLocalFile(name, FileMode::Read);
    if (!f) return false;
    CloseFile(f);
    return true;
}

Thread* Thread_Create(std::function<void()> func) {
    return (Thread*)new std::thread(func);
}

void Thread_Free(Thread* thread) {
    std::thread *th = (std::thread*)thread;
    /* melonDS' reference implementation uses Qt threads, so the below
       Thread_Wait typically calls Qt's QThread::wait before deleting the
       thread. QThread::wait is similar to pthread_join or std::thread::join.
       This call to std::thread::join is here for safety.
    */
    if (th->joinable())
        th->join();
    delete th;
}

void Thread_Wait(Thread* thread) {
    ((std::thread*)thread)->join();
}

Semaphore* Semaphore_Create() {
    return (Semaphore*)new JGSemaphore;
}

void Semaphore_Free(Semaphore* sema) {
    delete (JGSemaphore*)sema;
}

void Semaphore_Reset(Semaphore* sema) {
    JGSemaphore *s = (JGSemaphore*)sema;
    s->acquire(s->available());
}

void Semaphore_Wait(Semaphore* sema) {
    ((JGSemaphore*)sema)->acquire();
}

void Semaphore_Post(Semaphore* sema, int count) {
    ((JGSemaphore*)sema)->release(count);
}

Mutex* Mutex_Create() {
    return (Mutex*)new std::mutex;
}

void Mutex_Free(Mutex* mutex) {
    delete (std::mutex*)mutex;
}

void Mutex_Lock(Mutex* mutex) {
    ((std::mutex*)mutex)->lock();
}

void Mutex_Unlock(Mutex* mutex) {
    ((std::mutex*)mutex)->unlock();
}

bool Mutex_TryLock(Mutex* mutex) {
    return ((std::mutex*)mutex)->try_lock();
}

void SignalStop(StopReason, void*) {
}

void Sleep(u64 usecs) {
    std::this_thread::sleep_for(std::chrono::microseconds(usecs));
}

void WriteNDSSave(const u8* savedata, u32 savelen, u32, u32, void*) {
    std::string path =
        std::string(pathinfo.save) + "/" + std::string(gameinfo.name) + ".sav";
    std::ofstream stream(path, std::ios::out | std::ios::binary);

    if (stream.is_open()) {
        stream.write((const char*)savedata, savelen);
        stream.close();
        jg_cb_log(JG_LOG_DBG, "File saved %s\n", path.c_str());
    }
    else {
        jg_cb_log(JG_LOG_WRN, "Failed to save file: %s\n", path.c_str());
    }
}

void WriteGBASave(const u8*, u32, u32, u32, void*) {
}

void WriteFirmware(const Firmware& firmware, u32, u32, void*) {
    if (firmware.GetHeader().Identifier == GENERATED_FIRMWARE_IDENTIFIER) {
        jg_cb_log(JG_LOG_WRN, "Cannot write to this firmware image!\n");
        return;
    }

    // This writes directly to a firmware image, so write to a copy
    std::string path = std::string(pathinfo.save) + "/firmware.nv";
    std::ofstream stream(path, std::ios::out | std::ios::binary);

    if (stream.is_open()) {
        stream.write((const char*)firmware.Buffer(), firmware.Length());
        stream.close();
        jg_cb_log(JG_LOG_DBG, "File saved %s\n", path.c_str());
    }
    else {
        jg_cb_log(JG_LOG_WRN, "Failed to save file: %s\n", path.c_str());
    }
}

void WriteDateTime(int, int, int, int, int, int, void*) {
}

void MP_Begin(void*) {
}

void MP_End(void*) {
}

int MP_SendPacket(u8*, int, u64, void*) {
    return 0;
}

int MP_RecvPacket(u8*, u64*, void*) {
    return 0;
}

int MP_SendCmd(u8*, int, u64, void*) {
    return 0;
}

int MP_SendReply(u8*, int, u64, u16, void*) {
    return 0;
}

int MP_SendAck(u8*, int, u64, void*) {
    return 0;
}

int MP_RecvHostPacket(u8*, u64*, void*) {
    return 0;
}

u16 MP_RecvReplies(u8*, u64, u16, void*) {
    return 0;
}

int Net_SendPacket(u8*, int, void*) {
    return 0;
}

int Net_RecvPacket(u8*, void*) {
    return 0;
}

void Camera_Start(int, void*) {
}

void Camera_Stop(int, void*) {
}

void Camera_CaptureFrame(int, u32*, int, int, bool, void*) {
}

bool Addon_KeyDown(KeyType, void*) {
    return false;
}

float Addon_MotionQuery(MotionQueryType, void*) {
    return 0.0;
}

void Addon_RumbleStart(u32, void*) {
}

void Addon_RumbleStop(void*) {
}

int Mic_ReadInput(short*, int, void*) {
    return 0;
}

void Mic_Start(void*) {
}

void Mic_Stop(void*) {
}

AACDecoder* AAC_Init() {
    return nullptr;
}

void AAC_DeInit(AACDecoder*) {
}

bool AAC_Configure(AACDecoder*, int, int) {
    return false;
}

bool AAC_DecodeFrame(AACDecoder*, void const*, int, void*, int) {
    return false;
}

void Log(LogLevel level, const char* fmt, ...) {
    jg_cb_log(level, fmt);
}

} // namespace Platform

// Video Helpers
static void mds_video_render_vertical(void) {
    size_t scrsz = SCREEN_W * SCREEN_H; // Single screen size
    u32 *fbuf0 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr].get();
    u32 *fbuf1 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr ^ 1].get();
    u32 *vbuf0 = (u32*)vidinfo.buf;
    u32 *vbuf1 = vbuf0 + scrsz;

    for (size_t i = 0; i < scrsz; ++i) {
        *vbuf0++ = *fbuf0++;
        *vbuf1++ = *fbuf1++;
    }
}

static void mds_video_render_horizontal(void) {
    u32 *fbuf0 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr].get();
    u32 *fbuf1 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr ^ 1].get();
    u32 *vbuf0 = (u32*)vidinfo.buf;
    u32 *vbuf1 = vbuf0 + SCREEN_W;

    for (size_t j = 0; j < SCREEN_H; ++j) {
        for (size_t i = 0; i < SCREEN_W; ++i) {
            *vbuf0++ = *fbuf0++;
            *vbuf1++ = *fbuf1++;
        }
        vbuf0 += SCREEN_W;
        vbuf1 += SCREEN_W;
    }
}

static void mds_video_render_fib(void) {
    size_t scrsz = SCREEN_W * SCREEN_H; // Single screen size
    bool right = settings_mds[SCREEN_LAYOUT].val == 4;

    u32 *fbuf0 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][0].get();
    u32 *fbuf1 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][1].get();
    u32 *fbuf2 = nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr].get();
    u32 *vbuf = (u32*)vidinfo.buf;

    // Little screens
    u32 *vbuf0 = right ? (vbuf + (SCREEN_W << 1)) : vbuf;
    u32 *vbuf1 = vbuf0 + (SCREEN_W_FIB * SCREEN_H);

    // Big screen
    u32 *vbuf2 = right ? vbuf : vbuf0 + SCREEN_W;
    u32 *vbuf3 = vbuf2 + SCREEN_W_FIB;

    for (size_t j = 0; j < SCREEN_H; ++j) {
        for (size_t i = 0; i < SCREEN_W; ++i) {
            *vbuf0++ = *fbuf0++;
            *vbuf1++ = *fbuf1++;

            *vbuf2++ = *fbuf2; *vbuf2++ = *fbuf2;
            *vbuf3++ = *fbuf2; *vbuf3++ = *fbuf2++;
        }
        vbuf0 += SCREEN_W << 1;
        vbuf1 += SCREEN_W << 1;

        vbuf2 += SCREEN_W_FIB + SCREEN_W;
        vbuf3 = vbuf2 + SCREEN_W_FIB;
    }
}

static void mds_video_render_onescreen(void) {
    memcpy(vidinfo.buf,
        nds->GPU.Framebuffer[nds->GPU.FrontBuffer][mainscr].get(),
        SCREEN_W * SCREEN_H * sizeof(u32));
}

static void mds_params_video(void) {
    // Set the correct "main" screen
    mainscr = settings_mds[SCREEN_SWAP].val;

    // Set bounds for Touchscreen
    switch (settings_mds[SCREEN_LAYOUT].val) {
        default: case 0: { // Vertical
            bound_x0 = 0;
            bound_x1 = SCREEN_W;
            bound_y0 = mainscr ? 0 : SCREEN_H;
            bound_y1 = mainscr ? SCREEN_H : (SCREEN_H << 1);
            break;
        }
        case 1: { // Horizontal
            bound_x0 = mainscr ? 0 : SCREEN_W;
            bound_x1 = mainscr ? SCREEN_W : (SCREEN_W << 1);
            bound_y0 = 0;
            bound_y1 = SCREEN_H;
            break;
        }
        case 2: { // One Screen
            bound_x0 = 0;
            bound_x1 = mainscr ? SCREEN_W : 0;
            bound_y0 = 0;
            bound_y1 = mainscr ? SCREEN_H : 0;
            break;
        }
        case 3: { // Fibonacci (Left)
            bound_x0 = 0;
            bound_x1 = SCREEN_W;
            bound_y0 = SCREEN_H;
            bound_y1 = SCREEN_H << 1;
            break;
        }
        case 4: { // Fibonacci (Right)
            bound_x0 = SCREEN_W << 1;
            bound_x1 = SCREEN_W_FIB;
            bound_y0 = SCREEN_H;
            bound_y1 = SCREEN_H << 1;
            break;
        }
    }
}

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

static bool sswap_pressed = false;
static bool oc_lid_pressed = false;

static void mds_input_refresh(void) {
    // Buttons
    u32 buttons = 0xfff;

    for (int i = 0; i < NDEFS_NDSCONTROLS; ++i)
        if (input_device[0]->button[i]) buttons &= ~(1 << NDSMap[i]);

    nds->SetKeyMask(buttons);

    // Check if the pointing devices is within bounds
    bool in_bounds =
        (input_device[1]->coord[0] >= bound_x0) &&
        (input_device[1]->coord[0] <= bound_x1) &&
        (input_device[1]->coord[1] >= bound_y0) &&
        (input_device[1]->coord[1] <= bound_y1);

    if (in_bounds && input_device[1]->button[0])
        nds->TouchScreen(
            input_device[1]->coord[0] - bound_x0,
            input_device[1]->coord[1] - bound_y0);
    else
        nds->ReleaseScreen();

    // Screen Swap
    if (input_device[2]->button[0] && !sswap_pressed) {
        settings_mds[SCREEN_SWAP].val ^= 1;
        mds_params_video();
    }
    sswap_pressed = input_device[2]->button[0];

    // Open/Close Lid
    if (input_device[2]->button[1] && !oc_lid_pressed)
        nds->SetLidClosed(!nds->IsLidClosed());
    oc_lid_pressed = input_device[2]->button[1];
}

// Firmware helpers
static void mds_rtc_set() {
    std::time_t t = std::time(0);
    std::tm *now = std::localtime(&t);
    nds->RTC.SetDateTime(now->tm_year + 1900, now->tm_mon + 1, now->tm_mday,
        now->tm_hour, now->tm_min, now->tm_sec);
}

static int mds_firmware_load() {
    // All three files need to be loaded successfully
    std::string fwpath = std::string{pathinfo.save} + "/firmware.nv";
    if (!std::filesystem::exists(fwpath)) {
        fwpath = std::string{pathinfo.bios} + "/firmware.bin";
        jg_cb_log(JG_LOG_INF,
            "No saved firmware found, loading default from: %s\n",
            fwpath.c_str());
    }
    else {
        /* Only set the clock if loading saved firmware, this way fresh
           firmware will trigger the user to enter their details.
        */
        mds_rtc_set();
    }

    // Load NDS Firmware
    std::ifstream fw_ifs(fwpath.c_str(), std::ios::in | std::ios::binary);
    if (!fw_ifs.is_open()) {
        jg_cb_log(JG_LOG_WRN, "Failed to load %s\n", fwpath.c_str());
        return 0;
    }
    std::vector<uint8_t> fw((std::istreambuf_iterator<char>(fw_ifs)),
        std::istreambuf_iterator<char>());
    fw_ifs.close();
    std::unique_ptr<Firmware> firmware =
        std::make_unique<Firmware>(fw.data(), fw.size());
    nds->SetFirmware(std::move(*firmware));

    // Load ARM7 BIOS
    std::string b7path = std::string{pathinfo.bios} + "/bios7.bin";
    std::ifstream bios7_ifs(b7path);
    if (!bios7_ifs.is_open()) {
        jg_cb_log(JG_LOG_WRN, "Failed to load %s\n", b7path.c_str());
        return 0;
    }
    ARM7BIOSImage bios7;
    bios7_ifs.read(reinterpret_cast<char*>(bios7.data()), ARM7BIOSSize);
    nds->SetARM7BIOS(bios7);

    // Load ARM9 BIOS
    std::string b9path = std::string{pathinfo.bios} + "/bios9.bin";
    std::ifstream bios9_ifs(b9path);
    if (!bios9_ifs.is_open()) {
        jg_cb_log(JG_LOG_WRN, "Failed to load %s\n", b9path.c_str());
        return 0;
    }
    ARM9BIOSImage bios9;
    bios9_ifs.read(reinterpret_cast<char*>(bios9.data()), ARM9BIOSSize);
    nds->SetARM9BIOS(bios9);

    return 1; // Success!
}

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

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

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

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

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

int jg_init(void) {
    nds = new NDS();
    nds->SPU.SetInterpolation(AudioInterpolation::None);
    NDS::Current = nds;

    auto renderer = std::make_unique<SoftRenderer>();
    renderer->SetThreaded(settings_mds[THREADED_RENDERING].val, nds->GPU);
    nds->GPU.SetRenderer3D(std::move(renderer));

    // Load external firmware if desired, set RTC here for built-in firmware
    if (settings_mds[EXTERNAL_FIRMWARE].val) {
        if (!mds_firmware_load())
            return 0;
    }
    else {
        mds_rtc_set();
    }

    nds->Start();

    return 1;
}

void jg_deinit(void) {
    nds->Stop();
    delete nds;
    nds = nullptr;
    NDS::Current = nds;
}

void jg_reset(int) {
    nds->Reset();
    if (nds->NeedsDirectBoot())
        nds->SetupDirectBoot(gameinfo.fname);
}

void jg_exec_frame(void) {
    // Refresh input, run a frame
    mds_input_refresh();
    nds->RunFrame();

    // Output accumulated audio samples
    u32 samps = nds->SPU.GetOutputSize();
    nds->SPU.ReadOutput((int16_t*)audinfo.buf, samps);
#if JG_VERSION_NUMBER >= 10100
    jg_cb_audio(udata_audio, samps << 1);
#else
    jg_cb_audio(samps << 1);
#endif

    // Render the video frame
    mds_video_render();
}

int jg_game_load(void) {
    // Set up the cart
    auto cart = NDSCart::ParseROM((const u8*)gameinfo.data, gameinfo.size,
        nullptr, std::nullopt);

    // Load the save data
    std::string savepath =
        std::string(pathinfo.save) + "/" + std::string(gameinfo.name) + ".sav";
    std::ifstream stream(savepath, std::ios::in | std::ios::binary);

    if (stream.is_open()) {
        std::vector<u8> savedata((std::istreambuf_iterator<char>(stream)),
            std::istreambuf_iterator<char>());
        cart->SetSaveMemory(savedata.data(), savedata.size());
    }
    else {
        jg_cb_log(JG_LOG_DBG, "Failed to load file: %s\n", savepath.c_str());
    }

    nds->SetNDSCart(std::move(cart));

    // Set up input devices
    inputinfo[0] = jg_nds_inputinfo(JG_NDS_CONTROLS, 0);
    inputinfo[1] = jg_nds_inputinfo(JG_NDS_TOUCH, 1);
    inputinfo[2] = jg_nds_inputinfo(JG_NDS_SHELL, 2);

    return 1;
}

int jg_game_unload(void) {
    return 1;
}

int jg_state_load(const char *filename) {
    std::ifstream stream(filename, std::ios::in | std::ios::binary);
    std::vector<uint8_t> statefile((std::istreambuf_iterator<char>(stream)),
        std::istreambuf_iterator<char>());
    stream.close();

    Savestate *st = new Savestate(statefile.data(), statefile.size(), false);
    bool ret = nds->DoSavestate(st);
    delete st;
    return ret;

}

void jg_state_load_raw(const void *data) {
}

int jg_state_save(const char *filename) {
    Savestate *st = new Savestate;
    bool ret = false;

    if (nds->DoSavestate(st)) {
        std::ofstream stream(filename, std::ios::out | std::ios::binary);
        if (stream.is_open()) {
            stream.write((const char*)st->Buffer(), st->Length());
            stream.close();
            ret = true;
        }
    }

    delete st;
    return ret;
}

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

size_t jg_state_size(void) {
    return 0;
}

void jg_media_select(void) {
}

void jg_media_insert(void) {
}

void jg_media_mount(unsigned) {
}

void jg_media_set(unsigned) {
}

void jg_cheat_clear(void) {
}

void jg_cheat_set(const char *code) {
}

void jg_rehash(void) {
    mds_params_video();
}

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

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

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

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

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

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

jg_inputinfo_t* jg_get_inputlist(size_t *num) {
    if (!inputlist_built) {
        unsigned listnum = 0;
        inputlist[listnum++] = jg_nds_inputinfo(0, JG_NDS_CONTROLS);
        inputlist[listnum++] = jg_nds_inputinfo(0, JG_NDS_TOUCH);
        inputlist[listnum++] = jg_nds_inputinfo(0, JG_NDS_SHELL);
        inputlist_built = 1;
    }
    *num = TOTALINPUTS;
    return inputlist;
}

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

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

jg_mediainfo_t* jg_get_mediainfo(void) {
    return NULL;
}

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

void jg_setup_video(void) {
    // Set Layout
    switch (settings_mds[SCREEN_LAYOUT].val) {
        default: case 0: { // Vertical
            mds_video_render = &mds_video_render_vertical;
            vidinfo.w = SCREEN_W;
            vidinfo.p = SCREEN_W;
            vidinfo.aspect = SCREEN_W/(SCREEN_H * 2.0);
            break;
        }
        case 1: { // Horizontal
            mds_video_render = &mds_video_render_horizontal;
            vidinfo.w = SCREEN_W << 1;
            vidinfo.h = SCREEN_H;
            vidinfo.p = SCREEN_W << 1;
            vidinfo.aspect = (SCREEN_W * 2.0) / SCREEN_H;
            break;
        }
        case 2: { // One Screen
            mds_video_render = &mds_video_render_onescreen;
            vidinfo.w = SCREEN_W;
            vidinfo.h = SCREEN_H;
            vidinfo.p = SCREEN_W;
            vidinfo.aspect = (SCREEN_W * 1.0) / SCREEN_H;
            break;
        }
        case 3: case 4: { // Fibonacci (Left and Right)
            mds_video_render = &mds_video_render_fib;
            vidinfo.w = SCREEN_W_FIB;
            vidinfo.h = SCREEN_H << 1;
            vidinfo.p = SCREEN_W_FIB;
            vidinfo.aspect = 2.0;
            break;
        }
    }

    // Set additional parameters
    mds_params_video();
}

void jg_setup_audio(void) {
}

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

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

void jg_set_auxinfo(jg_fileinfo_t info, int index) {
}

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

unsigned jg_api_version(void) {
    return JG_VERSION_NUMBER;
}
