<?php
/**
 * Created by PhpStorm.
 * @author: crose
 * Date: 7/8/15
 * Time: 10:12 AM
 *
 * $Id$
 */

namespace IMATHUZH\Qfq\Core\Helper;


/**
 * Class KeyValueStringParser
 *
 * KeyValueStringParser is a parser for strings of the form
 *
 *     key1<delimiterA>value1<delimiterB>key2<delimiterA>value2<delimiterB>
 *
 * For instance
 *
 *     id=1,name=doe,firstname=john
 *
 * - Leading and trailing whitespace will be removed from keys and values.
 * - If a value is surrounded by quotes (`'`,  `"`), leading and trailing
 *   whitespace will be preserved (leading/trailing, quotes will be removed from value).
 * - Comments, lines starting with a '#' or ';', will be skipped.
 *
 * @package qfq
 */
class KeyValueStringParser {

    /**
     * Builds a string based on kvp array. Concatenate by the given delimiter.
     *
     * @param array $keyValueArray
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
     *
     * @return string
     */
    public static function unparse(array $keyValueArray, $keyValueDelimiter = PARAM_KEY_VALUE_DELIMITER, $listDelimiter = PARAM_LIST_DELIMITER, $flagEscape = false) {

        array_walk($keyValueArray, function (&$value) {
            if (!is_string($value) || $value === "" || strlen($value) === 1) {
                return;
            }

            if ($value[0] === " " && self::isFirstAndLastCharacterIdentical($value[0])) {
                $value = '"' . $value . '"';
            }
        });

        $newKeyValuePairImploded = array();
        foreach ($keyValueArray as $key => $value) {
            if ($flagEscape) {
                $key = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $key);
                $key = str_replace($listDelimiter, '\\' . $listDelimiter, $key);

                $value = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $value);
                $value = str_replace($listDelimiter, '\\' . $listDelimiter, $value);
            }
            $newKeyValuePairImploded[] = trim($key) . $keyValueDelimiter . $value;
        }

        return implode($listDelimiter, $newKeyValuePairImploded);
    }

    /**
     * @param $string
     *
     * @return bool
     */
    private static function isFirstAndLastCharacterIdentical($string) {
        if ($string === "") {
            return false;
        }

        return $string[0] === $string[strlen($string) - 1];
    }

    /**
     * Parse key/value pairs string and returns them as an assoc array. Respects escape '\'.
     *
     * Hint $keyValueString: "a:1,b:2,c:,d",  "," (empty key AND empty value)
     *
     * @param string $keyValueString string of key/value pairs. E.g.: 'a=100,b=test'
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
     * @param string $valueMode
     *                               * KVP_VALUE_GIVEN: If only a key is given, the value is ''.  E.G. 'a,b' >> [ 'a'
     *                               => '', 'b' => '' ]
     *                               * KVP_IF_VALUE_EMPTY_COPY_KEY: If only a key is given, the value is the same as
     *                               the key. E.G. 'a,b' >> [ 'a' => 'a', 'b' => 'b' ].
     * @param string $mode
     * @return array
     *                               * $mode = KVP_MODE_ASSOC: Associative array indexed by keys
     *                                 => [ "key1" => "val1", "key2" => "val2" ]
     *                               * $mode = KVP_MODE_ALL: Multidimensional array, containing associative array indexed by keys
     *                                 => [ [ "key1" => "val1" ], [ "key2" => "val2" ] ]
     * @throws \UserFormException Thrown if there is a value but no key.
     */

    public static function parse($keyValueString, $keyValueDelimiter = PARAM_KEY_VALUE_DELIMITER, $listDelimiter = PARAM_LIST_DELIMITER, $valueMode = KVP_VALUE_GIVEN, $mode = KVP_MODE_ASSOC) {
        if ($keyValueString === "") {
            return array();
        }

        // Clean any "\r\n" to "\n"
        $keyValueString = str_replace("\r\n", "\n", $keyValueString);

        // Allow {{ }} expressions to span several lines (replace \n inside these expressions)
        $keyValueString = OnString::removeNewlinesInNestedExpression($keyValueString);

        // Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
        if (strpos($keyValueString, '\\' . $listDelimiter) !== false) {
            $keyValuePairs = self::explodeEscape($listDelimiter, $keyValueString);
        } else {
            $keyValuePairs = explode($listDelimiter, $keyValueString);
        }

        $returnValue = array();
        for ($i = 0; $i < count($keyValuePairs); $i++) {

            $keyValuePairString = $keyValuePairs[$i];

            if (trim($keyValuePairString) === "") {
                continue;
            }

            // Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
            if (strpos($keyValueString, '\\' . $keyValueDelimiter) !== false) {
                $keyValueArray = self::explodeEscape($keyValueDelimiter, $keyValuePairString, 2);
            } else {
                $keyValueArray = explode($keyValueDelimiter, $keyValuePairString, 2);
            }

            $key = trim($keyValueArray[0]);

            // skip comments
            if (substr($key, 0, 1) == '#' || substr($key, 0, 1) == ';') {
                continue;
            }

            if ($key === '') {
                // ":", ":1"
                throw new \UserFormException(json_encode(
                    [ERROR_MESSAGE_TO_USER => "Value has no key: '$keyValuePairString'",
                        ERROR_MESSAGE_TO_DEVELOPER => "KVP='$keyValueString', keyValueDelimiter='$keyValueDelimiter', listDelimiter='$listDelimiter'"]),
                    ERROR_KVP_VALUE_HAS_NO_KEY);
            }

            // Mode: assoc
            if ($mode === KVP_MODE_ASSOC) {

                if (count($keyValueArray) === 2) {
                    // "a:1", "a:"
                    $returnValue[$key] = self::quoteUnwrap(trim($keyValueArray[1]));
                } else {
                    // no Value given: "a"
                    $returnValue[$key] = ($valueMode === KVP_VALUE_GIVEN) ? "" : $key;
                }

            // Mode: all
            } else {
                if (count($keyValueArray) === 2) {
                    // "a:1", "a:"
                    $returnValue[$i][$key] = self::quoteUnwrap(trim($keyValueArray[1]));
                } else {
                    // no Value given: "a"
                    $returnValue[$i][$key] = ($valueMode === KVP_VALUE_GIVEN) ? "" : $key;
                }
            }
        }

        return $returnValue;
    }

    /**
     * Explode string by delimiter and respects escaped delimiter. The result is replaced with escaped delimiter.
     * E.g. 'a,b\,c,d' becomes [ 'a', 'b,c', 'd']
     *
     * @param string $delimiter
     * @param string $data
     * @param int $max
     *
     * @return array
     */
    public static function explodeEscape($delimiter, $data, $max = 0) {

        // If the delimiter is a reserved regex char, it has to be escaped.
        $delimiterEscaped = preg_quote($delimiter);

        $items = preg_split('#(?<!\\\)' . $delimiterEscaped . '#', $data);

        // In case there is an upper limit of array elements
        if ($max > 0 && count($items) > $max) {
            $remain = array_slice($items, 0, $max);
            $remain[$max - 1] .= $delimiter . implode($delimiter, array_slice($items, $max));
            # Take care, that the 'escape' in last element is not cleaned!
            for ($ii = 0; $ii <= $max - 2; $ii++) {
                $remain[$ii] = str_replace('\\' . $delimiter, $delimiter, $remain[$ii]);
            }
            $items = $remain;
        } else {
            // Escaped tokens: the escape character needs to be replaced.
            $items = OnArray::arrayValueReplace($items, '\\' . $delimiter, $delimiter);
        }

        return $items;
    }

    /**
     * @param $string
     *
     * @return string
     */
    public static function quoteUnwrap($string) {
        $quotes = ['\'', '"'];

        if ($string === "" || strlen($string) === 1) {
            return $string;
        }

        if (in_array($string[0], $quotes) === true && self::isFirstAndLastCharacterIdentical($string)) {
            return substr($string, 1, strlen($string) - 2);
        }

        return $string;
    }

    /**
     * Works like PHP 'explode()', but respects $delimeter wrapped in ticks (single or double): those are not
     * interpreted as delimiter.
     *
     * E.g.: "a,b,'c,d',e" with delimiter ',' will result in [ 'a', 'b', 'c,d', 'e' ]
     *
     * @param     $delimiter
     * @param     $str
     * @param int $limit
     *
     * @return array|bool
     * @throws \CodeException
     */
    public static function explodeWrapped($delimiter, $str, $limit = PHP_INT_MAX) {

        if ($delimiter == '') {
            return false;
        }

        if ($limit < 0) {
            throw new \CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED);
        }

        if ($limit == 0) {
            $limit = 1;
        }

        $final = array();
        $startToken = '';
        $onHold = '';

        $cnt = 0;
        $arr = explode($delimiter, $str, PHP_INT_MAX);
        foreach ($arr as $value) {
            $trimmed = trim($value);
            if ($value == '' && $startToken == '') {

                if ($cnt < $limit) {
                    $final[] = '';
                    $cnt++;
                }
                continue;
            }

            if ($startToken == '') {
                switch ($trimmed[0]) {
                    case SINGLE_TICK:
                    case DOUBLE_TICK:
                        if ($trimmed[0] == substr($trimmed, -1)) {
                            break; // In case start and end token is in one exploded item
                        }
                        $startToken = $trimmed[0];
                        $onHold = $value;
                        continue 2;
                    default:
                        break;
                }

                if ($cnt >= $limit) {
                    $final[$cnt - 1] .= $delimiter . $value;
                } else {
                    $final[] = $value;
                    $cnt++;
                }
                continue;
            } else {
                $onHold .= $delimiter . $value;
                $lastChar = substr($trimmed, -1);
                if ($startToken == $lastChar) {

                    if ($cnt >= $limit) {
                        $final[$cnt - 1] .= $delimiter . $onHold;
                    } else {
                        $final[] = $onHold;
                        $cnt++;
                    }
                    $startToken = '';
                    $onHold = '';
                }
            }
        }

        return $final;
    }

    /**
     * Parse kvp string. Key has to be uniq otherwise only the last will be taken. Escaping not supported.
     *
     * p:{{pageSlug:T}}|q:Delete?:yes:no|download:file.pdf
     * @param $str
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
     */
    public static function explodeKvpSimple($str, $keyValueDelimiter = PARAM_KEY_VALUE_DELIMITER, $listDelimiter = PARAM_LIST_DELIMITER) {
        $result = array();

        $items = explode($listDelimiter, $str);
        foreach ($items as $item) {
            if ($item == '') {
                continue;
            }
            $arg = explode($keyValueDelimiter, $item, 2);
            $result[$arg[0]] = $arg[1] ?? '';
        }
        return $result;
    }
}
