/*
    Copyright 2016-2017 StapleButter

    This file is part of melonDS.

    melonDS 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 (at your option)
    any later version.

    melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/.
*/

#include <stdlib.h>
#include <time.h>
#include <stdio.h>

#include <SDL2/SDL.h>
#include "libui/ui.h"

#include "../types.h"
#include "../version.h"
#include "../Config.h"

#include "DlgEmuSettings.h"
#include "DlgInputConfig.h"

#include "../NDS.h"
#include "../GPU.h"
#include "../SPU.h"
#include "../Wifi.h"
#include "../Platform.h"


const int kScreenGap[] = {0, 1, 8, 64, 90, 128};


uiWindow* MainWindow;
uiArea* MainDrawArea;

uiMenuItem* MenuItem_Pause;
uiMenuItem* MenuItem_Reset;
uiMenuItem* MenuItem_Stop;

SDL_Thread* EmuThread;
int EmuRunning;
volatile int EmuStatus;

bool RunningSomething;
char ROMPath[1024];

bool ScreenDrawInited = false;
uiDrawBitmap* ScreenBitmap = NULL;
u32 ScreenBuffer[256*384];

uiRect TopScreenRect;
uiRect BottomScreenRect;

bool Touching = false;

u32 KeyInputMask;
SDL_Joystick* Joystick;



void AudioCallback(void* data, Uint8* stream, int len)
{
    SPU::ReadOutput((s16*)stream, len>>2);
}

int EmuThreadFunc(void* burp)
{
    NDS::Init();

    ScreenDrawInited = false;
    Touching = false;

    // DS:
    // 547.060546875 samples per frame
    // 32823.6328125 samples per second
    //
    // 48000 samples per second:
    // 800 samples per frame
    SDL_AudioSpec whatIwant, whatIget;
    memset(&whatIwant, 0, sizeof(SDL_AudioSpec));
    whatIwant.freq = 32824; // 32823.6328125
    whatIwant.format = AUDIO_S16LSB;
    whatIwant.channels = 2;
    whatIwant.samples = 1024;
    whatIwant.callback = AudioCallback;
    SDL_AudioDeviceID audio = SDL_OpenAudioDevice(NULL, 0, &whatIwant, &whatIget, 0);
    if (!audio)
    {
        printf("Audio init failed: %s\n", SDL_GetError());
    }
    else
    {
        SDL_PauseAudioDevice(audio, 0);
    }

    KeyInputMask = 0xFFF;

    // TODO: support more joysticks
    if (SDL_NumJoysticks() > 0)
        Joystick = SDL_JoystickOpen(0);
    else
        Joystick = NULL;

    u32 nframes = 0;
    u32 starttick = SDL_GetTicks();
    u32 lasttick = starttick;
    u32 lastmeasuretick = lasttick;
    u32 fpslimitcount = 0;
    bool limitfps = true;

    while (EmuRunning != 0)
    {
        if (EmuRunning == 1)
        {
            EmuStatus = 1;

            // poll input
            u32 keymask = KeyInputMask;
            u32 joymask = 0xFFF;
            if (Joystick)
            {
                SDL_JoystickUpdate();

                Uint32 hat = SDL_JoystickGetHat(Joystick, 0);
                Sint16 axisX = SDL_JoystickGetAxis(Joystick, 0);
                Sint16 axisY = SDL_JoystickGetAxis(Joystick, 1);

                for (int i = 0; i < 12; i++)
                {
                    int btnid = Config::JoyMapping[i];
                    if (btnid < 0) continue;

                    bool pressed;
                    if (btnid == 0x101) // up
                        pressed = (hat & SDL_HAT_UP) || (axisY <= -16384);
                    else if (btnid == 0x104) // down
                        pressed = (hat & SDL_HAT_DOWN) || (axisY >= 16384);
                    else if (btnid == 0x102) // right
                        pressed = (hat & SDL_HAT_RIGHT) || (axisX >= 16384);
                    else if (btnid == 0x108) // left
                        pressed = (hat & SDL_HAT_LEFT) || (axisX <= -16384);
                    else
                        pressed = SDL_JoystickGetButton(Joystick, btnid);

                    if (pressed) joymask &= ~(1<<i);
                }
            }
            NDS::SetKeyMask(keymask & joymask);

            // emulate
            u32 nlines = NDS::RunFrame();

            if (EmuRunning == 0) break;

            memcpy(ScreenBuffer, GPU::Framebuffer, 256*384*4);
            uiAreaQueueRedrawAll(MainDrawArea);

            // framerate limiter based off SDL2_gfx
            float framerate;
            if (nlines == 263) framerate = 1000.0f / 60.0f;
            else               framerate = ((1000.0f * nlines) / 263.0f) / 60.0f;

            fpslimitcount++;
            u32 curtick = SDL_GetTicks();
            u32 delay = curtick - lasttick;
            lasttick = curtick;

            u32 wantedtick = starttick + (u32)((float)fpslimitcount * framerate);
            if (curtick < wantedtick && limitfps)
            {
                SDL_Delay(wantedtick - curtick);
            }
            else
            {
                fpslimitcount = 0;
                starttick = curtick;
            }

            nframes++;
            if (nframes >= 30)
            {
                u32 tick = SDL_GetTicks();
                u32 diff = tick - lastmeasuretick;
                lastmeasuretick = tick;

                u32 fps = (nframes * 1000) / diff;
                nframes = 0;

                float fpstarget;
                if (framerate < 1) fpstarget = 999;
                else fpstarget = 1000.0f/framerate;

                char melontitle[100];
                sprintf(melontitle, "%d/%.0f FPS | melonDS " MELONDS_VERSION, fps, fpstarget);
                //uiWindowSetTitle(MainWindow, melontitle);
            }
        }
        else
        {
            EmuStatus = 2;

            // paused
            nframes = 0;
            lasttick = SDL_GetTicks();
            starttick = lasttick;
            lastmeasuretick = lasttick;
            fpslimitcount = 0;

            uiAreaQueueRedrawAll(MainDrawArea);
            SDL_Delay(100);
        }
    }

    EmuStatus = 0;

    if (Joystick) SDL_JoystickClose(Joystick);

    if (audio) SDL_CloseAudioDevice(audio);

    NDS::DeInit();

    return 44203;
}


void OnAreaDraw(uiAreaHandler* handler, uiArea* area, uiAreaDrawParams* params)
{
    if (!ScreenDrawInited)
    {
        ScreenBitmap = uiDrawNewBitmap(params->Context, 256, 384);
        ScreenDrawInited = true;
    }

    if (!ScreenBitmap) return;

    uiRect top = {0, 0, 256, 192};
    uiRect bot = {0, 192, 256, 192};

    uiDrawBitmapUpdate(ScreenBitmap, ScreenBuffer);

    uiDrawBitmapDraw(params->Context, ScreenBitmap, &top, &TopScreenRect);
    uiDrawBitmapDraw(params->Context, ScreenBitmap, &bot, &BottomScreenRect);
}

void OnAreaMouseEvent(uiAreaHandler* handler, uiArea* area, uiAreaMouseEvent* evt)
{
    int x = (int)evt->X;
    int y = (int)evt->Y;

    if (Touching && (evt->Up == 1))
    {
        Touching = false;
        NDS::ReleaseKey(16+6);
        NDS::ReleaseScreen();
    }
    else if (!Touching && (evt->Down == 1) &&
             (x >= BottomScreenRect.X) && (y >= BottomScreenRect.Y) &&
             (x < (BottomScreenRect.X+BottomScreenRect.Width)) && (y < (BottomScreenRect.Y+BottomScreenRect.Height)))
    {
        Touching = true;
        NDS::PressKey(16+6);
    }

    if (Touching)
    {
        x -= BottomScreenRect.X;
        y -= BottomScreenRect.Y;

        if (BottomScreenRect.Width != 256)
            x = (x * 256) / BottomScreenRect.Width;
        if (BottomScreenRect.Height != 192)
            y = (y * 192) / BottomScreenRect.Height;

        // clamp
        if (x < 0) x = 0;
        else if (x > 255) x = 255;
        if (y < 0) y = 0;
        else if (y > 191) y = 191;

        // TODO: take advantage of possible extra precision when possible? (scaled window for example)
        NDS::TouchScreen(x, y);
    }
}

void OnAreaMouseCrossed(uiAreaHandler* handler, uiArea* area, int left)
{
}

void OnAreaDragBroken(uiAreaHandler* handler, uiArea* area)
{
}

int OnAreaKeyEvent(uiAreaHandler* handler, uiArea* area, uiAreaKeyEvent* evt)
{
    // TODO: release all keys if the window loses focus? or somehow global key input?
    if (evt->Scancode == 0x38) // ALT
        return 0;
    if (evt->Modifiers == 0x2) // ALT+key
        return 0;

    if (evt->Up)
    {
        for (int i = 0; i < 12; i++)
            if (evt->Scancode == Config::KeyMapping[i])
                KeyInputMask |= (1<<i);
    }
    else if (!evt->Repeat)
    {
        for (int i = 0; i < 12; i++)
            if (evt->Scancode == Config::KeyMapping[i])
                KeyInputMask &= ~(1<<i);
    }

    return 1;
}

void OnAreaResize(uiAreaHandler* handler, uiArea* area, int width, int height)
{
    float ratio = (height/2) / (float)width;

    if (ratio <= 0.75)
    {
        // bars on the sides

        int targetW = (height * 256) / 384;
        int barW = (width - targetW) / 2;

        TopScreenRect.X = barW;
        TopScreenRect.Width = targetW;
        TopScreenRect.Y = 0;
        TopScreenRect.Height = height / 2;

        BottomScreenRect.X = barW;
        BottomScreenRect.Width = targetW;
        BottomScreenRect.Y = height / 2;
        BottomScreenRect.Height = height / 2;
    }
    else
    {
        // TODO: this should do bars on the top, and fixed screen gap
        // for now we'll adjust the screen gap in consequence

        int targetH = (width * 384) / 256;
        int gap = height - targetH;

        TopScreenRect.X = 0;
        TopScreenRect.Width = width;
        TopScreenRect.Y = 0;
        TopScreenRect.Height = targetH / 2;

        BottomScreenRect.X = 0;
        BottomScreenRect.Width = width;
        BottomScreenRect.Y = (targetH / 2) + gap;
        BottomScreenRect.Height = targetH / 2;
    }
}


void Run()
{
    EmuRunning = 1;
    RunningSomething = true;

    uiMenuItemEnable(MenuItem_Pause);
    uiMenuItemEnable(MenuItem_Reset);
    uiMenuItemEnable(MenuItem_Stop);
    uiMenuItemSetChecked(MenuItem_Pause, 0);
}

void Stop(bool internal)
{
    EmuRunning = 2;
    if (!internal) // if shutting down from the UI thread, wait till the emu thread has stopped
        while (EmuStatus != 2);
    RunningSomething = false;

    uiMenuItemDisable(MenuItem_Pause);
    uiMenuItemDisable(MenuItem_Reset);
    uiMenuItemDisable(MenuItem_Stop);
    uiMenuItemSetChecked(MenuItem_Pause, 0);

    memset(ScreenBuffer, 0, 256*384*4);
    uiAreaQueueRedrawAll(MainDrawArea);
}


int OnCloseWindow(uiWindow* window, void* blarg)
{
    uiQuit();
    return 1;
}

void OnDropFile(uiWindow* window, char* file, void* blarg)
{
    char* ext = &file[strlen(file)-3];

    if (!strcasecmp(ext, "nds") || !strcasecmp(ext, "srl"))
    {
        if (RunningSomething)
        {
            EmuRunning = 2;
            while (EmuStatus != 2);
        }

        strncpy(ROMPath, file, 1023);
        ROMPath[1023] = '\0';

        NDS::LoadROM(ROMPath, Config::DirectBoot);
        Run();
    }
}

void OnGetFocus(uiWindow* window, void* blarg)
{
    uiControlSetFocus(uiControl(MainDrawArea));
}

void OnLoseFocus(uiWindow* window, void* blarg)
{
    // TODO: shit here?
}

void OnCloseByMenu(uiMenuItem* item, uiWindow* window, void* blarg)
{
    uiControlDestroy(uiControl(window));
    uiQuit();
}

void OnOpenFile(uiMenuItem* item, uiWindow* window, void* blarg)
{
    int prevstatus = EmuRunning;
    EmuRunning = 2;
    while (EmuStatus != 2);

    char* file = uiOpenFile(window, "DS ROM (*.nds)|*.nds;*.srl|Any file|*.*", NULL);
    if (!file)
    {
        EmuRunning = prevstatus;
        return;
    }

    strncpy(ROMPath, file, 1023);
    ROMPath[1023] = '\0';
    uiFreeText(file);
    // TODO: change libui to store strings in stack-allocated buffers?
    // so we don't have to free it after use

    NDS::LoadROM(ROMPath, Config::DirectBoot);
    Run();
}

void OnRun(uiMenuItem* item, uiWindow* window, void* blarg)
{
    if (!RunningSomething)
    {
        ROMPath[0] = '\0';
        NDS::LoadBIOS();
    }

    Run();
}

void OnPause(uiMenuItem* item, uiWindow* window, void* blarg)
{
    if (!RunningSomething) return;

    if (EmuRunning == 1)
    {
        // enable pause
        EmuRunning = 2;
        uiMenuItemSetChecked(MenuItem_Pause, 1);
    }
    else
    {
        // disable pause
        EmuRunning = 1;
        uiMenuItemSetChecked(MenuItem_Pause, 0);
    }
}

void OnReset(uiMenuItem* item, uiWindow* window, void* blarg)
{
    if (!RunningSomething) return;

    EmuRunning = 2;
    while (EmuStatus != 2);

    if (ROMPath[0] == '\0')
        NDS::LoadBIOS();
    else
        NDS::LoadROM(ROMPath, Config::DirectBoot);

    Run();
}

void OnStop(uiMenuItem* item, uiWindow* window, void* blarg)
{
    if (!RunningSomething) return;

    Stop(false);
}

void OnOpenEmuSettings(uiMenuItem* item, uiWindow* window, void* blarg)
{
    DlgEmuSettings::Open();
}

void OnOpenInputConfig(uiMenuItem* item, uiWindow* window, void* blarg)
{
    DlgInputConfig::Open();
}


void ApplyNewSettings()
{
    if (!RunningSomething) return;

    int prevstatus = EmuRunning;
    EmuRunning = 2;
    while (EmuStatus != 2);

    GPU3D::SoftRenderer::SetupRenderThread();

    if (Wifi::MPInited)
    {
        Platform::MP_DeInit();
        Platform::MP_Init();
    }

    EmuRunning = prevstatus;
}


bool _fileexists(char* name)
{
    FILE* f = fopen(name, "rb");
    if (!f) return false;
    fclose(f);
    return true;
}


int main(int argc, char** argv)
{
    srand(time(NULL));

    printf("melonDS " MELONDS_VERSION "\n");
    printf(MELONDS_URL "\n");

    // http://stackoverflow.com/questions/14543333/joystick-wont-work-using-sdl
    SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");

    if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
        printf("SDL shat itself :(\n");
        return 1;
    }

    SDL_JoystickEventState(SDL_ENABLE);

    uiInitOptions ui_opt;
    memset(&ui_opt, 0, sizeof(uiInitOptions));
    const char* ui_err = uiInit(&ui_opt);
    if (ui_err != NULL)
    {
        printf("libui shat itself :( %s\n", ui_err);
        uiFreeInitError(ui_err);
        return 1;
    }

    Config::Load();

    if (!_fileexists("bios7.bin") || !_fileexists("bios9.bin") || !_fileexists("firmware.bin"))
    {
        uiMsgBoxError(
            NULL,
            "BIOS/Firmware not found",
            "One or more of the following required files don't exist or couldn't be accessed:\n\n"
            "bios7.bin -- ARM7 BIOS\n"
            "bios9.bin -- ARM9 BIOS\n"
            "firmware.bin -- firmware image\n\n"
            "Dump the files from your DS and place them in the directory you run melonDS from.\n"
            "Make sure that the files can be accessed.");

        uiUninit();
        SDL_Quit();
        return 0;
    }

    uiMenu* menu;
    uiMenuItem* menuitem;

    menu = uiNewMenu("File");
    menuitem = uiMenuAppendItem(menu, "Open ROM...");
    uiMenuItemOnClicked(menuitem, OnOpenFile, NULL);
    uiMenuAppendSeparator(menu);
    menuitem = uiMenuAppendItem(menu, "Quit");
    uiMenuItemOnClicked(menuitem, OnCloseByMenu, NULL);

    menu = uiNewMenu("System");
    menuitem = uiMenuAppendItem(menu, "Run");
    uiMenuItemOnClicked(menuitem, OnRun, NULL);
    menuitem = uiMenuAppendCheckItem(menu, "Pause");
    uiMenuItemOnClicked(menuitem, OnPause, NULL);
    MenuItem_Pause = menuitem;
    uiMenuAppendSeparator(menu);
    menuitem = uiMenuAppendItem(menu, "Reset");
    uiMenuItemOnClicked(menuitem, OnReset, NULL);
    MenuItem_Reset = menuitem;
    menuitem = uiMenuAppendItem(menu, "Stop");
    uiMenuItemOnClicked(menuitem, OnStop, NULL);
    MenuItem_Stop = menuitem;

    menu = uiNewMenu("Config");
    menuitem = uiMenuAppendItem(menu, "Emu settings");
    uiMenuItemOnClicked(menuitem, OnOpenEmuSettings, NULL);
    menuitem = uiMenuAppendItem(menu, "Input config");
    uiMenuItemOnClicked(menuitem, OnOpenInputConfig, NULL);
    /*uiMenuAppendSeparator();
    menuitem = uiMenuAppendItem(menu, "Mid-screen gap");
    {
        uiMenuItem* parent = menuitem;
        //menuitem = uiMenu
        // TODO: need submenu support in libui.
    }*/

    MainWindow = uiNewWindow("melonDS " MELONDS_VERSION, 256, 384, 1);
    uiWindowOnClosing(MainWindow, OnCloseWindow, NULL);

    uiWindowSetDropTarget(MainWindow, 1);
    uiWindowOnDropFile(MainWindow, OnDropFile, NULL);

    uiWindowOnGetFocus(MainWindow, OnGetFocus, NULL);
    uiWindowOnLoseFocus(MainWindow, OnLoseFocus, NULL);

    uiMenuItemDisable(MenuItem_Pause);
    uiMenuItemDisable(MenuItem_Reset);
    uiMenuItemDisable(MenuItem_Stop);

    uiAreaHandler areahandler;
    areahandler.Draw = OnAreaDraw;
    areahandler.MouseEvent = OnAreaMouseEvent;
    areahandler.MouseCrossed = OnAreaMouseCrossed;
    areahandler.DragBroken = OnAreaDragBroken;
    areahandler.KeyEvent = OnAreaKeyEvent;
    areahandler.Resize = OnAreaResize;

    MainDrawArea = uiNewArea(&areahandler);
    uiWindowSetChild(MainWindow, uiControl(MainDrawArea));
    uiControlSetMinSize(uiControl(MainDrawArea), 256, 384);

    EmuRunning = 2;
    RunningSomething = false;
    EmuThread = SDL_CreateThread(EmuThreadFunc, "melonDS magic", NULL);

    if (argc > 1)
    {
        char* file = argv[1];
        char* ext = &file[strlen(file)-3];

        if (!strcasecmp(ext, "nds") || !strcasecmp(ext, "srl"))
        {
            strncpy(ROMPath, file, 1023);
            ROMPath[1023] = '\0';

            NDS::LoadROM(ROMPath, Config::DirectBoot);
            Run();
        }
    }

    uiControlShow(uiControl(MainWindow));
    uiControlSetFocus(uiControl(MainDrawArea));
    uiMain();

    EmuRunning = 0;
    SDL_WaitThread(EmuThread, NULL);

    if (ScreenBitmap) uiDrawFreeBitmap(ScreenBitmap);

    uiUninit();
    SDL_Quit();
    return 0;
}

#ifdef __WIN32__

#include <windows.h>

int CALLBACK WinMain(HINSTANCE hinst, HINSTANCE hprev, LPSTR cmdline, int cmdshow)
{
    char cmdargs[16][256];
    int arg = 1;
    int j = 0;
    bool inquote = false;
    int len = strlen(cmdline);
    for (int i = 0; i < len; i++)
    {
        char c = cmdline[i];
        if (c == '\0') break;
        if (c == '"') inquote = !inquote;
        if (!inquote && c==' ')
        {
            if (j > 255) j = 255;
            if (arg < 16) cmdargs[arg][j] = '\0';
            arg++;
            j = 0;
        }
        else
        {
            if (arg < 16 && j < 255) cmdargs[arg][j] = c;
            j++;
        }
    }
    if (j > 255) j = 255;
    if (arg < 16) cmdargs[arg][j] = '\0';
    if (len > 0) arg++;

    // FIXME!!
    strncpy(cmdargs[0], "melonDS.exe", 256);

    char* cmdargptr[16];
    for (int i = 0; i < 16; i++)
        cmdargptr[i] = &cmdargs[i][0];

    return main(arg, cmdargptr);
}

#endif