/*
Looking Glass - KVM FrameRelay (KVMFR) Client
Copyright (C) 2017-2019 Geoffrey McRae <geoff@hostfission.com>
https://looking-glass.hostfission.com

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 2 of the License, or (at your option) 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, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include "tarkovread.h"

#include "interface/renderer.h"

#include "common/debug.h"
#include "common/option.h"
#include "common/sysinfo.h"
#include "common/time.h"
#include "utils.h"
#include "dynamic/fonts.h"

#include <SDL2/SDL_syswm.h>
#include <SDL2/SDL_egl.h>

#if defined(SDL_VIDEO_DRIVER_WAYLAND)
#include <wayland-egl.h>
#endif

#include "model.h"
#include "shader.h"
#include "desktop.h"
#include "cursor.h"
#include "fps.h"
#include "splash.h"
#include "alert.h"
#include "espobject.h"

#define SPLASH_FADE_TIME 1000000
#define ALERT_TIMEOUT 2000000

struct Options
{
    bool vsync;
};

struct Inst
{
    LG_RendererParams params;
    struct Options opt;

    EGLNativeWindowType nativeWind;
    EGLDisplay display;
    EGLConfig configs;
    EGLSurface surface;
    EGLContext context;

    EGL_Desktop *desktop;     // the desktop
    EGL_Cursor *cursor;       // the mouse cursor
    EGL_FPS *fps;             // the fps display
    EGL_Splash *splash;       // the splash screen
    EGL_Alert *alert;         // the alert display
    EGL_ESPObject *espobject; // the esp object display

    TarkovESPArray playerarray;
    TarkovESPArray itemarray;

    TarkovGame *Tarkov;

    LG_RendererFormat format;
    bool sourceChanged;
    uint64_t waitFadeTime;
    bool waitDone;

    bool showAlert;
    uint64_t alertTimeout;
    bool useCloseFlag;
    bool closeFlag;

    int width, height;
    LG_RendererRect destRect;

    float translateX, translateY;
    float scaleX, scaleY;
    float splashRatio;
    float screenScaleX, screenScaleY;
    bool useNearest;

    bool cursorVisible;
    int cursorX, cursorY;
    float mouseWidth, mouseHeight;
    float mouseScaleX, mouseScaleY;

    const LG_Font *font;
    LG_FontObj fontObj;
};

static struct Option egl_options[] =
    {
        {.module = "egl",
         .name = "vsync",
         .description = "Enable vsync",
         .type = OPTION_TYPE_BOOL,
         .value.x_bool = false},
        {.module = "egl",
         .name = "doubleBuffer",
         .description = "Enable double buffering",
         .type = OPTION_TYPE_BOOL,
         .value.x_bool = true},
        {.module = "egl",
         .name = "multisample",
         .description = "Enable Multisampling",
         .type = OPTION_TYPE_BOOL,
         .value.x_bool = true},
        {.module = "egl",
         .name = "nvGainMax",
         .description = "The maximum night vision gain",
         .type = OPTION_TYPE_INT,
         .value.x_int = 1},
        {.module = "egl",
         .name = "nvGain",
         .description = "The initial night vision gain at startup",
         .type = OPTION_TYPE_INT,
         .value.x_int = 0},
        {0}};

void update_mouse_shape(struct Inst *this);

const char *egltarkov_get_name()
{
    return "EGLTarkov";
}

void egltarkov_setup()
{
    option_register(egl_options);
}

bool egltarkov_create(void **opaque, const LG_RendererParams params)
{
    // create our local storage
    *opaque = malloc(sizeof(struct Inst));
    if (!*opaque)
    {
        DEBUG_INFO("Failed to allocate %lu bytes", sizeof(struct Inst));
        return false;
    }
    memset(*opaque, 0, sizeof(struct Inst));

    // safe off parameteres and init our default option values
    struct Inst *this = (struct Inst *)*opaque;
    memcpy(&this->params, &params, sizeof(LG_RendererParams));

    this->opt.vsync = option_get_bool("egl", "vsync");

    this->translateX = 0;
    this->translateY = 0;
    this->scaleX = 1.0f;
    this->scaleY = 1.0f;
    this->screenScaleX = 1.0f;
    this->screenScaleY = 1.0f;

    this->font = LG_Fonts[0];
    if (!this->font->create(&this->fontObj, NULL, 16))
    {
        DEBUG_ERROR("Failed to create a font instance");
        return false;
    }

    return true;
}

bool egltarkov_initialize(void *opaque, Uint32 *sdlFlags)
{
    struct Inst *this = (struct Inst *)opaque;

    const bool doubleBuffer = option_get_bool("egl", "doubleBuffer");
    DEBUG_INFO("Double buffering is %s", doubleBuffer ? "on" : "off");

    *sdlFlags = SDL_WINDOW_OPENGL;
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, doubleBuffer ? 1 : 0);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    if (option_get_bool("egl", "multisample"))
    {
        int maxSamples = sysinfo_gfx_max_multisample();
        if (maxSamples > 1)
        {
            if (maxSamples > 4)
                maxSamples = 4;

            DEBUG_INFO("Multsampling enabled, max samples: %d", maxSamples);
            SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
            SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, maxSamples);
        }
    }

    if (!GetTarkovGame(&this->Tarkov, "EscapeFromTarkov.exe", "UnityPlayer.dll"))
    {
        DEBUG_ERROR("Failed to find Tarkov");
        return false;
    }

    return true;
}

void egltarkov_deinitialize(void *opaque)
{
    struct Inst *this = (struct Inst *)opaque;

    if (this->font && this->fontObj)
        this->font->destroy(this->fontObj);

    egl_desktop_free(&this->desktop);
    egl_cursor_free(&this->cursor);
    egl_fps_free(&this->fps);
    egl_splash_free(&this->splash);
    egl_alert_free(&this->alert);
    egl_espobject_free(&this->espobject);
    freeArray(&this->playerarray);
    freeArray(&this->itemarray);

    DestroyTarkovGame(this->Tarkov);

    free(this);
}

void egltarkov_on_resize(void *opaque, const int width, const int height, const LG_RendererRect destRect)
{
    struct Inst *this = (struct Inst *)opaque;

    this->width = width;
    this->height = height;
    memcpy(&this->destRect, &destRect, sizeof(LG_RendererRect));

    glViewport(0, 0, width, height);

    if (destRect.valid)
    {
        this->translateX = 1.0f - (((destRect.w / 2) + destRect.x) * 2) / (float)width;
        this->translateY = 1.0f - (((destRect.h / 2) + destRect.y) * 2) / (float)height;
        this->scaleX = (float)destRect.w / (float)width;
        this->scaleY = (float)destRect.h / (float)height;
    }

    this->mouseScaleX = 2.0f / this->format.width;
    this->mouseScaleY = 2.0f / this->format.height;
    egl_cursor_set_size(this->cursor,
                        (this->mouseWidth * (1.0f / this->format.width)) * this->scaleX,
                        (this->mouseHeight * (1.0f / this->format.height)) * this->scaleY);

    this->splashRatio = (float)width / (float)height;
    this->screenScaleX = 1.0f / width;
    this->screenScaleY = 1.0f / height;

    egl_cursor_set_state(
        this->cursor,
        this->cursorVisible,
        (((float)this->cursorX * this->mouseScaleX) - 1.0f) * this->scaleX,
        (((float)this->cursorY * this->mouseScaleY) - 1.0f) * this->scaleY);
}

bool egltarkov_on_mouse_shape(void *opaque, const LG_RendererCursor cursor, const int width, const int height, const int pitch, const uint8_t *data)
{
    struct Inst *this = (struct Inst *)opaque;
    if (!egl_cursor_set_shape(this->cursor, cursor, width, height, pitch, data))
    {
        DEBUG_ERROR("Failed to update the cursor shape");
        return false;
    }

    this->mouseWidth = width;
    this->mouseHeight = height;
    egl_cursor_set_size(this->cursor,
                        (this->mouseWidth * (1.0f / this->format.width)) * this->scaleX,
                        (this->mouseHeight * (1.0f / this->format.height)) * this->scaleY);

    return true;
}

bool egltarkov_on_mouse_event(void *opaque, const bool visible, const int x, const int y)
{
    struct Inst *this = (struct Inst *)opaque;
    this->cursorVisible = visible;
    this->cursorX = x;
    this->cursorY = y;

    egl_cursor_set_state(
        this->cursor,
        this->cursorVisible,
        (((float)this->cursorX * this->mouseScaleX) - 1.0f) * this->scaleX,
        (((float)this->cursorY * this->mouseScaleY) - 1.0f) * this->scaleY);

    return true;
}

bool egltarkov_on_frame_event(void *opaque, const LG_RendererFormat format, const FrameBuffer *frame)
{
    struct Inst *this = (struct Inst *)opaque;
    this->sourceChanged = (this->sourceChanged ||
                           this->format.type != format.type ||
                           this->format.width != format.width ||
                           this->format.height != format.height ||
                           this->format.pitch != format.pitch);

    if (this->sourceChanged)
        memcpy(&this->format, &format, sizeof(LG_RendererFormat));

    this->useNearest = this->width < format.width || this->height < format.height;

    if (!egl_desktop_prepare_update(this->desktop, this->sourceChanged, format, frame))
    {
        DEBUG_INFO("Failed to prepare to update the desktop");
        return false;
    }

    return true;
}

void egltarkov_on_alert(void *opaque, const LG_MsgAlert alert, const char *message, bool **closeFlag)
{
    struct Inst *this = (struct Inst *)opaque;

    static const uint32_t colors[] =
        {
            0x0000CCCC, // LG_ALERT_INFO
            0x00CC00CC, // LG_ALERT_SUCCESS
            0xCC7F00CC, // LG_ALERT_WARNING
            0xFF0000CC  // LG_ALERT_ERROR
        };

    if (alert > LG_ALERT_ERROR || alert < 0)
    {
        DEBUG_ERROR("Invalid alert value");
        return;
    }

    egl_alert_set_color(this->alert, colors[alert]);
    egl_alert_set_text(this->alert, message);

    if (closeFlag)
    {
        this->useCloseFlag = true;
        *closeFlag = &this->closeFlag;
    }
    else
    {
        this->useCloseFlag = false;
        this->alertTimeout = microtime() + ALERT_TIMEOUT;
    }

    this->showAlert = true;
}

bool egltarkov_render_startup(void *opaque, SDL_Window *window)
{
    struct Inst *this = (struct Inst *)opaque;

    SDL_SysWMinfo wminfo;
    SDL_VERSION(&wminfo.version);
    if (!SDL_GetWindowWMInfo(window, &wminfo))
    {
        DEBUG_ERROR("SDL_GetWindowWMInfo failed");
        return false;
    }

    const char *client_exts = eglQueryString(NULL, EGL_EXTENSIONS);
    DEBUG_INFO("Supported extensions: %s", client_exts);

    bool useNative = false;
    if (strstr(client_exts, "EGL_KHR_platform_base") != NULL)
        useNative = true;

    DEBUG_INFO("use native: %s", useNative ? "true" : "false");

    switch (wminfo.subsystem)
    {
    case SDL_SYSWM_X11:
    {
        if (!useNative)
            this->display = eglGetPlatformDisplay(EGL_PLATFORM_X11_KHR, wminfo.info.x11.display, NULL);
        else
        {
            EGLNativeDisplayType native = (EGLNativeDisplayType)wminfo.info.x11.display;
            this->display = eglGetDisplay(native);
        }
        this->nativeWind = (EGLNativeWindowType)wminfo.info.x11.window;
        break;
    }

#if defined(SDL_VIDEO_DRIVER_WAYLAND)
    case SDL_SYSWM_WAYLAND:
    {
        int width, height;
        SDL_GetWindowSize(window, &width, &height);
        if (!useNative)
            this->display = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, wminfo.info.wl.display, NULL);
        else
        {
            EGLNativeDisplayType native = (EGLNativeDisplayType)wminfo.info.wl.display;
            this->display = eglGetDisplay(native);
        }
        this->nativeWind = (EGLNativeWindowType)wl_egl_window_create(wminfo.info.wl.surface, width, height);
        break;
    }
#endif

    default:
        DEBUG_ERROR("Unsupported subsystem");
        return false;
    }

    if (this->display == EGL_NO_DISPLAY)
    {
        DEBUG_ERROR("eglGetDisplay failed");
        return false;
    }

    if (!eglInitialize(this->display, NULL, NULL))
    {
        DEBUG_ERROR("Unable to initialize EGL");
        return false;
    }

    EGLint attr[] =
        {
            EGL_BUFFER_SIZE, 32,
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL_SAMPLE_BUFFERS, 1,
            EGL_SAMPLES, 4,
            EGL_NONE};

    EGLint num_config;
    if (!eglChooseConfig(this->display, attr, &this->configs, 1, &num_config))
    {
        DEBUG_ERROR("Failed to choose config (eglError: 0x%x)", eglGetError());
        return false;
    }

    this->surface = eglCreateWindowSurface(this->display, this->configs, this->nativeWind, NULL);
    if (this->surface == EGL_NO_SURFACE)
    {
        DEBUG_ERROR("Failed to create EGL surface (eglError: 0x%x)", eglGetError());
        return false;
    }

    EGLint ctxattr[] =
        {
            EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL_NONE};

    this->context = eglCreateContext(this->display, this->configs, EGL_NO_CONTEXT, ctxattr);
    if (this->context == EGL_NO_CONTEXT)
    {
        DEBUG_ERROR("Failed to create EGL context (eglError: 0x%x)", eglGetError());
        return false;
    }

    eglMakeCurrent(this->display, this->surface, this->surface, this->context);

    DEBUG_INFO("Vendor  : %s", glGetString(GL_VENDOR));
    DEBUG_INFO("Renderer: %s", glGetString(GL_RENDERER));
    DEBUG_INFO("Version : %s", glGetString(GL_VERSION));

    eglSwapInterval(this->display, this->opt.vsync ? 1 : 0);

    if (!egl_desktop_init(&this->desktop))
    {
        DEBUG_ERROR("Failed to initialize the desktop");
        return false;
    }

    if (!egl_cursor_init(&this->cursor))
    {
        DEBUG_ERROR("Failed to initialize the cursor");
        return false;
    }

    if (!egl_fps_init(&this->fps, this->font, this->fontObj))
    {
        DEBUG_ERROR("Failed to initialize the FPS display");
        return false;
    }

    if (!egl_splash_init(&this->splash))
    {
        DEBUG_ERROR("Failed to initialize the splash screen");
        return false;
    }

    if (!egl_alert_init(&this->alert, this->font, this->fontObj))
    {
        DEBUG_ERROR("Failed to initialize the alert display");
        return false;
    }

    if (!egl_espobject_init(&this->espobject, this->font, this->fontObj))
    {
        DEBUG_ERROR("Failed to initialize the esp object display");
        return false;
    }

    if (!initArray(&this->playerarray, 32))
    {
        DEBUG_ERROR("Failed to allocate esp player array");
        return false;
    }

    if (!initArray(&this->itemarray, 256))
    {
        DEBUG_ERROR("Failed to allocate esp item array");
        return false;
    }

    return true;
}

bool egltarkov_render(void *opaque, SDL_Window *window)
{
    struct Inst *this = (struct Inst *)opaque;

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    if (egl_desktop_render(this->desktop, this->translateX, this->translateY, this->scaleX, this->scaleY, this->useNearest))
    {
        if (!this->waitFadeTime)
            this->waitFadeTime = microtime() + SPLASH_FADE_TIME;
        egl_cursor_render(this->cursor);
    }

    if (!this->waitDone)
    {
        float a = 1.0f;
        if (!this->waitFadeTime)
            a = 1.0f;
        else
        {
            uint64_t t = microtime();
            if (t > this->waitFadeTime)
                this->waitDone = true;
            else
            {
                uint64_t delta = this->waitFadeTime - t;
                a = 1.0f / SPLASH_FADE_TIME * delta;
            }
        }

        if (!this->waitDone)
            egl_splash_render(this->splash, a, this->splashRatio);
    }

    if (this->showAlert)
    {
        bool close = false;
        if (this->useCloseFlag)
            close = this->closeFlag;
        else if (this->alertTimeout < microtime())
            close = true;

        if (close)
            this->showAlert = false;
        else
            egl_alert_render(this->alert, this->screenScaleX, this->screenScaleY);
    }

    bool TarkovResult = TickTarkov(this->Tarkov);
    if (!TarkovResult)
        return false;

    if (TarkovInGame(this->Tarkov))
    {
        /*
            0x0000CCCC, // LG_ALERT_INFO
            0x00CC00CC, // LG_ALERT_SUCCESS
            0xCC7F00CC, // LG_ALERT_WARNING
            0xFF0000CC  // LG_ALERT_ERROR
        */

        egl_espobject_set_color(this->espobject, 0x0000CCCC);

        GetTarkovPlayers(this->Tarkov, &this->playerarray, this->width, this->height);

        for (int i = 0; i < this->playerarray.used; i++)
        {
            TarkovESPObject Player = this->playerarray.array[i];

            egl_espobject_set_text(this->espobject, "Player");
            egl_espobject_set_screendata(this->espobject, Player.viewmatrix, Player.viewlocation, Player.targetlocation); // these are offsets not absolute
            egl_espobject_render(this->espobject, this->screenScaleX, this->screenScaleY, Player.screenw, Player.screenh);
        }

        GetTarkovLoot(this->Tarkov, &this->itemarray, this->width, this->height);

        for (int i = 0; i < this->itemarray.used; i++)
        {
            TarkovESPObject Item = this->itemarray.array[i];

            egl_espobject_set_text(this->espobject, Item.pName);
            egl_espobject_set_screendata(this->espobject, Item.viewmatrix, Item.viewlocation, Item.targetlocation); // these are offsets not absolute
            egl_espobject_render(this->espobject, this->screenScaleX, this->screenScaleY, Item.screenw, Item.screenh);
        }
    }

    egl_fps_render(this->fps, this->screenScaleX, this->screenScaleY);
    eglSwapBuffers(this->display, this->surface);

    // defer texture uploads until after the flip to avoid stalling
    egl_desktop_perform_update(this->desktop, this->sourceChanged);

    this->sourceChanged = false;
    return true;
}

void egltarkov_update_fps(void *opaque, const float avgUPS, const float avgFPS)
{
    struct Inst *this = (struct Inst *)opaque;
    if (!this->params.showFPS)
        return;

    egl_fps_update(this->fps, avgUPS, avgFPS);
}

struct LG_Renderer LGR_EGLTarkov =
    {
        .get_name = egltarkov_get_name,
        .setup = egltarkov_setup,
        .create = egltarkov_create,
        .initialize = egltarkov_initialize,
        .deinitialize = egltarkov_deinitialize,
        .on_resize = egltarkov_on_resize,
        .on_mouse_shape = egltarkov_on_mouse_shape,
        .on_mouse_event = egltarkov_on_mouse_event,
        .on_frame_event = egltarkov_on_frame_event,
        .on_alert = egltarkov_on_alert,
        .render_startup = egltarkov_render_startup,
        .render = egltarkov_render,
        .update_fps = egltarkov_update_fps
    };
