/*
 * ============================================================================
 *
 *  Zombie:Reloaded
 *
 *  File:           paramparser.inc
 *  Type:           Core
 *  Description:    Provides functions for parsing single line strings with
 *                  flags, and parameters in key=value format.
 *
 *                  Supports quoted strings and escaped characters like "\n"
 *                  and "\t".
 *
 *                  Examle raw string:
 *                  "type=interval -disabled msg="Title:\n\"Example\"."
 *
 *  Copyright (C) 2009-2013  Greyscale, Richard Helgeby
 *
 *  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/>.
 *
 * ============================================================================
 */

/**
 * @section Limit settings.
 */
#define PARAM_NAME_MAXLEN   64      /** Maximum length of key name or flag name. */
#define PARAM_VALUE_MAXLEN  256     /** Maximum length of value string. */
/**
 * @endsection
 */

/**
 * @section Parsing error codes.
 */
#define PARAM_ERROR_EMPTY               1   /** Source string is empty. */
#define PARAM_ERROR_FULL                2   /** Destination array is full. */
#define PARAM_ERROR_UNEXPECTED_KEY      3   /** Unexpected key name. Could not find a equation sign (=) after previous key name. */
#define PARAM_ERROR_UNEXPECTED_END      4   /** Unexpected end of source string. */
#define PARAM_ERROR_MISSING_QUOTE       5   /** Unexpected end of source string. Missing end quote character. */
#define PARAM_ERROR_UNKNOWN             6   /** Unknown error. The parser got a invalid result from a search function it couldn't handle. */
/**
 * @endsection
 */

/**
 * Modes for what to do and expect when parsing. White space characters between
 * modes are ignored.
 */
enum ParamModes
{
    ParamMode_TypeCheck,  /** Check if it's a flag or a key. */
    ParamMode_Flag,       /** Expect a flag name (starts with "-"). */
    ParamMode_Key,        /** Expect a key name. */
    ParamMode_Equal,      /** Expect a equation sign. */
    ParamMode_Value       /** Expect a value string. */
}

/**
 * Structure for storing a key/value pair.
 */
enum ParamParseResult
{
    bool:Param_IsFlag,                      /** Specifies whether it's a flag or not. */
    String:Param_Name[PARAM_NAME_MAXLEN],   /** Key or flag name. */
    String:Param_Value[PARAM_VALUE_MAXLEN]  /** Value. Only used if a key. */
}



/**************************************
 *                                    *
 *       PARAMETER FUNCTIONS          *
 *                                    *
 **************************************/

/**
 * Parses a parameter string in "key=value" format and store the result in a
 * ParamParseResult array.
 *
 * @param buffer        A ParamParseResult array to store results.
 * @param maxlen        Maximum number of keys that can be stored (first
 *                      dimension of buffer).
 * @param paramString   The source string to parse. String is trimmed before
 *                      parsing.
 * @param err           Opional output: Error code if parsing error.
 * @param errPos        Opional output: Position in paramString where the error
 *                      occoured.
 * @return              Number of keys parsed.
 */
stock ParamParseString(buffer[][ParamParseResult], maxlen, String:paramString[], &err = 0, &errPos = -1)
{
    /*
     *  VALIDATION OF INPUT AND BUFFERS
     */

    // Trim raw string.
    TrimString(paramString);

    // Check if raw string is empty.
    if (strlen(paramString) == 0)
    {
        err = PARAM_ERROR_EMPTY;
        errPos = 0;
        return 0;
    }

    // Validate specified length of destination buffer.
    if (maxlen == 0)
    {
        err = PARAM_ERROR_FULL;
        errPos = 0;
        return 0;
    }


    /*
     *  PARSE LOOP
     */

    // Get raw string length.
    new rawlen = sizeof(paramString);

    // Initialize. Expect the start to be a key or a flag.
    new ParamModes:mode = ParamMode_TypeCheck;

    // Counter for number of parameters parsed.
    new paramcount;

    // Buffers for temp values.
    new startpos;
    new endpos;
    new bool:quoteon;
    decl String:value[PARAM_VALUE_MAXLEN];

    // Loop through all characters in the string. Exclude null terminator.
    for (new strpos = 0; strpos < rawlen - 1; strpos++)
    {
        // Check if there's space left in the destination buffer.
        if (paramcount > maxlen)
        {
            // Exit loop. No more parameters can be parsed.
            err = PARAM_ERROR_FULL;
            errPos = strpos;
            break;
        }


        /*
         *  MODE CHECK
         */

        // Check mode for deciding what to do.
        switch (mode)
        {
            case ParamMode_TypeCheck:
            {
                // Find start position of first non white space character.
                startpos = ParamFindStartPos(paramString, strpos);

                // Check if it's a flag type.
                if (paramString[startpos] == '-')
                {
                    // It's a flag, change mode.
                    mode = ParamMode_Flag;

                    // Update current position.
                    strpos = startpos;
                }
                else
                {
                    // Expect a key name.
                    mode = ParamMode_Key;

                    // Update current position. Substract by one to include
                    // the current character in next mode.
                    strpos = startpos - 1;
                }
            }
            case ParamMode_Flag:
            {
                // Find stop position (last non white space character).
                endpos = ParamFindEndPos(paramString, strpos);

                // Extract key name.
                StrExtract(value, sizeof(value), paramString, strpos, endpos);

                // Copy flag to destination buffer.
                strcopy(buffer[paramcount][Param_Name], PARAM_NAME_MAXLEN, value);

                // Set flag type.
                buffer[paramcount][Param_IsFlag] = true;

                // Increment parameter counter.
                paramcount++;

                // Set next parse mode.
                mode = ParamMode_TypeCheck;
            }
            case ParamMode_Key:
            {
                // Find stop position.
                endpos = ParamFindEndPos(paramString, strpos);

                // Extract key name.
                StrExtract(value, sizeof(value), paramString, strpos, endpos);

                // Copy key name to destination buffer.
                strcopy(buffer[paramcount][Param_Name], PARAM_NAME_MAXLEN, value);

                // Make sure flag type is not set.
                buffer[paramcount][Param_IsFlag] = false;

                // Note: Do not increment parameter counter until the
                //       entire key/value pair is parsed.

                // Set next parse mode. Expect a equation sign.
                mode = ParamMode_Equal;
            }
            case ParamMode_Equal:
            {
                // Find start position of first non white space character.
                startpos = ParamFindStartPos(paramString, strpos);

                // Validate position.
                if (startpos >= 0)
                {
                    // Check if it's a equation sign.
                    if (paramString[startpos] == '=')
                    {
                        // Change mode to expect a value at next position.
                        mode = ParamMode_Value;

                        // Update current position.
                        strpos = startpos;
                    }
                    else
                    {
                        // Parse error.
                        err = PARAM_ERROR_UNEXPECTED_KEY;
                        errPos = startpos;
                        break;
                    }
                }
                else
                {
                    // Parse error.
                    err = PARAM_ERROR_UNEXPECTED_END;
                    errPos = strpos;
                    break;
                }
            }
            case ParamMode_Value:
            {
                // Find start position of first non white space character.
                startpos = ParamFindStartPos(paramString, strpos);

                // Validate start position.
                if (startpos >= 0)
                {
                    // Reset quote and escape settings.
                    quoteon = false;

                    // Loop through all characters starting from the current
                    // position. Exclude null terminator.
                    for (strpos = startpos; strpos < rawlen - 1; strpos++)
                    {
                        // Check if the current character is a special character.
                        if (paramString[startpos] == '"')
                        {
                            // Toggle quote if the current quote is not escaped.
                            if (paramString[startpos - 1] != '\\')
                            {
                                quoteon = !quoteon;
                            }

                            // Check quote state.
                            if (quoteon)
                            {
                                // Quote started, update start position.
                                startpos = strpos + 1;
                            }
                            else
                            {
                                // Quote end, set end position.
                                endpos = strpos - 1;
                            }
                        }

                        // Check if it's a white space character or end of the string.
                        else if (!quoteon && (IsCharSpace(paramString[strpos]) || strpos == rawlen - 1))
                        {
                            // End of value reached. Save positions.
                            endpos = strpos - 1;

                            // Exit loop.
                            break;
                        }
                    }

                    // Check if quote still haven't ended.
                    if (quoteon)
                    {
                        // Parse error.
                        err = PARAM_ERROR_MISSING_QUOTE;
                        errPos = strpos;
                        break;
                    }

                    // Extract value string.
                    StrExtract(value, sizeof(value), paramString, startpos, endpos);

                    // Unescape string (converting "\n" to newline, etc.).
                    StrUnescape(value);

                    // Copy value string to destination buffer.
                    strcopy(buffer[paramcount][Param_Value], PARAM_VALUE_MAXLEN, value);

                    // Make sure flag type is not set.
                    buffer[paramcount][Param_IsFlag] = false;

                    // Increment parameter counter.
                    paramcount++;

                    // Set next parse mode. Expect a key or a flag.
                    mode = ParamMode_TypeCheck;
                }
                else
                {
                    // Parse error.
                    err = PARAM_ERROR_UNEXPECTED_END;
                    errPos = strpos;
                    break;
                }
            }
        }
    }

    // Return number of parameters parsed.
    return paramcount;
}

/**
 * Finds the first key index in a parameter array matching the specified key.
 *
 * @param params            A ParamParseResult array to search through.
 * @param maxlen            Size of parameter array (first dimension).
 * @param key               Key to find.
 * @param caseSensitive     Specifies whether the search is case sensitive or
 *                          not (default).
 * @return                  Index of the key if found, -1 otherwise.
 */
stock ParamFindKey(const params[][ParamParseResult], maxlen, const String:key[], bool:caseSensitive = false)
{
    // Loop through all parameters.
    for (new index = 0; index < maxlen; index++)
    {
        // Check parameter type.
        if (params[index][Param_IsFlag])
        {
            // It's a flag type, skip index.
            continue;
        }

        // Match key name.
        if (StrEqual(params[index][Param_Name], key, caseSensitive))
        {
            // Key found, return the key index.
            return index;
        }
    }

    return -1;
}

/**
 * Checks if the specified flag is set in a parameter array.
 *
 * @param params            A ParamParseResult array to search through.
 * @param maxlen            Size of parameter array (first dimension).
 * @param flag              Flag to check.
 * @param caseSensitive     Specifies whether the search is case sensitive or
 *                          not (default).
 * @return                  True flag is found, false otherwise.
 */
stock bool:ParamHasFlag(const params[][ParamParseResult], maxlen, const String:flag[], bool:caseSensitive = false)
{
    // Loop through all parameters.
    for (new index = 0; index < maxlen; index++)
    {
        // Check parameter type.
        if (!params[index][Param_IsFlag])
        {
            // It's a key type, skip index.
            continue;
        }

        // Match flag name.
        if (StrEqual(params[index][Param_Name], flag, caseSensitive))
        {
            // Flag found.
            return true;
        }
    }

    return false;
}



/**************************************
 *                                    *
 *         HELPER FUNCTIONS           *
 *                                    *
 **************************************/

/**
 * Finds the position of the last non white space character from a specified start position.
 *
 * @param paramString   Raw string search in.
 * @param startPos      Optional. Position to start searching from.
 * @return              Position of the last non white space character, or -1
 *                      if failed.
 */
stock ParamFindEndPos(const String:paramString[], startPos = 0)
{
    new rawlen = sizeof(paramString);

    // Validate string length.
    if (rawlen == 0)
    {
        return -1;
    }

    // Loop through all characters from the specified start position.
    for (new strpos = startPos; strpos < rawlen; strpos++)
    {
        // Check if white space or if current position is the last
        // character before the null terminator.
        if (IsCharSpace(paramString[strpos]) || strpos == rawlen - 1)
        {
            return strpos - 1;
        }
    }

    // It should never reach this place. Added to satisfy compiler.
    return -1;
}

/**
 * Finds the first non white space character in a string, starting from the
 * specified position.
 *
 * @param paramString   Raw string to search in.
 * @param startPos      Optional. Position to start searching from.
 * @return              Position of first character or -1 if failed.
 */
stock ParamFindStartPos(const String:paramString[], startPos = 0)
{
    new rawlen = sizeof(paramString);

    // Validate string length.
    if (rawlen == 0)
    {
        return -1;
    }

    // Loop through all characters from the specified start position.
    for (new strpos = startPos; strpos < rawlen; strpos++)
    {
        // Check if not white space.
        if (!IsCharSpace(paramString[strpos]))
        {
            return strpos;
        }
    }

    // No character found.
    return -1;
}

/**
 * Extracts a area in a string between two positions.
 *
 * @param buffer    Destination string buffer.
 * @param maxlen    Size of destination buffer.
 * @param source    Source string to extract from.
 * @param startpos  Start position of string to extract.
 * @param endpos    End position of string to extract.
 * @return          Number of cells written.
 */
stock StrExtract(String:buffer[], maxlen, const String:source[], startpos, endpos)
{
    new len;

    // Calculate string length. Also add space for null terminator.
    len = endpos - startpos + 1;

    // Validate length.
    if (len < 0)
    {
        return 0;
    }

    // Extract string and store it in the buffer.
    return strcopy(buffer, len, source[startpos]);
}

/**
 * Unescapes a string (replaces "\n" with newlines, etc.).
 *
 * @param str   String to unescape.
 */
stock StrUnescape(String:str[])
{
    new len = sizeof(str);

    ReplaceString(str, len, "\\n", "\n");
    ReplaceString(str, len, "\\r", "\r");
    ReplaceString(str, len, "\\t", "\t");
    ReplaceString(str, len, "\\\"", "\"");
    ReplaceString(str, len, "\\\\", "\\");
}
