#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "fs_private.h"

enum {
    FS_SHIFTED_STRING_BUFFER_SIZE = 0x100,
    FS_MOD_DESCRIPTION_BUFFER_SIZE = 0x100,
    FS_MOD_DESCRIPTION_READ_LIMIT = 0x30
};

#define FS_MOD_DESCRIPTION_FILE "description.txt"
#define FS_PK3_EXTENSION ".pk3"
#define FS_MAIN_GAME_DIR "main"
#define FS_MAIN_GAME_DESCRIPTION "CoD:United Offensive Multiplayer"

qboolean FS_SV_FileExists(const char *path)
{
    char osPath[FS_OSPATH_SIZE];
    FILE *file;
    size_t osPathLength;

    FS_BuildOSPath(fs_homepath->string, path, "", osPath);
    osPathLength = strlen(osPath);
    osPath[osPathLength - 1U] = '\0';

    file = fopen(osPath, "rb");
    if (file == NULL) {
        return qfalse;
    }

    fclose(file);
    return qtrue;
}

int32_t FS_SV_FOpenFileWrite(const char *path)
{
    char osPath[FS_OSPATH_SIZE];
    int32_t handle;
    size_t osPathLength;

    FS_CheckFileSystemStarted();

    FS_BuildOSPath(fs_homepath->string, path, "", osPath);
    osPathLength = strlen(osPath);
    osPath[osPathLength - 1U] = '\0';

    handle = FS_HandleForFile(qfalse);
    fs_handleFiles[handle].zipArchive = NULL;

    if (fs_debug->integer != 0) {
        Com_Printf("FS_SV_FOpenFileWrite: %s\n", osPath);
    }

    if (FS_CreatePath(osPath) != 0) {
        return 0;
    }

    Com_DPrintf("writing to: %s\n", osPath);
    fs_handleFiles[handle].ioObject = fopen(osPath, "wb");
    Q_strncpyz(fs_handleFiles[handle].name, path, CODUO_FS_HANDLE_NAME_SIZE);
    fs_handleFiles[handle].sync = 0;

    if (fs_handleFiles[handle].ioObject == NULL) {
        return 0;
    }

    return handle;
}

int32_t FS_SV_FOpenFileRead(const char *path,
                                           int32_t *handleOut)
{
    char osPath[FS_OSPATH_SIZE];
    int32_t handle;
    size_t osPathLength;

    FS_CheckFileSystemStarted();

    handle = FS_HandleForFile(qfalse);
    fs_handleFiles[handle].zipArchive = NULL;
    Q_strncpyz(fs_handleFiles[handle].name, path, CODUO_FS_HANDLE_NAME_SIZE);

    FS_BuildOSPath(fs_homepath->string, path, "", osPath);
    osPathLength = strlen(osPath);
    osPath[osPathLength - 1U] = '\0';
    if (fs_debug->integer != 0) {
        Com_Printf("FS_SV_FOpenFileRead (fs_homepath): %s\n", osPath);
    }
    fs_handleFiles[handle].ioObject = fopen(osPath, "rb");
    fs_handleFiles[handle].sync = 0;

    if (fs_handleFiles[handle].ioObject == NULL &&
        Q_stricmp(fs_homepath->string, fs_basepath->string) != 0) {
        FS_BuildOSPath(fs_basepath->string, path, "", osPath);
        osPathLength = strlen(osPath);
        osPath[osPathLength - 1U] = '\0';
        if (fs_debug->integer != 0) {
            Com_Printf("FS_SV_FOpenFileRead (fs_basepath): %s\n", osPath);
        }
        fs_handleFiles[handle].ioObject = fopen(osPath, "rb");
        fs_handleFiles[handle].sync = 0;

        if (fs_handleFiles[handle].ioObject == NULL) {
            /* ORIGINAL_BINARY_BUG: clearing the selected handle here makes
             * the cdpath fallback below index reserved slot zero; a
             * successful open is then leaked while the function reports
             * failure. See original-bugs/
             * coduomp-fs-sv-read-cdpath-handle-zero-leak.md. */
            handle = 0;
        }
    }

    if (fs_handleFiles[handle].ioObject == NULL) {
        FS_BuildOSPath(fs_cdpath->string, path, "", osPath);
        osPathLength = strlen(osPath);
        osPath[osPathLength - 1U] = '\0';
        if (fs_debug->integer != 0) {
            Com_Printf("FS_SV_FOpenFileRead (fs_cdpath) : %s\n", osPath);
        }
        fs_handleFiles[handle].ioObject = fopen(osPath, "rb");
        fs_handleFiles[handle].sync = 0;

        if (fs_handleFiles[handle].ioObject == NULL) {
            handle = 0;
        }
    }

    *handleOut = handle;
    if (handle == 0) {
        return 0;
    }

    return FS_filelength(handle);
}

void FS_SV_Rename(const char *from, const char *to)
{
    char fromPath[FS_OSPATH_SIZE];
    char toPath[FS_OSPATH_SIZE];
    size_t osPathLength;

    FS_CheckFileSystemStarted();

    FS_BuildOSPath(fs_homepath->string, from, "", fromPath);
    FS_BuildOSPath(fs_homepath->string, to, "", toPath);

    osPathLength = strlen(fromPath);
    fromPath[osPathLength - 1U] = '\0';
    osPathLength = strlen(toPath);
    toPath[osPathLength - 1U] = '\0';

    if (fs_debug->integer != 0) {
        Com_Printf("FS_SV_Rename: %s --> %s\n", fromPath, toPath);
    }

    if (rename(fromPath, toPath) != 0) {
        FS_Copyfiles(fromPath, toPath);
        FS_Remove(fromPath);
    }
}

const char *FS_ShiftStr(const char *text, int key)
{
    static char shiftedText[FS_SHIFTED_STRING_BUFFER_SIZE];
    int32_t index;
    int32_t textLength;

    textLength = (int32_t)strlen(text);
    for (index = 0; index < textLength; ++index) {
        shiftedText[index] = (char)(text[index] + key);
    }
    shiftedText[index] = '\0';

    return shiftedText;
}

int32_t FS_Read2(void *buffer,
                                  int32_t length,
                                  int32_t handle)
{
    int32_t result;

    FS_CheckFileSystemStarted();

    if (handle == 0) {
        return 0;
    }

    if (fs_handleFiles[handle].seekCallbackGuard == 0) {
        return FS_Read(buffer, length, handle);
    }

    fs_handleFiles[handle].seekCallbackGuard = 0;
    result = Sys_StreamedRead(buffer, length, 1, handle);
    fs_handleFiles[handle].seekCallbackGuard = 1;

    return result;
}

int32_t FS_FileIsInPAK(const char *path, int32_t *checksumOut)
{
    int32_t hash;

    FS_CheckFileSystemStarted();

    if (path == NULL) {
        Com_Error(0, "\x15"
                     "FS_FOpenFileRead: NULL 'filename' parameter passed");
    }

    if (path[0] == '/' || path[0] == '\\') {
        path++;
    }

    if (strstr(path, "..") != NULL || strstr(path, "::") != NULL) {
        return -1;
    }

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

        if (pack == NULL) {
            continue;
        }

        hash = (int32_t)FS_HashFileName(path, pack->hashSize);
        if (pack->hashTable[hash] == NULL) {
            continue;
        }

        if (search->localized == 0 && FS_PakIsPure(pack) == qfalse) {
            continue;
        }

        for (fileInPack_t *packFile = pack->hashTable[hash];
             packFile != NULL; packFile = packFile->next) {
            if (FS_FilenameCompare(packFile->name, path) == 0) {
                if (checksumOut != NULL) {
                    *checksumOut = pack->pureChecksum;
                }
                return 1;
            }
        }
    }

    return -1;
}

int32_t FS_CountStringList(char **list)
{
    int32_t count;

    count = 0;
    if (list != NULL) {
        while (list[count] != NULL) {
            count++;
        }
    }

    return count;
}

char **FS_MergeStringLists(char **firstList, char **secondList, char **thirdList)
{
    int32_t firstCount;
    int32_t secondCount;
    int32_t thirdCount;
    int32_t mergedCount;
    int32_t slotCount;
    char **merged;
    char **writeSlot;

    firstCount = FS_CountStringList(firstList);
    secondCount = FS_CountStringList(secondList);
    thirdCount = FS_CountStringList(thirdList);
    mergedCount = firstCount + secondCount + thirdCount;
    slotCount = mergedCount + 1;

    merged = Z_Malloc((size_t)(uint32_t)slotCount *
                      sizeof(merged[0]));
    writeSlot = merged;

    if (firstList != NULL) {
        for (char **readSlot = firstList; *readSlot != NULL; ++readSlot) {
            *writeSlot++ = *readSlot;
        }
    }
    if (secondList != NULL) {
        for (char **readSlot = secondList; *readSlot != NULL; ++readSlot) {
            *writeSlot++ = *readSlot;
        }
    }
    if (thirdList != NULL) {
        for (char **readSlot = thirdList; *readSlot != NULL; ++readSlot) {
            *writeSlot++ = *readSlot;
        }
    }
    *writeSlot = NULL;

    if (firstList != NULL) {
        Z_Free(firstList);
    }
    if (secondList != NULL) {
        Z_Free(secondList);
    }
    if (thirdList != NULL) {
        Z_Free(thirdList);
    }

    return merged;
}

int FS_GetModList(char *listBuffer, int bufferSize)
{
    char **homeDirs;
    char **baseDirs;
    char **cdDirs;
    char **dirs;
    int32_t ignoredCount;
    int32_t dirCount;
    int32_t modCount;
    int32_t listBytes;

    *listBuffer = '\0';
    ignoredCount = 0;
    modCount = 0;
    listBytes = 0;

    homeDirs = Sys_ListFiles(fs_homepath->string, NULL, NULL, &ignoredCount,
                             qtrue);
    baseDirs = Sys_ListFiles(fs_basepath->string, NULL, NULL, &ignoredCount,
                             qtrue);
    cdDirs = NULL;
    if (fs_cdpath->string != NULL && fs_cdpath->string[0] != '\0') {
        cdDirs = Sys_ListFiles(fs_cdpath->string, NULL, NULL, &ignoredCount,
                               qtrue);
    }

    dirs = FS_MergeStringLists(homeDirs, baseDirs, cdDirs);
    dirCount = FS_CountStringList(dirs);

    for (int32_t dirIndex = 0; dirIndex < dirCount; ++dirIndex) {
        qboolean duplicate;
        int32_t pakCount;
        char osPath[FS_OSPATH_SIZE];
        char description[FS_MOD_DESCRIPTION_BUFFER_SIZE];
        char *dirName;

        dirName = dirs[dirIndex];
        duplicate = qfalse;
        for (int32_t previousIndex = 0; previousIndex < dirIndex;
             ++previousIndex) {
            if (Q_stricmp(dirs[previousIndex], dirName) == 0) {
                duplicate = qtrue;
                break;
            }
        }

        if (duplicate != qfalse || strncmp(dirName, ".", 1) == 0) {
            continue;
        }

        FS_BuildOSPath(fs_basepath->string, dirName, "", osPath);
        pakCount = 0;
        Sys_FreeFileList(Sys_ListFiles(osPath, FS_PK3_EXTENSION, NULL,
                                       &pakCount, qfalse));

        if (pakCount < 1) {
            FS_BuildOSPath(fs_cdpath->string, dirName, "", osPath);
            pakCount = 0;
            Sys_FreeFileList(Sys_ListFiles(osPath, FS_PK3_EXTENSION, NULL,
                                           &pakCount, qfalse));
        }

        if (pakCount < 1) {
            FS_BuildOSPath(fs_homepath->string, dirName, "", osPath);
            pakCount = 0;
            Sys_FreeFileList(Sys_ListFiles(osPath, FS_PK3_EXTENSION, NULL,
                                           &pakCount, qfalse));
        }

        if (pakCount > 0) {
            int32_t handle;
            int32_t fileLength;
            int32_t modNameLength;
            int32_t descriptionLength;

            modNameLength = (int32_t)strlen(dirName) + 1;
            description[0] = '\0';
            /* ORIGINAL_BINARY_BUG: stock appends the directory name and
             * fixed suffix without checking this 256-byte destination; see
             * original-bugs/
             * coduomp-modlist-description-path-stack-overflow.md. */
            strcpy(description, dirName);
            strcat(description, "/" FS_MOD_DESCRIPTION_FILE);

            handle = 0;
            fileLength = FS_SV_FOpenFileRead(description, &handle);
            if (fileLength < 1 || handle == 0) {
                if (Q_stricmp(dirName, FS_MAIN_GAME_DIR) == 0) {
                    strcpy(description, FS_MAIN_GAME_DESCRIPTION);
                } else {
                    strcpy(description, dirName);
                }
            } else {
                FILE *file;
                int32_t bytesRead;

                file = FS_LoadIOFile(FS_FileForHandle(handle));
                Com_Memset(description, 0, sizeof(description));
                bytesRead = (int32_t)fread(
                    description, 1, FS_MOD_DESCRIPTION_READ_LIMIT, file);
                if (bytesRead >= 0) {
                    description[bytesRead] = '\0';
                }
                FS_FCloseFile(handle);
            }

            descriptionLength = (int32_t)strlen(description) + 1;
            if (bufferSize <=
                modNameLength + listBytes + descriptionLength + 2) {
                break;
            }

            strcpy(listBuffer, dirName);
            listBuffer += modNameLength;
            strcpy(listBuffer, description);
            listBuffer += descriptionLength;
            listBytes += modNameLength + descriptionLength;
            modCount++;
        }
    }

    Sys_FreeFileList(dirs);
    return modCount;
}
