/*
Copyright (c) 2020, OpenEmu Team
Copyright (c) 2020, 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 <stdarg.h>

#if !(defined(__MINGW32__) || defined(__MINGW64__))
#include <sys/mman.h>
#else
#include <io.h>
#include <windows.h>
#include <sys/types.h>
#include "winrubbish.h"
#endif

#include "pico/pico_int.h"
#include "pico/state.h"
#include "pico/patch.h"
#include "platform/common/input_pico.h"
#include "version.h"

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

#define SAMPLERATE 48000
#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 = {
    "picodrive", "PicoDrive", JG_VERSION, "32x", NUMINPUTS, 0
};

static jg_videoinfo_t vidinfo = {
    JG_PIXFMT_RGB565,   // pixfmt
    320,                // wmax
    240,                // hmax
    320,                // w
    224,                // h
    0,                  // x
    0,                  // y
    320,                // p
    292.0/224.0,        // 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 *input_device[NUMINPUTS];

char savefile[256]; // Save file name
char biosfile[256]; // BIOS file name

// PicoDrive helper functions
static void sound_write(int len) {
    if (!len) return;
    jg_cb_audio(len / CHANNELS);
}

int emu_save_load_game(int load, const char *saveFname) {
    int ret = 0;
    
    FILE *sramFile;
    int sram_size;
    unsigned char *sram_data;
    
    sram_size = Pico.sv.size;
    sram_data = Pico.sv.data;
    
    if (sram_data == NULL)
        return 0; // cart saves forcefully disabled for this game
    
    if (load) {
        sramFile = fopen(saveFname, "rb");
        if (!sramFile)
            return -1;
        
        ret = fread(sram_data, 1, sram_size, sramFile);
        ret = ret > 0 ? 0 : -1;
        fclose(sramFile);
    }
    else {
        // sram save needs some special processing
        // see if we have anything to save
        for (; sram_size > 0; sram_size--)
            if (sram_data[sram_size-1]) break;
        
        if (sram_size) {
            sramFile = fopen(saveFname, "wb");
            //if (!sramFile) sramFile = fopen(saveFname, "wb"); // retry
            if (!sramFile) return -1;
            ret = fwrite(sram_data, 1, sram_size, sramFile);
            ret = (ret != sram_size) ? -1 : 0;
            fclose(sramFile);
        }
    }
    return ret;
}

void *plat_mmap(unsigned long addr, size_t size, int need_exec, int is_fixed) {
    int flags = MAP_PRIVATE | MAP_ANONYMOUS;
    void *req, *ret;
    
    req = (void *)(uintptr_t)addr;
    ret = mmap(req, size, PROT_READ | PROT_WRITE, flags, -1, 0);
    if (ret == MAP_FAILED) {
        jg_cb_log(JG_LOG_WRN, "mmap(%08lx, %zd) failed\n", addr, size);
        return NULL;
    }
    
    if (addr != 0 && ret != (void*)(uintptr_t)addr) {
        jg_cb_log(JG_LOG_WRN, "Wanted to map %08lx, got %p\n", addr, ret);

        if (is_fixed) {
            munmap(ret, size);
            return NULL;
        }
    }
    return ret;
}

void plat_munmap(void *ptr, size_t size) {
    if (ptr != NULL)
        munmap(ptr, size);
}

// Not using carthw.cfg so this is probably never called.
void *plat_mremap(void *ptr, size_t oldsize, size_t newsize) {
    void *tmp, *ret;
    size_t preserve_size;
    
    preserve_size = oldsize;
    if (preserve_size > newsize)
        preserve_size = newsize;
    
    tmp = malloc(preserve_size);
    if (tmp == NULL)
        return NULL;
    
    memcpy(tmp, ptr, preserve_size);
    
    munmap(ptr, oldsize);
    ret = mmap(ptr, newsize,
        PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    if (ret == MAP_FAILED) {
        free(tmp);
        return NULL;
    }
    
    memcpy(ret, tmp, preserve_size);
    free(tmp);
    return ret;
}

void *plat_mem_get_for_drc(size_t size) {
    return NULL;
}

int plat_mem_set_exec(void *ptr, size_t size) {
#if (defined(__MINGW32__) || defined(__MINGW64__))
    DWORD oldProtect = 0;
    int ret = VirtualProtect(ptr, size, PAGE_EXECUTE_READWRITE, &oldProtect);
    if (ret == 0)
        jg_cb_log(JG_LOG_WRN, "VirtualProtect(%p, %d) failed: %d\n",
            ptr, (int)size, GetLastError());
#else
    int ret = mprotect(ptr, size, PROT_READ | PROT_WRITE | PROT_EXEC);
    if (ret != 0)
        jg_cb_log(JG_LOG_WRN, "mprotect(%p, %zd) failed: %d\n", ptr, size, ret);
#endif
    return ret;
}

__attribute__((format(printf, 1, 2)))
void lprintf(const char *fmt, ...) {
    char buffer[256];
    va_list ap;
    va_start(ap, fmt);
    vsprintf(buffer, fmt, ap);
    jg_cb_log(JG_LOG_DBG, "%s", buffer);
    va_end(ap);
}

void emu_video_mode_change(int start_line, int line_count, int is_32cols) {
    vidinfo.y = start_line;
    vidinfo.h = line_count;
    vidinfo.w = is_32cols ? 256 : 320;
}

void emu_32x_startup(void) { }

static const int Sega32XMap[] = {
    GBTN_UP, GBTN_DOWN, GBTN_LEFT, GBTN_RIGHT, GBTN_MODE, GBTN_START,
    GBTN_A, GBTN_B, GBTN_C, GBTN_X, GBTN_Y, GBTN_Z
};

static void pd_input_poll(int port) {
    uint32_t buttons = 0;
    for (int i = 0; i < NDEFS_MDPAD6B; i++)
        if (input_device[port]->button[i]) buttons |= 1 << Sega32XMap[i];
    PicoIn.pad[port] = buttons;
}

// 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(void) {
    return 1;
}

void jg_deinit(void) {
}

void jg_reset(int hard) {
    PicoReset();
}

void jg_exec_frame(void) {
    for (int i = 0; i < NUMINPUTS; i++)
        pd_input_poll(i);
    PicoFrame();
}

static const char *pd_set_bios(int *region, const char *cd_fname) {
    switch (*region) {
        case 0x04: // US
            snprintf(biosfile, sizeof(biosfile), "%s/%s",
                pathinfo.bios, "bios_CD_U.bin");
            break;
        case 0x08: // EU
            snprintf(biosfile, sizeof(biosfile), "%s/%s",
                pathinfo.bios, "bios_CD_E.bin");
            break;
        default: // JP
            snprintf(biosfile, sizeof(biosfile), "%s/%s",
                pathinfo.bios, "bios_CD_J.bin");
            break;
    }
    
    return biosfile;
}

int jg_game_load(void) {
    PicoIn.opt = POPT_EN_STEREO|POPT_EN_FM|POPT_EN_PSG|POPT_EN_Z80
    | POPT_EN_MCD_PCM|POPT_EN_MCD_CDDA|POPT_EN_MCD_GFX
    | POPT_EN_32X|POPT_EN_PWM
    | POPT_ACC_SPRITES|POPT_DIS_32C_BORDER;
    
    PicoIn.sndRate = SAMPLERATE;
    PicoIn.autoRgnOrder = 0x184; // US, EU, JP
    
    PicoInit();
    
    PicoSetInputDevice(0, PICO_INPUT_PAD_6BTN);
    PicoSetInputDevice(1, PICO_INPUT_PAD_6BTN);
    inputinfo[0] = jg_md_inputinfo(0, JG_MD_PAD6B);
    inputinfo[1] = jg_md_inputinfo(1, JG_MD_PAD6B);
    
    enum media_type_e media_type;
    media_type = PicoLoadMedia(gameinfo.path, NULL, pd_set_bios, NULL);
    
    // Set up save file
    snprintf(savefile, sizeof(savefile),
        "%s/%s%s", pathinfo.save, gameinfo.name, ".sav");
    emu_save_load_game(1, savefile);
    
    switch (media_type) {
        case PM_BAD_DETECT:
           // Failed to detect ROM image type.
           return 0;
        case PM_ERROR:
           // Load error
           return 0;
        default: break;
    }
    
    PicoLoopPrepare();
    
    return 1;
}

int jg_game_unload(void) {
    emu_save_load_game(0, savefile);
    PicoExit();
    return 1;
}

int jg_state_load(const char *filename) {
    int ret = PicoState(filename, 0);
    return ret == 0;
}

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

int jg_state_save(const char *filename) {
    int ret = PicoState(filename, 1);
    return ret == 0;
}

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_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) {
    PicoDrawSetOutFormat(PDF_RGB555, 0);
    PicoDrawSetOutBuf(vidinfo.buf, vidinfo.wmax * sizeof(uint16_t));
}

void jg_setup_audio(void) {
    PicoIn.writeSound = sound_write;
    PicoIn.sndOut = audinfo.buf;
    PsndRerate(0);
}

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