#include "Database.h"

#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/Memory/MemoryArena.h>
#include <Windows.h>

namespace Romeo
{
    void DB_Init(Database& db, Juliet::Arena* arena)
    {
        printf("Entering DB_Init...\n");
        db.DbArena = arena;
        db.Entries.Create(arena JULIET_DEBUG_ONLY(, "RomeoDB"));
        printf("Exiting DB_Init...\n");
    }

    struct ScanContext
    {
        Database* Db;
        Juliet::Arena* TempArena;
    };

    static void ProcessFile(ScanContext& ctx, const Juliet::String& path, const WIN32_FIND_DATAW& findData)
    {
        // Simple filter: only .h and .cpp
        const char* pPath = CStr(path);
        size_t len = StringLength(path);

        if (len < 3)
        {
            return;
        }

        bool isHeader = false;
        bool isCpp = false;

        if (pPath[len - 2] == '.' && pPath[len - 1] == 'h')
        {
            isHeader = true;
        }
        else if (len >= 4 && pPath[len - 4] == '.' && pPath[len - 3] == 'c' && pPath[len - 2] == 'p' && pPath[len - 1] == 'p')
        {
            isCpp = true;
        }

        if (!isHeader && !isCpp)
        {
            return;
        }

        uint64_t lastModified = (static_cast<uint64_t>(findData.ftLastWriteTime.dwHighDateTime) << 32) | findData.ftLastWriteTime.dwLowDateTime;

        // Try to find if we already have it in the DB (ignoring \include\ or \src\ offset)
        const char* relStart = pPath;
        if (const char* p1 = strstr(pPath, "\\include\\"))
        {
            relStart = p1 + 9;
        }
        else if (const char* p2 = strstr(pPath, "\\src\\"))
        {
            relStart = p2 + 5;
        }
        
        size_t baseLen = len - static_cast<size_t>(relStart - pPath);
        baseLen -= (isHeader ? 2 : 4);

        FileEntry* entry = nullptr;
        for (FileEntry& e : ctx.Db->Entries)
        {
            Juliet::String dbPath = e.HeaderPath.Size > 0 ? e.HeaderPath : e.CppPath;
            if (dbPath.Size > 0) 
            {
                const char* pDbPath = CStr(dbPath);
                const char* dbRelStart = pDbPath;
                if (const char* p1 = strstr(pDbPath, "\\include\\"))
                {
                    dbRelStart = p1 + 9;
                }
                else if (const char* p2 = strstr(pDbPath, "\\src\\"))
                {
                    dbRelStart = p2 + 5;
                }

                bool dbIsHeader = (pDbPath[dbPath.Size - 1] == 'h');
                size_t dbBaseLen = dbPath.Size - static_cast<size_t>(dbRelStart - pDbPath);
                dbBaseLen -= (dbIsHeader ? 2 : 4);

                if (baseLen == dbBaseLen && strncmp(relStart, dbRelStart, baseLen) == 0)
                {
                    entry = &e;
                    break;
                }
            }
        }

        if (!entry)
        {
            FileEntry newEntry = {};
            ctx.Db->Entries.PushBack(newEntry);
            entry = ctx.Db->Entries.Back();
        }

        if (isHeader)
        {
            entry->HeaderPath = Juliet::StringCopy(ctx.Db->DbArena, path);
            entry->LastModifiedHeader = lastModified;
        }
        else
        {
            entry->CppPath = Juliet::StringCopy(ctx.Db->DbArena, path);
            entry->LastModifiedCpp = lastModified;
        }
    }

    static void ScanDirectoryRecursive(ScanContext& ctx, const Juliet::String& currentDir)
    {
        size_t searchPathLen = currentDir.Size + 3; // "\*" + null
        char* searchPathBuf = ArenaPushArray<char>(ctx.TempArena, searchPathLen JULIET_DEBUG_PARAM("RomeoDbSearchPathBuf"));
        snprintf(searchPathBuf, searchPathLen, "%s\\*", CStr(currentDir));
        Juliet::String searchPath = {searchPathBuf, searchPathLen - 1};
        
        // Convert to wide string for Win32
        int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, nullptr, 0);
        if (wLen <= 0)
        {
            return;
        }
        
        wchar_t* wSearchPath = ArenaPushArray<wchar_t>(ctx.TempArena, static_cast<size_t>(wLen) JULIET_DEBUG_PARAM("RomeoDbWSearch"));
        MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, wSearchPath, wLen);

        WIN32_FIND_DATAW findData;
        HANDLE hFind = FindFirstFileExW(wSearchPath, FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);

        if (hFind == INVALID_HANDLE_VALUE)
        {
            return;
        }

        do
        {
            if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
            {
                continue;
            }

            // Convert filename back to UTF8
            int uLen = WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, nullptr, 0, nullptr, nullptr);
            char* uFileName = ArenaPushArray<char>(ctx.TempArena, static_cast<size_t>(uLen) JULIET_DEBUG_PARAM("RomeoDbUFileName"));
            WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, uFileName, uLen, nullptr, nullptr);

            size_t fullPathLen = currentDir.Size + 1 + static_cast<size_t>(uLen); // "\" + null
            char* fullPathBuf = ArenaPushArray<char>(ctx.TempArena, fullPathLen JULIET_DEBUG_PARAM("RomeoDbFullPathBuf"));
            snprintf(fullPathBuf, fullPathLen, "%s\\%s", CStr(currentDir), uFileName);
            Juliet::String fullPath = {fullPathBuf, fullPathLen - 1};

            if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
                // Skip external/build dirs
                if (strcmp(uFileName, "External") != 0 && strcmp(uFileName, "Intermediate") != 0 && strcmp(uFileName, "bin") != 0 && strcmp(uFileName, ".git") != 0)
                {
                    ScanDirectoryRecursive(ctx, fullPath);
                }
            }
            else
            {
                ProcessFile(ctx, fullPath, findData);
            }
        } while (FindNextFileW(hFind, &findData) != 0);

        FindClose(hFind);
    }

    void DB_ScanWorkspace(Database& db, Juliet::Arena* tempArena, const Juliet::String& rootDir)
    {
        Assert(tempArena != nullptr);
        ScanContext ctx;
        ctx.Db = &db;
        ctx.TempArena = tempArena;

        // Check if src and include are directly under rootDir or under rootDir\Juliet
        char srcDirBuf[512];
        snprintf(srcDirBuf, sizeof(srcDirBuf), "%s\\src", CStr(rootDir));
        DWORD srcAttribs = GetFileAttributesA(srcDirBuf);
        if (srcAttribs == INVALID_FILE_ATTRIBUTES || !(srcAttribs & FILE_ATTRIBUTE_DIRECTORY))
        {
            snprintf(srcDirBuf, sizeof(srcDirBuf), "%s\\Juliet\\src", CStr(rootDir));
        }
        Juliet::String srcDir = Juliet::StringCopy(tempArena, Juliet::WrapString(srcDirBuf));

        char incDirBuf[512];
        snprintf(incDirBuf, sizeof(incDirBuf), "%s\\include", CStr(rootDir));
        DWORD incAttribs = GetFileAttributesA(incDirBuf);
        if (incAttribs == INVALID_FILE_ATTRIBUTES || !(incAttribs & FILE_ATTRIBUTE_DIRECTORY))
        {
            snprintf(incDirBuf, sizeof(incDirBuf), "%s\\Juliet\\include", CStr(rootDir));
        }
        Juliet::String incDir = Juliet::StringCopy(tempArena, Juliet::WrapString(incDirBuf));
        
        ScanDirectoryRecursive(ctx, srcDir);
        ScanDirectoryRecursive(ctx, incDir);
    }

    bool DB_Save(const Database& db, const Juliet::String& filePath)
    {
        FILE* file = nullptr;
        if (fopen_s(&file, CStr(filePath), "wb") != 0 || !file)
        {
            return false;
        }

        uint64_t entryCount = db.Entries.Size();
        fwrite(&entryCount, sizeof(uint64_t), 1, file);

        for (const FileEntry& entry : db.Entries)
        {
            uint64_t hLen = entry.HeaderPath.Size;
            fwrite(&hLen, sizeof(uint64_t), 1, file);
            if (hLen > 0) fwrite(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);

            uint64_t cLen = entry.CppPath.Size;
            fwrite(&cLen, sizeof(uint64_t), 1, file);
            if (cLen > 0) fwrite(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);

            fwrite(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
            fwrite(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
            fwrite(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
            fwrite(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);

            uint64_t dLen = entry.Description.Size;
            fwrite(&dLen, sizeof(uint64_t), 1, file);
            if (dLen > 0) fwrite(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
        }

        fclose(file);
        return true;
    }

    bool DB_Load(Database& db, const Juliet::String& filePath)
    {
        FILE* file = nullptr;
        if (fopen_s(&file, CStr(filePath), "rb") != 0 || !file)
        {
            return false;
        }

        db.Entries.Clear();

        uint64_t entryCount = 0;
        if (fread(&entryCount, sizeof(uint64_t), 1, file) != 1)
        {
            fclose(file);
            return false;
        }

        for (uint64_t i = 0; i < entryCount; ++i)
        {
            FileEntry entry = {};

            uint64_t hLen = 0;
            if (fread(&hLen, sizeof(uint64_t), 1, file) != 1) break;
            if (hLen > 0)
            {
                entry.HeaderPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(hLen + 1) JULIET_DEBUG_PARAM("DbLoadHeader"));
                entry.HeaderPath.Size = static_cast<size_t>(hLen);
                fread(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);
                entry.HeaderPath.Data[hLen] = '\0';
            }

            uint64_t cLen = 0;
            if (fread(&cLen, sizeof(uint64_t), 1, file) != 1) break;
            if (cLen > 0)
            {
                entry.CppPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(cLen + 1) JULIET_DEBUG_PARAM("DbLoadCpp"));
                entry.CppPath.Size = static_cast<size_t>(cLen);
                fread(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);
                entry.CppPath.Data[cLen] = '\0';
            }

            fread(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
            fread(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
            fread(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
            fread(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);

            uint64_t dLen = 0;
            if (fread(&dLen, sizeof(uint64_t), 1, file) != 1) break;
            if (dLen > 0)
            {
                entry.Description.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(dLen + 1) JULIET_DEBUG_PARAM("DbLoadDesc"));
                entry.Description.Size = static_cast<size_t>(dLen);
                fread(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
                entry.Description.Data[dLen] = '\0';
            }

            db.Entries.PushBack(entry);
        }

        fclose(file);
        return true;
    }

} // namespace Romeo
