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

#include "../core_memory/core_memory_private.h"
#include "core_runtime_private.h"

enum {
    HUNK_LOG_BUFFER_SIZE = 4096,
    HUNK_LOG_FILE_CLOSED_HANDLE = 0,
    HUNK_LOG_BLOCK_NOT_PRINTED = 0,
    HUNK_LOG_BLOCK_PRINTED = 1,
    HUNK_INIT_FILESYSTEM_STACK_EMPTY = 0,
    HUNK_INIT_ERROR_FATAL = 0,
    HUNK_MIN_MEGS_DEDICATED = 1,
    HUNK_MIN_MEGS_CLIENT = 80,
    HUNK_MEGABYTE_SHIFT = 20,
    HUNK_ALIGNMENT = 32,
    HUNK_ALIGNMENT_PAD = HUNK_ALIGNMENT - 1,
    HUNK_CVAR_FLAGS = CODUO_CVAR_ARCHIVE | CODUO_CVAR_LATCH
};

/*
 * Native64 hunk counters are host-width size_t values.  Keep the stock i386
 * %i text/argument shape while using a matching width for native64 diagnostics.
 */
#if UINTPTR_MAX > UINT32_MAX
#define HUNK_MEMINFO_SIZE_FORMAT "%8zu"
#define HUNK_MEMINFO_SIZE_ARG(value) (value)
#else
#define HUNK_MEMINFO_SIZE_FORMAT "%8i"
#define HUNK_MEMINFO_SIZE_ARG(value) ((int)(value))
#endif

#if UINTPTR_MAX > UINT32_MAX
#define HUNK_LOG_SIZE_FORMAT "%zu"
#define HUNK_LOG_SIZE_ARG(value) (value)
#else
#define HUNK_LOG_SIZE_FORMAT "%d"
#define HUNK_LOG_SIZE_ARG(value) ((int)(value))
#endif

void Cmd_AddCommand(const char *name, coduo_cmd_function_fn handler);
void FS_RefreshLookupCache(void);
void Sys_OutOfMemory(void);
void SV_ShutdownGameProgs(void);
void VM_Clear(void);
void Hunk_Clear(void);

/*
 * Mac MP symbols include Com_InitZoneMemory; the Linux dedicated build keeps
 * this early Com_Init zone-memory hook as a no-op.
 */
void Com_InitZoneMemory(void)
{
}

void Hunk_MemInfo_f(void)
{
    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " bytes total hunk\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.totalSize));
    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " bytes total zone\n",
               HUNK_MEMINFO_SIZE_ARG(hunk_totalZoneSize));
    Com_Printf("\n");

    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " low mark\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.lowMark));
    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " low permanent\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.lowUsed));
    if (hunk.lowTemp != hunk.lowUsed) {
        Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " low temp\n",
                   HUNK_MEMINFO_SIZE_ARG(hunk.lowTemp));
    }
    Com_Printf("\n");

    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " high mark\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.highMark));
    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " high permanent\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.highUsed));
    if (hunk.highTemp != hunk.highUsed) {
        Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " high temp\n",
                   HUNK_MEMINFO_SIZE_ARG(hunk.highTemp));
    }
    Com_Printf("\n");

    Com_Printf(HUNK_MEMINFO_SIZE_FORMAT " total hunk in use\n",
               HUNK_MEMINFO_SIZE_ARG(hunk.highUsed + hunk.lowUsed));
    Com_Printf("\n");
}

void Hunk_Log(void)
{
    coduo_hunk_log_block_t *block;
    int blockCount;
    size_t hunkMemory;
    char line[HUNK_LOG_BUFFER_SIZE];
    size_t length;

    if (hunk_logFile != HUNK_LOG_FILE_CLOSED_HANDLE &&
        FS_Initialized() != qfalse) {
        blockCount = 0;
        hunkMemory = 0;

        Com_sprintf(line, sizeof(line),
                    "\r\n================\r\nHunk log\r\n================\r\n");
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);

        for (block = hunk_logBlocks; block != NULL; block = block->next) {
            hunkMemory += block->size;
            blockCount++;
        }

        Com_sprintf(line, sizeof(line), HUNK_LOG_SIZE_FORMAT " Hunk memory\r\n",
                    HUNK_LOG_SIZE_ARG(hunkMemory));
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);

        Com_sprintf(line, sizeof(line), "%d hunk blocks\r\n", blockCount);
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);
    }
}

void Hunk_SmallLog(void)
{
    coduo_hunk_log_block_t *block;
    coduo_hunk_log_block_t *compareBlock;
    int blockCount;
    size_t hunkMemory;
    char line[HUNK_LOG_BUFFER_SIZE];
    size_t length;

    if (hunk_logFile != HUNK_LOG_FILE_CLOSED_HANDLE &&
        FS_Initialized() != qfalse) {
        for (block = hunk_logBlocks; block != NULL; block = block->next) {
            block->printed = HUNK_LOG_BLOCK_NOT_PRINTED;
        }

        blockCount = 0;
        hunkMemory = 0;

        Com_sprintf(line, sizeof(line),
                    "\r\n================\r\nHunk Small log\r\n================\r\n");
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);

        for (block = hunk_logBlocks; block != NULL; block = block->next) {
            if (block->printed == HUNK_LOG_BLOCK_NOT_PRINTED) {
                for (compareBlock = block->next;
                     compareBlock != NULL;
                     compareBlock = compareBlock->next) {
                    if (block->sourceLine == compareBlock->sourceLine &&
                        Q_stricmp(block->sourceFile,
                                  compareBlock->sourceFile) == 0) {
                        hunkMemory += compareBlock->size;
                        compareBlock->printed = HUNK_LOG_BLOCK_PRINTED;
                    }
                }
                hunkMemory += block->size;
                blockCount++;
            }
        }

        Com_sprintf(line, sizeof(line), HUNK_LOG_SIZE_FORMAT " Hunk memory\r\n",
                    HUNK_LOG_SIZE_ARG(hunkMemory));
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);

        Com_sprintf(line, sizeof(line), "%d hunk blocks\r\n", blockCount);
        length = strlen(line);
        FS_Write(line, (int32_t)length, hunk_logFile);
    }
}

void Hunk_InitMemory(void)
{
    cvar_t *com_hunkMegs;
    int minimumMegabytes;
    const char *minimumMessage;

    if (FS_LoadCount() != HUNK_INIT_FILESYSTEM_STACK_EMPTY) {
        /* rodata 0x95b00: original message has no trailing period. */
        Com_Error(HUNK_INIT_ERROR_FATAL,
                  "\x15"
                  "Hunk initialization failed. File system load stack not "
                  "zero");
    }

    com_hunkMegs = Cvar_Get("com_hunkMegs", "128", HUNK_CVAR_FLAGS);

    if (dedicated == NULL || dedicated->integer == 0) {
        minimumMegabytes = HUNK_MIN_MEGS_CLIENT;
        minimumMessage =
            "Minimum com_hunkMegs is %i, allocating %i megs.\n";
    } else {
        minimumMegabytes = HUNK_MIN_MEGS_DEDICATED;
        minimumMessage =
            "Minimum com_hunkMegs for a dedicated server is %i, allocating %i "
            "megs.\n";
    }

    if (com_hunkMegs->integer < minimumMegabytes) {
        hunk.totalSize = minimumMegabytes << HUNK_MEGABYTE_SHIFT;
        Com_Printf(minimumMessage, minimumMegabytes,
                   hunk.totalSize >> HUNK_MEGABYTE_SHIFT);
    } else {
        /* ORIGINAL_BINARY_BUG: stock performs this size conversion in one
         * wrapping dword, so sufficiently large com_hunkMegs values wrap to
         * a smaller allocation; see
         * original-bugs/engine-hunk-megs-dword-overflow.md. */
        hunk.totalSize =
            (size_t)((uint32_t)com_hunkMegs->integer << HUNK_MEGABYTE_SHIFT);
    }

    hunk_base = malloc((size_t)hunk.totalSize + HUNK_ALIGNMENT_PAD);
    if (hunk_base == NULL) {
        Sys_OutOfMemory();
    }

    hunk_allocBase = hunk_base;
    hunk_base = (uint8_t *)(((uintptr_t)hunk_base + HUNK_ALIGNMENT_PAD) &
                            ~(uintptr_t)HUNK_ALIGNMENT_PAD);

    Hunk_ClearData();
    Cmd_AddCommand("meminfo", Hunk_MemInfo_f);
}

void Hunk_ClearData(void)
{
    SV_ShutdownGameProgs();
    Hunk_Clear();
    VM_Clear();
}

void Hunk_Clear(void)
{
    hunk.lowMark = 0;
    hunk.lowTempMark = 0;
    hunk.lowUsed = 0;
    hunk.lowTemp = 0;
    hunk.highMark = 0;
    hunk.highTempMark = 0;
    hunk.highUsed = 0;
    hunk.highTemp = 0;
    com_hunkUsed = 0;

    Com_Printf("Hunk_Clear: reset the hunk ok\n");
    Hunk_RetouchMemory();
    FS_RefreshLookupCache();
}
