#include "runtime/file_tab.h"
#include "runtime/mem_arena.h"
#include "runtime/timing.h"

#ifdef _WIN32
#define GLFW_EXPOSE_NATIVE_WIN32
#elif defined(RT_USE_XLIB)
#define GLFW_EXPOSE_NATIVE_X11
#elif defined(RT_USE_WAYLAND)
#define GLFW_EXPOSE_NATIVE_WAYLAND
#endif
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

#include <runtime/runtime.h>
#include <runtime/config.h>
#include <runtime/dynamic_libs.h>
#include <renderer/renderer.h>
#include <runtime/fsutils.h>

#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
 
#include "game_api.h"

/* This is a launcher cvar, because launcher must configure this _before_ the renderer is initialized. */
RT_CVAR_S(l_Renderer, "The used renderer. Available options: vk, dx11. (Default: vk)", "vk");

RT_CVAR_S(l_WindowTitle, "The title used for the game window. (Default: rtengine)", "rtengine");
RT_CVAR_I(l_WindowWidth, "The window width. (Default: 1024)", 1024);
RT_CVAR_I(l_WindowHeight, "The window height. (Default: 768)", 768);
RT_CVAR_I(l_WindowMode, "The window mode. Available options: 0 (=Windowed), 1 (=Borderless Fullscreen), 2 (=Exclusive Fullscreen) (Default: 0)", 0);
RT_CVAR_I(l_FullscreenRefreshRate, "Requested refresh rate for exclusive fullscreen. Set to 0 to use the monitors current setting. (Default: 0)", 0);
RT_CVAR_S(l_Monitor, "Name of the monitor on which the window should be created. Leave empty to use the primary monitor. (Default: "")", "");

RT_CVAR_F(l_Framerate, "Target framerate in FPS. (Default: 60.0)", 60.0);

/* These are for experiments and debugging */
RT_CVAR_I(l_CreateGLContext, "Create an OpenGL context in the launcher. 1: on, 0: off. (Default: 0)", 0);
RT_CVAR_I(l_GLContextMajor, "OpenGL Major version. (Default: 4)", 4);
RT_CVAR_I(l_GLContextMinor, "OpenGL minor version. (Default: 5)", 5);

RT_CVAR_S(l_GameLib, "Path to the game library. Only usable in internal builds. (Default: (null))", "(null)");

enum {
  WINDOW_MODE_WINDOWED,
  WINDOW_MODE_BORDERLESS_FULLSCREEN,
  WINDOW_MODE_FULLSCREEN,
};

/* This is baked in during compilation to make tampering with it harder.
 * In debug (internal) builds, this can be overwritten via cvar or command line argument. */
#ifndef RT_GAME_LIB_PATH
#define RT_GAME_LIB_PATH "(null)"
#endif

static rt_dynlib *_game_lib = NULL;
static rt_game_api _game;
static rt_window _window;
static void *_game_obj = NULL;
static rt_launcher_api _launcher_api;

#ifdef _WIN32
static HINSTANCE _hInstance;
#endif

static void SetupConfig(void) {
    rtRegisterCVAR(&l_Renderer);
    rtRegisterCVAR(&l_WindowTitle);
    rtRegisterCVAR(&l_WindowWidth);
    rtRegisterCVAR(&l_WindowHeight);
    rtRegisterCVAR(&l_WindowMode);
    rtRegisterCVAR(&l_Monitor);

    rtRegisterCVAR(&l_Framerate);

    rtRegisterCVAR(&l_CreateGLContext);
    rtRegisterCVAR(&l_GLContextMajor);
    rtRegisterCVAR(&l_GLContextMinor);

    rtRegisterCVAR(&l_GameLib);

    rt_file_id config_fid = rtAddFile("cfg/launcher.cfg");
    if (rtProcessConfigFiles(1, &config_fid) != RT_SUCCESS) {
        rtLog("LAUNCHER", "Processing launcher configs failed.");
    }
}

static void LoadGameAndRendererConfig() {
    rt_file_id renderer_cfg_fid = rtAddFile("cfg/renderer.cfg");
    rt_file_id game_cfg_fid = rtAddFile("cfg/game.cfg");
    rt_file_id fids[2] = { renderer_cfg_fid, game_cfg_fid };

    rtProcessConfigFiles(RT_ARRAY_COUNT(fids), fids);
}

static void ParseCommandLineCVARs(int argc, char **argv) {
    for (int i = 1; i < argc - 1; ++i) {
        const char *name = argv[i];
        if (name[0] != '-' || name[1] != '-')
            continue;
        name = &name[2];
        rt_cvar *cvar = rtGetCVAR(name);
        if (!cvar) {
            ++i; /* Skip value */
            continue;
        }
        const char *value = argv[i + 1];
        
        if (rtSetCVARFromString(cvar, value) != RT_SUCCESS) {
            rtLog("LAUNCHER", "Failed to set %s to %s. Invalid value?", cvar->name, value);
        }

        /* Skip value */
        ++i;
    }
}

static char *ParseCommandLineGameLib(int argc, char **argv) {
    char *game_lib_cmdline = NULL;
#ifdef RT_DEBUG
    for (int i = 1; i < argc - 1; ++i) {
        if (strcmp(argv[i], "--game") == 0) {
            game_lib_cmdline = argv[i + 1];
            break;
        }
    }
#endif
    return game_lib_cmdline;
}

static void __NullGame_RegisterCVARs(rt_launcher_api *api) {}
static rt_result __NullGame_Init(rt_launcher_api *api) { return RT_SUCCESS; }
static void __NullGame_Shutdown(rt_launcher_api *api, void *game_obj) {}
static void __NullGame_Update(rt_launcher_api *api, rt_time_delta delta, void *game_obj) { RT_UNUSED(delta); }
static void __NullGame_Render(rt_launcher_api *api, void *game_obj) {}

static rt_game_api _null_api = {
    .RegisterCVARs    = __NullGame_RegisterCVARs,
    .Init             = __NullGame_Init,
    .Shutdown         = __NullGame_Shutdown,
    .Update           = __NullGame_Update,
    .Render           = __NullGame_Render,
    .OnGameObjectFree = NULL,
};

static rt_game_api LoadGame(const char *cmdline_gamelib) {
    const char *game_lib = RT_GAME_LIB_PATH;
#ifdef RT_DEBUG
    if (strlen(l_GameLib.s) > 0) {
        game_lib = l_GameLib.s;
    }
    if (cmdline_gamelib && strlen(cmdline_gamelib) > 0) {
        game_lib = cmdline_gamelib;
    }
#endif

    if (strcmp(game_lib, "(null)") != 0) {
        _game_lib = rtOpenLib(game_lib);
        if (!_game_lib) {
            rtReportError("LAUNCHER", "Failed to open game library: %s", game_lib);
            goto out;
        }
        rt_load_game_api_fn *LoadGameAPI = rtGetSymbol(_game_lib, "rtLoadGameAPI");
        if (!LoadGameAPI) {
            rtReportError("LAUNCHER", "%s is not a valid game library (rtLoadGameAPI symbol is missing).", game_lib);
            rtCloseLib(_game_lib);
            _game_lib = NULL;
            goto out;
        }
        return LoadGameAPI();
    }

out:
    /* Fall back to null implementation. */
    return _null_api;
}

static rt_window LauncherAPIGetWindow(void) {
    return _window;      
}

static void *LauncherAPIAllocGameObj(size_t sz) {
    if (_game_obj) {
        /* Free the old one */
        if (_game.OnGameObjectFree)
            _game.OnGameObjectFree(&_launcher_api, _game_obj);
        free(_game_obj);
    }
    _game_obj = malloc(sz);
    return _game_obj;
}

static void *LauncherAPIGetGameObject(void) {
    return _game_obj;
}

static void DisplayMonitors(void) {
    int count = 0;
    GLFWmonitor **monitors = glfwGetMonitors(&count);
    rtLog("LAUNCHER", "Available monitors:");
    for (int i = 0; i < count; ++i) {
        const char *name =glfwGetMonitorName(monitors[i]);
        if (monitors[i] != glfwGetPrimaryMonitor())
            rtLog("LAUNCHER", "  - %s", name);
        else
            rtLog("LAUNCHER", "  - %s (Primary)", name);
    }
}

static GLFWmonitor *ChooseMonitor(void) {
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    int count = 0;

    if (strcmp(l_Monitor.s, "") == 0) {
        return monitor;
    }
    GLFWmonitor **monitors = glfwGetMonitors(&count);
    for (int i = 0; i < count; ++i) {
        const char *name = glfwGetMonitorName(monitors[i]);
        if (strcmp(name, l_Monitor.s) == 0)
            return monitors[i];
    }
    return monitor;
}

static void GlfwErrorCB(int err, const char *desc) {
    rtReportError("GLFW", "GLFW Error %d: %s", err, desc);
}

static int Entry(int argc, char **argv) {
    if (rtInitRuntime() != RT_SUCCESS)
        return -1;
    SetupConfig();

    glfwSetErrorCallback(GlfwErrorCB);
    if (!glfwInit()) {
        rtShutdownRuntime();
        return -1;
    }
    DisplayMonitors();

    rtRegisterRenderCVARs();

    /* Load the renderer library.
     * We need it before window creation, to give it an opportunity to register its cvars */
    if (rtLoadRenderBackend() != RT_SUCCESS) {
        rtShutdownRuntime();
        glfwTerminate();
        return -1;
    }
    rtRegisterRenderBackendCVARs();

    _launcher_api = (rt_launcher_api){
        .GetWindow = LauncherAPIGetWindow,
        .AllocGameObject = LauncherAPIAllocGameObj,
        .GetGameObject = LauncherAPIGetGameObject,
    };

    /* Load the game */
    const char *game_lib_cmdline = ParseCommandLineGameLib(argc, argv);
    _game = LoadGame(game_lib_cmdline);
    _game.RegisterCVARs(&_launcher_api);

    LoadGameAndRendererConfig();
    ParseCommandLineCVARs(argc, argv);

    /* Create the window */
    GLFWmonitor *monitor = ChooseMonitor();

    GLFWwindow *window = NULL;
    if (!l_CreateGLContext.i) {
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    } else {
        rtLog("LAUNCHER", "Creating an OpenGL %d.%d context", l_GLContextMajor.i, l_GLContextMinor.i);
        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, l_GLContextMajor.i);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, l_GLContextMinor.i);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
    glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    glfwWindowHint(GLFW_CENTER_CURSOR, GLFW_TRUE);
    
    if (l_WindowMode.i == WINDOW_MODE_BORDERLESS_FULLSCREEN) {
        const GLFWvidmode *mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
        glfwWindowHint(GLFW_RED_BITS, mode->redBits);
        glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
        glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
        glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
        window = glfwCreateWindow(mode->width, mode->height, l_WindowTitle.s, monitor, NULL);
    }
    else if (l_WindowMode.i == WINDOW_MODE_FULLSCREEN) {
        int refresh_rate = l_FullscreenRefreshRate.i;
        if (refresh_rate == 0) {
            refresh_rate = glfwGetVideoMode(glfwGetPrimaryMonitor())->refreshRate;
        }
        else {
            int count;
            glfwGetVideoModes(NULL, &count);
            rt_temp_arena temp = rtGetTemporaryArena(NULL, 0);
            GLFWvidmode *modes = RT_ARENA_PUSH_ARRAY(temp.arena, GLFWvidmode, count);
            int is_supported = 0;
            for (int i = 0; i < count; ++i) {
                if (modes[i].refreshRate == refresh_rate) {
                    is_supported = 1;
                    break;
                }
            }
            rtReturnTemporaryArena(temp);
            if (!is_supported) {
                rtLog("LAUNCHER", "Requested refresh rate %d Hz is not supported. Using current setting instead.", refresh_rate);
                refresh_rate = glfwGetVideoMode(glfwGetPrimaryMonitor())->refreshRate;
            }
        }
        glfwWindowHint(GLFW_REFRESH_RATE, refresh_rate);
        window = glfwCreateWindow(l_WindowWidth.i, l_WindowHeight.i, l_WindowTitle.s, monitor, NULL);
    }
    else {
        window = glfwCreateWindow(l_WindowWidth.i, l_WindowHeight.i, l_WindowTitle.s, NULL, NULL);
    }

    if (!window) {
        rtReportError("LAUNCHER", "Failed to create the game window.");
        if (_game_lib)
            rtCloseLib(_game_lib);
        glfwTerminate();
        rtShutdownRuntime();
        return -1;
    }
    _window.type = RT_WINDOW_TYPE_GLFW;
    _window.glfw = window;

    if (l_CreateGLContext.i) {
        glfwMakeContextCurrent(window);
    }

    /* Initialize the renderer */
    rt_renderer_window_info renderer_init_info;
#ifdef _WIN32
    renderer_init_info.hWnd = glfwGetWin32Window(window);
    renderer_init_info.hInstance = _hInstance;
#elif defined(__linux__) && defined(RT_USE_XLIB)
    renderer_init_info.display = glfwGetX11Display();
    renderer_init_info.window = glfwGetX11Window(window);
#endif
    renderer_init_info.is_fullscreen = l_WindowMode.i != WINDOW_MODE_WINDOWED;
    glfwGetFramebufferSize(window, (int*)&renderer_init_info.width, (int*)&renderer_init_info.height);
    if (rtInitRenderer(&renderer_init_info) != RT_SUCCESS) {
        rtReportError("LAUNCHER", "Failed to initialize the renderer.");
        if (_game_lib)
            rtCloseLib(_game_lib);
        glfwTerminate();
        rtShutdownRuntime();
        return -1;
    }

    if (_game.Init(&_launcher_api) != RT_SUCCESS) {
        rtReportError("LAUNCHER", "Failed to initialize the renderer.");
        if (_game_lib)
            rtCloseLib(_game_lib);
        glfwTerminate();
        rtShutdownRuntime();
        return -1;
    }

    rt_time_delta time_per_update = 1.0 / (double)l_Framerate.f;
    rt_timestamp previous = rtTimeNow();
    rt_time_delta lag = time_per_update;
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        rt_timestamp current = rtTimeNow();
        rt_time_delta elapsed = rtTimeBetween(previous, current);
        previous = current;
        lag += elapsed;

        /* TODO: Process input */

        while (lag >= time_per_update) {
            _game.Update(&_launcher_api, time_per_update, _game_obj);
            lag -= time_per_update;
        }

        int disp_w, disp_h;
        glfwGetFramebufferSize(window, &disp_w, &disp_h);
        _game.Render(&_launcher_api, _game_obj);
        
        if (l_CreateGLContext.i) {
            glfwSwapBuffers(window);
        }
    }

    _game.Shutdown(&_launcher_api, _game_obj);
    rtShutdownRenderer();

    glfwDestroyWindow(window);
    glfwTerminate();
    if (_game_lib)
        rtCloseLib(_game_lib);
    rtShutdownRuntime();
    
    return 0;
}

#ifdef _WIN32
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pCmdLine, int nCmdShow) {
    _hInstance = hInstance;

    /* Convert to UTF-8 argv array */
    LPWSTR pWCmdLine = GetCommandLineW();
    int argc = 0;
    LPWSTR *pWArgv = CommandLineToArgvW(pWCmdLine, &argc);
    /* Determine total amount of memory needed */
    size_t mem_required = sizeof(char *) * argc; /* array of pointers */
    for (int i = 0; i < argc; ++i) {
        mem_required += (size_t)WideCharToMultiByte(CP_UTF8, WC_COMPOSITECHECK, pWArgv[i], -1, NULL, 0, NULL, NULL);
    }
    void *argv_mem = VirtualAlloc(NULL, mem_required, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    char **argv = argv_mem;
    char *arg = (char *)(argv + argc);
    for (int i = 0; i < argc; ++i) {
        int len = WideCharToMultiByte(CP_UTF8, WC_COMPOSITECHECK, pWArgv[i], -1, NULL, 0, NULL, NULL);
        WideCharToMultiByte(CP_UTF8, WC_COMPOSITECHECK, pWArgv[i], -1, arg, len, NULL, NULL);
        argv[i] = arg;
        arg += len;
    }
    LocalFree(pWArgv);

    for (int i = 0; i < argc; ++i) {
        rtLog("LAUNCHER", "argv[%d]: %s", i, argv[i]);
    }
    
    int res = Entry(argc, argv);
    VirtualFree(argv_mem, 0, MEM_RELEASE);
    return res;
}
#else
int main(int argc, char **argv) {
    return Entry(argc, argv);
}
#endif
