/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program 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 3 of the License, or
(at your option) any later version.

This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
/*****************************************************************************
*
*   $/Plasma20/Sources/Plasma/NucleusLib/pnIni/Private/pnIniCore.cpp
*   
***/

#include "../Pch.h"
#pragma hdrstop


/*****************************************************************************
*
*   Internal functions
*
***/

//===========================================================================
static wchar * TrimWhitespace (wchar * name) {
    while (isspace((char) *name))
        ++name;

    for (wchar * term = name; *term; ++term) {
        if (isspace((char) *term)) {
            *term = 0;
            break;
        }
    }

    return name;
}


/****************************************************************************
*
*   IniValue
*
***/

//===========================================================================
IniValue::IniValue (IniKey * key, unsigned lineNum)
:   fKey(key)
,   fLineNum(lineNum)
{
    fIndex = key->fValues.Add(this);
}

//===========================================================================
IniValue::~IniValue () {
    wchar ** cur = fArgs.Ptr();
    wchar ** end = fArgs.Term();
    for (; cur < end; ++cur)
        FREE(*cur);
}

//===========================================================================
static void AddValueString (
    IniValue *  value,
    const wchar src[]
) {
    unsigned chars = StrLen(src) + 1;
    wchar * dst = ALLOCA(wchar, chars);
    StrTokenize(&src, dst, chars, L" \t\r\n\"");
    value->fArgs.Add(StrDup(dst));
}


/****************************************************************************
*
*   IniKey
*
***/

//===========================================================================
IniKey::IniKey (IniSection * section, const wchar name[])
:   fSection(section)
{
    StrCopy(fName, name, (unsigned) -1);
    fSection->fKeys.Add(this);
}

//===========================================================================
IniKey::~IniKey () {
    IniValue ** cur = fValues.Ptr();
    IniValue ** end = fValues.Term();
    for (; cur < end; ++cur)
        DEL(*cur);
}

//===========================================================================
inline unsigned IniKey::GetHash () const {
    return StrHashI(fName);
}

//===========================================================================
inline bool IniKey::operator== (const CHashKeyStrPtrI & rhs) const {
    return !StrCmpI(fName, rhs.GetString());
}

//===========================================================================
static IniValue * AddKeyValue (
    IniSection *    section,
    wchar *         string,
    unsigned        lineNum
) {
    string = TrimWhitespace(string);

    // Find or create the key
    IniKey * key = section->fKeys.Find(string);
    if (!key) {
        key = new(ALLOC(
            sizeof(*key) - sizeof(key->fName) + StrBytes(string)
        )) IniKey(section, string);
    }

    // Add a new value holder for the key
    return NEW(IniValue)(key, lineNum);
}


/****************************************************************************
*
*   IniSection
*
***/

//===========================================================================
IniSection::IniSection (const wchar name[]) {
    StrCopy(fName, name, (unsigned) -1);
}

//===========================================================================
IniSection::~IniSection () {
    fKeys.Clear();
}

//===========================================================================
inline unsigned IniSection::GetHash () const {
    return StrHashI(fName);
}

//===========================================================================
inline bool IniSection::operator== (const CHashKeyStrPtrI & rhs) const {
    return !StrCmpI(fName, rhs.GetString());
}

//===========================================================================
static IniSection * AddSection (
    Ini *       ini,
    wchar *     string
) {
    // Find or create the section
    IniSection * section = ini->fSections.Find(string);
    if (!section) {
        section = new(ALLOC(
            sizeof(*section) - sizeof(section->fName) + StrBytes(string)
        )) IniSection(string);
        ini->fSections.Add(section);
    }
    return section;
}


/****************************************************************************
*
*   Ini
*
***/

//===========================================================================
Ini::~Ini () {
    fSections.Clear();
}


/****************************************************************************
*
*   ParseBuffer
*
***/

//===========================================================================
static void ParseBuffer (
    Ini *       ini,
    const wchar buffer[]
) {

    const wchar SECTION_OPEN_CHAR  = '[';
    const wchar SECTION_CLOSE_CHAR = ']';
    const wchar EQUIVALENCE_CHAR   = '=';
    const wchar VALUE_SEPARATOR    = ',';
    const wchar COMMENT_CHAR       = ';';
    const wchar QUOTE_CHAR         = '\"';
    const wchar NEWLINE            = '\n';

    enum {
        STATE_BEGIN,
        STATE_NEWLINE,
        STATE_SECTION,
        STATE_STRIP_TRAILING,
        STATE_KEY,
        STATE_VALUE,
    } state = STATE_BEGIN;

    IniSection * section    = nil;
    IniValue * value        = nil;
    const wchar * start     = nil;
    bool valInQuotes        = false;
    wchar dst[512];
    dst[0] = 0;

    for (unsigned lineNum = 1;; ++buffer) {

        // Get next character
        unsigned chr = *buffer;
        if (!chr)
            break;
        if (chr == '\r')
            continue;
        if (chr == '\n')
            ++lineNum;
        if (chr == '\t')
            chr = ' ';

        switch (state) {
            case STATE_BEGIN:
                ASSERT(chr == UNICODE_BOM);
                state = STATE_NEWLINE;
            break;

            case STATE_NEWLINE:
                if (chr == NEWLINE)
                    break;
                if (chr == ' ')
                    break;
                if (chr == SECTION_OPEN_CHAR) {
                    start = buffer + 1;
                    state = STATE_SECTION;
                }
                else if (chr == COMMENT_CHAR) {
                    state = STATE_STRIP_TRAILING;
                }
                else {
                    start = buffer;
                    state = STATE_KEY;
                }
            break;

            case STATE_SECTION:
                if (chr == NEWLINE) {
                    state = STATE_NEWLINE;
                    break;
                }
                
                if (chr == SECTION_CLOSE_CHAR) {
                    StrCopy(dst, start, min(buffer - start + 1, arrsize(dst)));
                    section = AddSection(ini, dst);
                    state = STATE_STRIP_TRAILING;
                }
            break;

            case STATE_STRIP_TRAILING:
                if (chr == NEWLINE)
                    state = STATE_NEWLINE;
            break;

            case STATE_KEY:
                if (chr == NEWLINE) {
                    state = STATE_NEWLINE;
                    break;
                }
                if (chr != EQUIVALENCE_CHAR)
                    break;

                if (!section) {
                    state = STATE_STRIP_TRAILING;
                    break;
                }

                StrCopy(dst, start, min(buffer - start + 1, arrsize(dst)));
                value = AddKeyValue(section, dst, lineNum);
                start = buffer + 1;
                state = STATE_VALUE;
                valInQuotes = false;
            break;

            case STATE_VALUE:
                if (chr == QUOTE_CHAR)
                    valInQuotes = !valInQuotes;
                if ((valInQuotes || chr != VALUE_SEPARATOR) && (chr != NEWLINE))
                    break;
                if (!value) {
                    state = chr == NEWLINE ? STATE_NEWLINE : STATE_STRIP_TRAILING;
                    break;
                }
                if (valInQuotes) {
                    state = chr == NEWLINE ? STATE_NEWLINE : STATE_STRIP_TRAILING;
                    break;
                }

                StrCopy(dst, start, min(buffer - start + 1, arrsize(dst)));
                AddValueString(value, dst);
                if (chr == VALUE_SEPARATOR)
                    start = buffer + 1;
                else
                    state = STATE_NEWLINE;
            break;
        }
    }

    // cleanup current value
    if (state == STATE_VALUE) {
        StrCopy(dst, start, min(buffer - start + 1, arrsize(dst)));
        AddValueString(value, dst);
    }
}


/****************************************************************************
*
*   ParseFile
*
***/

//===========================================================================
static void IniFileNotifyProc (
    AsyncFile         ,
    EAsyncNotifyFile  ,
    AsyncNotifyFile * ,
    void **
) {
}

//===========================================================================
static bool ParseFile (
    Ini *       ini,
    const wchar fileName[]
) {
    // Open file
    qword fileSize;
    qword fileLastWriteTime;
    EFileError error;
    AsyncFile file = AsyncFileOpen(
        fileName,
        IniFileNotifyProc,
        &error,
        kAsyncFileReadAccess,
        kAsyncFileModeOpenExisting,
        kAsyncFileShareRead,
        nil,
        &fileSize,
        &fileLastWriteTime
    );
    if (!file)
        return false;

    bool result;
    if (fileSize > 256 * 1024) {
        result = false;
    }
    else if (!fileSize) {
        result = true;
    }
    else {
        // Read entire file into memory and NULL terminate wchar
        byte * buffer = (byte *) ALLOC((unsigned) fileSize + sizeof(wchar));
        AsyncFileRead(file, 0, buffer, (unsigned) fileSize, kAsyncFileRwSync, nil);
        * (wchar *) &buffer[fileSize] = 0;

        // Convert to unicode if necessary
        if (* (wchar *) buffer != UNICODE_BOM) {
            byte * src  = buffer;
            // Allocate two extra spaces for UNICODE_BOM and terminator
            unsigned newBufferSize = ((unsigned) fileSize + 2) * sizeof(wchar);

            // Allocate new buffer
            wchar * dst = (wchar *) ALLOC(newBufferSize);
            
            // If it's UTF-8 file,convert to Unicode
            if (StrCmpI((char *)buffer, UTF8_BOM, StrLen(UTF8_BOM)) == 0) {
                // StrUtf8ToUnicode will convert UTF8_BOM to UNICODE_BOM
                StrUtf8ToUnicode(dst, (char *)src, newBufferSize);
            }
            else {
                // do simple conversion to Unicode
                dst[0] = UNICODE_BOM;
                for (unsigned index = 0;; ++index) {
                    if (0 == (dst[index + 1] = src[index]))
                        break;
                }
            }

            FREE(src);
            buffer = (byte *) dst;
        }

        ParseBuffer(ini, (const wchar *) buffer);
        FREE(buffer);
        result = true;
    }

    AsyncFileClose(file, kAsyncFileDontTruncate);
    return result;
}


/*****************************************************************************
*
*   Exports
*
***/

//===========================================================================
Ini * IniOpen (
    const wchar fileName[]
) {
    Ini * ini = NEW(Ini);
    if (!ParseFile(ini, fileName)) {
        IniClose(ini);
        return nil;
    }
    return ini;
}

//===========================================================================
void IniClose (Ini * ini) {
    DEL(ini);
}

//===========================================================================
const IniSection * IniGetFirstSection (
    const Ini *         ini,
    wchar *             name,
    unsigned            chars
) {
    if (chars)
        *name = 0;
    if (!ini)
        return nil;

    const IniSection * section = ini->fSections.Head();
    if (section)
        StrCopy(name, section->fName, chars);
    return section;
}

//===========================================================================
const IniSection * IniGetNextSection (
    const IniSection *  section,
    wchar *             name,
    unsigned            chars
) {
    if (chars)
        *name = 0;
    if (!section)
        return nil;

    section = section->fLink.Next();
    if (section)
        StrCopy(name, section->fName, chars);
    return section;
}

//===========================================================================
const IniSection * IniGetSection (
    const Ini *         ini,
    const wchar         name[]
) {
    if (!ini)
        return nil;

    return ini->fSections.Find(
        CHashKeyStrPtrI(name)
    );
}

//===========================================================================
const IniKey * IniGetFirstKey (
    const IniSection *  section,
    wchar *             name,
    unsigned            chars
) {
    if (chars)
        *name = 0;
    if (!section)
        return nil;

    const IniKey * key = section->fKeys.Head();
    if (key)
        StrCopy(name, key->fName, chars);
    return key;
}

//============================================================================
const IniKey * IniGetFirstKey (
    const Ini *         ini,
    const wchar         sectionName[],
    wchar *             name,
    unsigned            chars
) {
    if (const IniSection * section = IniGetSection(ini, sectionName))
        return IniGetFirstKey(section, name, chars);

    return nil;
}

//===========================================================================
const IniKey * IniGetNextKey (
    const IniKey *      key,
    wchar *             name,
    unsigned            chars
) {
    if (chars)
        *name = 0;
    if (!key)
        return nil;

    key = key->fLink.Next();
    if (key)
        StrCopy(name, key->fName, chars);
    return key;
}

//===========================================================================
const IniKey * IniGetKey (
    const IniSection *  section,
    const wchar         name[]
) {
    if (!section)
        return nil;

    return section->fKeys.Find(
        CHashKeyStrPtrI(name)
    );
}

//===========================================================================
const IniValue * IniGetFirstValue (
    const IniKey *      key,
    unsigned *          lineNum
) {
    if (lineNum)
        *lineNum = 0;

    const IniValue * value = nil;
    for (;;) {
        if (!key)
            break;

        value = key->fValues[0];
        if (lineNum)
            *lineNum = value->fLineNum;
        break;
    }

    return value;
}

//===========================================================================
const IniValue * IniGetFirstValue (
    const IniSection *  section,
    const wchar         keyName[],
    unsigned *          lineNum
) {
    const IniValue * value = nil;
    if (lineNum)
        *lineNum = 0;

    for (;;) {
        if (!section)
            break;

        const IniKey * key = section->fKeys.Find(
            CHashKeyStrPtrI(keyName)
        );
        value = IniGetFirstValue(key, lineNum);
        break;
    }

    return value;
}

//===========================================================================
const IniValue * IniGetFirstValue (
    const Ini *     ini,
    const wchar     sectionName[],
    const wchar     keyName[],
    unsigned *      lineNum
) {
    const IniValue * value = nil;
    if (lineNum)
        *lineNum = 0;

    for (;;) {
        if (!ini)
            break;

        const IniSection * section = ini->fSections.Find(
            CHashKeyStrPtrI(sectionName)
        );

        value = IniGetFirstValue(section, keyName, lineNum);
        break;
    }

    return value;
}

//===========================================================================
const IniValue * IniGetNextValue (
    const IniValue *    value,
    unsigned *          lineNum
) {
    if (lineNum)
        *lineNum = 0;

    const IniKey * key = value->fKey;
    if (value->fIndex + 1 < key->fValues.Count()) {
        value = key->fValues[value->fIndex + 1];
        if (lineNum)
            *lineNum = value->fLineNum;
    }
    else {
        value = nil;
    }

    return value;
}

//===========================================================================
bool IniGetUnsigned (
    const IniValue *    value,
    unsigned *          result,
    unsigned            index,
    unsigned            defaultValue
) {
    ASSERT(result);

    for (;;) {
        if (!value)
            break;
            
        wchar str[32];
        if (!IniGetString(value, str, arrsize(str), index, nil))
            break;

        if (!str[0])
            break;

        *result = StrToUnsigned(str, nil, 0);
        return true;
    }

    *result = defaultValue;
    return false;
}

//===========================================================================
bool IniGetString (
    const IniValue *    value,
    wchar *             result,
    unsigned            resultChars,
    unsigned            index,
    const wchar         defaultValue[]
) {
    ASSERT(result);

    bool found = value && index < value->fArgs.Count();

    if (found)
        StrCopy(result, value->fArgs[index], resultChars);
    else if (defaultValue)
        StrCopy(result, defaultValue, resultChars);

    return found;
}

//===========================================================================
bool IniGetUuid (
    const IniValue *    value,
    Uuid *              uuid,
    unsigned            index,
    const Uuid &        defaultValue
) {
    wchar str[128];
    if (IniGetString(value, str, arrsize(str), index, nil))
        return GuidFromString(str, uuid);
    else
        *uuid = defaultValue;

    return false;
}

//===========================================================================
unsigned IniGetBoundedValue (
    const IniValue *    value,
    const wchar         section[],
    const wchar         key[],
    unsigned            index,
    unsigned            minVal,
    unsigned            maxVal,
    unsigned            defVal
) {
    if (!value)
        return defVal;

    unsigned result;
    IniGetUnsigned(value, &result, index, defVal);
    if ((result < minVal) || (result > maxVal)) {
        result = defVal;
    }

    return result;
}

//===========================================================================
unsigned IniGetBoundedValue (
    const Ini * ini,
    const wchar section[],
    const wchar key[],
    unsigned    index,
    unsigned    minVal,
    unsigned    maxVal,
    unsigned    defVal
) {
    unsigned lineNum;
    const IniValue * value = IniGetFirstValue(
        ini,
        section,
        key,
        &lineNum
    );

    return IniGetBoundedValue(
        value,
        section,
        key,
        index,
        minVal,
        maxVal,
        defVal
    );
}