/*
 * melonDS - Jolly Good API Port
 *
 * Copyright (C) 2022 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 <chrono>
#include <condition_variable>
#include <fstream>
#include <mutex>
#include <thread>
#include <vector>

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

#include <samplerate.h>

#include "Platform.h"
#include "NDS.h"
#include "GPU.h"
#include "SPU.h"

#define SAMPLERATE 48000
#define SAMPLERATE_IN 32823.6328125
#define FRAMERATE 60
#define CHANNELS 2
#define NUMINPUTS 2

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 = {
    "melonds", "melonDS",
    MELONDS_VERSION,
    "nds",
    NUMINPUTS,
    JG_HINT_INPUT_AUDIO | JG_HINT_INPUT_VIDEO
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888, // pixfmt
    256,                // wmax
    384,                // hmax
    256,                // w
    384,                // h
    0,                  // x
    0,                  // y
    256,                // p
    256.0/384.0,        // aspect
    NULL                // buf
};

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

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

// Resampling
static SRC_STATE *srcstate = nullptr;
static SRC_DATA srcdata;
static int16_t inbuf[((SAMPLERATE / FRAMERATE) * CHANNELS) << 1];
static float inbuf_f[((SAMPLERATE / FRAMERATE) * CHANNELS) << 1];

/* 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.
*/
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 Platform {

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

void DeInit() {
}

void StopEmu() {
}

int InstanceID() {
    return 0;
}
std::string InstanceFileSuffix() {
    return "";
}

int GetConfigInt(ConfigEntry entry) {
    switch (entry) {
        case Firm_Language:
            return 1; // English, yes yes... Jolly Good!
        case Firm_BirthdayMonth:
            return 1;
        case Firm_BirthdayDay:
            return 1;
        case Firm_Color:
            return 0;
        case AudioBitrate:
            return 2; // 16-bit
    }

    return 0;
}

bool GetConfigBool(ConfigEntry entry) {
    switch (entry) {
        case ExternalBIOSEnable:
            return 0;
    }

    return false;
}

std::string GetConfigString(ConfigEntry entry) {
    switch (entry) {
        case Firm_Username:
            return "melonDS";
        case Firm_Message:
            return "Jolly Good!";
    }
    return "";
}

bool GetConfigArray(ConfigEntry entry, void* data) {
    return false;
}

FILE* OpenFile(std::string path, std::string mode, bool mustexist) {
    return fopen(path.c_str(), mode.c_str());
}

FILE* OpenLocalFile(std::string path, std::string mode) {
    return OpenFile(path, mode, true);
}

FILE* OpenDataFile(std::string path) {
    return OpenLocalFile(path, "rb");
}

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 Sleep(u64 usecs) {
    std::this_thread::sleep_for(std::chrono::microseconds(usecs));
}

void WriteNDSSave(const u8* savedata, u32 savelen, u32 woffset, u32 wlen) {
    if (woffset || wlen) { }

    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* savedata, u32 savelen, u32 woffset, u32 wlen) {
    if (savedata || savelen || woffset || wlen) { }
}

bool MP_Init() {
    return false;
}

void MP_DeInit() {
}

void MP_Begin() {
}

void MP_End() {
}

int MP_SendPacket(u8* data, int len, u64 timestamp) {
    return 0;
}

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

int MP_SendCmd(u8* data, int len, u64 timestamp) {
    return 0;
}

int MP_SendReply(u8* data, int len, u64 timestamp, u16 aid) {
    return 0;
}

int MP_SendAck(u8* data, int len, u64 timestamp) {
    return 0;
}

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

u16 MP_RecvReplies(u8* data, u64 timestamp, u16 aidmask) {
    return 0;
}

bool LAN_Init() {
    return false;
}

void LAN_DeInit() {
}

int LAN_SendPacket(u8* data, int len) {
    return 0;
}

int LAN_RecvPacket(u8* data) {
    return 0;
}

void Camera_Start(int num) {
}

void Camera_Stop(int num) {
}

void Camera_CaptureFrame(int num, u32* frame, int width, int height, bool yuv) {
}

} // namespace Platform

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

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

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

    NDS::SetKeyMask(buttons);

    // Touchscreen
    if (input_device[1]->button[0] && input_device[1]->coord[1] >= 192)
        NDS::TouchScreen(input_device[1]->coord[0],
            input_device[1]->coord[1] - 192);
    else
        NDS::ReleaseScreen();
}

static void mds_audio_push(void) {
    u32 insamps = SPU::GetOutputSize();
    SPU::ReadOutput(inbuf, insamps);

    src_short_to_float_array(inbuf, inbuf_f, insamps << 1);
    srcdata.data_in = inbuf_f;
    srcdata.data_out = (float*)audinfo.buf;
    srcdata.input_frames = insamps;
    srcdata.output_frames = audinfo.spf;
    src_process(srcstate, &srcdata);

    jg_cb_audio(srcdata.output_frames_gen << 1);
}

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) {
    GPU::RenderSettings vsettings;
    vsettings.Soft_Threaded = true;

    NDS::Init();

    GPU::InitRenderer(0); // Software rendering, GL not currently enabled
    GPU::SetRenderSettings(0, vsettings);

    SPU::SetInterpolation(0); // Just leave the audio samples alone

    NDS::SetConsoleType(0); // Hardcoded for DS

    return 1;
}

void jg_deinit(void) {
    GPU::DeInitRenderer();
    NDS::DeInit();

    if (srcstate)
        srcstate = src_delete(srcstate);
    srcstate = nullptr;
}

void jg_reset(int hard) {
    if (hard) { }
    NDS::Reset();
    NDS::SetupDirectBoot(gameinfo.fname);
    NDS::Start();
}

void jg_exec_frame(void) {
    // Refresh input, run a frame, push audio to resampler
    mds_input_refresh();
    NDS::RunFrame();
    mds_audio_push();

    // Draw each screen's internal buffer to the shared output buffer
    size_t scrsize = (256 * 192 * sizeof(u32)); // Size of a single screen
    u8 *vbuf0 = (u8*)vidinfo.buf;
    u8 *vbuf1 = vbuf0 + scrsize;
    memcpy(vbuf0, GPU::Framebuffer[GPU::FrontBuffer][0], scrsize);
    memcpy(vbuf1, GPU::Framebuffer[GPU::FrontBuffer][1], scrsize);
}

int jg_game_load(void) {
    // Load the BIOS and Cartridge
    NDS::LoadBIOS();
    NDS::LoadCart((u8*)gameinfo.data, gameinfo.size, nullptr, 0);

    // 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>());
        NDS::LoadSave(savedata.data(), savedata.size());
    }
    else {
        jg_cb_log(JG_LOG_DBG, "Failed to load file: %s\n", savepath.c_str());
    }

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

    return 1;
}

int jg_game_unload(void) {
    return 1;
}

int jg_state_load(const char *filename) {
    Savestate *state = new Savestate(filename, false);
    bool ret = NDS::DoSavestate(state);
    delete state;
    return ret;
}

void jg_state_load_raw(const void *data) {
}

int jg_state_save(const char *filename) {
    Savestate *state = new Savestate(filename, true);
    bool ret = NDS::DoSavestate(state);
    delete state;
    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_cheat_clear(void) {
}

void jg_cheat_set(const char *code) {
}

void jg_rehash(void) {
}

void jg_data_push(uint32_t type, int port, const void *ptr, size_t size) {
    if (type || port || ptr || size) { }
}

jg_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 = 0;
    return NULL;
}

void jg_setup_video(void) {
}

void jg_setup_audio(void) {
    if (srcstate == nullptr) {
        int err;
        srcstate = src_new(SRC_SINC_FASTEST, CHANNELS, &err);
        memset(&srcdata, 0, sizeof(SRC_DATA)); // MUST be zero initialized
    }

    src_reset(srcstate);
    srcdata.src_ratio = SAMPLERATE / SAMPLERATE_IN;
}

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