#include <stdlib.h>
#include <string.h>

#include "fs_private.h"

#include "../core_command/cmd_private.h"
#include "../core_runtime/core_runtime_private.h"

enum {
    FS_STOCK_PAK_VARIANT_COUNT = 24,
    FS_MISSING_PAK_LOCAL_NAME_SIZE = 256,
    FS_PAK_INFO_STRING_SIZE = 8192,
    FS_RESTRICTED_DEMO_PAK0_CHECKSUM = (int32_t)0xb1f595f5u
};

enum fs_pak_reference_e {
    FS_PAK_REFERENCE_GENERAL = 0,
    FS_PAK_REFERENCE_UI = 1,
    FS_PAK_REFERENCE_CGAME = 2
};

#define FS_LOCALIZED_PAK_PREFIX "localized_"

static char fs_purePakChecksums[FS_PAK_INFO_STRING_SIZE];
static char fs_referencedPakPureChecksums[FS_PAK_INFO_STRING_SIZE];

qboolean FS_idPak(const char *path, const char *mainGame,
                  const char *baseGame)
{
    const char *localized;
    char localPath[CODUO_MAX_QPATH];

    if (FS_FilenameCompare(path, va("%s/mp_bin", mainGame)) == 0) {
        return qtrue;
    }

    for (int32_t pakIndex = 0; pakIndex < FS_STOCK_PAK_VARIANT_COUNT;
         ++pakIndex) {
        if (FS_FilenameCompare(path, va("%s/pak%x", mainGame, pakIndex)) == 0 ||
            FS_FilenameCompare(path, va("%s/pakuo%x", baseGame, pakIndex)) == 0 ||
            FS_FilenameCompare(path, va("%s/pakuo0%x", baseGame, pakIndex)) == 0 ||
            FS_FilenameCompare(path, va("%s/mp_pak%x", mainGame, pakIndex)) == 0 ||
            FS_FilenameCompare(path, va("%s/sp_pak%x", mainGame, pakIndex)) == 0) {
            return qtrue;
        }
    }

    localized = strstr(path, FS_LOCALIZED_PAK_PREFIX);
    if (localized == NULL) {
        return qfalse;
    }

    /* ORIGINAL_BINARY_BUG: both stock copies into this 64-byte path array
     * are unbounded; see
     * original-bugs/coduomp-server-pak-name-stack-overflow.md. */
    strcpy(localPath, path);
    localPath[(size_t)(localized - path) +
              (sizeof(FS_LOCALIZED_PAK_PREFIX) - 1U)] = '\0';
    if (FS_FilenameCompare(localPath, va("%s/%s", mainGame,
                                         FS_LOCALIZED_PAK_PREFIX)) != 0 &&
        FS_FilenameCompare(localPath, va("%s/%s", baseGame,
                                         FS_LOCALIZED_PAK_PREFIX)) != 0) {
        return qfalse;
    }

    strcpy(localPath, localized + sizeof(FS_LOCALIZED_PAK_PREFIX) - 1U);
    Q_strlwr(localPath);

    for (int32_t pakIndex = 0; pakIndex < FS_STOCK_PAK_VARIANT_COUNT;
         ++pakIndex) {
        if (strstr(localPath, va("_pak%x", pakIndex)) != NULL ||
            strstr(localPath, va("_pakuo%x", pakIndex)) != NULL ||
            strstr(localPath, va("_pakuo0%x", pakIndex)) != NULL) {
            return qtrue;
        }
    }

    return qfalse;
}

qboolean FS_serverPak(const char *pakName)
{
    char localName[CODUO_MAX_QPATH];

    /* ORIGINAL_BINARY_BUG: stock copies an unchecked pak name into this
     * 64-byte path array; see
     * original-bugs/coduomp-server-pak-name-stack-overflow.md. */
    strcpy(localName, pakName);
    Q_strlwr(localName);

    return strstr(localName, "_svr_") != NULL ? qtrue : qfalse;
}

qboolean FS_ComparePaks(char *missingPaks, int32_t missingPaksSize,
                        qboolean includeAlternateNames)
{
    char localName[FS_MISSING_PAK_LOCAL_NAME_SIZE];

    if (fs_numServerReferencedPaks == 0) {
        return qfalse;
    }

    missingPaks[0] = '\0';
    for (int32_t pakIndex = 0;
         pakIndex < fs_numServerReferencedPaks;
         ++pakIndex) {
        const char *pakName = fs_serverReferencedPakNames[pakIndex];
        qboolean found = qfalse;

        if (FS_idPak(pakName, "main", fs_basegame->string) != qfalse ||
            FS_serverPak(pakName) != qfalse) {
            continue;
        }

        for (searchpath_t *search = FS_SearchpathHead();
             search != NULL; search = FS_SearchpathNext(search)) {
            if (search->pack != NULL &&
                search->pack->checksum ==
                    fs_serverReferencedPaks[pakIndex]) {
                found = qtrue;
                break;
            }
        }

        if (found != qfalse || pakName == NULL || pakName[0] == '\0') {
            continue;
        }

        if (includeAlternateNames == qfalse) {
            Q_strcat(missingPaks, missingPaksSize, pakName);
            Q_strcat(missingPaks, missingPaksSize, ".pk3");
            if (FS_SV_FileExists(va("%s.pk3", pakName)) != qfalse) {
                Q_strcat(missingPaks, missingPaksSize,
                             " (local file exists with wrong checksum)");
            }
            Q_strcat(missingPaks, missingPaksSize, "\n");
            continue;
        }

        Q_strcat(missingPaks, missingPaksSize, "@");
        Q_strcat(missingPaks, missingPaksSize, pakName);
        Q_strcat(missingPaks, missingPaksSize, ".pk3");
        Q_strcat(missingPaks, missingPaksSize, "@");
        if (FS_SV_FileExists(va("%s.pk3", pakName)) == qfalse) {
            Q_strcat(missingPaks, missingPaksSize, pakName);
            Q_strcat(missingPaks, missingPaksSize, ".pk3");
        } else {
            Com_sprintf(localName, sizeof(localName), "%s.%08x.pk3",
                         pakName, fs_serverReferencedPaks[pakIndex]);
            Q_strcat(missingPaks, missingPaksSize, localName);
        }
    }

    if (missingPaks[0] == '\0') {
        return qfalse;
    }

    Com_Printf("Need paks: %s\n", missingPaks);
    return qtrue;
}

void FS_RemoveCommands(void)
{
    Cmd_RemoveCommand("path");
    Cmd_RemoveCommand("dir");
    Cmd_RemoveCommand("fdir");
    Cmd_RemoveCommand("touchFile");
}

void FS_CheckRestrictedDemoPaks(void)
{
    if (fs_restrict->integer == 0) {
        return;
    }

    Cvar_Set("fs_restrict", "0");
    Com_Printf("\nRunning in restricted demo mode.\n\n");

    for (searchpath_t *search = FS_SearchpathHead(); search != NULL;
         search = FS_SearchpathNext(search)) {
        if (FS_UseSearchPath(search) != qfalse && search->pack != NULL &&
            search->pack->checksum != FS_RESTRICTED_DEMO_PAK0_CHECKSUM) {
            Com_Error(0, "Corrupted pak0.pk3: %u",
                      (uint32_t)search->pack->checksum);
        }
    }
}

const char *FS_LoadedPakPureChecksums(void)
{
    fs_purePakChecksums[0] = '\0';

    for (searchpath_t *search = FS_SearchpathHead(); search != NULL;
         search = FS_SearchpathNext(search)) {
        if (search->pack != NULL && search->localized == qfalse) {
            Q_strcat(fs_purePakChecksums, sizeof(fs_purePakChecksums),
                         va("%i ", search->pack->pureChecksum));
        }
    }

    return fs_purePakChecksums;
}

const char *FS_ReferencedPakPureChecksums(void)
{
    uint32_t checksum = (uint32_t)fs_checksumFeed;
    uint32_t checksumCount = 0;

    fs_referencedPakPureChecksums[0] = '\0';

    for (int32_t reference = FS_PAK_REFERENCE_CGAME;
         reference >= FS_PAK_REFERENCE_GENERAL; --reference) {
        if (reference == FS_PAK_REFERENCE_GENERAL) {
            size_t length;

            length = strlen(fs_referencedPakPureChecksums);
            fs_referencedPakPureChecksums[length + 1U] = '\0';
            length = strlen(fs_referencedPakPureChecksums);
            fs_referencedPakPureChecksums[length + 2U] = '\0';
            length = strlen(fs_referencedPakPureChecksums);
            fs_referencedPakPureChecksums[length] = '@';
            length = strlen(fs_referencedPakPureChecksums);
            fs_referencedPakPureChecksums[length] = ' ';
        }

        for (searchpath_t *search = FS_SearchpathHead();
             search != NULL; search = FS_SearchpathNext(search)) {
            pack_t *pack = search->pack;
            qboolean noted;

            if (pack == NULL || search->localized != qfalse) {
                continue;
            }

            if (reference == FS_PAK_REFERENCE_CGAME) {
                noted = pack->cgameModuleReference != 0;
            } else if (reference == FS_PAK_REFERENCE_UI) {
                noted = pack->uiModuleReference != 0;
            } else {
                noted = pack->generalReference != 0;
            }

            if (noted == qfalse) {
                continue;
            }

            Q_strcat(fs_referencedPakPureChecksums,
                     sizeof(fs_referencedPakPureChecksums),
                     va("%i ", pack->pureChecksum));

            if (reference == FS_PAK_REFERENCE_CGAME ||
                reference == FS_PAK_REFERENCE_UI) {
                break;
            }

            checksum ^= (uint32_t)pack->pureChecksum;
            checksumCount++;
        }

        if (fs_fakeChkSum != 0) {
            Q_strcat(fs_referencedPakPureChecksums,
                     sizeof(fs_referencedPakPureChecksums),
                     va("%i ", fs_fakeChkSum));
        }
    }

    Q_strcat(fs_referencedPakPureChecksums,
             sizeof(fs_referencedPakPureChecksums),
             va("%i ", checksum ^ checksumCount));
    return fs_referencedPakPureChecksums;
}

void FS_PureServerSetLoadedPaks(const char *checksumText,
                                const char *nameText)
{
    char *names[FS_MAX_SERVER_PAKS];
    int32_t checksums[FS_MAX_SERVER_PAKS];
    int32_t checksumCount;
    int32_t nameCount;

    Cmd_TokenizeString(checksumText);
    checksumCount = Cmd_Argc();
    if (checksumCount > FS_MAX_SERVER_PAKS) {
        checksumCount = FS_MAX_SERVER_PAKS;
    }

    for (int32_t index = 0; index < checksumCount; ++index) {
        checksums[index] = atoi(Cmd_Argv((uint32_t)index));
    }

    Cmd_TokenizeString(nameText);
    nameCount = Cmd_Argc();
    if (nameCount > FS_MAX_SERVER_PAKS) {
        nameCount = FS_MAX_SERVER_PAKS;
    }

    for (int32_t index = 0; index < nameCount; ++index) {
        names[index] = CopyString(Cmd_Argv((uint32_t)index));
    }

    if (checksumCount != nameCount) {
        Com_Error(1, "pak sum/name mismatch");
    }

    /* ORIGINAL_BINARY_BUG: installed matches are not consumed, so duplicate
     * proposed rows can all match one old row; see
     * original-bugs/coduomp-pure-pak-duplicate-set-match.md. */
    if (checksumCount == fs_numServerPaks) {
        for (int32_t index = 0; index < checksumCount; ++index) {
            qboolean matched = qfalse;

            for (int32_t oldIndex = 0; oldIndex < fs_numServerPaks;
                 ++oldIndex) {
                if (checksums[index] == fs_serverPaks[oldIndex] &&
                    Q_stricmp(names[index],
                              fs_serverPakNames[oldIndex]) == 0) {
                    matched = qtrue;
                    break;
                }
            }

            if (matched == qfalse) {
                goto replace_loaded_paks;
            }
        }

        for (int32_t index = 0; index < nameCount; ++index) {
            Z_Free(names[index]);
        }
        return;
    }

replace_loaded_paks:
    FS_ShutdownServerPakNames();
    fs_numServerPaks = checksumCount;
    if (checksumCount == 0) {
        return;
    }

    Com_DPrintf("Connected to a pure server.\n");
    Com_Memcpy(fs_serverPaks, checksums,
               (size_t)fs_numServerPaks * sizeof(checksums[0]));
    Com_Memcpy(fs_serverPakNames, names,
               (size_t)fs_numServerPaks * sizeof(names[0]));
    fs_fakeChkSum = 0;
}

void FS_PureServerSetReferencedPaks(const char *checksumText,
                                    const char *nameText)
{
    int32_t checksumCount;
    int32_t nameCount;

    Cmd_TokenizeString(checksumText);
    checksumCount = Cmd_Argc();
    if (checksumCount > FS_MAX_SERVER_PAKS) {
        checksumCount = FS_MAX_SERVER_PAKS;
    }

    FS_ShutdownServerReferencedPaks();
    for (int32_t index = 0; index < checksumCount; ++index) {
        fs_serverReferencedPaks[index] =
            atoi(Cmd_Argv((uint32_t)index));
    }

    if (nameText == NULL || nameText[0] == '\0') {
        if (checksumCount != 0) {
            Com_Error(1, "pak sum/name mismatch");
        }
        fs_numServerReferencedPaks = checksumCount;
        return;
    }

    Cmd_TokenizeString(nameText);
    nameCount = Cmd_Argc();
    if (nameCount > FS_MAX_SERVER_PAKS) {
        nameCount = FS_MAX_SERVER_PAKS;
    }

    if (checksumCount != nameCount) {
        Com_Error(1, "pak sum/name mismatch");
    }

    for (int32_t index = 0; index < nameCount; ++index) {
        fs_serverReferencedPakNames[index] =
            CopyString(Cmd_Argv((uint32_t)index));
    }

    fs_numServerReferencedPaks = checksumCount;
}
