/*
Copyright (c) 2013, 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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include "ProSystem.h"
#include "Database.h"
#include "Sound.h"
#include "Palette.h"
#include "Maria.h"
#include "Tia.h"
#include "Pokey.h"
#include "Cartridge.h"
#include "version.h"

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

#define SAMPLERATE 48000
#define FRAMERATE 60
#define CHANNELS 1
#define NUMINPUTS 3 // 2 Joysticks, 1 Console

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 = {
    "prosystem", "ProSystem", JG_VERSION, "7800", NUMINPUTS, 0
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_XRGB8888, 320, 292, 320, 225, 0, 0, 320, 1.3061224, 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];

static uint8_t _inputState[17];
static uint32_t _displayPalette[256];
static uint8_t *_soundBuffer = NULL;
static bool _isLightgunEnabled = false;

static int diffswitch[2] = {0};

static void ps_input_poll(void) {
    // Joystick 1
    if (!_isLightgunEnabled) {
        _inputState[0] = input_device[0]->button[3]; // 00 | Joystick 1 | Right
        _inputState[1] = input_device[0]->button[2]; // 01 | Joystick 1 | Left
        _inputState[2] = input_device[0]->button[1]; // 02 | Joystick 1 | Down
        _inputState[3] = input_device[0]->button[0]; // 03 | Joystick 1 | Up
        _inputState[4] = input_device[0]->button[4]; // 04 | Joystick 1 | B1
        _inputState[5] = input_device[0]->button[5]; // 05 | Joystick 1 | B2
    }
    else {
        int yoffset = (cartridge_region == REGION_NTSC ? 2 : -2);
        
        _inputState[3] = input_device[0]->button[0] ^ 1;
        lightgun_cycle = input_device[0]->coord[0] +
            HBLANK_CYCLES + LG_CYCLES_INDENT;
        lightgun_scanline = input_device[0]->coord[1] +
            (maria_visibleArea.top - maria_displayArea.top + 1) + yoffset;
        
        if(lightgun_cycle > CYCLES_PER_SCANLINE) {
            lightgun_scanline++;
            lightgun_cycle -= CYCLES_PER_SCANLINE;
        }
    }
    
    // Joystick 2
    _inputState[6] = input_device[1]->button[3]; // 06 | Joystick 2 | Right
    _inputState[7] = input_device[1]->button[2]; // 07 | Joystick 2 | Left
    _inputState[8] = input_device[1]->button[1]; // 08 | Joystick 2 | Down
    _inputState[9] = input_device[1]->button[0]; // 09 | Joystick 2 | Up
    _inputState[10] = input_device[1]->button[4]; // 10 | Joystick 2 | B1
    _inputState[11] = input_device[1]->button[5]; // 11 | Joystick 2 | B2
    
    // Console
    _inputState[12] = input_device[2]->button[0]; // 12 | Console | Reset
    _inputState[13] = input_device[2]->button[1]; // 13 | Console | Select
    _inputState[14] = input_device[2]->button[2]; // 14 | Console | Pause
    
    // Difficulty switches on console
    if (input_device[2]->button[3]) { // It has been pressed or is held
        diffswitch[0] = 1; // Mark the button as being held
    }
    else if (diffswitch[0]) { // It has been released
        _inputState[15] ^= 1;
        jg_cb_log(JG_LOG_DBG, "Left Difficulty Switch: %s\n",
            _inputState[15] ? "Beginner" : "Advanced");
        diffswitch[0] = 0; // Release the "held" bit
    }
    
    if (input_device[2]->button[4]) {
        diffswitch[1] = 1;
    }
    else if (diffswitch[1]) {
        _inputState[16] ^= 1;
        jg_cb_log(JG_LOG_DBG, "Right Difficulty Switch: %s\n",
            _inputState[16] ? "Beginner" : "Advanced");
        diffswitch[1] = 0;
    }
}

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) {
    _soundBuffer = (uint8_t *)malloc(8192);
    sound_SetSampleRate(SAMPLERATE);
    return 1;
}

void jg_deinit(void) {
    free(_soundBuffer);
    cartridge_Release();
}

void jg_reset(int hard) {
    if (hard)
        prosystem_Reset();
    else
        jg_cb_log(JG_LOG_WRN,
            "Soft reset should be done with user-defined inputs\n");
}

void jg_exec_frame(void) {
    ps_input_poll();
    prosystem_ExecuteFrame(_inputState);
    
    vidinfo.w = ((maria_displayArea.right - maria_displayArea.left) + 1);
    vidinfo.h = ((maria_visibleArea.bottom - maria_visibleArea.top) + 1);
    //printf("w: %d, h: %d\n", vidinfo.w, vidinfo.h);
    
    uint8_t *buffer = maria_surface +
        ((maria_visibleArea.top - maria_displayArea.top) *
        vidinfo.w);
    
    uint32_t *surface = (uint32_t *)vidinfo.buf;
    int pitch = 320;
    
    for (unsigned indexY = 0; indexY < vidinfo.h; indexY++) {
        for (unsigned indexX = 0; indexX < vidinfo.w; indexX += 4) {
            surface[indexX + 0] = _displayPalette[buffer[indexX + 0]];
            surface[indexX + 1] = _displayPalette[buffer[indexX + 1]];
            surface[indexX + 2] = _displayPalette[buffer[indexX + 2]];
            surface[indexX + 3] = _displayPalette[buffer[indexX + 3]];
        }
        surface += pitch;
        buffer += vidinfo.w;
    }
    
    // Grab unsigned 8-bit sound samples from the core
    int len = sound_Store(_soundBuffer);
    
    // Convert to signed 16-bit samples
    int16_t *abuf = (int16_t*)audinfo.buf;
    for (int i = 0; i < len; i++)
        abuf[i] = _soundBuffer[i] << 8;

    jg_cb_audio(len);
}

int jg_game_load(void) {
    const int LEFT_DIFF_SWITCH  = 15;
    const int RIGHT_DIFF_SWITCH = 16;
    const int LEFT_POSITION  = 1; // also know as "B"
    const int RIGHT_POSITION = 0; // also know as "A"
    
    memset(_inputState, 0, sizeof(_inputState));
    
    // Difficulty switches: Left = (B)eginner, Right = (A)dvanced
    // Left difficulty switch defaults to left position, "(B)eginner"
    _inputState[LEFT_DIFF_SWITCH] = LEFT_POSITION;

    // Right difficulty switch defaults to right position,
    // "(A)dvanced", which fixes Tower Toppler
    _inputState[RIGHT_DIFF_SWITCH] = RIGHT_POSITION;
    
    cartridge_controller[0] = cartridge_controller[1] = 1;
    
    if (cartridge_Load(gameinfo.data, gameinfo.size)) {
        char dbpath[256];
        snprintf(dbpath, sizeof(dbpath), "%s/ProSystem.dat", pathinfo.core);
        database_filename = dbpath;
        database_enabled = true;
        
        database_Load(cart_digest);
        
        char biosROM[256];
        
        if (cartridge_region == REGION_NTSC) {
            jg_cb_frametime(60.0);
            snprintf(biosROM, sizeof(biosROM),
                "%s%s", pathinfo.bios, "/7800 BIOS (U).rom");
        }
        else {
            jg_cb_frametime(50.0);
            snprintf(biosROM, sizeof(biosROM),
                "%s%s", pathinfo.bios, "/7800 BIOS (E).rom");
        }
        
        // States don't load if you have a BIOS loaded! FIXME
        /*if (bios_Load(biosROM))
            bios_enabled = true;
        //*/
        prosystem_Reset();
        
        for (int i = 0; i < 256; i++) {
            uint32_t r = palette_data[(i * 3) + 0] << 16;
            uint32_t g = palette_data[(i * 3) + 1] << 8;
            uint32_t b = palette_data[(i * 3) + 2];
            _displayPalette[i] = r | g | b;
        }
        
        _isLightgunEnabled =
            (cartridge_controller[0] & CARTRIDGE_CONTROLLER_LIGHTGUN);
        // The light gun 'trigger' is a press on the 'up' button (0x3)
        // and needs the bit toggled
        if(_isLightgunEnabled)
            _inputState[3] = 1;
        
        // Set switch overrides from database
        _inputState[LEFT_DIFF_SWITCH] = cartridge_left_switch;
        _inputState[RIGHT_DIFF_SWITCH] = cartridge_right_switch;
        
        if (cart_in_db) {
            jg_cb_log(JG_LOG_INF, "Title: %s\n", cartridge_title);
            jg_cb_log(JG_LOG_INF, "Cart Type: %d\n", cartridge_type);
            jg_cb_log(JG_LOG_INF, "Region: %s\n",
                cartridge_region == REGION_NTSC ? "NTSC" : "PAL");
            jg_cb_log(JG_LOG_INF, "Pokey: %s\n",
                cartridge_pokey ? "true" : "false");
        }
        
        if (_isLightgunEnabled)
            inputinfo[0] = jg_7800_inputinfo(0, JG_7800_GUN);
        else
            inputinfo[0] = jg_7800_inputinfo(0, JG_7800_JS);
        
        inputinfo[1] = jg_7800_inputinfo(1, JG_7800_JS);
        inputinfo[2] = jg_7800_inputinfo(2, JG_7800_SYS);
    }
    
    return 1;
}

int jg_game_unload(void) {
    return 1;
}

int jg_state_load(const char *filename) {
    return prosystem_Load(filename);
}

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

int jg_state_save(const char *filename) {
    return prosystem_Save(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) {
}

void jg_cheat_set(const char *code) {
    if (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 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(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;
}

// JG Functions that set values in the emulator core
void jg_setup_video(void) {
}

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) {
    if (info.size || index) { }
}

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