/*
===========================================================================
Copyright (C) 2015 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

OpenMoHAA source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// configurator.cpp: Config class for INI files.

#include "configurator.h"

CLASS_DECLARATION(Class, Configurator, NULL) {
    {NULL, NULL}
};

Configurator::Configurator(const char *filename)
{
    Parse(filename);
}

Configurator::Configurator()
{
    m_bNoWrite   = false;
    m_bNeedWrite = false;
}

Configurator::~Configurator()
{
    Close();
}

size_t Configurator::GetLine(char *dest, const char *data, size_t size)
{
    const char *p = data;
    size_t      i = 0;

    while (*p == '\n') {
        p++;
    }

    if (p >= data + size) {
        return 0;
    }

    while (*p && *p != '\n' && i < size) {
        dest[i++] = *p++;
    }

    if (i < size) {
        dest[i++] = '\n';
    }

    dest[i] = 0;

    return p - data;
}

void Configurator::RemoveSection(configSection_t *section)
{
    configKey_t *key;
    int          i;

    for (i = 0; i < section->key.NumObjects(); i++) {
        key = section->key[i];
        RemoveKey(section, key);
    }

    m_reverseSections.RemoveObject(section);
    m_sections.remove(section->name);
}

void Configurator::RemoveKey(configSection_t *section, configKey_t *key)
{
    section->key.RemoveObject(key);
    delete key;
}

configSection_t *Configurator::CreateSection(const char *section)
{
    configSection_t *s = &m_sections.addKeyValue(section);

    if (s->key.NumObjects()) {
        RemoveSection(s);
        s = &m_sections.addKeyValue(section);
    } else {
        m_reverseSections.AddObject(s);
    }

    s->name = section;
    return s;
}

int Configurator::GetKeyArray(char *key)
{
    int    count;
    size_t len;
    int    arrayindex = -1;
    char  *p1, *p2;

    len = strlen(key) + 1;
    p1  = new char[len];
    p2  = new char[len];

    count = sscanf(key, "%[^[] [%[^]]", p1, p2);
    if (count == 2) {
        // Check for an array [num]
        strcpy(key, p1);
        arrayindex = atoi(p2);
        if (arrayindex < 0 || arrayindex > MAX_CONFIGURATOR_ARRAY_SIZE) {
            arrayindex = -2;
        }
    }

    delete[] p1;
    delete[] p2;

    return arrayindex;
}

int Configurator::GetKeyArray(str& key)
{
    int    count;
    size_t len;
    int    arrayindex = -1;
    str    arrayval;
    char  *p1, *p2;

    len = key.length() + 1;
    p1  = new char[len];
    p2  = new char[len];

    count = sscanf(key, "%[^[] [%[^]]", p1, p2);
    if (count == 2) {
        // Check for an array [num]
        arrayval   = p2;
        arrayindex = atoi(arrayval.c_str());

        key = p1;

        if (arrayindex < 0 || arrayindex > MAX_CONFIGURATOR_ARRAY_SIZE) {
            arrayindex = -2;
        }
    }

    delete[] p1;
    delete[] p2;

    return arrayindex;
}

configKey_t *Configurator::CreateKey(configSection_t *section, const char *key, unsigned int *index)
{
    configKey_t *k;
    str          keyname;
    int          arrayindex;

    if (index) {
        *index = 0;
    }

    keyname = key;

    arrayindex = GetKeyArray(keyname);
    if (arrayindex == -2) {
        return NULL;
    } else if (arrayindex >= 0) {
        if (index) {
            *index = arrayindex;
        }
    }

    k = FindKey(section, keyname);
    if (k) {
        if (arrayindex == -1 || !k->value.NumObjects()) {
            RemoveKey(section, k);
            k = NULL;
        }
    }

    if (!k) {
        k = new configKey_t;
        section->key.AddObject(k);
        k->name         = keyname;
        k->m_bArray     = arrayindex != -1;
        k->m_bNeedWrite = false;
    }

    if (arrayindex >= k->value.MaxObjects()) {
        k->value.Resize((arrayindex + 1) * 2);
    }

    return k;
}

configSection_t *Configurator::FindSection(const char *section)
{
    configSection_t *s = m_sections.findKeyValue(section);
    return s;
}

configKey_t *Configurator::FindKey(configSection_t *section, const char *key)
{
    configKey_t *k;
    int          i;

    for (i = 0; i < section->key.NumObjects(); i++) {
        k = section->key[i];
        if (!k->name.icmp(key)) {
            return k;
        }
    }

    return NULL;
}

int Configurator::CutLine(char *data)
{
    char *p = data;
    while (*p == '\n') {
        *p++ = 0;
    }
    while (*p && *p != '\n') {
        *p++ = 0;
    }
    return p - data;
}

bool Configurator::SetupLine(char *line, int& lineno, size_t& len, size_t& last)
{
    lineno++;
    len = (int)strlen(line) - 1;
    if (len <= 0) {
        return false;
    }

    if (line[len] != '\n') {
        last = 0;
        glbs.DPrintf("Configurator::SetupLine: input line too long in %s (line %d)\n", m_filename.c_str(), lineno);
        return false;
    }

    while ((len >= 0) && ((line[len] == '\n') || (isspace(line[len])))) {
        line[len] = 0;
        len--;
    }
    if (line[len] == '\\') {
        last = len;
        return false;
    } else {
        last = 0;
    }

    return true;
}

void Configurator::WriteData(char **data, size_t *size)
{
    configSection_t *section;
    configKey_t     *key;
    configValue_t   *val;
    str              value;
    char            *oldData     = *data;
    char            *currentData = *data;
    size_t           currentSize = *size;
    size_t           len, tlen = 0;
    int              i, j, k;
    size_t           offset, offset2;
    bool             bFound;

    for (i = 1; i <= m_reverseSections.NumObjects(); i++) {
        section = m_reverseSections.ObjectAt(i);

        if (!FindData(LINE_SECTION, section->name, NULL, &offset, currentData, currentSize)) {
            oldData = currentData;

            len = (offset ? 2 : 0) + 1 + section->name.length() + 1; // \n\n + [ + section + ]
            currentSize += len;
            currentData = new char[currentSize + 1];
            strncpy(currentData, oldData, offset);
            Com_sprintf(
                currentData + offset, currentSize + 1 - offset, offset ? "\n\n[%s]" : "[%s]", section->name.c_str()
            );
            Q_strncpyz(currentData + offset + len, oldData + offset, currentSize + 1 - offset - len);

            if (currentData != oldData) {
                delete[] oldData;
            }
        }

        for (j = 0; j < section->key.NumObjects(); j++) {
            key = section->key[j];
            if (!key->m_bNeedWrite) {
                continue;
            }

            if (key->m_bArray) {
                for (k = 0; k < key->value.NumObjects(); k++) {
                    val = &key->value[k];
                    if (!val->m_bNeedWrite) {
                        continue;
                    }
                    value = val->value;

                    bFound = FindData(
                        LINE_VALUE, section->name, key->name + "[" + k + "]", &offset, currentData, currentSize
                    );
                    {
                        int digits, l;

                        oldData = currentData;

                        l      = k;
                        digits = 0;
                        if (!l) {
                            l = 1;
                        }
                        while (l) {
                            l /= 10;
                            digits++;
                        }

                        if (bFound) {
                            len = CutLine(currentData + offset);
                            currentSize -= len;
                            offset2 = offset + len;
                        } else {
                            offset2 = offset;
                        }

                        len = 1 + key->name.length() + 1 + digits + 1 + 1
                            + value.length(); // \n + name + [ + digits + ] + = + value
                        currentSize += len;
                        currentData = new char[currentSize + 1];
                        strncpy(currentData, oldData, offset);
                        Com_sprintf(
                            currentData + offset,
                            currentSize + 1 - offset,
                            "\n%s[%d]=%s",
                            key->name.c_str(),
                            k,
                            value.c_str()
                        );
                        Q_strncpyz(currentData + offset + len, oldData + offset2, currentSize + 1 - offset - len);

                        if (currentData != oldData) {
                            delete[] oldData;
                        }
                    }
                }
            } else {
                val = &key->value[0];
                if (!val->m_bNeedWrite) {
                    continue;
                }
                value = val->value;

                bFound = FindData(LINE_VALUE, section->name, key->name, &offset, currentData, currentSize);
                {
                    oldData = currentData;

                    if (bFound) {
                        len = CutLine(currentData + offset);
                        currentSize -= len;
                        offset2 = offset + len;
                    } else {
                        offset2 = offset;
                    }

                    len = 1 + key->name.length() + 1 + value.length(); // \n + name + = + value
                    currentSize += len;
                    currentData = new char[currentSize + 1];
                    strncpy(currentData, oldData, offset);
                    Com_sprintf(
                        currentData + offset, currentSize + 1 - offset "\n%s=%s", key->name.c_str(), value.c_str()
                    );
                    Q_strncpyz(currentData + offset + len, oldData + offset2, currentSize + 1 - offset - len);

                    if (currentData != oldData) {
                        delete[] oldData;
                    }
                }
            }
        }
    }

    if (currentData[currentSize - 1] != '\n') {
        currentData[currentSize] = '\n';
        currentSize++;
    }

    *data = currentData;
    *size = currentSize;
}

void Configurator::WriteData2(char **data, size_t *size)
{
    configSection_t *section;
    configKey_t     *key;
    configValue_t   *val;
    str              value;
    char            *oldData     = *data;
    char            *currentData = *data;
    size_t           currentSize = *size;
    char            *p           = currentData;
    size_t           tlen        = 0;
    int              i, j, k;

    for (i = 1; i <= m_reverseSections.NumObjects(); i++) {
        section = m_reverseSections.ObjectAt(i);

        if (i == 1) {
            tlen += 1 + section->name.length() + 1 + 1; // [ + name + ] + \n
        } else {
            tlen += 1 + 1 + section->name.length() + 1 + 1; // \n + [ name + ] + \n
        }

        if (tlen > currentSize) {
            oldData = currentData;

            currentSize = tlen + 1024;
            currentData = new char[currentSize];
            p           = currentData + (p - oldData);
            Q_strncpyz(currentData, oldData, currentSize);

            delete[] oldData;
        }

        if (i == 1) {
            p += Com_sprintf(p, currentSize - (p - currentData), "[%s]\n", section->name.c_str());
        } else {
            p += Com_sprintf(p, currentSize - (p - currentData), "\n[%s]\n", section->name.c_str());
        }

        for (j = 0; j < section->key.NumObjects(); j++) {
            key = section->key[j];
            if (!key->m_bNeedWrite) {
                continue;
            }

            if (key->m_bArray) {
                for (k = 0; k < key->value.NumObjects(); k++) {
                    val = &key->value[k];
                    if (!val->m_bNeedWrite) {
                        continue;
                    }
                    value = val->value;

                    {
                        int digits, l;

                        l      = k;
                        digits = 0;
                        if (!l) {
                            l = 1;
                        }
                        while (l) {
                            l /= 10;
                            digits++;
                        }

                        tlen += key->name.length() + 1 + digits + 1 + 1 + value.length()
                              + 1; // name + [ + digits + ] + = + value + \n
                        if (tlen > currentSize) {
                            oldData = currentData;

                            currentSize = tlen + 1024;
                            currentData = new char[currentSize];
                            p           = currentData + (p - oldData);
                            strcpy(currentData, oldData);

                            delete[] oldData;
                        }

                        p += Com_sprintf(
                            p, currentSize - (p - currentData), "%s[%d]=%s\n", key->name.c_str(), k, value.c_str()
                        );
                    }
                }
            } else {
                val = &key->value[0];
                if (!val->m_bNeedWrite) {
                    continue;
                }
                value = val->value;

                tlen += key->name.length() + 1 + value.length() + 1; // name + = + value + \n
                if (tlen > currentSize) {
                    oldData = currentData;

                    currentSize = tlen + 1024;
                    currentData = new char[currentSize];
                    p           = currentData + (p - oldData);
                    strcpy(currentData, oldData);

                    delete[] oldData;
                }

                p += Com_sprintf(p, currentSize - (p - currentData), "%s=%s\n", key->name.c_str(), value.c_str());
            }
        }
    }

    *data = currentData;
    *size = tlen;
}

bool Configurator::FindData(int type, const char *s, const char *k, size_t *offset, const char *data, size_t size)
{
    static char line[MAX_STRING_TOKENS + 1];
    static char section[MAX_STRING_TOKENS + 1];
    static char key[MAX_STRING_TOKENS + 1];
    static char val[MAX_STRING_TOKENS + 1];

    size_t last = 0;
    size_t len;
    size_t olen   = 0;
    size_t tlen   = 0;
    int    lineno = 0;
    int    arrayindex;

    bool bFoundSection = false;

    *offset = 0;

    while ((olen = GetLine(line + last, data + tlen, MAX_STRING_TOKENS - last)) != 0) {
        tlen += olen;
        if (!SetupLine(line + last, lineno, len, last)) {
            continue;
        }

        switch (ParseLine(line, section, key, val)) {
        case LINE_SECTION:
            if (bFoundSection) {
                // Return the last line of the section to append
                *offset = tlen - olen;
                return false;
            }

            if (!Q_stricmp(s, section)) {
                bFoundSection = true;

                if (type == LINE_SECTION) {
                    *offset = tlen;
                    return true;
                }
            }
            break;

        case LINE_VALUE:
            arrayindex = GetKeyArray(key);
            if (arrayindex >= 0) {
                Com_sprintf(key, sizeof(key), "%s[%d]", key, arrayindex);
            }

            if (type == LINE_VALUE && bFoundSection && !Q_stricmp(k, key)) {
                *offset = tlen - olen;
                return true;
            }
            break;

        default:
            break;
        }
    }

    *offset = tlen;
    return false;
}

void Configurator::ParseData(const char *data, size_t size)
{
    static char line[MAX_STRING_TOKENS + 1];
    static char section[MAX_STRING_TOKENS + 1];
    static char key[MAX_STRING_TOKENS + 1];
    static char val[MAX_STRING_TOKENS + 1];

    size_t       last = 0;
    size_t       len;
    size_t       tlen   = 0;
    int          lineno = 0;
    unsigned int index  = 0;

    configSection_t *current;
    configKey_t     *k;
    configValue_t    v;

    while ((len = GetLine(line + last, data + tlen, MAX_STRING_TOKENS - last)) != 0) {
        tlen += len;
        if (!SetupLine(line + last, lineno, len, last)) {
            continue;
        }

        switch (ParseLine(line, section, key, val)) {
        case LINE_EMPTY:
        case LINE_COMMENT:
            break;

        case LINE_SECTION:
            current = CreateSection(section);
            break;

        case LINE_VALUE:
            k = CreateKey(current, key, &index);
            if (!k) {
                glbs.DPrintf("Configurator::ParseData: Could not create key:\n");
                goto __error;
            }
            v.m_bNeedWrite = false;
            v.value        = val;
            k->value.AddObjectAt(index + 1, v);
            break;

        case LINE_ERROR:
            glbs.DPrintf("Configurator::ParseData: syntax error:\n");

        __error:
            glbs.DPrintf("(%s, %d):", m_filename.c_str(), lineno);
            glbs.DPrintf("%s\n", line);
            break;

        default:
            break;
        }
    }
}

int Configurator::ParseLine(char *line, char *section, char *key, char *value)
{
    size_t len;
    int    result;

    len = strlen(line);

    if (len < 1) {
        result = LINE_EMPTY;
    } else if (line[0] == ';') {
        result = LINE_COMMENT;
    } else if (line[0] == '[' && line[len - 1] == ']') {
        sscanf(line, "[%[^]]", section);
        strcpy(section, strstrip(section));
        strlwc(section);

        result = LINE_SECTION;
    } else if ((sscanf(line, "%[^=] = \"%[^\"]\"", key, value)) == 2
               || (sscanf(line, "%[^=] = %[^;]", key, value)) == 2) {
        strcpy(key, strstrip(key));
        strlwc(key);
        strcpy(value, strstrip(value));

        // sscanf cannot handle empty "" values
        if (!strcmp(value, "\"\"")) {
            value = "";
        }

        result = LINE_VALUE;
    } else if ((sscanf(line, "%[^=] = %[;]", key, value)) == 2 || (sscanf(line, "%[^=] %[=]", key, value)) == 2) {
        strcpy(key, strstrip(key));
        strlwc(key);
        value[0] = 0;

        result = LINE_VALUE;
    } else {
        result = LINE_ERROR;
    }

    return result;
}

str Configurator::GetValue(const char *section, const char *key, str defaultValue, int index)
{
    configKey_t  *k = GetKey(section, key, index);
    configValue_t val;

    if (!k) {
        return defaultValue;
    }

    if (index < 0) {
        index = 0;
    }

    if (k->m_bNeedWrite) {
        val.m_bNeedWrite = true;
        val.value        = defaultValue;
        k->value.AddObjectAt(index + 1, val);
    }

    return k->value[index].value;
}

configKey_t *Configurator::GetKey(const char *section, const char *key, int index)
{
    configSection_t *s;
    configKey_t     *k;

    bool bArray = index >= 0;
    if (index < 0) {
        index = 0;
    }

    s = FindSection(section);
    if (!s) {
        if (m_bNoWrite) {
            return NULL;
        }

        s               = CreateSection(section);
        s->m_bNeedWrite = true;
        m_bNeedWrite    = true;
    }

    k = FindKey(s, key);
    if (!k) {
        if (m_bNoWrite) {
            return NULL;
        }

        str keyname;
        if (bArray) {
            keyname = str(key) + "[" + str(index) + "]";
        } else {
            keyname = key;
        }
        k               = CreateKey(s, keyname, NULL);
        k->m_bNeedWrite = true;
        m_bNeedWrite    = true;
    } else if (index >= k->value.NumObjects()) {
        k->m_bNeedWrite = true;
        m_bNeedWrite    = true;
    }

    return k;
}

//======================================
// Configurator::GetString
//======================================
str Configurator::GetString(const char *section, const char *key, str defaultValue, int index)
{
    str value = GetValue(section, key, defaultValue, index);
    return value;
}

//======================================
// Configurator::GetInteger
//======================================
int Configurator::GetInteger(const char *section, const char *key, int defaultValue, int index)
{
    str value = GetValue(section, key, defaultValue, index);
    return atoi(value.c_str());
}

//======================================
// Configurator::GetFloat
//======================================
float Configurator::GetFloat(const char *section, const char *key, float defaultValue, int index)
{
    str value = GetValue(section, key, defaultValue, index);
    return atof(value.c_str());
}

//======================================
// Configurator::SetString
//======================================
void Configurator::SetString(const char *section, const char *key, str value, int index)
{
    configKey_t  *k = GetKey(section, key, index);
    configValue_t v;

    if (!k) {
        return;
    }

    if (index < 0) {
        index = 0;
    }

    if (index >= k->value.NumObjects() || k->value[index].value != value) {
        m_bNeedWrite    = true;
        v.m_bNeedWrite  = true;
        v.value         = value;
        k->m_bNeedWrite = true;
        k->value.AddObjectAt(index + 1, v);
    }
}

//======================================
// Configurator::SetInteger
//======================================
void Configurator::SetInteger(const char *section, const char *key, int value, int index)
{
    return SetString(section, key, value, index);
}

//======================================
// Configurator::SetFloat
//======================================
void Configurator::SetFloat(const char *section, const char *key, float value, int index)
{
    return SetString(section, key, value, index);
}

//======================================
// Configurator::SetWrite
//======================================
void Configurator::SetWrite(bool bWrite)
{
    m_bNoWrite = !bWrite;
}

//======================================
// Configurator::Parse
//======================================
void Configurator::Parse(const char *filename)
{
    FILE  *file;
    size_t size;
    char  *buffer;

    m_bNoWrite   = false;
    m_bNeedWrite = false;

    this->m_filename = filename;

    file = fopen(filename, "r");
    if (!file) {
        return;
    }

    fseek(file, 0, SEEK_END);
    size = ftell(file);
    rewind(file);

    buffer       = new char[size + 1];
    size         = fread(buffer, 1, size, file);
    buffer[size] = 0;

    fclose(file);

    ParseData(buffer, size);
}

//======================================
// Configurator::Close
//======================================
void Configurator::Close(void)
{
    FILE  *file;
    char  *buffer;
    int    i;
    size_t size;

    if (!m_filename.length() || !m_bNeedWrite) {
        goto _cleanup;
    }

    file = fopen(m_filename.c_str(), "r+");
    if (!file) {
        file = fopen(m_filename.c_str(), "w+");
        if (!file) {
            return;
        }
        file = freopen(m_filename.c_str(), "r+", file);
        if (!file) {
            return;
        }
    }

    fseek(file, 0, SEEK_END);
    size = ftell(file);
    rewind(file);

    buffer       = new char[size + 1];
    size         = fread(buffer, 1, size, file);
    buffer[size] = 0;

    glbs.DPrintf("Configurator::Close: Writing data to %s...\n", m_filename.c_str());
    WriteData(&buffer, &size);
    glbs.DPrintf("Configurator::Close: Done.\n");
    file = freopen(m_filename.c_str(), "w+", file);
    fwrite(buffer, size, 1, file);
    fclose(file);

    m_filename = "";

    delete[] buffer;

_cleanup:
    for (i = m_reverseSections.NumObjects(); i > 0; i--) {
        RemoveSection(m_reverseSections.ObjectAt(i));
    }
}

void test_config(void)
{
    Configurator config("test.ini");
    str          value;

    value = config.GetString("section1", "testvar1", "100");
    glbs.DPrintf("value = %s\n", value.c_str());
    value = config.GetString("section1", "testarray1", "100", 0);
    glbs.DPrintf("array1 = %s\n", value.c_str());
    value = config.GetString("section1", "testarray1", "100", 1);
    glbs.DPrintf("array2 = %s\n", value.c_str());
    value = config.GetString("section1", "testarray1", "100", 2);
    glbs.DPrintf("array3 = %s\n", value.c_str());

    value = config.GetString("section1", "testforcewrite", "500");
    glbs.DPrintf("testforcewrite = %s\n", value.c_str());

    value = config.GetString("section2", "testforcewrite2", "500");
    glbs.DPrintf("testforcewrite2 = %s\n", value.c_str());

    //for( int i = 0; i < 2048; i++ )
    //{
    //	value = config.GetString( "section1", "testforcewrite5", "2048", i );
    //}

    config.SetString("section1", "testforcewrite5", "newstring", 2048);

    config.Close();
}
