#include "bx/filepath.h"
#include <cstdlib>
#include <cwchar>
#include <fstream>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#undef min
#undef max

#include <bx/allocator.h>
#include <bx/mpscqueue.h>
#include <bx/string.h>
#include <cstdio>

#include "Shared.h"
#include "Window.h"

#ifdef _MSC_VER
constexpr const char* DLLPath = "PuzGame.dll";
constexpr const wchar_t* DLLWatch = L"PuzGame2.dll";
#else
constexpr const char* DLLPath = "libPuzGame.dll";
constexpr const wchar_t* DLLWatch = L"libPuzGame2.dll";
#endif

namespace
{
    bx::AllocatorI* defaultAllocator = new bx::DefaultAllocator{};
}

enum class FileChangeType
{
    DLL,
    Shader,
    CompiledShader,
};

struct FileWatcherStartup
{
    char DirPath[512]{0};
    wchar_t CompName[512]{0};
    FileChangeType ChangeType = FileChangeType::Shader;
    bool Shutdown = false;
};

struct FileWatcherData
{
    FileWatcherStartup DLLWatcher;
    FileWatcherStartup ShaderWatcher;
    FileWatcherStartup CompiledShaderWatcher;
    bx::MpScUnboundedQueueT<FileChangeNotification> ShaderQueue{defaultAllocator};
    bx::MpScUnboundedQueueT<FileChangeNotification> DLLQueue{defaultAllocator};
};

struct DevelopmentData
{
    HMODULE GameLib = NULL;
    FileWatcherData FileWatcher;
};

struct EngineData
{
    EngineWindow Window;
};

namespace
{
    DevelopmentData DevData;
    EngineData Engine;
    SharedData Shared;
    Startup StartupFunc;
    Update UpdateFunc;
    Shutdown ShutdownFunc;
} // namespace

void FileChangeCheck(HANDLE dirHandle, FileChangeType changeType, const wchar_t* compName = nullptr)
{
    if (dirHandle == NULL) return;

    uint8_t fileChangeBuffer[1024]{0};
    bx::memSet(fileChangeBuffer, 0, sizeof(fileChangeBuffer));
    DWORD bytesReturned = 0;
    if (ReadDirectoryChangesW(dirHandle,
                              fileChangeBuffer,
                              sizeof(fileChangeBuffer),
                              true,
                              FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE,
                              &bytesReturned,
                              NULL,
                              NULL))
    {
        uint32_t offset = 0;
        while (true)
        {
            FILE_NOTIFY_INFORMATION* notifyData = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&fileChangeBuffer[offset]);
            if (notifyData->Action == FILE_ACTION_ADDED || notifyData->Action == FILE_ACTION_MODIFIED ||
                notifyData->Action == FILE_ACTION_RENAMED_NEW_NAME)
            {
                if (compName == nullptr || compName[0] == 0 || wcscmp(notifyData->FileName, compName) == 0)
                {
                    char buf[1024]{0};
                    wcstombs(buf, notifyData->FileName, sizeof(buf));
                    printf("file change of type %u: %s\n", changeType, buf);

                    FileChangeNotification notif;
                    wcscpy(notif.FileName, notifyData->FileName);
                    if (changeType == FileChangeType::DLL)
                    {
                        DevData.FileWatcher.DLLQueue.push(&notif);
                    }
                    else if (changeType == FileChangeType::Shader)
                    {
                        printf("shader source change!\n");
                        std::system("powershell.exe .\\shadercompile.ps1 > shadercompile.log");
                        Sleep(1000);
                        std::ifstream shaderLogFile("shadercompile.log");
                        if (shaderLogFile.is_open())
                        {
                            shaderLogFile.seekg(0, std::ios::end);
                            uint32_t size = bx::min(BX_COUNTOF(Shared.Dev.ShaderLog) - 1, shaderLogFile.tellg());
                            shaderLogFile.seekg(0);
                            shaderLogFile.read(Shared.Dev.ShaderLog, size);
                            shaderLogFile.close();
                            Shared.Dev.ShaderLog[size] = 0;
                        }
                    }
                    else if (changeType == FileChangeType::CompiledShader)
                    {
                        printf("compiled shader change!\n");
                        DevData.FileWatcher.ShaderQueue.push(&notif);
                    }
                }
            }

            if (notifyData->NextEntryOffset == 0) break;
            offset += notifyData->NextEntryOffset;
        }
    }
}

HANDLE LoadDirHandle(const char* name)
{
    HANDLE h = CreateFileA(name,
                           FILE_LIST_DIRECTORY,
                           FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                           NULL,
                           OPEN_EXISTING,
                           FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                           NULL);

    if (h == NULL)
    {
        printf("Failed to load dir handle for %s", name);
    }
    return h;
}

unsigned long FileWatcherThread(void* data)
{
    FileWatcherStartup* startupData = reinterpret_cast<FileWatcherStartup*>(data);
    printf("Starting file watcher of type %u\n", startupData->ChangeType);
    if (startupData->DirPath[0] == 0)
    {
        printf("Invalid file watcher path!\n");
        return 1;
    }
    HANDLE dirHandle = LoadDirHandle(startupData->DirPath);
    if (dirHandle == NULL)
    {
        printf("Failed to load dir %s for file watcher!\n", startupData->DirPath);
        return 1;
    }

    while (!startupData->Shutdown)
    {
        FileChangeCheck(dirHandle, startupData->ChangeType, startupData->CompName);
    }
    printf("File watcher thread ended!\n");
    return 0;
}

bool ReloadDLL()
{
    if (DevData.GameLib != NULL)
    {
        FreeLibrary(DevData.GameLib);
    }

    char exePath[1024];
    GetModuleFileName(nullptr, exePath, BX_COUNTOF(exePath));
    bx::FilePath exeFilePath{exePath};
    bx::FilePath exeDir{exeFilePath.getPath()};
    bx::FilePath libPath = exeDir;
#ifdef _MSC_VER
    libPath.join("PuzGame.dll");
#else
    libPath.join("libPuzGame.dll");
#endif
    bx::FilePath lib2Path = exeDir;
#ifdef _MSC_VER
    lib2Path.join("PuzGame2.dll");
#else
    lib2Path.join("libPuzGame2.dll");
#endif
    if (!CopyFile(lib2Path.getCPtr(), libPath.getCPtr(), false))
    {
        DWORD err = GetLastError();
        printf("[%lu] Failed to copy game DLL from %s to %s!\n", err, libPath.getCPtr(), lib2Path.getCPtr());
        return false;
    }

    HMODULE gameLibReloaded = LoadLibraryEx(DLLPath, NULL, 0);
    if (gameLibReloaded == NULL)
    {
        DWORD err = GetLastError();
        printf("[%lu] Failed to load game DLL from %s!\n", err, DLLPath);
        return false;
    }
    DevData.GameLib = gameLibReloaded;

#ifdef _MSC_VER
    Startup StartupReloaded = (Startup)GetProcAddress(DevData.GameLib, "?Setup@Game@@YAXAEAUSharedData@@@Z");
#else
    Startup StartupReloaded = (Startup)GetProcAddress(DevData.GameLib, "_ZN4Game5SetupER10SharedData");
#endif
    if (StartupReloaded == NULL)
    {
        DWORD err = GetLastError();
        printf("[%lu] Failed to load startup function from game DLL!\n", err);
        return false;
    }

#ifdef _MSC_VER
    Update UpdateReloaded = (Update)GetProcAddress(DevData.GameLib, "?Update@Game@@YAXXZ");
#else
    Update UpdateReloaded = (Update)GetProcAddress(DevData.GameLib, "_ZN4Game6UpdateEv");
#endif
    if (UpdateReloaded == NULL)
    {
        printf("Failed to load update function from game DLL!\n");
        return false;
    }

#ifdef _MSC_VER
    Shutdown ShutdownReloaded = (Shutdown)GetProcAddress(DevData.GameLib, "?Shutdown@Game@@YAXXZ");
#else
    Shutdown ShutdownReloaded = (Shutdown)GetProcAddress(DevData.GameLib, "_ZN4Game8ShutdownEv");
#endif
    if (ShutdownReloaded == NULL)
    {
        printf("Failed to load shutdown function from game DLL\n");
        return false;
    }

    StartupFunc = StartupReloaded;
    UpdateFunc = UpdateReloaded;
    ShutdownFunc = ShutdownReloaded;

    printf("Loaded Game DLL successfully!\n");
    return true;
}

int main()
{
    char PathBuf[512]{0};
    GetCurrentDirectory(sizeof(PathBuf), PathBuf);
    printf("Current path: %s\n", PathBuf);

    if (!ReloadDLL()) return 1;

    Engine.Window.Startup(Shared.Window);
    if (Shared.Window.Handle == nullptr)
    {
        printf("Failed to set up window!\n");
        return 1;
    }

    DevData.FileWatcher.DLLWatcher.ChangeType = FileChangeType::DLL;
    DevData.FileWatcher.ShaderWatcher.ChangeType = FileChangeType::Shader;
    DevData.FileWatcher.CompiledShaderWatcher.ChangeType = FileChangeType::CompiledShader;

    char exePath[1024];
    GetModuleFileName(nullptr, exePath, BX_COUNTOF(exePath));
    bx::FilePath exeFilePath{exePath};
    bx::FilePath exeDir{exeFilePath.getPath()};
    bx::strCopy(
        DevData.FileWatcher.DLLWatcher.DirPath, sizeof(DevData.FileWatcher.DLLWatcher.DirPath), exeDir.getCPtr());
    bx::strCopy(
        DevData.FileWatcher.ShaderWatcher.DirPath, sizeof(DevData.FileWatcher.ShaderWatcher.DirPath), "game/shaders");
    bx::strCopy(DevData.FileWatcher.CompiledShaderWatcher.DirPath,
                sizeof(DevData.FileWatcher.CompiledShaderWatcher.DirPath),
                "game/compiled-shaders/dx11");

    wcscpy(DevData.FileWatcher.DLLWatcher.CompName, DLLWatch);

    DWORD fileWatcherThreadId = 0;
    HANDLE dllThread =
        CreateThread(NULL, 0, FileWatcherThread, &DevData.FileWatcher.DLLWatcher, 0, &fileWatcherThreadId);
    HANDLE shaderThread =
        CreateThread(NULL, 0, FileWatcherThread, &DevData.FileWatcher.ShaderWatcher, 0, &fileWatcherThreadId);
    HANDLE compiledShaderThread =
        CreateThread(NULL, 0, FileWatcherThread, &DevData.FileWatcher.CompiledShaderWatcher, 0, &fileWatcherThreadId);

    Shared.Game.PermanentStorageSize = 1024 * 1024;
    Shared.Game.PermanentStorage = VirtualAllocEx(
        GetCurrentProcess(), NULL, Shared.Game.PermanentStorageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    Shared.Game.EntityStorageSize = 1024 * 1024;
    Shared.Game.EntityStorage = VirtualAllocEx(
        GetCurrentProcess(), NULL, Shared.Game.EntityStorageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    Shared.Game.TransientStorageSize = 1024 * 1024 * 1024;
    Shared.Game.TransientStorage = VirtualAllocEx(
        GetCurrentProcess(), NULL, Shared.Game.TransientStorageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    StartupFunc(Shared);

    bool isRunning = true;
    while (isRunning)
    {
        Engine.Window.Update(Shared.Window);
        if (Engine.Window.CloseRequested)
        {
            isRunning = false;
        }
        FileChangeNotification* DLLChange = nullptr;
        if (DevData.FileWatcher.DLLQueue.pop())
        {
            // Empty queue to avoid multiple reloads
            while (DevData.FileWatcher.DLLQueue.pop())
            {
            }
            ShutdownFunc();
            ReloadDLL();
            StartupFunc(Shared);
        }

        FileChangeNotification* CompiledShaderChange = nullptr;
        while ((CompiledShaderChange = DevData.FileWatcher.ShaderQueue.pop()))
        {
            if (Shared.Dev.ChangedShaderCount < BX_COUNTOF(Shared.Dev.ChangedShaders))
            {
                wcscpy(Shared.Dev.ChangedShaders[Shared.Dev.ChangedShaderCount].FileName,
                       CompiledShaderChange->FileName);
                ++Shared.Dev.ChangedShaderCount;
            }
            else
            {
                printf("Ran out of shader change buffer!\n");
            }
        }

        UpdateFunc();
        ++Shared.Window.FrameCounter;
    }

    ShutdownFunc();
    Engine.Window.Shutdown();

    DevData.FileWatcher.DLLWatcher.Shutdown = true;
    DevData.FileWatcher.ShaderWatcher.Shutdown = true;
    DevData.FileWatcher.CompiledShaderWatcher.Shutdown = true;
    WaitForSingleObject(dllThread, 100);
    WaitForSingleObject(shaderThread, 100);
    WaitForSingleObject(compiledShaderThread, 100);

    return 0;
}
