/*
MIT License

Copyright (c) 2018-2025 Lior Halphon
Copyright (c) 2020-2025 Rupert Carmichael

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

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

#include "Core/gb.h"

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

#define VIDEO_WIDTH 160
#define VIDEO_HEIGHT 144
#define VIDEO_ASPECT (160.0/144.0)
#define VIDEO_ASPECT_2P (160.0/288.0)
#define VIDEO_WIDTH_SGB 256
#define VIDEO_HEIGHT_SGB 224
#define VIDEO_ASPECT_SBG (256.0/224.0)

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 = {
    "sameboy", "SameBoy", GB_VERSION, "gb", NUMINPUTS, 0
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888, // pixfmt
    VIDEO_WIDTH_SGB,    // wmax
    VIDEO_HEIGHT_SGB,   // hmax
    VIDEO_WIDTH_SGB,    // w
    VIDEO_HEIGHT_SGB,   // h
    0,                  // x
    0,                  // y
    VIDEO_WIDTH_SGB,    // p
    VIDEO_ASPECT_SBG,   // aspect
    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 *gbinput[NUMINPUTS];

static size_t abufsize = 0;
static size_t truespf = 0;

static GB_gameboy_t gb[2];
static bool vblank[2];
static bool sendbit[2] = { true, true };
static char savename[2][292];

static unsigned numsystems = 1;

static void (*sb_input_poll[NUMINPUTS])(int);

// Emulator-specific settings
static jg_setting_t settings_sb[] = {
    { "bios_external", "External BIOS",
      "0 = Prefer Internal BIOS, 1 = Prefer External BIOS",
      "Use internal homebrew BIOS or external dumped BIOS",
      0, 0, 1, 1
    },
    { "color_correction", "Color Correction",
      "0 = Disabled, 1 = Correct Color Curves, 2 = Modern - Balanced, "
      "3 = Modern - Boost Contrast, 4 = Reduce Contrast, 5 = Harsh Reality, "
      "6 = Modern - Accurate",
      "Select the Color Correction mode",
      2, 0, 6, 0
    },
    { "highpass_filter", "Highpass Filter",
      "0 = Off, 1 = Accurate, 2 = DC Offset",
      "Select the Highpass Filter mode",
      1, 0, 2, 0
    },
    { "model", "Game Boy Model",
      "0 = Game Boy, 1 = Game Boy Pocket,"
      "2 = Game Boy Color, 3 = Game Boy Advance,"
      "4 = Super Game Boy, 5 = Super Game Boy 2",
      "Select the model of system to emulate",
      2, 0, 5, 1
    },
    { "revision_gbc", "Game Boy Color Revision",
      "0 = CGB, 1 = CGB A, 2 = CGB B, 3 = CGB C, 4 = CGB D, 5 = CGB E",
      "Select Game Boy Color CPU revision to emulate",
      4, 0, 5, 1
    },
    { "rumble", "Rumble Support",
      "0 = Disabled, 1 = Rumble Games, 2 = All Games",
      "Enable Force Feedback when the game supports it, or always",
      1, 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, 2, 9, 0
    }
};

enum {
    BIOSEXT,
    COLCORRECT,
    HIGHPASS,
    MODEL,
    REVISION_GBC,
    RUMBLE,
    TURBORATE
};

static void sb_serial_start0(GB_gameboy_t *gb, bool bit_received) {
    sendbit[0] = bit_received;
}

static bool sb_serial_end0(GB_gameboy_t *g) {
    bool ret = GB_serial_get_data_bit(&gb[1]);
    GB_serial_set_data_bit(&gb[1], sendbit[0]);
    return ret;
}

static void sb_serial_start1(GB_gameboy_t *g, bool bit_received) {
    sendbit[1] = bit_received;
}

static bool sb_serial_end1(GB_gameboy_t *g) {
    bool ret = GB_serial_get_data_bit(&gb[0]);
    GB_serial_set_data_bit(&gb[0], sendbit[1]);
    return ret;
}

static void sb_infrared0(GB_gameboy_t *g, bool output) {
    GB_set_infrared_input(&gb[1], output);
}

static void sb_infrared1(GB_gameboy_t *g, bool output) {
    GB_set_infrared_input(&gb[0], output);
}

static void sb_vblank0(GB_gameboy_t *g, GB_vblank_type_t type) {
    vblank[0] = true;
}

static void sb_vblank1(GB_gameboy_t *g, GB_vblank_type_t type) {
    vblank[1] = true;
}

static uint32_t sb_rgb_encode(GB_gameboy_t *g, uint8_t red, uint8_t green,
    uint8_t blue) {
    return red << 16 | green << 8 | blue;
}

static void sb_rumble(GB_gameboy_t *g, double rumble_amplitude) {
    jg_cb_rumble(0, rumble_amplitude, 250);
}

static void sb_audio_push(GB_gameboy_t *g, GB_sample_t *samp) {
    // Push audio frame to the frontend
    int16_t *sampbuf = (int16_t*)audinfo.buf;

    sampbuf[abufsize++] = samp->left;
    sampbuf[abufsize++] = samp->right;

    if (abufsize == truespf) {
        jg_cb_audio(truespf);
        abufsize = 0;
    }
}

static void sb_audio_null(GB_gameboy_t *g, GB_sample_t *samp) {
    if (g || samp) { }
}

void sb_inputstate_null(int i) {
    if (i) { }
}

static void sb_inputstate(int i) {
    GB_set_key_state(&gb[i], GB_KEY_UP, gbinput[i]->button[0]);
    GB_set_key_state(&gb[i], GB_KEY_DOWN, gbinput[i]->button[1]);
    GB_set_key_state(&gb[i], GB_KEY_LEFT, gbinput[i]->button[2]);
    GB_set_key_state(&gb[i], GB_KEY_RIGHT, gbinput[i]->button[3]);
    GB_set_key_state(&gb[i], GB_KEY_SELECT, gbinput[i]->button[4]);
    GB_set_key_state(&gb[i], GB_KEY_START, gbinput[i]->button[5]);
    GB_set_key_state(&gb[i], GB_KEY_A, gbinput[i]->button[6]);
    GB_set_key_state(&gb[i], GB_KEY_B, gbinput[i]->button[7]);

    // Turbo
    if (gbinput[i]->button[8]) {
        if (gbinput[i]->button[8] == settings_sb[TURBORATE].val) {
            GB_set_key_state(&gb[i], GB_KEY_A, gbinput[i]->button[8]);
            gbinput[i]->button[8] = 1;
        }
        else {
            ++gbinput[i]->button[8];
        }
    }
    if (gbinput[i]->button[9]) {
        if (gbinput[i]->button[9] == settings_sb[TURBORATE].val) {
            GB_set_key_state(&gb[i], GB_KEY_B, gbinput[i]->button[9]);
            gbinput[i]->button[9] = 1;
        }
        else {
            ++gbinput[i]->button[9];
        }
    }
}

static void sb_inputstate_acclerometer(int i) {
    double x = 1.0 - (gbinput[i]->axis[0] + 32768) / 32768.0;
    double y = 1.0 - (gbinput[i]->axis[1] + 32768) / 32768.0;
    GB_set_accelerometer_values(&gb[0], x, y);
}

static void sb_log_callback(GB_gameboy_t *g, const char *string,
    GB_log_attributes_t attributes) {
    jg_cb_log(JG_LOG_DBG, string);
}

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

static void sb_init(unsigned i) {
    char bootrompath[196];

    switch (settings_sb[MODEL].val) {
        case 0: { // Game Boy
            GB_init(&gb[i], GB_MODEL_DMG_B);
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "dmg_boot.bin");
            break;
        }
        case 1: { // Game Boy Pocket
            GB_init(&gb[i], GB_MODEL_MGB);	
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "mgb_boot.bin");
            break; 
        }
        case 2: { // Game Boy Color
            GB_init(&gb[i], GB_MODEL_CGB_0 + settings_sb[REVISION_GBC].val);
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "cgb_boot.bin");
            break;
        }
        case 3: { // Game Boy Advance
            GB_init(&gb[i], GB_MODEL_AGB);
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "agb_boot.bin");
            break;
        }
        case 4: { // Super Game Boy
            GB_init(&gb[i], GB_MODEL_SGB);
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "sgb_boot.bin");
            break;
        }
        case 5: { // Super Game Boy 2
            GB_init(&gb[i], GB_MODEL_SGB2);
            snprintf(bootrompath, sizeof(bootrompath), "%s/%s",
                settings_sb[BIOSEXT].val ? pathinfo.bios : pathinfo.core,
                "sgb2_boot.bin");
            break;
        }
        default: {
            break;
        }
    }

    GB_set_log_callback(&gb[i], sb_log_callback);
    GB_set_rgb_encode_callback(&gb[i], sb_rgb_encode);
    GB_set_rumble_callback(&gb[i], sb_rumble);

    if (GB_load_boot_rom(&gb[i], bootrompath))
        jg_cb_log(JG_LOG_ERR, "Failed to load boot ROM: %s\n", bootrompath);

    GB_set_color_correction_mode(&gb[i], settings_sb[COLCORRECT].val);
    GB_set_highpass_filter_mode(&gb[i], settings_sb[HIGHPASS].val);
    GB_set_rumble_mode(&gb[i], settings_sb[RUMBLE].val);
    GB_set_sample_rate(&gb[i], SAMPLERATE);
}

int jg_init(void) {
    sb_init(0);
    GB_apu_set_sample_callback(&gb[0], sb_audio_push);
    GB_set_vblank_callback(&gb[0], sb_vblank0);

    if (settings_sb[MODEL].val < 4) {
        vidinfo.wmax = vidinfo.w = vidinfo.p = VIDEO_WIDTH;
        vidinfo.hmax = vidinfo.h = VIDEO_HEIGHT;
        vidinfo.aspect = VIDEO_ASPECT;
    }

    double frametime = GB_get_usual_frame_rate(&gb[0]);

    truespf = (int)((SAMPLERATE / frametime) * CHANNELS);
    truespf -= truespf % CHANNELS;

    jg_cb_frametime(frametime);

    return 1;
}

void jg_deinit(void) {
}

void jg_reset(int hard) {
    if (hard > 1)
        return;

    for (unsigned i = 0; i < numsystems; ++i) {
        GB_save_battery(&gb[i], savename[i]);
        GB_reset(&gb[i]);
    }
}

void jg_exec_frame(void) {
    for (int i = 0; i < NUMINPUTS; ++i)
        sb_input_poll[i](i);

    vblank[0] = vblank[1] = false;

    if (numsystems == 2) {
        signed delta = 0;
        while (!vblank[0] || !vblank[1]) {
            if (delta >= 0)
                delta -= GB_run(&gb[0]);
            else
                delta += GB_run(&gb[1]);
        }
    }
    else {
        GB_run_frame(&gb[0]);
    }

}

int jg_game_load(void) {
    GB_load_rom_from_buffer(&gb[0], gameinfo.data, gameinfo.size);
    snprintf(savename[0], sizeof(savename[0]),
        "%s/%s.srm", pathinfo.save, gameinfo.name);
    GB_load_battery(&gb[0], savename[0]);

    inputinfo[0] = jg_gb_inputinfo(0, JG_GB_SYSTEM);
    sb_input_poll[0] = &sb_inputstate;

    if (GB_has_accelerometer(&gb[0])) {
        inputinfo[1] = jg_gb_inputinfo(1, JG_GB_ACCELEROMETER);
        sb_input_poll[1] = &sb_inputstate_acclerometer;
    }
    else if (numsystems != 2) {
        sb_input_poll[1] = &sb_inputstate_null;
    }
    return 1;
}

int jg_game_unload(void) {
    for (unsigned i = 0; i < numsystems; ++i)
        GB_save_battery(&gb[i], savename[i]);
    return 1;
}

int jg_state_load(const char *filename) {
    return !GB_load_state(&gb[0], filename);
}

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

int jg_state_save(const char *filename) {
    return !GB_save_state(&gb[0], filename);
}

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) {
    while (gb[0].cheats)
        GB_remove_cheat(&gb[0], gb[0].cheats[0]);

    GB_set_cheats_enabled(&gb[0], false);
}

void jg_cheat_set(const char *code) {
    GB_import_cheat(&gb[0], code, "", true);
    GB_set_cheats_enabled(&gb[0], true);
}

void jg_rehash(void) {
    for (unsigned i = 0; i < numsystems; ++i) {
        GB_set_color_correction_mode(&gb[i], settings_sb[COLCORRECT].val);
        GB_set_highpass_filter_mode(&gb[i], settings_sb[HIGHPASS].val);
    }
}

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 = sizeof(settings_sb) / sizeof(jg_setting_t);
    return settings_sb;
}

void jg_setup_video(void) {
    GB_set_pixels_output(&gb[0], vidinfo.buf);
    if (numsystems == 2)
        GB_set_pixels_output(&gb[1],
            (uint32_t*)vidinfo.buf + (VIDEO_WIDTH * VIDEO_HEIGHT));
}

void jg_setup_audio(void) {
}

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

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

void jg_set_auxinfo(jg_fileinfo_t info, int index) {
    /* In SameBoy's case, use the auxiliary file as the game in the second
       emulated system.
    */
    if (index) { }
    numsystems = 2;

    // Do not allow SGB mode in 2 Player mode
    if (settings_sb[MODEL].val > 3) {
        settings_sb[MODEL].val = 2;
        jg_init(); // Reinit the first emulated system as CGB
    }

    // Initialize the second emulated system
    sb_init(1);
    GB_apu_set_sample_callback(&gb[1], sb_audio_null); // No audio output
    GB_set_vblank_callback(&gb[1], sb_vblank1);

    GB_load_rom_from_buffer(&gb[1], info.data, info.size);
    snprintf(savename[1], sizeof(savename[1]),
        "%s/%s.srm", pathinfo.save, info.name);
    GB_load_battery(&gb[1], savename[1]);

    inputinfo[1] = jg_gb_inputinfo(1, JG_GB_SYSTEM);
    sb_input_poll[1] = &sb_inputstate;

    vidinfo.wmax = vidinfo.w = vidinfo.p = VIDEO_WIDTH;
    vidinfo.hmax = vidinfo.h = VIDEO_HEIGHT * 2;
    vidinfo.aspect = VIDEO_ASPECT_2P;

    GB_set_serial_transfer_bit_start_callback(&gb[0], sb_serial_start0);
    GB_set_serial_transfer_bit_end_callback(&gb[0], sb_serial_end0);
    GB_set_serial_transfer_bit_start_callback(&gb[1], sb_serial_start1);
    GB_set_serial_transfer_bit_end_callback(&gb[1], sb_serial_end1);
    GB_set_infrared_callback(&gb[0], sb_infrared0);
    GB_set_infrared_callback(&gb[1], sb_infrared1);
}

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