/*
Copyright (c) 2015, OpenEmu Team
Copyright (c) 2020-2023, Rupert Carmichael

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
  copyright notice, this list of conditions and the following disclaimer
  in the documentation and/or other materials provided with the
  distribution.
* Neither the names of the copyright holders nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <vector>
#include <cstddef>
#include <stdint.h>

#include <soxr.h>

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

#include "libgambatte/gambatte.h"
#include "resample/resamplerinfo.h"
#include "resample/resampler.h"

#include "gbcpalettes.h"
#include "version.h"

#define SAMPLERATE 48000
#define FRAMERATE 60
#define CHANNELS 2
#define NUMINPUTS 1

/* fps = 4194304.0 / 70224.0;
   in_rate = fps * 35112;
*/
#define SAMPLERATE_IN 2097152

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 = {
    "gambatte", "Gambatte JG", JG_VERSION, "gb", NUMINPUTS, 0
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888, 160, 144, 160, 144, 0, 0, 160, 160.0/144.0, NULL
};

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_inputstate_t *input_device[NUMINPUTS];

// Emulator settings
static jg_setting_t settings_gmbt[] = {
    { "cgbcolcorrect", "CGB Color Correction",
      "0 = Default, 1 = Modern",
      "Choose to use default or modern color correction",
      0, 0, 1, 0
    },
    { "dmgpalette", "DMG Palette",
      "0 = Internal, 1 = Original Green, 2 = GB Pocket, 3 = Blue, 4 = Brown,"
      "5 = Dark Blue, 6 = Dark Brown, 7 = Dark Green, 8 = Grayscale, 9 = Green,"
      "10 = Inverted, 11 = Orange, 12 = Pastel Mix, 13 = Red, 14 = Yellow",
      "Choose the palette to use in DMG Mode",
      0, 0, 14, 0
    },
    { "resampler", "Resampler",
      "0 = Internal, 1 = SoX",
      "Choose the internal resampler (fast) or SoX (high quality)",
      0, 0, 1, 1
    },
    { "rsqual", "Internal Resampler Quality",
      "0 = Fast, 1 = High, 2 = Very High, 3 = Highest",
      "Quality level for the internal resampler",
      3, 0, 3, 1
    },
    { "system", "Emulated System",
      "0 = Auto (CGB), 1 = DMG, 2 = GBA-CGB",
      "Choose which type of Game Boy system to emulate",
      0, 0, 2, 1
    },
    { "turbo_rate", "Turbo Pulse Rate",
      "N = Pulse every N frames",
      "Set the speed (in frames) at which the turbo buttons are pulsed",
      3, 3, 9, 0
    }
};

enum {
    COLCORRECT,
    PALETTE,
    RESAMP,
    RSQUAL,
    GBSYS,
    TURBO
};

// SoX Resampler
static soxr_t soxr;

// Gambatte
static gambatte::GB gb;
static Resampler *resampler;
static std::vector<std::string> chtlist;
static uint32_t *audiobuf_in;
static unsigned abufpos = 0;

static const int GBMap[] = {
    gambatte::InputGetter::UP, gambatte::InputGetter::DOWN,
    gambatte::InputGetter::LEFT, gambatte::InputGetter::RIGHT,
    gambatte::InputGetter::SELECT, gambatte::InputGetter::START,
    gambatte::InputGetter::A, gambatte::InputGetter::B
};

class GetInput : public gambatte::InputGetter {
public:
    unsigned operator()() {
        unsigned buttons = 0;
        for (int i = 0; i < NDEFS_GB - 2; i++)
            if (input_device[0]->button[i]) buttons |= GBMap[i];

        // Turbo
        if (input_device[0]->button[8]) {
            if (input_device[0]->button[8] == settings_gmbt[TURBO].val) {
                buttons |= gambatte::InputGetter::A;
                input_device[0]->button[8] = 1;
            }
            else {
                ++input_device[0]->button[8];
            }
        }
        if (input_device[0]->button[9]) {
            if (input_device[0]->button[9] == settings_gmbt[TURBO].val) {
                buttons |= gambatte::InputGetter::B;
                input_device[0]->button[9] = 1;
            }
            else {
                ++input_device[0]->button[9];
            }
        }

        return buttons;
    }
} static GetInput;

static void gmbt_palette_set(int palette) {
        unsigned short *gbc_bios_palette = NULL;

        switch (palette) {
            case 0: // Internal Palette
                gbc_bios_palette = const_cast<unsigned short*>
                    (findGbcTitlePal(gb.romTitle().c_str()));
                if (!gbc_bios_palette) return;
                break;
            case 1: // Original Green
                gb.setDmgPaletteColor(0, 0, 8369468);
                gb.setDmgPaletteColor(0, 1, 6728764);
                gb.setDmgPaletteColor(0, 2, 3629872);
                gb.setDmgPaletteColor(0, 3, 3223857);
                gb.setDmgPaletteColor(1, 0, 8369468);
                gb.setDmgPaletteColor(1, 1, 6728764);
                gb.setDmgPaletteColor(1, 2, 3629872);
                gb.setDmgPaletteColor(1, 3, 3223857);
                gb.setDmgPaletteColor(2, 0, 8369468);
                gb.setDmgPaletteColor(2, 1, 6728764);
                gb.setDmgPaletteColor(2, 2, 3629872);
                gb.setDmgPaletteColor(2, 3, 3223857);
                return;
            case 2: // GB Pocket
                gb.setDmgPaletteColor(0, 0, 13487791);
                gb.setDmgPaletteColor(0, 1, 10987158);
                gb.setDmgPaletteColor(0, 2, 6974033);
                gb.setDmgPaletteColor(0, 3, 2828823);
                gb.setDmgPaletteColor(1, 0, 13487791);
                gb.setDmgPaletteColor(1, 1, 10987158);
                gb.setDmgPaletteColor(1, 2, 6974033);
                gb.setDmgPaletteColor(1, 3, 2828823);
                gb.setDmgPaletteColor(2, 0, 13487791);
                gb.setDmgPaletteColor(2, 1, 10987158);
                gb.setDmgPaletteColor(2, 2, 6974033);
                gb.setDmgPaletteColor(2, 3, 2828823);
                return;
            case 3: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Blue"));
                break;
            case 4: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Brown"));
                break;
            case 5: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Dark Blue"));
                break;
            case 6: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Dark Brown"));
                break;
            case 7: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Dark Green"));
                break;
            case 8: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Grayscale"));
                break;
            case 9: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Green"));
                break;
            case 10: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Inverted"));
                break;
            case 11: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Orange"));
                break;
            case 12: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Pastel Mix"));
                break;
            case 13: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Red"));
                break;
            case 14: gbc_bios_palette = const_cast<unsigned short*>
                (findGbcDirPal("GBC - Yellow"));
                break;
        }

        unsigned long rgb32 = 0;
        for (unsigned palnum = 0; palnum < 3; ++palnum) {
            for (unsigned colornum = 0; colornum < 4; ++colornum) {
                rgb32 = gbcToRgb32(gbc_bios_palette[palnum * 4 + colornum]);
                gb.setDmgPaletteColor(palnum, colornum, rgb32);
            }
        }
}

static void (*gmbt_audio_out)(size_t);

static void gmbt_audio_out_internal(size_t frames) {
    if (!frames)
        return;

    int16_t *abuf = (int16_t*)audinfo.buf + abufpos;
    size_t len = resampler->resample(abuf,
        reinterpret_cast<const int16_t*>(audiobuf_in), frames);

    abufpos += len * CHANNELS;
    if (abufpos >= audinfo.spf) {
        jg_cb_audio(abufpos);
        abufpos = 0;
    }
}

static void gmbt_audio_out_sox(size_t frames) {
    if (!frames)
        return;

    int16_t *abuf = (int16_t*)audinfo.buf + abufpos;

    size_t outsamps;
    soxr_process(soxr, audiobuf_in, frames, NULL, abuf, 48, &outsamps);

    abufpos += outsamps * CHANNELS;
    if (abufpos >= audinfo.spf) {
        jg_cb_audio(abufpos);
        abufpos = 0;
    }
}

// Jolly Good API Calls
void jg_set_cb_audio(jg_cb_audio_t func) {
    jg_cb_audio = func;
}

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

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

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

int jg_init() {
    // Set the save dir
    gb.setSaveDir(std::string(pathinfo.save));

    // Set input state callback
    gb.setInputGetter(&GetInput);
    inputinfo[0] = jg_gb_inputinfo(0, 0);

    if (settings_gmbt[RESAMP].val) { // SoX Resampler
        gmbt_audio_out = &gmbt_audio_out_sox;
        soxr_io_spec_t io_spec = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I);
        soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_VHQ, SOXR_VR);
        soxr = soxr_create(SAMPLERATE_IN, SAMPLERATE, CHANNELS,
            NULL, &io_spec, &q_spec, NULL);
    }
    else {
        gmbt_audio_out = &gmbt_audio_out_internal;
        resampler = ResamplerInfo::get(settings_gmbt[RSQUAL].val).create(
            SAMPLERATE_IN, SAMPLERATE, 2064 * CHANNELS
        );

        unsigned long mul, div;
        resampler->exactRatio(mul, div); // 47994.326636 output rate
    }

    audiobuf_in = (uint32_t*)calloc(2064 * CHANNELS * 4, sizeof(uint32_t));

    // Set frame time
    jg_cb_frametime(59.727501);

    return 1;
}

void jg_deinit() {
    if (resampler)
        delete resampler;
    if (soxr)
        soxr_delete(soxr);
    free(audiobuf_in);
}

void jg_reset(int hard) {
    if (hard) {}
    gb.reset();
}

void jg_exec_frame() {
    size_t samples = 2064;
    uint32_t *vbuf = (uint32_t*)vidinfo.buf;

    while (gb.runFor(vbuf, vidinfo.w, audiobuf_in, samples) == -1)
        gmbt_audio_out(samples);

    gmbt_audio_out(samples);
}

int jg_game_load() {
    // Set system type
    unsigned flags = 0;
    if (settings_gmbt[GBSYS].val == 1)
        flags = gambatte::GB::FORCE_DMG;
    else if (settings_gmbt[GBSYS].val == 2)
        flags = gambatte::GB::GBA_CGB;

    // Load the game
    int ret = !gb.load(gameinfo.data, gameinfo.size, gameinfo.path, flags);

    // Set palette and color correction
    gb.setCgbColorCorrection(settings_gmbt[COLCORRECT].val);
    gmbt_palette_set(settings_gmbt[PALETTE].val);

    return ret;
}

int jg_game_unload() {
    gb.saveSavedata();
    return 1;
}

int jg_state_load(const char *filename) {
    return gb.loadState(filename);
}

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

int jg_state_save(const char *filename) {
    return gb.saveState(0, 0, filename);
}

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

size_t jg_state_size(void) {
    return 0;
}

void jg_media_select() {
}

void jg_media_insert() {
}

void jg_cheat_clear() {
    // Sending a blank string in will clear the cheats
    std::string blank = "";
    gb.setGameGenie(blank);
    gb.setGameShark(blank);
    chtlist.clear();
}

void jg_cheat_set(const char *code) {
    // Convert to a C++ string in upper case
    std::string chtcode = std::string(code);
    std::transform(chtcode.begin(), chtcode.end(), chtcode.begin(), ::toupper);

    // Add a semicolon and add to the code list every time a new cheat is added
    chtlist.push_back(chtcode + ";");
    std::string codelist;

    for (size_t i = 0; i < chtlist.size(); ++i)
        codelist += chtlist[i];

    if (chtcode.find("-") != std::string::npos)
        gb.setGameGenie(codelist);
    else
        gb.setGameShark(codelist);
}

void jg_rehash() {
    gmbt_palette_set(settings_gmbt[PALETTE].val);
    gb.setCgbColorCorrection(settings_gmbt[COLCORRECT].val);
}

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

// JG Functions that return values to the frontend
jg_coreinfo_t* jg_get_coreinfo(const char *sys) {
    if (sys) {}
    return &coreinfo;
}

jg_videoinfo_t* jg_get_videoinfo() {
    return &vidinfo;
}

jg_audioinfo_t* jg_get_audioinfo() {
    return &audinfo;
}

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

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

void jg_setup_video() {
}

void jg_setup_audio() {
}

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

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

void jg_set_auxinfo(jg_fileinfo_t info, int index) {
    if (info.size || index) {}
}

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