/*
 * Gearcoleco - ColecoVision Emulator
 * Copyright (C) 2021  Ignacio Sanchez

 * 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 3 of the License, or
 * 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 http://www.gnu.org/licenses/
 *
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <math.h>

#include <stdio.h>
#include "libretro.h"
#include "libretro_core_options.h"

#include "../../src/gearcoleco.h"
#include "../../src/Mapper.h"

#ifdef _WIN32
static const char slash = '\\';
#else
static const char slash = '/';
#endif

#define MAX_PADS 2
#define JOYPAD_BUTTONS 16

#define RETRO_DEVICE_COLECOVISION RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 0)

static struct retro_log_callback logging;
retro_log_printf_t log_cb;
static char retro_system_directory[4096];
static char retro_game_path[4096];

static s16 audio_buf[GC_AUDIO_BUFFER_SIZE];
static int audio_sample_count = 0;
static int current_screen_width = 0;
static int current_screen_height = 0;
static float current_aspect_ratio = 0;
static bool allow_up_down = false;
static bool libretro_supports_bitmasks = false;
static int spinner_support = 0;
static int spinner_sensitivity = 1;
static float aspect_ratio = 0.0f;
static bool categories_supported = false;

static GearcolecoCore* core;
static u8* frame_buffer;
static Cartridge::ForceConfiguration config;

static int joypad[MAX_PADS][JOYPAD_BUTTONS];
static int joypre[MAX_PADS][JOYPAD_BUTTONS];
static int joypad_ext[MAX_PADS][4];
static int joypre_ext[MAX_PADS][4];
static unsigned input_device[MAX_PADS] = {
    RETRO_DEVICE_COLECOVISION,
    RETRO_DEVICE_COLECOVISION
};
static bool mouse[2];
static bool mousepre[2];

static GC_Keys keymap[] = {
    Key_Up,
    Key_Down,
    Key_Left,
    Key_Right,
    Key_Right_Button,
    Key_Left_Button,
    Keypad_2,
    Keypad_1,
    Keypad_Asterisk,
    Keypad_Hash,
    Keypad_3,
    Keypad_4,
    Keypad_5,
    Keypad_6,
    Keypad_7,
    Keypad_8,
    Keypad_0,
    Keypad_9,
    Key_Blue,
    Key_Purple
};

static void fallback_log(enum retro_log_level level, const char *fmt, ...)
{
    (void)level;
    va_list va;
    va_start(va, fmt);
    vfprintf(stderr, fmt, va);
    va_end(va);
}

static retro_environment_t environ_cb;

static bool IsJoypadDevice(unsigned device)
{
    return ((device == RETRO_DEVICE_JOYPAD) || (device == RETRO_DEVICE_COLECOVISION));
}

static void clear_input_state(void);
static void reset_controller_devices(void);
static void apply_controller_device(unsigned port, unsigned device, bool log_device);

void retro_init(void)
{
    if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging))
        log_cb = logging.log;
    else
        log_cb = fallback_log;

    const char *dir = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir) && dir) {
        snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", dir);
    }
    else {
        snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", ".");
    }

    log_cb(RETRO_LOG_INFO, "%s (%s) libretro\n", GEARCOLECO_TITLE, EMULATOR_BUILD);

    core = new GearcolecoCore();

#ifdef PS2
    core->Init(GC_PIXEL_BGR555);
#else
    core->Init(GC_PIXEL_RGB565);
#endif

    frame_buffer = new u8[GC_RESOLUTION_WIDTH_WITH_OVERSCAN * GC_RESOLUTION_HEIGHT_WITH_OVERSCAN * 2];

    audio_sample_count = 0;

    config.region = Cartridge::CartridgeUnknownRegion;
    config.type = Cartridge::CartridgeNotSupported;

    clear_input_state();

    for (int i = 0; i < MAX_PADS; i++)
        apply_controller_device(i, input_device[i], false);

    libretro_supports_bitmasks = environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL);
}

void retro_deinit(void)
{
    SafeDeleteArray(frame_buffer);
    SafeDelete(core);

    audio_sample_count = 0;
    current_screen_width = 0;
    current_screen_height = 0;
    current_aspect_ratio = 0.0f;
    aspect_ratio = 0.0f;
    libretro_supports_bitmasks = false;

    reset_controller_devices();
    clear_input_state();
}

unsigned retro_api_version(void)
{
    return RETRO_API_VERSION;
}

void retro_set_controller_port_device(unsigned port, unsigned device)
{
    if (port >= MAX_PADS)
    {
        if (log_cb)
            log_cb(RETRO_LOG_DEBUG, "retro_set_controller_port_device invalid port number: %u\n", port);
        return;
    }

    input_device[port] = device;

    apply_controller_device(port, device, true);
}

static void clear_input_state(void)
{
    for (int i = 0; i < MAX_PADS; i++)
    {
        for (int j = 0; j < JOYPAD_BUTTONS; j++)
        {
            joypad[i][j] = 0;
            joypre[i][j] = 0;
        }

        for (int j = 0; j < 4; j++)
        {
            joypad_ext[i][j] = 0;
            joypre_ext[i][j] = 0;
        }

        mouse[i] = false;
        mousepre[i] = false;
    }
}

static void reset_controller_devices(void)
{
    for (int i = 0; i < MAX_PADS; i++)
        input_device[i] = RETRO_DEVICE_COLECOVISION;
}

static void apply_controller_device(unsigned port, unsigned device, bool log_device)
{
    if (log_device && log_cb)
        log_cb(RETRO_LOG_DEBUG, "Plugging device %u into port %u.\n", device, port);

    struct retro_input_descriptor joypad[] = {

        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,                              "Joystick Up" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,                            "Joystick Down" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,                            "Joystick Left" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,                           "Joystick Right" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,                               "Yellow (Left)" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,                               "Red (Right)" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,                               "Keypad 1" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,                               "Keypad 2" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L,                               "Keypad 3" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R,                               "Keypad 4" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2,                              "Keypad 5" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2,                              "Keypad 6" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3,                              "Keypad 7" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3,                              "Keypad 8" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,                           "Keypad *" },
        { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT,                          "Keypad #" },
        { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y,  "Keypad 9" },
        { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X,  "Keypad 0" },
        { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, "Purple" },
        { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, "Blue" },

        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP,                              "Joystick Up" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN,                            "Joystick Down" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT,                            "Joystick Left" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT,                           "Joystick Right" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B,                               "Yellow (Left)" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A,                               "Red (Right)" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y,                               "Keypad 1" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X,                               "Keypad 2" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L,                               "Keypad 3" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R,                               "Keypad 4" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2,                              "Keypad 5" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2,                              "Keypad 6" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3,                              "Keypad 7" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3,                              "Keypad 8" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START,                           "Keypad *" },
        { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT,                          "Keypad #" },
        { 1, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y,  "Keypad 9" },
        { 1, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X,  "Keypad 0" },
        { 1, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, "Purple" },
        { 1, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, "Blue" },

        { 0, 0, 0, 0, NULL }
    };

    struct retro_input_descriptor desc[] = {
        { 0, 0, 0, 0, NULL }
    };

    if (!environ_cb)
        return;

    bool joypad_connected = false;
    for (int i = 0; i < MAX_PADS; i++)
    {
        if (IsJoypadDevice(input_device[i]))
        {
            joypad_connected = true;
            break;
        }
    }

    if (joypad_connected)
        environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, joypad);
    else
        environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc);
}

void retro_get_system_info(struct retro_system_info *info)
{
    memset(info, 0, sizeof(*info));
    info->library_name     = "Gearcoleco";
    info->library_version  = GEARCOLECO_VERSION;
    info->need_fullpath    = false;
    info->valid_extensions = "col|cv|bin|rom";
}

static retro_video_refresh_t video_cb;
static retro_audio_sample_t audio_cb;
static retro_audio_sample_batch_t audio_batch_cb;
static retro_input_poll_t input_poll_cb;
static retro_input_state_t input_state_cb;

void retro_get_system_av_info(struct retro_system_av_info *info)
{
    GC_RuntimeInfo runtime_info;
    core->GetRuntimeInfo(runtime_info);

    current_screen_width = runtime_info.screen_width;
    current_screen_height = runtime_info.screen_height;

    info->geometry.base_width   = runtime_info.screen_width;
    info->geometry.base_height  = runtime_info.screen_height;
    info->geometry.max_width    = GC_RESOLUTION_WIDTH_WITH_OVERSCAN;
    info->geometry.max_height   = GC_RESOLUTION_HEIGHT_WITH_OVERSCAN;
    info->geometry.aspect_ratio = aspect_ratio;
    info->timing.fps            = runtime_info.region == Region_NTSC ? 60.0 : 50.0;
    info->timing.sample_rate    = 44100.0;
}

void retro_set_environment(retro_environment_t cb)
{
    environ_cb = cb;

    static const struct retro_controller_description port_1[] = {
        { "Joypad Auto", RETRO_DEVICE_JOYPAD },
        { "Joypad Port Empty", RETRO_DEVICE_NONE },
        { "ColecoVision", RETRO_DEVICE_COLECOVISION },
    };

    static const struct retro_controller_description port_2[] = {
        { "Joypad Auto", RETRO_DEVICE_JOYPAD },
        { "Joypad Port Empty", RETRO_DEVICE_NONE },
        { "ColecoVision", RETRO_DEVICE_COLECOVISION },
    };

    static const struct retro_controller_info ports[] = {
        { port_1, 3 },
        { port_2, 3 },
        { NULL, 0 },
    };

    environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports);

    libretro_set_core_options(environ_cb, &categories_supported);
}

void retro_set_audio_sample(retro_audio_sample_t cb)
{
    audio_cb = cb;
}

void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb)
{
    audio_batch_cb = cb;
}

void retro_set_input_poll(retro_input_poll_t cb)
{
    input_poll_cb = cb;
}

void retro_set_input_state(retro_input_state_t cb)
{
    input_state_cb = cb;
}

void retro_set_video_refresh(retro_video_refresh_t cb)
{
    video_cb = cb;
}

static void load_bootroms(void)
{
    char bios_path[4113];

    snprintf(bios_path, 4113, "%s%ccolecovision.rom", retro_system_directory, slash);
    core->GetMemory()->LoadBios(bios_path);

    if (!core->GetMemory()->IsBiosLoaded())
    {
        // Fallback to coleco.rom if colecovision.rom is not found
        snprintf(bios_path, 4113, "%s%ccoleco.rom", retro_system_directory, slash);
        core->GetMemory()->LoadBios(bios_path);
    }

    if (!core->GetMemory()->IsBiosLoaded())
    {
        struct retro_message msg = {};
        msg.msg = "BIOS not found: coleco.rom";
        msg.frames = 360;
        environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg);
        log_cb(RETRO_LOG_ERROR, "BIOS not found: colecovision.rom or coleco.rom\n");
    }
}

static void update_input(void)
{
    int16_t joypad_bits[MAX_PADS];

    input_poll_cb();

    if (libretro_supports_bitmasks)
    {
        for (int j = 0; j < MAX_PADS; j++)
        {
            if (IsJoypadDevice(input_device[j]))
                joypad_bits[j] = input_state_cb(j, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK);
            else
                joypad_bits[j] = 0;
        }
    }
    else
    {
        for (int j = 0; j < MAX_PADS; j++)
        {
            joypad_bits[j] = 0;
            if (IsJoypadDevice(input_device[j]))
            {
                for (int i = 0; i < (RETRO_DEVICE_ID_JOYPAD_R3+1); i++)
                    joypad_bits[j] |= input_state_cb(j, RETRO_DEVICE_JOYPAD, 0, i) ? (1 << i) : 0;
            }
        }
    }

    // Copy previous state
    for (int j = 0; j < MAX_PADS; j++)
    {
        for (int i = 0; i < JOYPAD_BUTTONS; i++)
            joypre[j][i] = joypad[j][i];
        for (int i = 0; i < 4; i++)
            joypre_ext[j][i] = joypad_ext[j][i];
    }

    // Get current state
    for (int j = 0; j < MAX_PADS; j++)
    {
        if (allow_up_down)
        {
            joypad[j][0] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_UP)     ? 1 : 0;
            joypad[j][1] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_DOWN)   ? 1 : 0;
            joypad[j][2] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_LEFT)   ? 1 : 0;
            joypad[j][3] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_RIGHT)  ? 1 : 0;
        }
        else
        {
            bool raw_up = (joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_UP)) != 0;
            bool raw_down = (joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_DOWN)) != 0;
            bool raw_left = (joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_LEFT)) != 0;
            bool raw_right = (joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_RIGHT)) != 0;
            bool up = raw_up;
            bool down = raw_down;
            bool left = raw_left;
            bool right = raw_right;

            if (raw_up && raw_down)
            {
                if (joypre[j][0] == 1)
                {
                    up = true;
                    down = false;
                }
                else if (joypre[j][1] == 1)
                {
                    up = false;
                    down = true;
                }
                else
                {
                    up = true;
                    down = false;
                }
            }

            if (raw_left && raw_right)
            {
                if (joypre[j][2] == 1)
                {
                    left = true;
                    right = false;
                }
                else if (joypre[j][3] == 1)
                {
                    left = false;
                    right = true;
                }
                else
                {
                    left = true;
                    right = false;
                }
            }

            joypad[j][0] = up ? 1 : 0;
            joypad[j][1] = down ? 1 : 0;
            joypad[j][2] = left ? 1 : 0;
            joypad[j][3] = right ? 1 : 0;
        }
        joypad[j][4] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_A)      ? 1 : 0;
        joypad[j][5] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_B)      ? 1 : 0;
        joypad[j][6] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_X)      ? 1 : 0;
        joypad[j][7] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_Y)      ? 1 : 0;
        joypad[j][8] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_START)  ? 1 : 0;
        joypad[j][9] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_SELECT) ? 1 : 0;
        joypad[j][10] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_L)     ? 1 : 0;
        joypad[j][11] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_R)     ? 1 : 0;
        joypad[j][12] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_L2)    ? 1 : 0;
        joypad[j][13] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_R2)    ? 1 : 0;
        joypad[j][14] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_L3)    ? 1 : 0;
        joypad[j][15] = joypad_bits[j] & (1 << RETRO_DEVICE_ID_JOYPAD_R3)    ? 1 : 0;

        int analog_left_x = 0;
        int analog_left_y = 0;
        int analog_right_x = 0;
        int analog_right_y = 0;

        if (IsJoypadDevice(input_device[j]))
        {
            analog_left_x = input_state_cb( j, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X);
            analog_left_y = input_state_cb( j, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y);
            analog_right_x = input_state_cb( j, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X);
            analog_right_y = input_state_cb( j, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y);
        }

        const int threshold = 4000;

        joypad_ext[j][0] = (analog_left_x > threshold || analog_left_x < -threshold) ? 1 : 0;
        joypad_ext[j][1] = (analog_left_y > threshold || analog_left_y < -threshold) ? 1 : 0;
        joypad_ext[j][2] = (analog_right_x > threshold || analog_right_x < -threshold) ? 1 : 0;
        joypad_ext[j][3] = (analog_right_y > threshold || analog_right_y < -threshold) ? 1 : 0;
    }

    for (int j = 0; j < MAX_PADS; j++)
    {
        for (int i = 0; i < JOYPAD_BUTTONS; i++)
        {
            if (joypad[j][i])
                core->KeyPressed(static_cast<GC_Controllers>(j), keymap[i]);
            else
                core->KeyReleased(static_cast<GC_Controllers>(j), keymap[i]);
        }

        for (int i = 0; i < 4; i++)
        {
            if (joypad_ext[j][i])
                core->KeyPressed(static_cast<GC_Controllers>(j), keymap[i + JOYPAD_BUTTONS]);
            else
                core->KeyReleased(static_cast<GC_Controllers>(j), keymap[i + JOYPAD_BUTTONS]);
        }
    }

    if (spinner_support > 0)
    {
        for (int i=0; i<2; i++)
        {
            mousepre[i] = mouse[i];
        }

        mouse[0] = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT);
        mouse[1] = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_RIGHT);

        int mouse_x = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_X);
        int mouse_y = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_Y);

        int sen = spinner_sensitivity - 1;
        if (sen < 0)
            sen = 0;
        float senf = (float)(sen / 2.0f) + 1.0f;
        float relx = (float)(mouse_x) * senf;

        switch (spinner_support)
        {
            // SAC
            case (1):
            {
                core->Spinner1((int)-relx);
                break;
            }
            // Wheel
            case (2):
            {
                core->Spinner1((int)relx);
                break;
            }
            // Roller
            case (3):
            {
                float rely = (float)(mouse_y) * senf;
                core->Spinner1((int)relx);
                core->Spinner2((int)rely);
                break;
            }
            default:
                break;
        }

        if (mouse[0])
            core->KeyPressed(Controller_1, Key_Left_Button);
        else
            core->KeyReleased(Controller_1, Key_Left_Button);

        if (mouse[1])
            core->KeyPressed(Controller_1, Key_Right_Button);
        else
            core->KeyReleased(Controller_1, Key_Right_Button);
    }
}

static void check_variables(void)
{
    struct retro_variable var = {0};

    var.key = "gearcoleco_up_down_allowed";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "Enabled") == 0)
            allow_up_down = true;
        else
            allow_up_down = false;
    }

    var.key = "gearcoleco_timing";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "Auto") == 0)
            config.region = Cartridge::CartridgeUnknownRegion;
        else if (strcmp(var.value, "NTSC (60 Hz)") == 0)
            config.region = Cartridge::CartridgeNTSC;
        else if (strcmp(var.value, "PAL (50 Hz)") == 0)
            config.region = Cartridge::CartridgePAL;
        else
            config.region = Cartridge::CartridgeUnknownRegion;
    }

    var.key = "gearcoleco_aspect_ratio";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "1:1 PAR") == 0)
            aspect_ratio = 0.0f;
        else if (strcmp(var.value, "4:3 DAR") == 0)
            aspect_ratio = 4.0f / 3.0f;
        else if (strcmp(var.value, "16:9 DAR") == 0)
            aspect_ratio = 16.0f / 9.0f;
            else if (strcmp(var.value, "16:10 DAR") == 0)
            aspect_ratio = 16.0f / 10.0f;
        else
            aspect_ratio = 0.0f;
    }

    var.key = "gearcoleco_overscan";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "Disabled") == 0)
            core->GetVideo()->SetOverscan(Video::OverscanDisabled);
        else if (strcmp(var.value, "Top+Bottom") == 0)
            core->GetVideo()->SetOverscan(Video::OverscanTopBottom);
        else if (strcmp(var.value, "Full (284 width)") == 0)
            core->GetVideo()->SetOverscan(Video::OverscanFull284);
        else if (strcmp(var.value, "Full (320 width)") == 0)
            core->GetVideo()->SetOverscan(Video::OverscanFull320);
        else
            core->GetVideo()->SetOverscan(Video::OverscanDisabled);
    }

    var.key = "gearcoleco_no_sprite_limit";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "Enabled") == 0)
            core->GetVideo()->SetNoSpriteLimit(true);
        else
            core->GetVideo()->SetNoSpriteLimit(false);
    }

    var.key = "gearcoleco_spinners";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        if (strcmp(var.value, "Disabled") == 0)
            spinner_support = 0;
        else if (strcmp(var.value, "Super Action Controller") == 0)
            spinner_support = 1;
        else if (strcmp(var.value, "Wheel Controller") == 0)
            spinner_support = 2;
        else if (strcmp(var.value, "Roller Controller") == 0)
            spinner_support = 3;
        else
            spinner_support = 0;
    }

    var.key = "gearcoleco_spinner_sensitivity";
    var.value = NULL;

    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
    {
        spinner_sensitivity = atoi(var.value);
    }
}

void retro_run(void)
{
    bool updated = false;
    if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated)
    {
        check_variables();
    }

    update_input();

    core->RunToVBlank(frame_buffer, audio_buf, &audio_sample_count);

    GC_RuntimeInfo runtime_info;
    core->GetRuntimeInfo(runtime_info);

    if ((runtime_info.screen_width != current_screen_width) ||
        (runtime_info.screen_height != current_screen_height) ||
        (aspect_ratio != current_aspect_ratio))
    {
        current_screen_width = runtime_info.screen_width;
        current_screen_height = runtime_info.screen_height;
        current_aspect_ratio = aspect_ratio;

        retro_system_av_info info;
        info.geometry.base_width   = runtime_info.screen_width;
        info.geometry.base_height  = runtime_info.screen_height;
        info.geometry.max_width    = runtime_info.screen_width;
        info.geometry.max_height   = runtime_info.screen_height;
        info.geometry.aspect_ratio = aspect_ratio;

        environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &info.geometry);
    }

    video_cb((uint8_t*)frame_buffer, runtime_info.screen_width, runtime_info.screen_height, runtime_info.screen_width * sizeof(u8) * 2);

    if (audio_sample_count > 0)
        audio_batch_cb(audio_buf, audio_sample_count / 2);

    audio_sample_count = 0;
}

void retro_reset(void)
{
    check_variables();
    load_bootroms();
    core->ResetROMPreservingRAM(&config);
}

bool retro_load_game(const struct retro_game_info *info)
{
    core->GetCartridge()->Reset();
    check_variables();
    load_bootroms();

    if (!core->LoadROMFromBuffer(reinterpret_cast<const u8*>(info->data), info->size, &config))
    {
        log_cb(RETRO_LOG_ERROR, "Invalid or corrupted ROM.\n");
        return false;
    }

    enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_RGB565;
    if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
    {
        log_cb(RETRO_LOG_ERROR, "RGB565 is not supported.\n");
        return false;
    }

    snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path ? info->path : "");

    struct retro_memory_descriptor descs[7];

    memset(descs, 0, sizeof(descs));

    // BIOS
    descs[0].ptr   = core->GetMemory()->GetBios();
    descs[0].start = 0x0000;
    descs[0].len   = 0x2000;
    // EXPANSION
    descs[1].ptr   = NULL;
    descs[1].start = 0x2000;
    descs[1].len   = 0x4000;
    // RAM
    descs[2].ptr   = core->GetMemory()->GetRam();
    descs[2].start = 0x6000;
    descs[2].len   = 0x0400;
    // RAM MIRROR
    descs[3].ptr   = core->GetMemory()->GetRam();
    descs[3].start = 0x7000;
    descs[3].len   = 0x0400;
    // CART
    descs[4].ptr   = core->GetCartridge()->GetROM();
    descs[4].start = 0x8000;
    descs[4].len   = core->GetCartridge()->GetROMSize();
    // SGM LOWER
    descs[5].ptr   = core->GetMemory()->GetSGMRam();
    descs[5].start = 0x010000;
    descs[5].len   = 0x2000;
    // SGM UPPER
    descs[6].ptr   = core->GetMemory()->GetSGMRam() + 0x2000;
    descs[6].start = 0x012000;
    descs[6].len   = 0x6000;

    struct retro_memory_map mmaps;
    mmaps.descriptors = descs;
    mmaps.num_descriptors = sizeof(descs) / sizeof(descs[0]);
    environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, &mmaps);

    bool achievements = true;
    environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &achievements);

    return true;
}

void retro_unload_game(void)
{
}

unsigned retro_get_region(void)
{
    return core->GetCartridge()->IsPAL() ? RETRO_REGION_PAL : RETRO_REGION_NTSC;
}

bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num)
{
    return false;
}

size_t retro_serialize_size(void)
{
    size_t size = 0;
    core->SaveState(NULL, size);
    return size;
}

bool retro_serialize(void *data, size_t size)
{
    return core->SaveState(reinterpret_cast<u8*>(data), size);
}

bool retro_unserialize(const void *data, size_t size)
{
    return core->LoadState(reinterpret_cast<const u8*>(data), size);
}

void *retro_get_memory_data(unsigned id)
{
    switch (id)
    {
        case RETRO_MEMORY_SAVE_RAM:
        {
            Mapper* mapper = core->GetMemory()->GetMapper();
            if (mapper)
                return mapper->GetSaveData();
            return NULL;
        }
        case RETRO_MEMORY_SYSTEM_RAM:
            return core->GetMemory()->GetRam();
    }

    return NULL;
}

size_t retro_get_memory_size(unsigned id)
{
    switch (id)
    {
        case RETRO_MEMORY_SAVE_RAM:
        {
            Mapper* mapper = core->GetMemory()->GetMapper();
            if (mapper)
                return mapper->GetSaveDataSize();
            return 0;
        }
        case RETRO_MEMORY_SYSTEM_RAM:
            return 0x400;
    }

    return 0;
}

void retro_cheat_reset(void)
{
    // TODO
}

void retro_cheat_set(unsigned index, bool enabled, const char *code)
{
    // TODO
}
