#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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_COM_ERROR_FATAL = 0,
    SOUND_ALIAS_FILE_HANDLE_CLOSED = 0,
    SOUND_ALIAS_FILE_READ_ERROR = -1,
    SOUND_ALIAS_MAX_CSV_COLUMNS = 256,
    SOUND_ALIAS_CSV_FIELD_BUFFER_SIZE = 1024,
    SOUND_ALIAS_OSPATH_SIZE = 256,
    SOUND_ALIAS_SUBTITLE_REFERENCE_PREFIX_CHARS = 9,
    SOUND_ALIAS_SUBTITLE_LOCALIZED_NO = 0,
    SOUND_ALIAS_SUBTITLE_LOCALIZED_YES = 1,
    SOUND_ALIAS_FSEEK_START = SEEK_SET,
    SOUND_ALIAS_FSEEK_END = SEEK_END,
    SOUND_ALIAS_FILE_TRANSFER_ELEMENT_SIZE = 1
};

#define SOUND_ALIAS_EMPTY_STRING ""
#define SOUND_ALIAS_SUBTITLE_PREFIX "SUBTITLE_"
#define SOUND_ALIAS_SUBTITLE_STRING_FILE "soundaliases/subtitle.st"
#define SOUND_ALIAS_SOURCE_SUBTITLE_STRING_FILE \
    "../source_data/string_resources/subtitle.st"
#define SOUND_ALIAS_TEMP_CSV_FILE "soundaliases/temp.csv"
#define SOUND_ALIAS_CSV_UPDATE_MODE "r+"
#define SOUND_ALIAS_STRINGED_READ_BINARY_MODE "rb"
#define SOUND_ALIAS_STRINGED_WRITE_BINARY_MODE "wb"
#define SOUND_ALIAS_CSV_EXTENSION "csv"
#define SOUND_ALIAS_CSV_DIRECTORY "soundaliases"
#define SOUND_ALIAS_CSV_QPATH_FORMAT "soundaliases/%s"
#define SOUND_ALIAS_CSV_CRLF "\r\n"
#define SOUND_ALIAS_CSV_CR "\r"
#define SOUND_ALIAS_CSV_COMMA ","
#define SOUND_ALIAS_CSV_HEADER_CHATEAU "#Chateau"

static const char *const soundAlias_localizeColumnNameTable
    [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"
};

/* NOT_FROM_ORIGINAL_SOURCE:
 * Local factoring of the repeated comma/space/newline
 * quote check emitted inline in Com_LocalizeSoundAliasCsvFile.
 */
static qboolean coduo_sound_alias_csv_field_needs_quotes(const char *text)
{
    if (strchr(text, ',') != NULL) {
        return qtrue;
    }

    if (strchr(text, ' ') != NULL) {
        return qtrue;
    }

    if (strchr(text, '\n') != NULL) {
        return qtrue;
    }

    if (strchr(text, '\r') != NULL) {
        return qtrue;
    }

    return qfalse;
}

void Com_LocalizeSoundAliasCsvFile(const char *csvPath,
                                   const char *unusedPath)
{
    char *fileBuffer;
    char *parseCursor;
    char *rowStart;
    char *rowEnd;
    char *token;
    const char *outputText;
    FILE *writableProbe;
    int32_t outputHandle;
    coduo_sound_alias_parse_column_value_t
        columnMap[SOUND_ALIAS_MAX_CSV_COLUMNS];
    coduo_sound_alias_parse_seen_column_table_t seenColumns;
    coduo_sound_alias_parse_node_t parseNode;
    char cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SLOT_COUNT]
                   [SOUND_ALIAS_CSV_FIELD_BUFFER_SIZE];
    char subtitleReference[SOUND_ALIAS_CSV_FIELD_BUFFER_SIZE];
    char tempCsvOSPath[SOUND_ALIAS_OSPATH_SIZE];
    char csvOSPath[SOUND_ALIAS_OSPATH_SIZE];
    char csvWritableOSPath[SOUND_ALIAS_OSPATH_SIZE];
    size_t textLength;
    int columnCount;
    int columnIndex;
    int localizedCount;
    int shouldLocalizeSubtitle;
    qboolean hasNameColumn;
    qboolean hasFileColumn;

    (void)unusedPath;

    FS_BuildOSPath(fs_basepath->string, fs_currentGameDir, csvPath,
                   csvWritableOSPath);
    Com_Printf("Processing sound alias file %s..\n", csvWritableOSPath);

    writableProbe = fopen(csvWritableOSPath, SOUND_ALIAS_CSV_UPDATE_MODE);
    if (writableProbe == NULL) {
        Com_Printf("WARNING: Can not write to sound alias file %s\n",
                   csvWritableOSPath);
        return;
    }

    fclose(writableProbe);
    if (FS_ReadFile(csvPath, (void **)&fileBuffer) < 0) {
        Com_Printf("WARNING: Could not read sound alias file %s\n", csvPath);
        return;
    }

    outputHandle = FS_FOpenFileWrite(SOUND_ALIAS_TEMP_CSV_FILE);
    if (outputHandle == SOUND_ALIAS_FILE_HANDLE_CLOSED) {
        Com_Printf("WARNING: Could not open output file %s for writing\n",
                   SOUND_ALIAS_TEMP_CSV_FILE);
        return;
    }

    Com_BeginParseSession(csvPath);
    Com_SetCSV(qtrue);

    parseCursor = fileBuffer;
    columnCount = 0;
    localizedCount = 0;

    while (parseCursor != NULL) {
        while (*parseCursor == '\r') {
            parseCursor++;
        }

        if (*parseCursor == '\n') {
            parseCursor++;
            FS_Write(SOUND_ALIAS_CSV_CRLF,
                     (int32_t)strlen(
                         SOUND_ALIAS_CSV_CRLF),
                     outputHandle);
        }

        rowStart = parseCursor;
        token = Com_Parse(&parseCursor);
        if (parseCursor == NULL) {
            break;
        }

        if (Q_stricmp(token, SOUND_ALIAS_CSV_HEADER_CHATEAU) == 0) {
            columnIndex = 0;
        }

        if (token[0] == '\0' || token[0] == '#') {
            Com_SkipRestOfLine(&parseCursor);
            if (rowStart[0] == '\n') {
                FS_Write(SOUND_ALIAS_CSV_CR,
                         (int32_t)strlen(
                             SOUND_ALIAS_CSV_CR),
                         outputHandle);
            }
            rowEnd = parseCursor;
            FS_Write(rowStart, (int32_t)(rowEnd - rowStart),
                     outputHandle);
            continue;
        }

        if (columnCount == 0) {
            hasNameColumn = qfalse;
            hasFileColumn = qfalse;

            do {
                columnMap[columnCount] =
                    CODUO_SOUND_ALIAS_PARSE_COLUMN_UNKNOWN;

                for (columnIndex = CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME;
                     columnIndex < CODUO_SOUND_ALIAS_PARSE_COLUMN_COUNT;
                     columnIndex++) {
                    if (Q_stricmp(
                            soundAlias_localizeColumnNameTable[columnIndex],
                            token) == 0) {
                        columnMap[columnCount] =
                            (coduo_sound_alias_parse_column_value_t)
                                columnIndex;
                        if (columnIndex ==
                            CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME) {
                            hasNameColumn = qtrue;
                        } else if (columnIndex ==
                                   CODUO_SOUND_ALIAS_PARSE_COLUMN_FILE) {
                            hasFileColumn = qtrue;
                        }
                        break;
                    }
                }

                columnCount++;
                if (columnCount == SOUND_ALIAS_MAX_CSV_COLUMNS ||
                    parseCursor == NULL || parseCursor[0] == '\n') {
                    break;
                }

                token = Com_ParseOnLine(&parseCursor);
            } while (1);

            if (!hasNameColumn || !hasFileColumn) {
                Com_Error(SOUND_ALIAS_FATAL_DROP,
                          "\x15" "Sound alias file %s: missing 'name' "
                                 "and/or 'file' columns\n",
                          soundAlias_currentFile);
            }

            Com_SkipRestOfLine(&parseCursor);
            if (rowStart[0] == '\n') {
                FS_Write(SOUND_ALIAS_CSV_CR,
                         (int32_t)strlen(
                             SOUND_ALIAS_CSV_CR),
                         outputHandle);
            }
            rowEnd = parseCursor;
            FS_Write(rowStart, (int32_t)(rowEnd - rowStart),
                     outputHandle);
            continue;
        }

        memset(seenColumns, 0, sizeof(seenColumns));
        Com_InitSoundAliasParseNode(&parseNode, qfalse);

        for (columnIndex = 0; columnIndex < columnCount; columnIndex++) {
            strcpy(cellValues[columnMap[columnIndex]], token);
            if (token[0] != '\0') {
                Com_ParseSoundAliasColumn(
                    SOUND_ALIAS_EMPTY_STRING, token, columnMap[columnIndex],
                    seenColumns, &parseNode);
            }

            if (columnIndex != columnCount - 1) {
                token = Com_ParseOnLine(&parseCursor);
            }
        }

        if (seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME] == 0 ||
            seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_FILE] == 0) {
            Com_Error(SOUND_ALIAS_FATAL_DROP,
                      "\x15" "Sound alias file %s: alias entry missing name "
                             "and/or file\n",
                      soundAlias_currentFile);
        }

        shouldLocalizeSubtitle = SOUND_ALIAS_SUBTITLE_LOCALIZED_NO;
        if (seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE] != 0) {
            textLength =
                strlen(cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]);
            for (columnIndex = 0;
                 columnIndex < (int)textLength &&
                 ((cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]
                            [columnIndex] > '@' &&
                   cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]
                            [columnIndex] < '[') ||
                  (cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]
                            [columnIndex] > '/' &&
                   cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]
                            [columnIndex] < ':') ||
                  cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]
                            [columnIndex] == '_');
                 columnIndex++) {
            }

            if (columnIndex < (int)textLength ||
                Q_strncmp(cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE],
                          SOUND_ALIAS_SUBTITLE_PREFIX,
                          SOUND_ALIAS_SUBTITLE_REFERENCE_PREFIX_CHARS) != 0 ||
                !Com_SoundAliasSubtitleReferenceExists(
                    cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE])) {
                shouldLocalizeSubtitle = SOUND_ALIAS_SUBTITLE_LOCALIZED_YES;
            }
        }

        if (shouldLocalizeSubtitle == SOUND_ALIAS_SUBTITLE_LOCALIZED_NO) {
            Com_SkipRestOfLine(&parseCursor);
            rowEnd = parseCursor;
            FS_Write(rowStart, (int32_t)(rowEnd - rowStart),
                     outputHandle);
            continue;
        }

        for (columnIndex = 0; columnIndex < columnCount; columnIndex++) {
            coduo_sound_alias_parse_column_value_t mappedColumn =
                columnMap[columnIndex];

            if (mappedColumn == CODUO_SOUND_ALIAS_PARSE_COLUMN_UNKNOWN ||
                seenColumns[mappedColumn] == 0) {
                if (columnIndex != columnCount - 1) {
                    FS_Write(SOUND_ALIAS_CSV_COMMA,
                             (int32_t)strlen(
                                 SOUND_ALIAS_CSV_COMMA),
                             outputHandle);
                }
            } else if (mappedColumn ==
                       CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE) {
                outputText = Com_SoundAliasSubtitleReferenceForText(
                    cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]);
                if (outputText == NULL) {
                    if (seenColumns[CODUO_SOUND_ALIAS_PARSE_COLUMN_SEQUENCE] ==
                        0) {
                        Com_sprintf(
                            subtitleReference, sizeof(subtitleReference),
                            "%s%s", SOUND_ALIAS_SUBTITLE_PREFIX,
                            cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME]);
                    } else {
                        Com_sprintf(
                            subtitleReference, sizeof(subtitleReference),
                            "%s%s_%s", SOUND_ALIAS_SUBTITLE_PREFIX,
                            cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_NAME],
                            cellValues
                                [CODUO_SOUND_ALIAS_PARSE_COLUMN_SEQUENCE]);
                    }
                    outputText = Q_strupr(subtitleReference);
                    Com_UpdateSoundAliasSubtitleFile(
                        subtitleReference,
                        cellValues[CODUO_SOUND_ALIAS_PARSE_COLUMN_SUBTITLE]);
                    localizedCount++;
                } else {
                    Com_sprintf(subtitleReference, sizeof(subtitleReference),
                                "%s%s", SOUND_ALIAS_SUBTITLE_PREFIX,
                                outputText);
                    outputText = Q_strupr(subtitleReference);
                }

                textLength = strlen(outputText);
                FS_Write(outputText, (int32_t)textLength,
                         outputHandle);
            } else {
                const char *cellText = cellValues[mappedColumn];
                qboolean needsQuotes =
                    coduo_sound_alias_csv_field_needs_quotes(cellText);

                if (columnIndex == columnCount - 1) {
                    if (needsQuotes) {
                        outputText = va("\"%s\"", cellText);
                    } else {
                        outputText = va("%s", cellText);
                    }
                } else {
                    if (needsQuotes) {
                        outputText = va("\"%s\",", cellText);
                    } else {
                        outputText = va("%s,", cellText);
                    }
                }

                textLength = strlen(outputText);
                FS_Write(outputText, (int32_t)textLength,
                         outputHandle);
            }
        }

        FS_Write(SOUND_ALIAS_CSV_CRLF,
                 (int32_t)strlen(SOUND_ALIAS_CSV_CRLF),
                 outputHandle);
        Com_SkipRestOfLine(&parseCursor);
    }

    Com_EndParseSession();
    FS_FCloseFile(outputHandle);

    FS_BuildOSPath(fs_basepath->string, fs_currentGameDir,
                   SOUND_ALIAS_TEMP_CSV_FILE, tempCsvOSPath);
    FS_BuildOSPath(fs_basepath->string, fs_currentGameDir, csvPath,
                   csvOSPath);
    if (localizedCount != 0) {
        FS_Copyfiles(tempCsvOSPath, csvOSPath);
    }
    FS_Remove(tempCsvOSPath);

    Com_Printf("Localized %i sound alias subtitles\n", localizedCount);
}

void COM_WriteFinalStringEdFile(const char *sourcePath, const char *destPath)
{
    FILE *file;
    size_t fileLength;
    void *buffer;
    size_t transferCount;

    file = fopen(sourcePath, SOUND_ALIAS_STRINGED_READ_BINARY_MODE);
    if (file == NULL) {
        return;
    }

    fseek(file, 0, SOUND_ALIAS_FSEEK_END);
    fileLength = (size_t)ftell(file);
    fseek(file, 0, SOUND_ALIAS_FSEEK_START);

    buffer = malloc(fileLength);
    transferCount = fread(buffer, SOUND_ALIAS_FILE_TRANSFER_ELEMENT_SIZE,
                          fileLength, file);
    if (transferCount != fileLength) {
        Com_Error(SOUND_ALIAS_COM_ERROR_FATAL,
                  "\x15" "Short read in COM_WriteFinalStringEdFile()\n");
    }

    fclose(file);
    file = fopen(destPath, SOUND_ALIAS_STRINGED_WRITE_BINARY_MODE);
    if (file == NULL) {
        free(buffer);
        return;
    }

    transferCount = fwrite(buffer, SOUND_ALIAS_FILE_TRANSFER_ELEMENT_SIZE,
                           fileLength, file);
    if (transferCount != fileLength) {
        Com_Error(SOUND_ALIAS_COM_ERROR_FATAL,
                  "\x15" "Short write in COM_WriteFinalStringEdFile()\n");
    }

    fclose(file);
    free(buffer);
}

void Com_LocalizeSoundAliasSubtitleText(void)
{
    char sourceStringEdPath[SOUND_ALIAS_OSPATH_SIZE];
    char localStringEdPath[SOUND_ALIAS_OSPATH_SIZE];
    char csvPath[SOUND_ALIAS_OSPATH_SIZE];
    FILE *writableProbe;
    char **soundAliasFiles;
    size_t sourcePathLength;
    int soundAliasFileCount;
    int fileIndex;

    FS_BuildOSPath(fs_homepath->string,
                   SOUND_ALIAS_SOURCE_SUBTITLE_STRING_FILE,
                   SOUND_ALIAS_EMPTY_STRING, sourceStringEdPath);
    sourcePathLength = strlen(sourceStringEdPath);
    sourceStringEdPath[sourcePathLength - 1] = '\0';

    writableProbe = fopen(sourceStringEdPath, SOUND_ALIAS_CSV_UPDATE_MODE);
    if (writableProbe == NULL) {
        Com_Printf("WARNING: Can not write to StringEd file %s\n",
                   sourceStringEdPath);
        return;
    }

    fclose(writableProbe);
    FS_BuildOSPath(fs_basepath->string, fs_currentGameDir,
                   SOUND_ALIAS_SUBTITLE_STRING_FILE, localStringEdPath);
    FS_Copyfiles(sourceStringEdPath, localStringEdPath);

    if (!FS_FileExists(SOUND_ALIAS_SUBTITLE_STRING_FILE)) {
        Com_Printf("WARNING: Could not make local copy of StringEd file %s\n",
                   SOUND_ALIAS_SUBTITLE_STRING_FILE);
        return;
    }

    Com_Printf("Localizing sound alias subtitle text...\n");
    Com_Printf("Writing to StringEd file %s\n", sourceStringEdPath);

    soundAliasFiles = FS_ListFiles(SOUND_ALIAS_CSV_DIRECTORY,
                                   SOUND_ALIAS_CSV_EXTENSION,
                                   &soundAliasFileCount);
    if (soundAliasFileCount == 0) {
        Com_Printf(
            "WARNING: can't find any sound alias files "
            "(soundaliases/*.csv)\n");
        return;
    }

    for (fileIndex = 0; fileIndex < soundAliasFileCount; fileIndex++) {
        Hunk_SetLowTempMark();
        Com_sprintf(csvPath, sizeof(csvPath), SOUND_ALIAS_CSV_QPATH_FORMAT,
                    soundAliasFiles[fileIndex]);
        Com_LocalizeSoundAliasCsvFile(csvPath, localStringEdPath);
        Hunk_ClearToLowTempMark();
    }

    FS_FreeFileList(soundAliasFiles);
    COM_WriteFinalStringEdFile(localStringEdPath, sourceStringEdPath);
    FS_Remove(localStringEdPath);
    Com_Printf("done\n");
}
