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

#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "sound_alias_private.h"

enum {
    SOUND_ALIAS_FATAL_DROP = 1,
    SOUND_ALIAS_CHANNEL_LIST_BUFFER_SIZE = 16384,
    SOUND_ALIAS_LOADSPEC_MAX_CHARS = 65535,
    SOUND_ALIAS_LOADSPEC_COPY_SIZE = SOUND_ALIAS_LOADSPEC_MAX_CHARS + 1,
    SOUND_ALIAS_ALL_MP_COMPARE_CHARS = 6,
    SOUND_ALIAS_GAME_PREFIX_CHARS = 5
};

#define SOUND_ALIAS_TOKEN_BOUNDARY_MIN '!'
#define SOUND_ALIAS_SPACE ' '

static const char *const soundAlias_channelNameTable
    [CODUO_SOUND_ALIAS_CHANNEL_NAME_COUNT] = {
        "auto",      "menu",  "weapon",    "voice",      "item",
        "body",      "local", "music",     "announcer",  "shellshock"
};

static const char *const soundAlias_parseColumnNameTable
    [CODUO_SOUND_ALIAS_PARSE_COLUMN_SLOT_COUNT] = {
        NULL,         "name",        "sequence", "file",
        "subtitle",  "vol_min",     "vol_max",  "pitch_min",
        "pitch_max", "dist_min",    "dist_max", "channel",
        "type",      "loop",        "probability",
        "loadspec",  "masterslave", "lod_min",  "lod_max"
};

void Com_InitSoundAliasParseNode(coduo_sound_alias_parse_node_t *node,
                                 qboolean defaultLoadspec)
{
    /* ORIGINAL_BINARY_BUG: a loadspec token can exceed sourceFile and writes
     * into following parse-node arrays before name[0] terminates it; preserved
     * per original-bugs/engine-sound-alias-loadspec-source-file-overrun.md. */
    strcpy(node->sourceFile, soundAlias_currentFile);
    node->name[0] = '\0';
    node->sequence = 0;
    node->file[0] = '\0';
    node->subtitle[0] = '\0';
    node->volumeMin = 1.0f;
    node->volumeMax = 1.0f;
    node->pitchMin = 1.0f;
    node->pitchMax = 1.0f;
    node->distMin = 120.0f;
    node->distMax = 0.0f;
    node->channel = CODUO_SOUND_ALIAS_CHANNEL_AUTO;
    node->type = CODUO_SOUND_ALIAS_TYPE_LOADED;
    node->loop = CODUO_SOUND_ALIAS_NONLOOPING;
    node->selectionWeight = 1.0f;
    node->loadspec = defaultLoadspec != qfalse;
    node->isMaster = 0;
    node->isSlave = 0;
    node->slavePercentage = 1.0f;
    node->selectorMin = -1.0f;
    node->selectorMax = -1.0f;
    node->next = 0;
}

coduo_sound_alias_channel_t Com_ParseSoundAliasChannel(const char *text)
{
    char expectedChannels[SOUND_ALIAS_CHANNEL_LIST_BUFFER_SIZE];
    int length;
    int channel;

    for (channel = 0; channel < CODUO_SOUND_ALIAS_CHANNEL_NAME_COUNT;
         channel++) {
        if (Q_stricmp(text, soundAlias_channelNameTable[channel]) == 0) {
            return channel;
        }
    }

    length = 0;
    for (channel = 0; channel < CODUO_SOUND_ALIAS_CHANNEL_NAME_COUNT;
         channel++) {
        length += sprintf(expectedChannels + length, "%s",
                          soundAlias_channelNameTable[channel]);
        if (channel < CODUO_SOUND_ALIAS_CHANNEL_ANNOUNCER) {
            length += sprintf(expectedChannels + length, ", ");
        } else if (channel == CODUO_SOUND_ALIAS_CHANNEL_ANNOUNCER) {
            length += sprintf(expectedChannels + length, " or ");
        }
    }

    Com_Error(SOUND_ALIAS_FATAL_DROP,
              "\x15" "Sound alias file %s: Unknown sound channel '%s'; "
                     "should be %s\n",
              soundAlias_currentFile, text, expectedChannels);
    return CODUO_SOUND_ALIAS_CHANNEL_AUTO;
}

coduo_sound_alias_type_t Com_ParseSoundAliasType(const char *text)
{
    if (Q_stricmp(text, "streamed") == 0) {
        return CODUO_SOUND_ALIAS_TYPE_STREAMED;
    }

    if (Q_stricmp(text, "loaded") == 0) {
        return CODUO_SOUND_ALIAS_TYPE_LOADED;
    }

    Com_Error(SOUND_ALIAS_FATAL_DROP,
              "\x15" "Sound alias file %s: Unknown sound type '%s'; "
                     "should be streamed or loaded\n",
              soundAlias_currentFile, text);
    return CODUO_SOUND_ALIAS_TYPE_UNKNOWN;
}

coduo_sound_alias_loop_value_t Com_ParseSoundAliasLoop(const char *text)
{
    if (Q_stricmp(text, "looping") == 0) {
        return CODUO_SOUND_ALIAS_LOOPING;
    }

    if (Q_stricmp(text, "nonlooping") == 0) {
        return CODUO_SOUND_ALIAS_NONLOOPING;
    }

    Com_Error(SOUND_ALIAS_FATAL_DROP,
              "\x15" "Sound alias file %s: Unknown sound looping type '%s'; "
                     "should be looping or nonlooping\n",
              soundAlias_currentFile, text);
    return CODUO_SOUND_ALIAS_NONLOOPING;
}

int32_t
Com_ParseSoundAliasLoadspec(const char *sourceFile, const char *text)
{
    size_t sourceLength;
    char loadspec[SOUND_ALIAS_LOADSPEC_COPY_SIZE];
    char *cursor;
    char *match;
    qboolean foundModifier;

    Cvar_Get("fs_game", "", 0);
    sourceLength = strlen(sourceFile);
    loadspec[SOUND_ALIAS_LOADSPEC_MAX_CHARS] = '\0';
    strncpy(loadspec, text, sizeof(loadspec));
    if (loadspec[SOUND_ALIAS_LOADSPEC_MAX_CHARS] != '\0') {
        Com_Error(SOUND_ALIAS_FATAL_DROP,
                  "\x15" "Sound alias file %s: loadspec is > %i "
                         "characters\n",
                  soundAlias_currentFile, SOUND_ALIAS_LOADSPEC_MAX_CHARS);
    }

    strlwr(loadspec);
    cursor = loadspec;

    if (loadspec[0] == '!') {
        do {
            ++cursor;
            if (SOUND_ALIAS_SPACE < *cursor) {
                break;
            }
        } while (*cursor != '\0');

        if (strcmp(sourceFile, "menu") == 0 &&
            strstr(cursor, "menu") == NULL) {
            return 0;
        }

        match = strstr(cursor, "all_");
        if (match != NULL &&
            strncmp(match, "all_mp", SOUND_ALIAS_ALL_MP_COMPARE_CHARS) == 0) {
            return 0;
        }

        if (fs_startupScratch[0] != '\0') {
            match = strstr(cursor, "game_");
            if (match != NULL &&
                strncmp(match + SOUND_ALIAS_GAME_PREFIX_CHARS,
                        fs_startupScratch,
                        strlen(fs_startupScratch)) == 0) {
                return 0;
            }
        }

        while ((cursor = strstr(cursor, sourceFile)) != NULL) {
            if ((cursor == loadspec ||
                 cursor[-1] < SOUND_ALIAS_TOKEN_BOUNDARY_MIN) &&
                cursor[sourceLength] < SOUND_ALIAS_TOKEN_BOUNDARY_MIN) {
                return 0;
            }
            ++cursor;
        }

        return 1;
    }

    while ((match = strstr(cursor, sourceFile)) != NULL) {
        if ((match == loadspec ||
             match[-1] < SOUND_ALIAS_TOKEN_BOUNDARY_MIN) &&
            match[sourceLength] < SOUND_ALIAS_TOKEN_BOUNDARY_MIN) {
            return 1;
        }
        cursor = match + 1;
    }

    foundModifier = qfalse;
    if (strcmp(sourceFile, "menu") == 0 && strstr(cursor, "menu") == NULL) {
        return 0;
    }

    match = strstr(cursor, "all_");
    if (match != NULL) {
        foundModifier = qtrue;
        if (strncmp(match, "all_mp", SOUND_ALIAS_ALL_MP_COMPARE_CHARS) != 0) {
            return 0;
        }
    }

    if (fs_startupScratch[0] != '\0') {
        match = strstr(cursor, "game_");
        if (match != NULL) {
            foundModifier = qtrue;
            if (strncmp(match + SOUND_ALIAS_GAME_PREFIX_CHARS,
                        fs_startupScratch,
                        strlen(fs_startupScratch)) != 0) {
                return 0;
            }
        }
    }

    if (foundModifier) {
        return 1;
    }

    return 0;
}

void Com_ParseSoundAliasMasterSlave(const char *text,
                                    coduo_sound_alias_parse_node_t *node)
{
    if (strcasecmp(text, "master") == 0) {
        node->isMaster = 1;
        node->isSlave = 0;
    } else {
        node->isMaster = 0;
        node->isSlave = 1;
        node->slavePercentage = (float)atof(text);
    }
}

void Com_ParseSoundAliasColumn(
    const char *sourceFile, const char *text,
    int32_t column,
    coduo_sound_alias_parse_seen_column_table_t seenColumns,
    coduo_sound_alias_parse_node_t *node)
{
    size_t length;
    int index;

    if (column == CODUO_SOUND_ALIAS_PARSE_COLUMN_UNKNOWN) {
        return;
    }

    if (seenColumns[column] != 0) {
        Com_Error(SOUND_ALIAS_FATAL_DROP,
                  "\x15" "Sound alias file %s: Duplicate entries for the "
                         "'%s' column\n",
                  soundAlias_currentFile, soundAlias_parseColumnNameTable[column]);
    }

    seenColumns[column] = 1;

    switch (column) {
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME:
        length = strlen(text);
        if (length >= sizeof(node->name) - 1) {
            Com_Error(SOUND_ALIAS_FATAL_DROP,
                      "\x15" "Sound alias file %s: Alias name '%s' is "
                             "longer than %i characters\n",
                      soundAlias_currentFile, text,
                      (int)(sizeof(node->name) - 1));
        }
        if (Com_IsValidSoundAliasName(text) == qfalse) {
            Com_Error(SOUND_ALIAS_FATAL_DROP,
                      "\x15" "Sound alias file %s: Alias name '%s' is "
                             "invalid\n",
                      soundAlias_currentFile, text);
        }
        strcpy(node->name, text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_SEQUENCE:
        node->sequence = atoi(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_FILE:
        length = strlen(text);
        if (length >= sizeof(node->file) - 1) {
            Com_Error(SOUND_ALIAS_FATAL_DROP,
                      "\x15" "Sound alias file %s: Sound file '%s' is "
                             "longer than %i characters\n",
                      soundAlias_currentFile, text,
                      (int)(sizeof(node->file) - 1));
        }
        strcpy(node->file, text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE:
        length = strlen(text);
        if (length >= sizeof(node->subtitle) - 1) {
            Com_Error(SOUND_ALIAS_FATAL_DROP,
                      "\x15" "Sound alias file %s: Subtitle '%s' is longer "
                             "than %i characters\n",
                      soundAlias_currentFile, text,
                      (int)(sizeof(node->subtitle) - 1));
        }
        for (index = 0; text[index] != '\0'; index++) {
            if ((signed char)text[index] < 0) {
                Com_Error(SOUND_ALIAS_FATAL_DROP,
                          "\x15" "Sound alias file %s: Subtitle '%s' has "
                                 "invalid character '%c' ascii %i\n",
                          soundAlias_currentFile, text,
                          (int)(signed char)text[index],
                          (unsigned char)text[index]);
            }
        }
        strcpy(node->subtitle, text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_VOL_MIN:
        node->volumeMin = (float)atof(text);
        if (seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_VOL_MAX] == 0) {
            node->volumeMax = node->volumeMin;
        }
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_VOL_MAX:
        node->volumeMax = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_PITCH_MIN:
        node->pitchMin = (float)atof(text);
        if (seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_PITCH_MAX] == 0) {
            node->pitchMax = node->pitchMin;
        }
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_PITCH_MAX:
        node->pitchMax = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_DIST_MIN:
        node->distMin = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_DIST_MAX:
        node->distMax = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_CHANNEL:
        node->channel = Com_ParseSoundAliasChannel(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_TYPE:
        node->type = Com_ParseSoundAliasType(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_LOOP:
        node->loop = Com_ParseSoundAliasLoop(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_PROBABILITY:
        node->selectionWeight = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_LOADSPEC:
        node->loadspec = Com_ParseSoundAliasLoadspec(sourceFile, text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_MASTERSLAVE:
        Com_ParseSoundAliasMasterSlave(text, node);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_LOD_MIN:
        node->selectorMin = (float)atof(text);
        break;
    case CODUO_SOUND_ALIAS_PARSE_COLUMN_LOD_MAX:
        node->selectorMax = (float)atof(text);
        break;
    default:
        break;
    }
}
