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

#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "coduo_ctype_compat.h"
#include "coduo_engine_structs.h"
#include "script_memory_private.h"
#include "script_runtime_private.h"

enum {
    SCRIPT_STRING_ENTRY_HEADER_SIZE = 4,
    SCRIPT_STRING_FORMAT_BUFFER_SIZE = 128,
    SCRIPT_STRING_HASH_SLOT_MASK = 0x3fff,
    SCRIPT_STRING_HASH_LONG_TEXT_SHIFT = 2,
    SCRIPT_STRING_HASH_SHORT_TEXT_LIMIT = 0x100,
    SCRIPT_STRING_HASH_MULTIPLIER = 0x1f,
    SCRIPT_STRING_HASH_OCCUPIED = 0x8000,
    SCRIPT_STRING_HASH_CHAINED = 0x4000,
    SCRIPT_STRING_HASH_FLAGS_MASK = 0xc000,
    SCRIPT_STRING_HASH_LINK_MASK = 0x3fff,
    SCRIPT_STRING_HASH_EMPTY = 0,
    SCRIPT_STRING_HASH_FIRST_SLOT = 1,
    SCRIPT_STRING_CANONICAL_FILENAME_BUFFER_SIZE = 1024,
    SCRIPT_STRING_CANONICAL_FILENAME_LIMIT = 0x400,
    SCRIPT_STRING_DEFAULT_TYPE = 6,
    SCRIPT_STRING_CANONICAL_FILENAME_TYPE = 7,
    SCRIPT_STRING_INTERN_RUNTIME_TYPE = 0x0e,
    SCRIPT_STRING_TEMP_HASH_BYTE_SIZE = 0x20000,
    SCRIPT_STRING_USAGE_CANONICAL = 2
};

typedef struct script_string_entry_s {
    uint16_t refCount;
    uint8_t sizeByte;
    uint8_t flags;
    char text[];
} script_string_entry_t;

void SL_FreeString(uint16_t handle,
                   const void *text, uint32_t size);

script_string_entry_t *
GetRefStringByHandle(uint16_t handle)
{
    return (script_string_entry_t *)&script_memoryAllocatorHeader
                .arenaBase[handle];
}

script_string_entry_t *GetRefStringByText(const char *text)
{
    return (script_string_entry_t *)(text - SCRIPT_STRING_ENTRY_HEADER_SIZE);
}

const char *
SL_ConvertToString(uint16_t handle)
{
    if (handle == 0) {
        return NULL;
    }

    return GetRefStringByHandle(handle)->text;
}

uint16_t
SL_ConvertFromString(const char *text)
{
    script_string_entry_t *entry = GetRefStringByText(text);
    ptrdiff_t offset =
        (uint8_t *)entry -
        (uint8_t *)script_memoryAllocatorHeader.arenaBase;

    if (offset < 0) {
        offset += SCRIPT_MEMORY_BLOCK_SIZE - 1;
    }

    return (uint16_t)
        ((offset / SCRIPT_MEMORY_BLOCK_SIZE) & SCRIPT_MEMORY_INDEX_MASK);
}

uint16_t
GetHashCode(const char *text, uint32_t size)
{
    uint32_t hash;

    if (size < SCRIPT_STRING_HASH_SHORT_TEXT_LIMIT) {
        hash = 0;
        for (uint32_t index = 0; index < size; ++index) {
            hash = hash * SCRIPT_STRING_HASH_MULTIPLIER +
                   (int8_t)(uint8_t)text[index];
        }
    } else {
        hash = (uint32_t)(size >> SCRIPT_STRING_HASH_LONG_TEXT_SHIFT);
    }

    return (uint16_t)
        (hash % SCRIPT_STRING_HASH_LINK_MASK + SCRIPT_STRING_HASH_FIRST_SLOT);
}

void SL_Init(void)
{
    MT_Init();

    script_stringHashSlots[0].linkAndFlags = 0;
    uint16_t previous = 0;
    for (uint16_t slot = SCRIPT_STRING_HASH_FIRST_SLOT;
         slot < SCRIPT_STRING_HASH_SLOT_COUNT; ++slot) {
        script_stringHashSlots[slot].linkAndFlags = SCRIPT_STRING_HASH_EMPTY;
        script_stringHashSlots[previous].linkAndFlags =
            (uint16_t)(script_stringHashSlots[previous].linkAndFlags | slot);
        script_stringHashSlots[slot].stringHandle = previous;
        previous = slot;
    }
    script_stringHashSlots[0].stringHandle = previous;
}

/* NOT_FROM_ORIGINAL_SOURCE: local predicate for the repeated string match. */
static qboolean coduomp_script_string_entry_matches(
    script_string_hash_slot_t *slot, const char *text, uint32_t size)
{
    script_string_entry_t *entry = GetRefStringByHandle(slot->stringHandle);

    /* ORIGINAL_BINARY_BUG: stock 0x080a465b/0x080a46e0 and
     * 0x080a48ea/0x080a499d gate on only the low size byte before passing
     * the complete dword size to memcmp. */
    return entry->sizeByte == (uint8_t)size &&
                   memcmp(entry->text, text, size) == 0
               ? qtrue
               : qfalse;
}

/* NOT_FROM_ORIGINAL_SOURCE: factors the original hash-chain head rotation. */
static void coduomp_script_string_move_slot_to_hash_head(
    uint16_t hash,
    uint16_t previousSlot,
    uint16_t matchSlot)
{
    script_string_hash_slot_t *head = &script_stringHashSlots[hash];
    script_string_hash_slot_t *previous =
        &script_stringHashSlots[previousSlot];
    script_string_hash_slot_t *match = &script_stringHashSlots[matchSlot];

    previous->linkAndFlags =
        (uint16_t)((match->linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK) |
                   (previous->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK));
    match->linkAndFlags =
        (uint16_t)((head->linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK) |
                   (match->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK));
    head->linkAndFlags =
        (uint16_t)(matchSlot |
                   (head->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK));

    uint16_t movedHandle = match->stringHandle;
    match->stringHandle = head->stringHandle;
    head->stringHandle = movedHandle;
}

uint16_t
SL_FindStringOfLen(const char *text, uint32_t size)
{
    uint16_t hash = GetHashCode(text, size);
    script_string_hash_slot_t *head = &script_stringHashSlots[hash];

    if ((head->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK) !=
        SCRIPT_STRING_HASH_OCCUPIED) {
        return 0;
    }

    if (coduomp_script_string_entry_matches(head, text, size) != qfalse) {
        return head->stringHandle;
    }

    uint16_t previousSlot = hash;
    uint16_t currentSlot = head->linkAndFlags;
    for (;;) {
        currentSlot &= SCRIPT_STRING_HASH_LINK_MASK;
        script_string_hash_slot_t *current =
            &script_stringHashSlots[currentSlot];
        if (current == head) {
            return 0;
        }

        if (coduomp_script_string_entry_matches(current, text, size) != qfalse) {
            coduomp_script_string_move_slot_to_hash_head(
                hash, previousSlot, currentSlot);
            return head->stringHandle;
        }

        previousSlot = currentSlot;
        currentSlot = current->linkAndFlags;
    }
}

uint16_t SL_FindString(const char *text)
{
    return SL_FindStringOfLen(text, (uint32_t)strlen(text) + 1U);
}

uint16_t
SL_FindLowercaseString(const char *text)
{
    uint32_t size = (uint32_t)strlen(text) + 1U;
    /* ORIGINAL_BINARY_BUG: stock 0x080a480e reserves the complete
     * input-derived byte count directly on the stack. */
    char lowercase[size];

    for (int32_t index = (int32_t)size - 1; index >= 0; --index) {
        lowercase[index] =
            (char)tolower(coduo_ctype_signed_byte_arg(text[index]));
    }

    return SL_FindStringOfLen(lowercase, size);
}

/* NOT_FROM_ORIGINAL_SOURCE: factors free-list exhaustion handling. */
static uint16_t coduomp_script_string_pop_free_hash_slot(void)
{
    uint16_t freeSlot =
        script_stringHashSlots[0].linkAndFlags;

    if (freeSlot == 0) {
        Scr_DumpScriptThreads();
        Scr_DumpScriptVariables();
        Com_Error(1, "\x15" "exceeded maximum number of script strings\n");
    }

    script_stringHashSlots[0].linkAndFlags =
        script_stringHashSlots[freeSlot].linkAndFlags &
        SCRIPT_STRING_HASH_LINK_MASK;
    script_stringHashSlots[script_stringHashSlots[0].linkAndFlags]
        .stringHandle = 0;

    return freeSlot;
}

/* NOT_FROM_ORIGINAL_SOURCE: factors inserting a chain node after a hash head. */
static void coduomp_script_string_insert_into_occupied_head(
    uint16_t hash,
    uint16_t freeSlot)
{
    script_string_hash_slot_t *head = &script_stringHashSlots[hash];
    script_string_hash_slot_t *slot = &script_stringHashSlots[freeSlot];

    slot->linkAndFlags =
        (uint16_t)((head->linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK) |
                   SCRIPT_STRING_HASH_CHAINED);
    head->linkAndFlags =
        (uint16_t)((head->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK) |
                   freeSlot);
    slot->stringHandle = head->stringHandle;
}

/* NOT_FROM_ORIGINAL_SOURCE: factors unlinking a hash slot from the free list. */
static void coduomp_script_string_unlink_free_slot(uint16_t slot)
{
    uint16_t next =
        script_stringHashSlots[slot].linkAndFlags &
        SCRIPT_STRING_HASH_LINK_MASK;
    uint16_t previous =
        script_stringHashSlots[slot].stringHandle;

    script_stringHashSlots[previous].linkAndFlags =
        (uint16_t)(next |
                   (script_stringHashSlots[previous].linkAndFlags &
                    SCRIPT_STRING_HASH_FLAGS_MASK));
    script_stringHashSlots[next].stringHandle = previous;
}

/* NOT_FROM_ORIGINAL_SOURCE: factors adding a new head for a chained hash slot. */
static void coduomp_script_string_insert_into_linked_hash(
    uint16_t hash,
    uint16_t freeSlot)
{
    uint16_t previousSlot =
        script_stringHashSlots[hash].linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK;

    while ((script_stringHashSlots[previousSlot].linkAndFlags &
            SCRIPT_STRING_HASH_LINK_MASK) != hash) {
        previousSlot =
            script_stringHashSlots[previousSlot].linkAndFlags &
            SCRIPT_STRING_HASH_LINK_MASK;
    }

    script_stringHashSlots[previousSlot].linkAndFlags =
        (uint16_t)((script_stringHashSlots[previousSlot].linkAndFlags &
                    SCRIPT_STRING_HASH_FLAGS_MASK) |
                   freeSlot);
    script_stringHashSlots[freeSlot].linkAndFlags =
        (uint16_t)((script_stringHashSlots[hash].linkAndFlags &
                    SCRIPT_STRING_HASH_LINK_MASK) |
                   SCRIPT_STRING_HASH_CHAINED);
    script_stringHashSlots[freeSlot].stringHandle =
        script_stringHashSlots[hash].stringHandle;
}

uint16_t
SL_GetStringOfLen(const char *text, uint8_t user, uint32_t size,
                  int32_t type)
{
    uint16_t hash = GetHashCode(text, size);
    script_string_hash_slot_t *head = &script_stringHashSlots[hash];

    if ((head->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK) ==
        SCRIPT_STRING_HASH_OCCUPIED) {
        if (coduomp_script_string_entry_matches(head, text, size) != qfalse) {
            script_string_entry_t *entry =
                GetRefStringByHandle(head->stringHandle);
            if ((entry->flags & user) == 0) {
                entry->flags |= user;
                /* ORIGINAL_BINARY_BUG: stock 0x080a493d..0x080a4941
                 * increments this 16-bit count modulo 65,536. */
                entry->refCount++;
            }
            return head->stringHandle;
        }

        uint16_t previousSlot = hash;
        uint16_t currentSlot = head->linkAndFlags;
        for (;;) {
            currentSlot &= SCRIPT_STRING_HASH_LINK_MASK;
            script_string_hash_slot_t *current =
                &script_stringHashSlots[currentSlot];
            if (current == head) {
                break;
            }

            if (coduomp_script_string_entry_matches(
                    current, text, size) != qfalse) {
                coduomp_script_string_move_slot_to_hash_head(
                    hash, previousSlot, currentSlot);
                script_string_entry_t *entry =
                    GetRefStringByHandle(head->stringHandle);
                if ((entry->flags & user) == 0) {
                    entry->flags |= user;
                    /* ORIGINAL_BINARY_BUG: stock 0x080a4a83..0x080a4a87
                     * performs the same unchecked 16-bit increment. */
                    entry->refCount++;
                }
                return head->stringHandle;
            }

            previousSlot = currentSlot;
            currentSlot = current->linkAndFlags;
        }

        coduomp_script_string_insert_into_occupied_head(
            hash, coduomp_script_string_pop_free_hash_slot());
    } else {
        if ((head->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK) ==
            SCRIPT_STRING_HASH_EMPTY) {
            coduomp_script_string_unlink_free_slot(hash);
        } else {
            coduomp_script_string_insert_into_linked_hash(
                hash, coduomp_script_string_pop_free_hash_slot());
        }
        head->linkAndFlags = hash | SCRIPT_STRING_HASH_OCCUPIED;
    }

    head->stringHandle =
        MT_AllocIndex(size + SCRIPT_STRING_ENTRY_HEADER_SIZE, type);
    script_string_entry_t *entry = GetRefStringByHandle(head->stringHandle);
    memcpy(entry->text, text, size);
    entry->flags = user;
    entry->refCount = 0;
    entry->sizeByte = (uint8_t)size;

    return head->stringHandle;
}

uint16_t
SL_GetString_(const char *text, uint8_t user, int32_t type)
{
    return SL_GetStringOfLen(
        text, user, (uint32_t)strlen(text) + 1U, type);
}

uint16_t
SL_GetString(const char *text, uint8_t user)
{
    return SL_GetString_(text, user, SCRIPT_STRING_DEFAULT_TYPE);
}

uint16_t
SL_GetLowercaseStringOfLen(const char *text, uint8_t user, uint32_t size,
                           int32_t type)
{
    /* ORIGINAL_BINARY_BUG: stock 0x080a4dc6 reserves the caller-supplied
     * byte count directly on the stack. */
    char lowercase[size];

    for (int32_t index = (int32_t)size - 1; index >= 0; --index) {
        lowercase[index] =
            (char)tolower(coduo_ctype_signed_byte_arg(text[index]));
    }

    return SL_GetStringOfLen(lowercase, user, size, type);
}

uint16_t
SL_GetLowercaseString_(const char *text, uint8_t user, int32_t type)
{
    return SL_GetLowercaseStringOfLen(
        text, user, (uint32_t)strlen(text) + 1U, type);
}

uint16_t
SL_GetLowercaseString(const char *text, uint8_t user)
{
    return SL_GetLowercaseString_(
        text, user, SCRIPT_STRING_DEFAULT_TYPE);
}

void SL_BeginLoadScripts(void)
{
    Hunk_SetHighTempMark();
    script_stringCanonicalMap = Hunk_Alloc(SCRIPT_STRING_TEMP_HASH_BYTE_SIZE);
    script_stringCanonicalCount = 0;
}

void SL_EndLoadScripts(void)
{
    Hunk_ClearToHighTempMark();
}

uint16_t SL_FindCanonicalString(const char *text)
{
    uint16_t hash = SL_FindString(text);

    return script_stringCanonicalMap[hash & UINT16_MAX];
}

void SL_AddRefToString(uint16_t handle)
{
    script_string_entry_t *entry = GetRefStringByHandle(handle);

    /* ORIGINAL_BINARY_BUG: stock 0x080a4f22..0x080a4f26 increments the
     * 16-bit reference count modulo 65,536 without an overflow check. */
    entry->refCount++;
}

void SL_RemoveRefToString(uint16_t handle)
{
    script_string_entry_t *entry = GetRefStringByHandle(handle);

    /* ORIGINAL_BINARY_BUG: stock 0x080a50bd..0x080a50cd tests only zero
     * before decrementing the same wrapping 16-bit count. */
    if (entry->refCount == 0) {
        SL_FreeString(
            handle, entry->text, (uint32_t)strlen(entry->text) + 1U);
        return;
    }

    entry->refCount--;
}

void SL_RemoveRefToStringOfLen(uint16_t handle,
                              uint32_t size)
{
    script_string_entry_t *entry = GetRefStringByHandle(handle);

    /* ORIGINAL_BINARY_BUG: stock 0x080a511d..0x080a512d repeats the same
     * zero-test/decrement behavior for the explicit-length release. */
    if (entry->refCount == 0) {
        SL_FreeString(handle, entry->text, size);
        return;
    }

    entry->refCount--;
}

void SL_TransferRefToString(uint16_t handle,
                           uint8_t usage)
{
    script_string_entry_t *entry = GetRefStringByHandle(handle);

    if ((entry->flags & usage) == 0) {
        entry->flags |= usage;
        return;
    }

    /* ORIGINAL_BINARY_BUG: stock 0x080a4ee5..0x080a4ee9 decrements the
     * wrapping 16-bit count without an underflow guard on this path. */
    entry->refCount--;
}

uint16_t
SL_TransferToCanonicalString(uint16_t handle)
{
    SL_TransferRefToString(handle, SCRIPT_STRING_USAGE_CANONICAL);

    uint16_t canonical =
        script_stringCanonicalMap[handle];
    if (canonical == 0) {
        script_stringCanonicalCount++;
        script_stringCanonicalMap[handle] = script_stringCanonicalCount;
        canonical = script_stringCanonicalCount;
    }

    return canonical;
}

void SL_FreeString(uint16_t handle,
                   const void *text, uint32_t size)
{
    uint16_t hash =
        GetHashCode((const char *)text, size);
    script_string_hash_slot_t *head = &script_stringHashSlots[hash];
    script_string_hash_slot_t *freeSlot = head;

    MT_FreeIndex(handle, size + SCRIPT_STRING_ENTRY_HEADER_SIZE);

    uint16_t replacementSlot =
        head->linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK;
    script_string_hash_slot_t *replacement =
        &script_stringHashSlots[replacementSlot];
    uint16_t freeSlotIndex = hash;

    if (head->stringHandle == handle) {
        if (replacement != head) {
            head->linkAndFlags =
                (uint16_t)((replacement->linkAndFlags &
                            SCRIPT_STRING_HASH_LINK_MASK) |
                           SCRIPT_STRING_HASH_OCCUPIED);
            head->stringHandle = replacement->stringHandle;
            script_stringFreedHashSlot = head;
            freeSlot = replacement;
            freeSlotIndex = replacementSlot;
        }
    } else {
        uint16_t previousSlot = hash;
        while (replacement->stringHandle != handle) {
            previousSlot = replacementSlot;
            replacementSlot =
                replacement->linkAndFlags & SCRIPT_STRING_HASH_LINK_MASK;
            replacement = &script_stringHashSlots[replacementSlot];
        }
        script_stringHashSlots[previousSlot].linkAndFlags =
            (uint16_t)((replacement->linkAndFlags &
                        SCRIPT_STRING_HASH_LINK_MASK) |
                       (script_stringHashSlots[previousSlot].linkAndFlags &
                        SCRIPT_STRING_HASH_FLAGS_MASK));
        freeSlot = replacement;
        freeSlotIndex = replacementSlot;
    }

    uint16_t oldFreeHead =
        script_stringHashSlots[0].linkAndFlags;

    freeSlot->linkAndFlags = oldFreeHead;
    freeSlot->stringHandle = 0;
    script_stringHashSlots[oldFreeHead].stringHandle = freeSlotIndex;
    script_stringHashSlots[0].linkAndFlags = freeSlotIndex;
}

void Scr_SetString(uint16_t *slot,
                            uint16_t value)
{
    if (*slot != 0) {
        SL_RemoveRefToString(*slot);
    }
    if (value != 0) {
        SL_AddRefToString(value);
    }
    *slot = value;
}

uint16_t Scr_AllocString(const char *text)
{
    return SL_GetString(text, 1);
}

void SL_ShutdownSystem(uint8_t usage)
{
    for (uint16_t slot = SCRIPT_STRING_HASH_FIRST_SLOT;
         slot < SCRIPT_STRING_HASH_SLOT_COUNT; ++slot) {
        for (;;) {
            script_string_hash_slot_t *hashSlot =
                &script_stringHashSlots[slot];
            if ((hashSlot->linkAndFlags & SCRIPT_STRING_HASH_FLAGS_MASK) ==
                SCRIPT_STRING_HASH_EMPTY) {
                break;
            }

            script_string_entry_t *entry =
                GetRefStringByHandle(hashSlot->stringHandle);
            if ((entry->flags & usage) == 0) {
                break;
            }

            entry->flags &= (uint8_t)~usage;
            script_stringFreedHashSlot = NULL;
            SL_RemoveRefToString(hashSlot->stringHandle);
            if (script_stringFreedHashSlot == NULL) {
                break;
            }
        }
    }
}

void CreateCanonicalFilename(char *dest, const char *source,
                             int32_t maxLength)
{
    for (;;) {
        uint8_t ch;
        do {
            do {
                ch = (uint8_t)*source++;
            } while (ch == '\\');
        } while (ch == '/');

        while (ch > 0x1f) {
            *dest++ = (char)tolower(coduo_ctype_signed_byte_arg(ch));
            maxLength--;
            if (maxLength == 0) {
                Com_Error(1, "\x15" "Filename '%s' exceeds maximum length of %d",
                          source, maxLength);
            }
            if (ch == '\\') {
                break;
            }

            ch = (uint8_t)*source++;
            if (ch == '/') {
                ch = '\\';
            }
        }

        if (ch == '\0') {
            *dest = '\0';
            return;
        }
    }
}

uint16_t
Scr_CreateCanonicalFilename(const char *filename)
{
    char canonical[SCRIPT_STRING_CANONICAL_FILENAME_BUFFER_SIZE];

    CreateCanonicalFilename(
        canonical, filename, SCRIPT_STRING_CANONICAL_FILENAME_LIMIT);
    return SL_GetString_(
        canonical, 0, SCRIPT_STRING_CANONICAL_FILENAME_TYPE);
}

uint16_t SL_GetStringForFloat(float value)
{
    char text[SCRIPT_STRING_FORMAT_BUFFER_SIZE];

    sprintf(text, "%g", (double)value);
    return SL_GetString_(text, 0, SCRIPT_STRING_INTERN_RUNTIME_TYPE);
}

uint16_t SL_GetStringForInt(int32_t value)
{
    char text[SCRIPT_STRING_FORMAT_BUFFER_SIZE];

    sprintf(text, "%i", value);
    return SL_GetString_(text, 0, SCRIPT_STRING_INTERN_RUNTIME_TYPE);
}

uint16_t
SL_GetStringForVector(const float *vector)
{
    char text[SCRIPT_STRING_FORMAT_BUFFER_SIZE];

    /* ORIGINAL_BINARY_BUG: stock 0x080a5248 uses unbounded sprintf for
     * three promoted floats in this 128-byte destination. */
    sprintf(text, "(%.2f, %.2f, %.2f)", (double)vector[0],
            (double)vector[1], (double)vector[2]);
    return SL_GetString_(text, 0, SCRIPT_STRING_INTERN_RUNTIME_TYPE);
}
