#include <Core/HAL/DynLib/DynamicLibrary.h>
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/HAL/Win32.h>
#include <Core/HotReload/HotReload.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Core/Memory/Allocator.h>
#include <Core/Memory/MemoryArena.h>

namespace Juliet
{
    namespace
    {
        FILETIME GetLastWriteTime(const char* filename)
        {
            FILETIME lastWriteTime = {};

            WIN32_FILE_ATTRIBUTE_DATA Data;
            if (GetFileAttributesExA(filename, GetFileExInfoStandard, &Data))
            {
                lastWriteTime = Data.ftLastWriteTime;
            }

            return lastWriteTime;
        }

        constexpr size_t kMaxAttempts            = 256;
        constexpr size_t kMaxDLLID               = 256;
        constexpr size_t kTempDLLBufferSizeForID = 5; // Max ID numbers
    } // namespace

    void LoadCode(HotReloadCode& code)
    {
        // TODO : Create and use a TransientAllocator
        // Create temp dll name

        char*                     lockFilename = code.LockFullPath.Data;
        WIN32_FILE_ATTRIBUTE_DATA Ignored;
        if (!GetFileAttributesExA(lockFilename, GetFileExInfoStandard, &Ignored))
        {
            const char* dllName = code.DLLFullPath.Data;

            FILETIME       lastWriteTime = GetLastWriteTime(dllName);
            ULARGE_INTEGER result{ .LowPart = lastWriteTime.dwLowDateTime, .HighPart = lastWriteTime.dwHighDateTime };
            code.LastWriteTime = result.QuadPart;

            // Create filename for the temp dll until we find a valid id.
            // This is not infinite, in a big session we could reach the 128 attempts and fail...
            // We'll see for better later.

            // Get the app base path and build the dll path from there.
            String basePath       = GetBasePath();
            size_t basePathLength = StringLength(basePath);

            const size_t tempDllMaxBufferSize =
                basePathLength + StringLength(code.TransientDLLName) + /* _ */ 1 + kTempDLLBufferSizeForID + 1 /* \0 */;

            // Allocate from Scratch Arena (transient)
            TempArena temp        = ArenaTempBegin(code.Arena);
            auto      tempDllPath = ArenaPushArray<char>(temp.Arena, tempDllMaxBufferSize);

            for (uint32 attempt = 0; attempt < kMaxAttempts; ++attempt)
            {
                // int to char
                char idToStr[kTempDLLBufferSizeForID + 1];
                int  idLength = snprintf(idToStr, sizeof(idToStr), "%u", code.UniqueID);
                if (idLength < 0)
                {
                    // Scratch memory, no free needed
                    Log(LogLevel::Error, LogCategory::Core, "Cannot create temp full path");
                    return;
                }

                int writtenSize = snprintf(tempDllPath, tempDllMaxBufferSize, "%s%s_%s", CStr(basePath), idToStr,
                                           CStr(code.TransientDLLName));
                if (writtenSize < 0)
                {
                    // Scratch memory, no free needed
                    Log(LogLevel::Error, LogCategory::Core, "Cannot create temp full path");
                    return;
                }

                if (static_cast<size_t>(writtenSize) + 1 <
                    basePathLength + static_cast<size_t>(idLength) + code.TransientDLLName.Size)
                {
                    // Scratch memory, no free needed
                    Log(LogLevel::Error, LogCategory::Core, "Cannot create temp full path");
                    return;
                }

                if (++code.UniqueID >= kMaxDLLID)
                {
                    code.UniqueID = 0;
                }
                if (CopyFileA(dllName, tempDllPath, false))
                {
                    break;
                }
            }
            ArenaTempEnd(temp);

            code.Dll = LoadDynamicLibrary(tempDllPath);
            if (code.Dll)
            {
                code.IsValid = true;
                for (size_t FunctionIndex = 0; FunctionIndex < code.FunctionCount; ++FunctionIndex)
                {
                    if (auto function = reinterpret_cast<void*>(LoadFunction(code.Dll, code.FunctionNames[FunctionIndex])))
                    {
                        code.Functions[FunctionIndex] = function;
                    }
                    else
                    {
                        code.IsValid = false;
                    }
                }
            }

            // Scratch memory, no free needed
        }

        if (!code.IsValid)
        {
            UnloadCode(code);
        }
    }

    void UnloadCode(HotReloadCode& code)
    {
        code.IsValid = false;
        if (code.Dll)
        {
            UnloadDynamicLibrary(code.Dll);
        }
        code.Dll = nullptr;

        code.LastWriteTime = 0;
        ZeroDynArray(code.FunctionCount, code.Functions);
    }

    bool ShouldReloadCode(const HotReloadCode& code)
    {
        ULARGE_INTEGER largeInt          = {};
        FILETIME       codeLastWriteTime = {};
        largeInt.QuadPart                = code.LastWriteTime;
        codeLastWriteTime.dwHighDateTime = largeInt.HighPart;
        codeLastWriteTime.dwLowDateTime  = largeInt.LowPart;

        FILETIME lastWriteTime = GetLastWriteTime(code.DLLFullPath.Data);
        int      compare       = CompareFileTime(&lastWriteTime, &codeLastWriteTime);
        return compare != 0;
    }
} // namespace Juliet
