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

#include "core_runtime_private.h"

enum {
    COM_PARSE_INITIAL_LINE = 1,
    COM_PARSE_STACK_BASE_DEPTH = 0,
    COM_PARSE_MAX_STACK_DEPTH = CODUO_COM_PARSE_STATE_COUNT - 1,
    COM_PARSE_ERROR_DROP = 1,
    COM_PARSE_ERROR_MESSAGE_BUFFER_SIZE = 32000,
    COM_PARSE_TOKEN_LAST_INDEX = CODUO_COM_PARSE_TOKEN_SIZE - 1,
    COM_DEFAULT_EXTENSION_TEMP_SIZE = 64,
    COM_BIT_WORD_SHIFT = 5,
    COM_BIT_INDEX_MASK = 0x1f,
    Q_COLOR_DEFAULT_INDEX = 7,
    Q_COLOR_DIGIT_MAX = 9
};

typedef struct com_parse_mark_s {
    int32_t line;
    char *parse;
    qboolean ungetToken;
    int32_t savedLine;
    char *savedParse;
} com_parse_mark_t;

com_parse_session_stack_t com_parseStates;
com_parse_session_t *com_parseCurrentState = &com_parseStates[0];
int32_t com_parseStackDepth;
char *com_parsePreviousCursor;
char *com_parseCurrentCursor;
coduo_com_parse_accumulator_t com_parseAccumulator;
coduo_com_parse_operator_string_table_t com_parseOperatorStrings = {
    "+=", "-=", "*=", "/=", "&=", "|=", "++", "--", "&&", "||", "<=", ">=",
    "==", "!=", NULL
};

/* NOT_FROM_ORIGINAL_SOURCE: factored parser digit predicate. */
static qboolean Com_ParseIsDigit(char value)
{
    return value >= '0' && value <= '9';
}

/* NOT_FROM_ORIGINAL_SOURCE: factored parser alpha predicate. */
static qboolean Com_ParseIsAlpha(char value)
{
    return (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z');
}

/* NOT_FROM_ORIGINAL_SOURCE: factored parser name-character predicate. */
static qboolean Com_ParseIsNameChar(char value)
{
    return Com_ParseIsAlpha(value) || Com_ParseIsDigit(value) ||
           value == '_' || value == '/' || value == '\\' ||
           value == ':' || value == '.';
}

/* NOT_FROM_ORIGINAL_SOURCE: factored capped token append. */
static void Com_ParseAppendChar(int32_t *tokenLength, char value)
{
    if (*tokenLength < COM_PARSE_TOKEN_LAST_INDEX) {
        com_parseCurrentState->token[*tokenLength] = value;
        (*tokenLength)++;
    }
}

void Com_BeginParseSession(const char *name)
{
    if (com_parseStackDepth == COM_PARSE_MAX_STACK_DEPTH) {
        Com_Error(0, "\x15" "Com_BeginParseSession: session overflow");
    }

    com_parseStackDepth++;
    com_parseCurrentState = &com_parseStates[com_parseStackDepth];
    com_parseCurrentState->line = COM_PARSE_INITIAL_LINE;
    com_parseCurrentState->ungetToken = qfalse;
    com_parseCurrentState->spaceDelimited = qtrue;
    com_parseCurrentState->csv = qfalse;
    Q_strncpyz(com_parseCurrentState->name, name,
               sizeof(com_parseCurrentState->name));
}

void Com_EndParseSession(void)
{
    if (com_parseStackDepth == COM_PARSE_STACK_BASE_DEPTH) {
        Com_Error(0, "\x15" "Com_EndParseSession: session underflow");
    }

    com_parseStackDepth--;
    com_parseCurrentState = &com_parseStates[com_parseStackDepth];
}

void Com_ResetParseSessions(void)
{
    com_parseStackDepth = COM_PARSE_STACK_BASE_DEPTH;
    com_parseCurrentState = &com_parseStates[COM_PARSE_STACK_BASE_DEPTH];
}

void Com_SetSpaceDelimited(qboolean spaceDelimited)
{
    com_parseCurrentState->spaceDelimited = spaceDelimited;
}

void Com_SetCSV(qboolean csv)
{
    com_parseCurrentState->csv = csv;
}

void Com_SetParseNegativeNumbers(qboolean parseNegativeNumbers)
{
    com_parseCurrentState->parseNegativeNumbers = parseNegativeNumbers;
}

int32_t Com_GetCurrentParseLine(void)
{
    return com_parseCurrentState->line;
}

void Com_ScriptError(const char *format, ...)
{
    char message[COM_PARSE_ERROR_MESSAGE_BUFFER_SIZE];
    va_list args;

    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock formats without a bound into this 32,000-byte
     * stack array; see
     * original-bugs/coduomp-parser-diagnostic-stack-overflow.md. */
    vsprintf(message, format, args);
    va_end(args);

    Com_Error(COM_PARSE_ERROR_DROP, "\x15" "File %s, line %i: %s",
              com_parseCurrentState->name, com_parseCurrentState->line,
              message);
}

void Com_ScriptWarning(const char *format, ...)
{
    char message[COM_PARSE_ERROR_MESSAGE_BUFFER_SIZE];
    va_list args;

    va_start(args, format);
    /* ORIGINAL_BINARY_BUG: stock formats without a bound into this 32,000-byte
     * stack array; see
     * original-bugs/coduomp-parser-diagnostic-stack-overflow.md. */
    vsprintf(message, format, args);
    va_end(args);

    Com_Printf("File %s, line %i: %s", com_parseCurrentState->name,
               com_parseCurrentState->line, message);
}

void Com_UngetToken(void)
{
    if (com_parseCurrentState->ungetToken != qfalse) {
        Com_ScriptError("UngetToken called twice");
    }

    com_parseCurrentState->ungetToken = qtrue;
    com_parsePreviousCursor = com_parseCurrentCursor;
}

void Com_ParseSetMark(char **data, com_parse_mark_t *mark)
{
    mark->line = com_parseCurrentState->line;
    mark->parse = *data;
    mark->ungetToken = com_parseCurrentState->ungetToken;
    mark->savedLine = com_parseCurrentState->savedLine;
    mark->savedParse = com_parseCurrentState->savedParse;
}

void Com_ParseReturnToMark(char **data, const com_parse_mark_t *mark)
{
    com_parseCurrentState->line = mark->line;
    *data = mark->parse;
    com_parseCurrentState->ungetToken = mark->ungetToken;
    com_parseCurrentState->savedLine = mark->savedLine;
    com_parseCurrentState->savedParse = mark->savedParse;
}

static char *SkipWhitespace(char *data, qboolean *hasNewLines)
{
    while (*data <= ' ') {
        if (*data == '\0') {
            return NULL;
        }
        if (*data == '\n') {
            com_parseCurrentState->line++;
            *hasNewLines = qtrue;
        }
        data++;
    }

    return data;
}

int32_t Com_Compress(char *data)
{
    int32_t size;
    char *out;
    char *scan;

    size = 0;
    out = data;
    scan = data;

    if (data != NULL) {
        while (*scan != '\0') {
            if (*scan == '\r' || *scan == '\n') {
                *out = *scan;
                out++;
                size++;
                scan++;
            } else if (scan[0] == '/' && scan[1] == '/') {
                while (*scan != '\0' && *scan != '\n') {
                    scan++;
                }
            } else if (scan[0] == '/' && scan[1] == '*') {
                while (*scan != '\0' &&
                       (scan[0] != '*' || scan[1] != '/')) {
                    if (*scan == '\n') {
                        *out = '\n';
                        out++;
                        size++;
                    }
                    scan++;
                }
                if (*scan != '\0') {
                    scan += 2;
                }
            } else {
                *out = *scan;
                out++;
                size++;
                scan++;
            }
        }
    }

    /* ORIGINAL_BINARY_BUG: a NULL input skips the loop but leaves out equal
     * to NULL, so stock writes this terminator through address zero; see
     * original-bugs/coduomp-compress-null-output-write.md. */
    *out = '\0';
    return size;
}

char *Com_GetLastTokenPos(void)
{
    return com_parsePreviousCursor;
}

static char *Com_ParseCSV(char **data, qboolean allowLineBreaks)
{
    char *text = *data;
    int32_t tokenLength = 0;

    com_parseCurrentState->token[0] = '\0';

    if (allowLineBreaks == qfalse) {
        if (*text == '\r' || *text == '\n') {
            return com_parseCurrentState->token;
        }
    } else {
        /* ORIGINAL_BINARY_BUG: stock advances over CSV row separators without
         * updating the parse-session line; see
         * original-bugs/coduomp-csv-line-state.md. */
        while (*text == '\r' || *text == '\n') {
            text++;
        }
    }

    com_parseCurrentCursor = com_parsePreviousCursor;
    com_parsePreviousCursor = text;

    while (*text != '\0' && *text != ',' && *text != '\n') {
        if (*text == '\r') {
            text++;
        } else if (*text == '"') {
            text++;
            for (;;) {
                /* ORIGINAL_BINARY_BUG: the quoted CSV loop has no NUL exit,
                 * so an unterminated field reads beyond its source; see
                 * original-bugs/coduomp-csv-unterminated-quote-oob-read.md. */
                while (*text != '"') {
                    Com_ParseAppendChar(&tokenLength, *text);
                    text++;
                }
                if (text[1] != '"') {
                    break;
                }
                Com_ParseAppendChar(&tokenLength, '"');
                text += 2;
            }
            text++;
        } else {
            Com_ParseAppendChar(&tokenLength, *text);
            text++;
        }
    }

    if (*text == '\0') {
        *data = NULL;
    } else {
        if (*text != '\n') {
            text++;
        }
        *data = text;
    }

    com_parseCurrentState->token[tokenLength] = '\0';
    return com_parseCurrentState->token;
}

static char *Com_ParseExt(char **data, qboolean allowLineBreaks)
{
    char *text;
    char value;
    qboolean hasNewLines;
    int32_t tokenLength;

    if (data == NULL) {
        Com_Error(0, "\x15" "Com_ParseExt: NULL data_p");
    }

    text = *data;
    hasNewLines = qfalse;
    tokenLength = 0;
    com_parseCurrentState->token[0] = '\0';

    if (text == NULL) {
        *data = NULL;
        return com_parseCurrentState->token;
    }

    com_parseCurrentState->savedLine = com_parseCurrentState->line;
    com_parseCurrentState->savedParse = text;

    if (com_parseCurrentState->csv != qfalse) {
        return Com_ParseCSV(data, allowLineBreaks);
    }

    while ((text = SkipWhitespace(text, &hasNewLines)) != NULL) {
        if (hasNewLines != qfalse && allowLineBreaks == qfalse) {
            *data = text;
            return com_parseCurrentState->token;
        }

        value = *text;
        if (value == '/' && text[1] == '/') {
            while (*text != '\0' && *text != '\n') {
                text++;
            }
            continue;
        }

        if (value == '/' && text[1] == '*') {
            while (*text != '\0' && (*text != '*' || text[1] != '/')) {
                if (*text == '\n') {
                    com_parseCurrentState->line++;
                }
                text++;
            }
            if (*text != '\0') {
                text += 2;
            }
            continue;
        }

        com_parseCurrentCursor = com_parsePreviousCursor;
        com_parsePreviousCursor = text;

        if (value == '"') {
            text++;
            for (;;) {
                char *next;

                value = *text;
                next = text + 1;
                if (value == '\\' && *next == '"') {
                    value = *next;
                    text += 2;
                } else {
                    if (value == '"' || value == '\0') {
                        /* ORIGINAL_BINARY_BUG: next is one byte beyond a
                         * terminating NUL for an unterminated quote; see
                         * original-bugs/coduomp-unterminated-quoted-token-oob-read.md. */
                        com_parseCurrentState->token[tokenLength] = '\0';
                        *data = next;
                        return com_parseCurrentState->token;
                    }
                    text = next;
                    /* ORIGINAL_BINARY_BUG: stock tests the following byte,
                     * missing a newline just consumed after the opening or an
                     * escaped quote; see
                     * original-bugs/coduomp-quoted-leading-newline-line-count.md. */
                    if (*next == '\n') {
                        com_parseCurrentState->line++;
                    }
                }

                Com_ParseAppendChar(&tokenLength, value);
            }
        }

        if (com_parseCurrentState->spaceDelimited != qfalse) {
            do {
                Com_ParseAppendChar(&tokenLength, value);
                text++;
                value = *text;
            } while (value > ' ');

            com_parseCurrentState->token[tokenLength] = '\0';
            *data = text;
            return com_parseCurrentState->token;
        }

        if (Com_ParseIsDigit(value) ||
            (com_parseCurrentState->parseNegativeNumbers != qfalse &&
             value == '-' && Com_ParseIsDigit(text[1])) ||
            (value == '.' && Com_ParseIsDigit(text[1]))) {
            do {
                Com_ParseAppendChar(&tokenLength, value);
                text++;
                value = *text;
            } while (Com_ParseIsDigit(value) || value == '.');

            if (value == 'e' || value == 'E') {
                Com_ParseAppendChar(&tokenLength, value);
                text++;
                value = *text;

                if (value == '-' || value == '+') {
                    Com_ParseAppendChar(&tokenLength, value);
                    text++;
                    value = *text;
                }

                /* ORIGINAL_BINARY_BUG: stock unconditionally appends and
                 * advances once after an exponent prefix/sign, so a
                 * terminating NUL causes the next digit test to read beyond
                 * it; see
                 * original-bugs/coduomp-parse-malformed-exponent-oob-read.md. */
                do {
                    Com_ParseAppendChar(&tokenLength, value);
                    text++;
                    value = *text;
                } while (Com_ParseIsDigit(value));
            }

            com_parseCurrentState->token[tokenLength] = '\0';
            *data = text;
            return com_parseCurrentState->token;
        }

        if (Com_ParseIsAlpha(value) || value == '_' ||
            value == '/' || value == '\\') {
            do {
                Com_ParseAppendChar(&tokenLength, value);
                text++;
                value = *text;
            } while (Com_ParseIsNameChar(value));

            com_parseCurrentState->token[tokenLength] = '\0';
            *data = text;
            return com_parseCurrentState->token;
        }

        for (const char *const *operatorString = com_parseOperatorStrings;
             *operatorString != NULL; ++operatorString) {
            const char *op = *operatorString;
            size_t length = strlen(op);

            if (strncmp(text, op, length) == 0) {
                memcpy(com_parseCurrentState->token, op, length);
                com_parseCurrentState->token[length] = '\0';
                *data = text + length;
                return com_parseCurrentState->token;
            }
        }

        com_parseCurrentState->token[0] = *text;
        com_parseCurrentState->token[1] = '\0';
        *data = text + 1;
        return com_parseCurrentState->token;
    }

    *data = NULL;
    return com_parseCurrentState->token;
}

char *Com_Parse(char **data)
{
    if (com_parseCurrentState->ungetToken != qfalse) {
        com_parseCurrentState->ungetToken = qfalse;
        *data = com_parseCurrentState->savedParse;
        com_parseCurrentState->line = com_parseCurrentState->savedLine;
    }

    return Com_ParseExt(data, qtrue);
}

char *Com_ParseOnLine(char **data)
{
    if (com_parseCurrentState->ungetToken != qfalse) {
        com_parseCurrentState->ungetToken = qfalse;
        if (com_parseCurrentState->spaceDelimited == qfalse) {
            return com_parseCurrentState->token;
        }

        *data = com_parseCurrentState->savedParse;
        com_parseCurrentState->line = com_parseCurrentState->savedLine;
    }

    return Com_ParseExt(data, qfalse);
}

void Com_MatchToken(char **data, const char *match, qboolean warning)
{
    char *token = Com_Parse(data);

    if (strcmp(token, match) != 0) {
        if (warning == qfalse) {
            Com_ScriptError("MatchToken: %s != %s", token, match);
        } else {
            Com_ScriptWarning("MatchToken: %s != %s", token, match);
        }
    }
}

qboolean Com_SkipBracedSection(char **data, int32_t depth)
{
    qboolean skippedOuterBrace = qfalse;
    int32_t braceDepth = 0;

    do {
        char *token = Com_Parse(data);

        if (token[1] == '\0') {
            if (token[0] == '{') {
                if (braceDepth == depth) {
                    skippedOuterBrace = qtrue;
                } else {
                    braceDepth++;
                }
            } else if (token[0] == '}') {
                braceDepth--;
            }
        }
    } while (braceDepth != 0 && *data != NULL);

    return skippedOuterBrace;
}

void Com_SkipRestOfLine(char **data)
{
    char *text = *data;

    if (text == NULL) {
        return;
    }

    for (;;) {
        char value = *text;

        if (value == '\0') {
            break;
        }

        text++;
        if (value == '\n') {
            com_parseCurrentState->line++;
            break;
        }
    }

    *data = text;
}

char *Com_ParseRestOfLine(char **data)
{
    com_parseAccumulator[0] = '\0';

    for (;;) {
        char *token = Com_ParseOnLine(data);

        if (token[0] == '\0') {
            break;
        }

        if (com_parseAccumulator[0] != '\0') {
            Q_strcat(com_parseAccumulator, sizeof(com_parseAccumulator),
                         " ");
        }
        Q_strcat(com_parseAccumulator, sizeof(com_parseAccumulator),
                     token);
    }

    return com_parseAccumulator;
}

float Com_ParseFloat(char **data)
{
    char *token = Com_Parse(data);

    if (token[0] == '\0') {
        return 0.0f;
    }

    return (float)atof(token);
}

int32_t Com_ParseInt(char **data)
{
    char *token = Com_Parse(data);

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

    return atoi(token);
}

void Com_Parse1DMatrix(char **data, int32_t x, float *m)
{
    Com_MatchToken(data, "(", qfalse);

    for (int32_t index = 0; index < x; ++index) {
        m[index] = (float)atof(Com_Parse(data));
    }

    Com_MatchToken(data, ")", qfalse);
}

void Com_Parse2DMatrix(char **data, int32_t y, int32_t x, float *m)
{
    Com_MatchToken(data, "(", qfalse);

    for (int32_t row = 0; row < y; ++row) {
        Com_Parse1DMatrix(data, x, &m[row * x]);
    }

    Com_MatchToken(data, ")", qfalse);
}

void Com_Parse3DMatrix(char **data, int32_t z, int32_t y, int32_t x,
                       float *m)
{
    Com_MatchToken(data, "(", qfalse);

    for (int32_t plane = 0; plane < z; ++plane) {
        Com_Parse2DMatrix(data, y, x, &m[plane * y * x]);
    }

    Com_MatchToken(data, ")", qfalse);
}

char ColorIndex(char value)
{
    uint8_t index = (uint8_t)(value - '0');

    if (index > Q_COLOR_DIGIT_MAX) {
        index = Q_COLOR_DEFAULT_INDEX;
    }

    return (char)index;
}

float Com_Clamp(float min, float max, float value)
{
    if (value < min) {
        return min;
    }

    if (max < value) {
        return max;
    }

    return value;
}

char *Com_SkipPath(char *path)
{
    char *name = path;

    for (; *path != '\0'; ++path) {
        if (*path == '/') {
            name = path + 1;
        }
    }

    return name;
}

void Com_StripExtension(const char *in, char *out)
{
    while (*in != '\0' && *in != '.') {
        *out = *in;
        ++in;
        ++out;
    }

    *out = '\0';
}

void Com_StripFilename(const char *in, char *out)
{
    /* ORIGINAL_BINARY_BUG: stock passes strlen(in), so Q_strncpyz drops the
     * last byte and an empty input expands its -1 copy count to 0xffffffff;
     * see original-bugs/coduomp-path-helper-empty-input-accesses.md. */
    Q_strncpyz(out, in, (int)strlen(in));
    *Com_SkipPath(out) = '\0';
}

void Com_DefaultExtension(char *path, int maxSize, const char *extension)
{
    char oldPath[COM_DEFAULT_EXTENSION_TEMP_SIZE];
    /* ORIGINAL_BINARY_BUG: stock forms and dereferences path - 1 for an empty
     * string; see
     * original-bugs/coduomp-path-helper-empty-input-accesses.md. */
    char *scan = &path[strlen(path) - 1];

    while (*scan != '/' && scan != path) {
        if (*scan == '.') {
            return;
        }
        scan--;
    }

    Q_strncpyz(oldPath, path, sizeof(oldPath));
    Com_sprintf(path, maxSize, "%s%s", oldPath, extension);
}

int32_t Com_BitCheck(const uint32_t *bits, int32_t bitNum)
{
    return (int32_t)((bits[bitNum >> COM_BIT_WORD_SHIFT] >>
                      (bitNum & COM_BIT_INDEX_MASK)) &
                     1U);
}

void Com_BitSet(uint32_t *bits, int32_t bitNum)
{
    bits[bitNum >> COM_BIT_WORD_SHIFT] |=
        1U << (bitNum & COM_BIT_INDEX_MASK);
}

void Com_BitClear(uint32_t *bits, int32_t bitNum)
{
    bits[bitNum >> COM_BIT_WORD_SHIFT] &=
        ~(1U << (bitNum & COM_BIT_INDEX_MASK));
}
