<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 2/10/18
 * Time: 3:14 PM
 */

namespace IMATHUZH\Qfq\Core\Helper;


use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Exception\RedirectResponse;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/**
 * Class OnString
 * @package qfq
 */
class OnString {

    /**
     * @var Store
     */
    private static $store = null;

    /**
     * @var Database
     */
    private static $db = null;

    /**
     * Returns part of haystack string starting from and including the last occurrence of needle to the end of haystack.
     *
     * strrstr('hello/world/to/the/limit', '/') = 'limit'
     *
     * @param $haystack
     * @param $needle
     * @return string
     */
    public static function strrstr($haystack, $needle) {
        if (empty($needle) || empty($haystack)) {
            return '';
        }

        return substr($haystack, strrpos($haystack, $needle) + strlen($needle));
    }

    /**
     * Like str_replace() but only replaces the first occurrence.
     * @param $from
     * @param $to
     * @param $content
     * @return array|string|string[]|null
     */
    public static function strReplaceFirst($from, $to, $content) {
        $from = '/' . preg_quote($from, '/') . '/';
        return preg_replace($from, $to, $content, 1);
    }

    /**
     * Strips the first char $c from $data if the first char is equal to $c.
     * Example: with $c='_' the $data='_pId' becomes 'pId'
     *
     * @param $c
     * @param $data
     * @return string
     */
    public static function stripFirstCharIf($c, $data) {

        if (empty($data)) {
            return $data;
        }

        if ($data[0] == $c) {
            $data = substr($data, 1);
        }

        return $data;
    }

    /**
     * If the given $str is enclosed in SINGLE_TICK or DOUBLE_TICK, remove it. Only the outermost are removed.
     *
     * @param string $str
     * @return string  remove unquoted string
     */
    public static function trimQuote($str) {

        $len = strlen($str);

        if ($len < 2) {
            return $str;
        }

        switch ($str[0]) {
            case SINGLE_TICK:
                if ($str[$len - 1] == SINGLE_TICK) {
                    return substr($str, 1, $len - 2);
                }
                break;
            case DOUBLE_TICK:
                if ($str[$len - 1] == DOUBLE_TICK) {
                    return substr($str, 1, $len - 2);
                }
                break;
            case BACK_TICK:
                if ($str[$len - 1] == BACK_TICK) {
                    return substr($str, 1, $len - 2);
                }
                break;
            default:
                break;
        }

        return $str;
    }

    /**
     * If the given $str is enclosed LEFT|GREATER_SIGN, remove it. Only the outermost are removed.
     *
     * @param string $str
     * @return string  remove unquoted string
     */
    public static function trimQuoteTag($str) {
        $len = strlen($str);

        if ($len < 2) {
            return $str;
        }

        switch ($str[0]) {
            case LESS_SIGN:
                if ($str[$len - 1] == GREATER_SIGN) {
                    return substr($str, 1, $len - 2);
                }
                break;
            default:
                break;
        }

        return $str;
    }

    /**
     * Split an Excel position string line to column and row.
     * E.g.: $pos = 'A1' to [ 'A', '1'], resp.  'ACD4567' to [ 'ACD', '4567' ]
     *
     * @param $pos
     * @param &$column - return the alpha part of $pos
     * @param &$row - return the digit part of $pos
     * @return bool - true if a alpha string and a numeric string is found, else false.
     */
    public static function splitExcelPos($pos, &$column, &$row) {

        preg_match_all('/[A-Z]+|\d+/', $pos, $matches);

        if (count($matches[0]) != 2) {
            return false;
        }

        $column = $matches[0][0];
        $row = $matches[0][1];

        return true;
    }

    /**
     * Split the wkthtml Parameter between:
     * - general 'urlParam' for the specific website (page which will be converted to PDF).
     * - `wkthml` Parameter. Those will always start with '-' and control wkhtml how to render the PDF.
     * - '_sip' - to activate that `urlParam` parameters will be SIP encoded.
     *
     * @param string $urlParamString
     * @param array $rcArgs
     * @param bool $rcSipEncode
     *
     * @return array The remaining 'real' URL parameter to call the T3 page.
     * @throws \UserFormException
     */
    public static function splitParam($urlParamString, array &$rcArgs, &$rcSipEncode) {
        $urlParamNew = array();
        $rcArgs[WKHTML_OPTION_VIEWPORT] = WKHTML_OPTION_VIEWPORT_VALUE; // Forces wkhtml to render Bootstrap page in 'md', not 'xs'

        $urlParam = KeyValueStringParser::parse($urlParamString, '=', '&', KVP_IF_VALUE_EMPTY_COPY_KEY);
        foreach ($urlParam as $key => $value) {
            switch ($key[0] ?? '') {
                case '-':
                    $rcArgs[$key] = $value;
                    break;
                case '_':
                    if ($key == DOWNLOAD_SIP_ENCODE_PARAMETER) {
                        $rcSipEncode = true;
                    }
                    break;
                default:
                    $urlParamNew[$key] = $value;
                    break;
            }
        }

        return $urlParamNew;
    }

    /**
     * Parse uid token value into name and parameters.
     *
     * Supports both ? and & as first separator for backwards compatibility.
     * Uses whichever separator appears first in the string.
     *
     * Examples:
     *   "funcName" => ['name' => 'funcName', 'params' => [], 'paramString' => '']
     *   "funcName?param1=val1&param2=val2" => ['name' => 'funcName', 'params' => ['param1' => 'val1', ...], 'paramString' => 'param1=val1&param2=val2']
     *   "funcName&param1=val1&param2=val2" => ['name' => 'funcName', 'params' => ['param1' => 'val1', ...], 'paramString' => 'param1=val1&param2=val2']
     *
     * @param string $value The uid token value (e.g., "funcName?param1=val1&param2=val2")
     * @return array ['name' => string, 'params' => array, 'paramString' => string]
     */
    public static function parseUidToken(string $value): array {
        if ($value === '') {
            return ['name' => '', 'params' => [], 'paramString' => ''];
        }

        // Find first occurrence of either separator
        $posQuestion = strpos($value, '?');
        $posAmpersand = strpos($value, '&');

        // Determine which separator comes first (if any)
        if ($posQuestion === false && $posAmpersand === false) {
            // No separator found - just a name without params
            return ['name' => $value, 'params' => [], 'paramString' => ''];
        }

        if ($posQuestion === false) {
            $separatorPos = $posAmpersand;
        } elseif ($posAmpersand === false) {
            $separatorPos = $posQuestion;
        } else {
            $separatorPos = min($posQuestion, $posAmpersand);
        }

        $name = substr($value, 0, $separatorPos);
        $paramString = substr($value, $separatorPos + 1);

        $params = [];
        if ($paramString !== '') {
            parse_str($paramString, $params);
        }

        return ['name' => $name, 'params' => $params, 'paramString' => $paramString];
    }

    /**
     * Removes new lines (\n) from expressions like {{var:SC0:alnumx}} and replaces them with spaces.
     * Handles nested expressions like {{SELECT {{var::alnumx}}}}
     * This function is useful to allow multiline expressions in form definitions such as:
     *   sqlInsert = {{INSERT INTO test (grId)
     *                 VALUES(123) }}
     *
     * @param $str - the string to be parsed
     * @return string - the resulting string with new lines in expressions replaced
     * @throws \UserFormException - alerts the user if the delimiters are not balanced
     */
    public static function removeNewlinesInNestedExpression($str) {
        $delimStart = '{{';
        $delimEnd = '}}';
        $lastDelimPos = -strlen($delimStart);
        $exprDepth = 0; // keeps count of the level of expression depth
        $nestingStart = 0;

        // Process the string one start/end delimiter at a time
        while (true) {
            // find the next start/end delimiter
            $nextDelimStartPos = strpos($str, $delimStart, $lastDelimPos + strlen($delimStart));
            $nextDelimEndPos = strpos($str, $delimEnd, $lastDelimPos + strlen($delimEnd));
            if ($nextDelimStartPos === false && $nextDelimEndPos === false) break;
            $nextDelimPos = min($nextDelimStartPos, $nextDelimEndPos);
            if ($nextDelimStartPos === false) $nextDelimPos = $nextDelimEndPos;
            if ($nextDelimEndPos === false) $nextDelimPos = $nextDelimStartPos;

            if ($nextDelimPos == $nextDelimStartPos) { // opening delimiter
                if ($exprDepth == 0) $nestingStart = $nextDelimPos;
                $exprDepth++;
            } else { // closing delimiter
                $exprDepth--;
                if ($exprDepth < 0) {
                    throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Too many closing delimiters '$delimEnd'",
                        ERROR_MESSAGE_TO_DEVELOPER => "in '$str'"]), ERROR_MISSING_OPEN_DELIMITER);


                    break;
                } elseif ($exprDepth == 0) {
                    // end of nesting -> replace \n inside nested expression with space
                    $pre = substr($str, 0, $nestingStart);
                    $nest = substr($str, $nestingStart, $nextDelimPos - $nestingStart);
                    $nestNew = str_replace('\n', ' ', $nest);
                    $post = substr($str, $nextDelimPos);
                    $str = substr($str, 0, $nestingStart) .
                        str_replace("\n", " ", substr($str, $nestingStart, $nextDelimPos - $nestingStart)) .
                        substr($str, $nextDelimPos);
                }
            }

            $lastDelimPos = $nextDelimPos;
        }
        if ($exprDepth > 0) {
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Missing close delimiter '$delimEnd'",
                ERROR_MESSAGE_TO_DEVELOPER => "in '$str'"]), ERROR_MISSING_CLOSE_DELIMITER);
        }

        return $str;
    }

    /**
     * Split a $_SERVER['PATH_INFO'] of the form '/form1/id1/form2/id2/form3/id3/.../formN[/idN])' to
     *   $rcArrIds=[ id1, id2, ..., idN]
     *   return: 'formN'
     *
     * @param $pathInfo
     * @param array $rcArrId
     * @param array $rcArrForm
     * @return string
     * @throws \UserFormException
     */
    public static function splitPathInfoToIdForm($pathInfo, array &$rcArrId, array &$rcArrForm) {

        // Empty: do nothing
        if ($pathInfo == '') {
            return '';
        }

        // Remove optional leading '/'
        if ($pathInfo[0] == '/') {
            $pathInfo = substr($pathInfo, 1);
        }

        // Remove optional trailing '/'
        $len = strlen($pathInfo);
        if ($len > 0 && $pathInfo[$len - 1] == '/') {
            $pathInfo = substr($pathInfo, 0, $len - 1);
        }

        // Empty? do nothing
        if ($pathInfo == '') {
            return '';
        }

        $param = explode('/', $pathInfo);
        $cnt = count($param);

        // No 'id'. Append '0'
        if ($cnt % 2 == 1) {
            array_push($param, 0);
        }

        $rcArrId = array();
        $rcArrForm = array();

        while (count($param) > 0) {

            $form = array_shift($param);
            if (!ctype_alnum($form)) {
                throw new \UserFormException('Expect alphanumeric string', ERROR_BROKEN_PARAMETER);
            }
            $rcArrForm[] = $form;

            $id = array_shift($param);
            if (!ctype_digit((string)$id)) {
                throw new \UserFormException('Expect numerical id', ERROR_BROKEN_PARAMETER);
            }
            $rcArrId[] = $id;
        }

        return $form;
    }

    /**
     * Splits a path '/name1/name2/' to [ 'name1', 'name2' ]
     *
     * @param $pathInfo
     * @return array
     */
    public static function splitPathToArray($pathInfo) {
        $arr = explode('/', $pathInfo);
        $final = array();
        foreach ($arr as $item) {
            if ($item != '') {
                $final[] = $item;
            }
        }
        return $final;
    }

    /**
     * Returns true if the subject string contains any of the given words in targets.
     * Case insensitive!
     *
     * @param array $words
     * @param string $subject
     * @return bool
     */
    public static function containsOneOfWords(array $words, string $subject): bool {
        foreach ($words as $word) {
            if (preg_match("/\b" . $word . "\b/i", $subject)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Performs a case-sensitive check indicating if haystack begins with needle.
     * As of PHP 8 you can use str_starts_with() instead.
     *
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function strStartsWith(string $haystack, string $needle): bool {
        return substr_compare($haystack, $needle, 0, strlen($needle)) === 0;
    }

    /**
     * Performs a case-sensitive check indicating if haystack ends with needle.
     * As of PHP 8 you can use str_ends_with() instead.
     *
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function strEndsWith(string $haystack, string $needle): bool {
        return substr_compare($haystack, $needle, -strlen($needle)) === 0;
    }

    /**
     * Performs a case-sensitive check indicating if needle is contained in haystack.
     * As of PHP 8 you can use str_contains() instead.
     *
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function strContains(string $haystack, string $needle): bool {
        return strpos($haystack, $needle) !== false;
    }

    /**
     * Split a cmd "getFeUser(pId, pName, ...) : accountId, feUserUid, ..." into:
     *  rcFunctionName = getFeUser
     *  rcFunctionParam = [ 'pId', 'pName', ... ]
     *  rcReturnParam = [ 'accountId', 'feUserUid', ... ]
     *
     * @param $cmd
     * @param $rcFunctionName
     * @param $rcFunctionParam
     * @param $rcReturnParam
     */
    public static function splitFunctionCmd($cmd, &$rcFunctionName, &$rcFunctionParam, &$rcReturnParam) {
        $rcFunctionName = '';
        $rcFunctionParam = array();
        $rcReturnParam = array();

        if ($cmd == '') {
            return;
        }

        // $cmd = "getFeUser(pId, pName) : accountId, feUserUid"
        $split = explode('=>', $cmd, 2);
        // $split[0] = getFeUser(pId, pName), $split[1]=accountId, feUserUid

        $functionArr = explode('(', $split[0], 2);
        // $functionArr[0]='getFeUser', $functionArr[1]='pId, pName) '

        $args = explode(')', $functionArr[1] ?? '', 2);
        // $args[0]='pId, pName', $args[1]=' '

        $rcFunctionName = trim($functionArr[0]);
        $rcFunctionParam = OnArray::trimArray(explode(',', $args[0] ?? ''));
        $rcReturnParam = OnArray::trimArray(explode(',', $split[1] ?? ''));
    }

    /**
     * Escape $value by list of $escapeTypes.
     *
     * @param $escapeTypes
     * @param $value
     * @param $rcFlagWipe
     * @param $parameter
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function escape($escapeTypes, $value, &$rcFlagWipe) {

        // escape ticks
        if (is_string($value)) {

            // Get encryption/decryption token and adjust variable
            $store = Store::getInstance();
            $encryptionKey = $store::getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
            $encryptionToken = explode('=', $escapeTypes, 2)[1] ?? '';
            $escapeTypes = explode('=', $escapeTypes, 2)[0] ?? '';

            // Process all escape requests in the given order.
            for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {

                $escape = $escapeTypes[$ii];
                if ($escape == TOKEN_ESCAPE_CONFIG) {
                    $escape = self::getEscapeTypeDefault();
                }

                switch ($escape) {
                    case TOKEN_ESCAPE_SINGLE_TICK:
                        $value = str_replace("'", "\\'", $value);
                        break;
                    case TOKEN_ESCAPE_DOUBLE_TICK:
                        $value = str_replace('"', '\\"', $value);
                        break;
                    case TOKEN_ESCAPE_COLON:
                        $value = str_replace(':', '\\:', $value);
                        break;
                    case TOKEN_ESCAPE_LDAP_FILTER:
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
                        break;
                    case TOKEN_ESCAPE_LDAP_DN:
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
                        break;
                    case TOKEN_ESCAPE_MYSQL:
                        if (self::$db === null) {
                            self::$db = new Database();
                        }
                        $value = self::$db->realEscapeString($value);
                        break;
                    case TOKEN_ESCAPE_NONE: // do nothing
                        break;
                    case TOKEN_ESCAPE_PASSWORD_T3FE:
                        $value = T3Handler::getHash($value);
                        break;
                    case TOKEN_ESCAPE_STOP_REPLACE:
                        $value = Support::encryptDoubleCurlyBraces($value);
                        break;
                    case TOKEN_ESCAPE_EXCEPTION:
                        // empty values will be handled in 'else'.
                        break;
                    case TOKEN_ESCAPE_WIPE:
                        $rcFlagWipe = true;
                        break;
                    case TOKEN_ESCAPE_TIMEZONE:
                        $value = self::getEuropeanTimezone($value);
                        break;
                    case TOKEN_ESCAPE_HTML_SPECIAL_CHAR:
                        $value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
                        break;
                    case TOKEN_ESCAPE_HTML_ENTITY:
                        $value = Support::htmlEntityEncodeDecode(MODE_ENCODE_ALL, $value);
                        break;
                    case TOKEN_ESCAPE_STRIP_HTML_TAGS:
                        $value = strip_tags($value);
                        break;
                    case TOKEN_ESCAPE_ENCRYPT:
                        if (isset($encryptionToken) && $encryptionToken !== '') {
                            if (EncryptDecrypt::checkForValidEncryptMethod($encryptionToken)) {
                                $encryptionMethod = $encryptionToken;
                            } else {
                                throw new \UserReportException("Invalid encryption method used in variable", ERROR_VARIABLE_INVALID_ENCRYPTION_METHOD);
                            }
                        } else {
                            $encryptionMethod = $store::getVar(SYSTEM_ENCRYPTION_METHOD, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
                        }
                        $value = EncryptDecrypt::buildDatabaseValue($value, $encryptionKey, $encryptionMethod);
                        break;
                    case TOKEN_ESCAPE_DECRYPT:
                        $value = EncryptDecrypt::getPlaintext($value, $encryptionKey);
                        break;
                    case TOKEN_ESCAPE_FIND:
                        $value = self::generateWildcardPattern($value);
                        break;
                    case TOKEN_ESCAPE_PIPE:
                        $value = str_replace('|', '\\|', $value);
                        break;
                    default:
                        throw new \UserFormException("Unknown escape qualifier: $escape", ERROR_UNKNOW_SANITIZE_CLASS);
                        break;
                }
            }
        } else {
            // In case the value is not found and the escape class forces a full stop
            if (strpos($escapeTypes, TOKEN_ESCAPE_EXCEPTION) !== false) {
                throw new \UserFormException($arrToken[VAR_INDEX_MESSAGE] ?? '', ERROR_QUIT_QFQ_REGULAR);
            }
        }

        return $value;
    }

    /**
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private static function getEscapeTypeDefault() {
        static $escapeTypeDefault = null;

        if ($escapeTypeDefault === null) {
            self::$store = Store::getInstance();
            $escapeTypeDefault = self::$store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }

        return $escapeTypeDefault;
    }

    /**
     * Get the CET/CEST Timezone for a given date, or if date is '' based on the current date.
     *
     * @param string $dateStr
     * @return string
     */
    public static function getEuropeanTimezone($dateStr = '') {

        $ts = ($dateStr == '') ? time() : strtotime($dateStr);
        $offset = date("Z", $ts) / 3600;
        switch ($offset) {
            case 1:
                $tz = "CET";
                break;
            case 2:
                $tz = "CEST";
                break;
            default:
                $tz = 'GMT' . sprintf("%+d", $offset);
        }

        return $tz;
    }

    /**
     * Replaces single tick in a HTML code by html entity
     * or if a attribute is quoted with single tick convert the attribute to double quoted.
     * If there is a double quote in single tick quoted attribute, it's replaced by '&quot;'.
     *
     * Content (outside of an HTML tag): single ticks will be replaced by '&apos;':
     *   <b>John's</b> >> <b>John&apos;s</b>
     *
     * HTML tag attribute: A single tick quoted attribute will be converted to a double tick quoted attribute.
     *   <img title='Moon'>  >> <img title="Moon">
     *
     * HTML tag attribute: A single ticks in a double tick quoted string will be replaced by '&apos;'
     *   <img title='echo "hello"'> >> <img title="echo &quot;hello&quot;'>
     *
     * @param $line
     * @return string
     */
    public static function escapeSingleTickInHtml($line): string {
        $flagTag = false;
        $flagAttribute = false;
        $flagAttributeStartSingleTick = false;
        $flagAttributeStartDoubleTick = false;
        $flagAttributeStarted = false;
        $posStartSingleTick = null;

        $new = '';
        $length = strlen($line);
        for ($ii = 0; $ii < $length; $ii++) {
            $c = $line[$ii];
            switch ($c) {
                case '>':
                    // HTML tag ends here: close all open flags
                    $flagTag = false;
                    $flagAttribute = false;
                    $flagAttributeStartSingleTick = false;
                    $flagAttributeStartDoubleTick = false;
                    $flagAttributeStarted = false;
                    $posStartSingleTick = null;
                    break;

                case '<':
                    // HTML tag starts here
                    $flagTag = true;
                    break;

                case '=':
                    if ($flagTag && !$flagAttribute) {
                        // Inside a tag and outside a running attribute: here starts a new attribute
                        $flagAttribute = true;
                    }
                    break;

                case ' ':
                    // Space after start attribute and no quotes used, means: attribute ends here.
                    if ($flagTag && $flagAttribute && $flagAttributeStarted && !$flagAttributeStartSingleTick && !$flagAttributeStartDoubleTick) {
                        $flagAttribute = false;
                        $flagAttributeStartSingleTick = false;
                        $flagAttributeStartDoubleTick = false;
                        $flagAttributeStarted = false;
                        $posStartSingleTick = null;
                    }
                    break;

                case "'":
                    if ($flagTag) {
                        if ($flagAttribute) {
                            if (!$flagAttributeStartSingleTick && !$flagAttributeStartDoubleTick && !$flagAttributeStarted) {
                                // Attribute quoted by single tick.
                                // Remember position to later replace by double tick, if there is no double tick inside.
                                $flagAttributeStartSingleTick = true;
                                $posStartSingleTick = strlen($new);
                                $flagAttributeStarted = true;
                                break;
                            }

                            if ($flagAttributeStartSingleTick) {
                                // Closing single tick. Attribute ends here.
                                $flagAttributeStartSingleTick = false;
                                $flagAttribute = false;
                                $flagAttributeStarted = false;

                                // No double tick found: single ticks can be replaced by double tick
                                $new[$posStartSingleTick] = '"';
                                $new .= '"';
                                continue 2;
                            }

                            if ($flagAttributeStartDoubleTick) {
                                // Single tick inside a double tick quoted attribute: can be replaced
                                $new .= '&apos;';
                                continue 2;
                            }
                        }

                    } else {
                        // regular content (no tag)
                        $new .= '&apos;';
                        continue 2;
                    }
                    break;
                case '"':
                    if ($flagTag) {
                        if ($flagAttribute) {
                            if (!$flagAttributeStartSingleTick && !$flagAttributeStartDoubleTick && !$flagAttributeStarted) {
                                // Attribute starting with double tick.
                                $flagAttributeStartDoubleTick = true;
                                $flagAttributeStarted = true;
                                break;
                            }
                            if ($flagAttributeStartDoubleTick) {
                                // Attribute ending with double tick
                                $flagAttributeStartDoubleTick = false;
                                $flagAttribute = false;
                                $flagAttributeStarted = false;
                                break;
                            }
                            if ($flagAttributeStartSingleTick) {
                                // Double tick quoted by singe tick: replace by &quot;
                                $new .= '&quot;';
                                continue 2;
                            }
                        }
                    }
                    break;
                default:
                    if ($flagAttribute) {
                        $flagAttributeStarted = true;
                    }
            }
            $new .= $c;
        }

        return $new;
    }

    /**
     * Checks for any leading braces in $data. Spaces will be ignored.
     *
     * 'SELE..': returns $data unchanged
     * ' ( SELECT...': returns 'SELECT...'
     * ' ( (SELECT...': returns 'SELECT...'
     *
     * @param $data
     * @return string
     */
    public static function removeLeadingBrace($data) {

        $trimmed = false;
        $value = trim($data);
        while ($value != '' && $value[0] == '(') {
            $trimmed = true;

            $value = trim(substr($value, 1));
        }
        if ($trimmed) {
            $data = $value;
        }
        return $value;
    }


    /*
     * Check the given $url for ending with a index.php or a query or an anker.
     * If one of them found: remove it.
     * Return cleaned $url
     */
    public static function urlStripFile($url) {

        if ($url == '') {
            return '';
        }

        // Take care all %dd are replaced by real characters
        $url = urldecode($url);

        // Explode to examine last part
        $arr = explode('/', $url);

        $last = count($arr) - 1;

        if ($arr[$last] == '') {
            unset($arr[$last]);
            $last--;
        }

        if ($last < 1) {
            return $url;
        }

        // Last component starts with 'index.php' or contains a '?' or '#' - that's likely not to be a part of baserurl
        if (0 == strcmp('index.php', substr($arr[$last], 0, 9)) ||
            strpos($arr[$last], '?') !== false || strpos($arr[$last], '#') !== false) {
            unset($arr[$last]);
        }

        return implode('/', $arr);
    }

    /**
     * Split $value into email and realname. If realname is not given, take email as realname
     * Example: 'john@doe.com'             >> [ 'email' -> 'john@doe.com', 'realname' -> '']
     *          '<john@doe.com>'           >> [ 'email' -> 'john@doe.com', 'realname' -> '']
     *          'John Doe <john@doe.com>'  >> [ 'email' -> 'john@doe.com', 'realname' -> 'John Doe']
     *          'John Doe john@doe.com'    >> [ 'email' -> 'john@doe.com', 'realname' -> 'John Doe']
     * @param $value
     * @return array
     */
    public static function splitEmailRealname($value): array {
        $arr = explode(' ', $value);
        $params[NAME_EMAIL] = self::trimQuoteTag(array_pop($arr));
        $params[NAME_REALNAME] = self::trimQuoteTag(trim(implode(' ', $arr)));

        return $params;
    }

    /**
     * Very specific function: converts $arr to JSON and checks if it exceeds $max.
     * If it exceeds, search for the biggest element, replace the content by ERROR_MSG_TOO_BIG.
     * Repeat that, until it's below $max
     *
     * @param array $arr
     * @param $max
     * @return array|mixed
     */

    public static function limitSizeJsonEncode(array $arr, $currentLength, $max) {
        $maxValue = 0;
        $maxKey = '';
        $origArrSize = 0;
        $replaceLength = strlen(ERROR_MSG_TOO_BIG);

        // Stop searching
        if ($max == 0 || empty($arr)) {
            return array();
        }

        // Search the biggest element.
        foreach ($arr as $key => $value) {

            if (is_array($value)) {
                // Radios might be delivered as Array: pseudo flatten by misusing json_encode()
                $value = json_encode($value);
            }

            $len = strlen($value);
//            $origArrSize+=$len;
            if ($len > $maxValue) {
                $maxValue = strlen($value);
                $maxKey = $key;
            }
        }

        // Found a biggest element
        if ($maxKey == '') {
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "No biggest element found",
                ERROR_MESSAGE_TO_DEVELOPER => "Strange: element is to big ($origArrSize) but found no 'biggest' element."]), ERROR_MISSING_OPEN_DELIMITER);
        } else {
            // Copy all elements, replace the biggest
            foreach ($arr as $key => $value) {
                if ($key == $maxKey) {

                    // Check that the payload is bigger than ERROR_MSG_TOO_BIG
                    if (!is_array($arr[$key]) && (strlen($arr[$key]) < $replaceLength)) {
                        throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Can't shrink array",
                            ERROR_MESSAGE_TO_DEVELOPER => "Makes no sense: the replacement[$replaceLength] is bigger than the payload[" . strlen($arr[$key]) . ']']), ERROR_MISSING_OPEN_DELIMITER);
                    }
                    $arrNew[$key] = ERROR_MSG_TOO_BIG;
                } else {
                    $arrNew[$key] = $value;
                }
            }

            $newLen = strlen(json_encode($arrNew, JSON_UNESCAPED_UNICODE));

            // Final Size still too big?
            if ($newLen > $maxValue) {

                // Detect infinite loop.
                if ($newLen >= $currentLength) {
                    throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => "Can't shrink array",
                        ERROR_MESSAGE_TO_DEVELOPER => "max: $max, current: " . $currentLength . ", new reduced: " . $newLen]), ERROR_MISSING_OPEN_DELIMITER);
                } else {
                    // Dive deeper to replace the next biggest element.
                    $arrNew = self::limitSizeJsonEncode($arrNew, $newLen, $maxValue);
                }
            }
        }

        return $arrNew;
    }

    public static function isValidNumberList($string): bool {
        // Regex pattern: Empty string or numbers, commas, and optional spaces
        return preg_match('/^[\d,\s]*$/', $string) || $string === "";
    }

    /**
     * Strip double curly braces incl. optional '!' after leading double curly braces.
     *
     * @param $str
     * @return string
     */
    public static function removeDoubleCurlyBraces($str): string {
        $str1 = trim($str);
        if (substr($str1, 0, 2) === '{{' && substr($str1, -2) === '}}') {
            // Remove the first 2 characters and the last 2 characters
            $str1 = trim(substr($str1, 2, strlen($str1) - 4));
            if ($str1[0] === '!') {
                $str1 = substr($str1, 1);
            }
            return $str1;
        }
        return $str;
    }

    /**
     * Check if braces are balanced.
     *
     * @param string $content
     * @return bool
     */
    public static function validateBraceBalance(string $content): bool {
        $len = strlen($content) - 1;
        $open = 0;

        for ($i = 0; $i < $len; $i++) {
            if ($content[$i] === '{' && $content[$i + 1] === '{') {
                $open++;
                $i++;
            } elseif ($content[$i] === '}' && $content[$i + 1] === '}') {
                if ($open === 0) {
                    return false;
                }  // Unmatched `}}`

                $open--;  // Closing `}}`
                $i++; // Skip next character
            } elseif ($content[$i] === '}' && $open === 0) {
                return false;  // Stray `}`
            }
        }
        return $open === 0;
    }

    /**
     * Extract the uploads array from a given string.
     *
     * This function is specifically used for multiUploads. It looks for a JSON array
     * assigned to "uploads=" within the SIP string using a regular expression. If found,
     * it decodes the JSON into a PHP array and returns it; otherwise, it returns false if no match was found or null if the upload=[] is invalid json.
     *
     * Example:
     * - $sipString: _feName=MUpload
     *         &_sipForForm=67e3d02499d55
     *         &accept=application/pdf
     *         &fake_uniq_never_use_this=67e53086b8b5b
     *         &form=MUpload
     *         &maxFileSize=33554432
     *         &r=8488
     *         &uploads=[{"id":73,"full_path":"\/var\/www\/html\/fileadmin\/multiUpload\/8488\/dummy_2.pdf","fileSize":"7448","mimeType":"application\/pdf","name":"dummy_2.pdf"},{"id":74,"full_path":"\/var\/www\/html\/fileadmin\/multiUpload\/8488\/dummy_2__6th_copy_.pdf","fileSize":"7448","mimeType":"application\/pdf","name":"dummy_2__6th_copy_.pdf"},{"name":"Dummy_2 (3rd copy).pdf","full_path":"Dummy_2 (3rd copy).pdf","type":"application\/pdf","tmp_name":"\/tmp\/phpIxzu6E","error":0,"size":7448,"newUpload":1}]
     *         &s=67e53086b90df
     *
     * - return: [
     *              [
     *                  "id"        => 73,
     *                  "full_path" => "/var/www/html/fileadmin/multiUpload/8488/dummy_2.pdf",
     *                  "fileSize"  => "7448",
     *                  "mimeType"  => "application/pdf",
     *                  "name"      => "dummy_2.pdf"
     *              ],
     *              [
     *                  "id"        => 74,
     *                  "full_path" => "/var/www/html/fileadmin/multiUpload/8488/dummy_2__6th_copy_.pdf",
     *                  "fileSize"  => "7448",
     *                  "mimeType"  => "application/pdf",
     *                  "name"      => "dummy_2__6th_copy_.pdf"
     *              ],
     *              [
     *                  "name"      => "Dummy_2 (3rd copy).pdf",
     *                  "full_path" => "Dummy_2 (3rd copy).pdf",
     *                  "type"      => "application/pdf",
     *                  "tmp_name"  => "/tmp/phpIxzu6E",
     *                  "error"     => 0,
     *                  "size"      => 7448,
     *                  "newUpload" => 1
     *              ]
     *           ]
     *
     * @param $sipString
     * @return bool|array|null
     */
    public static function extractUploadsFromSipString($sipString): bool|array|null {

        // - "uploads=" literal match
        // - (\[[^&]+?]) matches a capture group that starts with '[',
        // - until it finds the closing ']'.
        $pattern = '/uploads=(\[[^&]+?])/';

        if (preg_match($pattern, $sipString, $matches)) {
            $uploadsJson = $matches[1];

            $uploadsArray = json_decode($uploadsJson, true);

            return $uploadsArray;
        }
        return false;
    }

    /**
     * Converts an input string by replacing all non unicode letters and numbers with '%'.
     * Ensures that the string starts and ends with '%', and collapses multiple consecutive '%' characters into a single '%'.
     * If the string to be converted is a Sanitize violation it will just return it unchanged.
     *
     * Examples:
     *
     * 1. Input: "First Last"
     * - Intermediate: "First%Last"
     * - Output: "%First%Last%"
     *
     * 2. Input: "Hello, World!"
     * - Intermediate: "Hello%World"
     * - Output: "%Hello%World%"
     *
     * 3. Input: "user@example.com"
     * - Intermediate: "user%example%com"
     * - Output: "%user%example%com%"
     *
     * 4. Input: "José Álvarez"
     * - Intermediate: "José%Álvarez"
     * - Output: "%José%Álvarez%"
     *
     * @param string $sting
     * @return string
     */
    public static function generateWildcardPattern(string $sting): string {
        // Skip if it is a Sanitize violation
        $patternsToSkip = [
            SANITIZE_VIOLATE . SANITIZE_ALLOW_ALNUMX . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_DIGIT . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_NUMERICAL . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_EMAIL . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_ALLBUT . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_ALL . SANITIZE_VIOLATE,
            SANITIZE_VIOLATE . SANITIZE_ALLOW_PATTERN . SANITIZE_VIOLATE,
        ];

        if (in_array($sting, $patternsToSkip)) {
            return $sting;
        }

        // Replace
        $result = preg_replace('/[^\p{L}\p{N}]+/u', '%', $sting);

        // Trim any '%' characters from both ends and then add one at the beginning and one at the end.
        $result = trim($result, '%');

        // If the trimmed result is empty, just return '%'
        if ($result === '') {
            return '%';
        }

        return '%' . $result . '%';
    }

    /**
     * Check $combined:
     * - if is there a ':' os second last character, then this indicates "copy to store 'x'".
     * - if yes, check allowed store.
     *
     * Check $store===false if 'copy to store' is meant.
     *
     * Return
     * - if a valid <varname>:<store> detected: cleaned varname (delimiter und store stripped ':<x>' ) and store
     * - if not, [ $combined, false]
     *
     * @param $combined // e.g. 'varname:U'
     * @return array|false[] // [ $combined, false] or [ 'varname', 'U' ]
     * @throws \UserReportException
     */
    public static function checkAndSplitForVarStoreSyntax($combined) {

        $length = strlen($combined);
        if ($length < 3 || $combined[$length - 2] != PARAM_VAR_DELIMITER) {
            return [$combined, false];
        }

        $store = $combined[$length - 1];
        $keyOnly = substr($combined, 0, $length - 2);

        if (!str_contains(STORE_SET_ALLOWED, $store)) {
            throw new \UserReportException("Copy to store is only allowed for store " . STORE_SET_ALLOWED . " - not $store", ERROR_INVALID_VALUE);
        }

        return [$keyOnly, $store];
    }

    /**
     * Search for the last token in a string.
     * This is used to explode strings like {{"Hello World  -  Hello City" AS '_greeting:U'}} by ' '.
     * It's important not to change the spaces in the first token (="Hello World  -  Hello City").
     * The last token will be trimmed!
     * Attention: quoted strings are not treated as quoted string! Quotation has no impact.
     *
     * @param $line // Example: "Hello World  -  Hello City" AS '_greeting:U'
     * @param $delimiter
     * @return array  // In the example [ '"Hello World  -  Hello City" AS', "'_greeting:U'"]
     */
    public static function splitLastToken($line, $delimiter = ' ') {
        $start = trim($line, $delimiter);
        $last = false;

        if (false !== ($pos = strrpos($start, $delimiter))) {
            $last = substr($start, $pos + 1);
            $start = rtrim(substr($start, 0, $pos));
        }

        return [$start, $last];
    }

    /**
     * Like splitLastToken(), but splits the last two token + remaining.
     *
     * @param $line
     * @return array // [ first, second, third ]. 'second' and 'third' will be false if not found.
     */
    public static function split2LastToken($line) {

        [$start, $third] = OnString::splitLastToken($line);
        [$first, $second] = OnString::splitLastToken($start);
        if ($second === false) {
            // Only one token given: rotate third to second, set third=false.
            $second = $third;
            $third = false;
        }
        return [$first, $second, $third];
    }

    /**
     * Replaces image sources in HTML that are wrapped in `{{ ... AS link }}` with secure SIP URLs.
     *
     * This method scans an HTML string for <img> tags whose `src` attributes contain a special template format
     * (e.g., `{{ r:7|M:file|d|F:fileadmind/.. AS link }}`) and replaces those URLs with secure SIP-generated links.
     *
     * Conditions:
     * - Only <img> tags with `src` matching `{{ ... AS link }}` are processed.
     * - Query parameters and fragments are stripped from the path before generating the secure link.
     * - Paths explicitly pointing to the QFQ download API are excluded from replacement.
     *
     * Regex Breakdown:
     * - `/ <img\s+[^>]*src=(["\'])(.*?)\1 /`
     * - Finds all <img> tags with a `src` attribute.
     * - Captures the `src` value inside either single or double quotes.
     * - `["']` captures the opening quote.
     * - `(.*?)\1` captures the actual URL up to the matching quote (non-greedy).
     *
     * - `/ \{\{.*\s+AS\s+link\s*}} /`
     * - Ensures the `src` contains a valid `{{ ... AS link }}` template marker.
     * - `\{\{` matches the literal `{{`
     * - `.*\s+AS\s+link` ensures there is an `AS link` portion with flexible spacing.
     * - `\s*}}` matches the closing `}}` with optional whitespace.
     *
     * - `/ ^\{\{\s* | \s+AS\s+link}}$ /`
     * - Used to clean the template and extract just the file path from `{{ path AS link }}`
     * - `^\{\{\s*` matches the opening `{{` with optional space.
     * - `\s+AS\s+link}}$` matches the closing part with spacing flexibility.
     * @throws \UserFormException
     * @throws \CodeException
     * @throws RedirectResponse
     * @throws \UserReportException
     * @throws \DbException
     */
    public static function replaceImageSourceWithSip(string $value): string {
        $link = new Link(new Sip());
        // Match all <img src="..."> tags and extract the src
        if (preg_match_all('/<img\s+[^>]*src=(["\'])(.*?)\1/', $value, $matches)) {
            foreach ($matches[2] as $src) {
                // Only process if it contains the template marker
                if (!preg_match('/\{\{.*\s+AS\s+link\s*}}/', $src)) {
                    continue;
                }

                // Remove the base URL to isolate the path
                $baseUrl = Path::urlApp();
                $path = str_replace($baseUrl, '', $src);
                // Replace {{ ... AS link}} with '' so we can parse the code
                $path = preg_replace('/^\{\{\s*|\s+AS\s+link}}$/', '', $path);

                // Make sure we don’t accidentally include query strings or fragments
                $path = strtok($path, '?');
                $path = strtok($path, '#');

                // Skip when path == Download API since it is a Form save and does not have to be replaced.
                if ($path === 'typo3conf/ext/qfq/Classes/Api/download.php') {
                    continue;
                }

                // Build secure SIP link
                $secureUrl = $link->renderLink($path);

                // Replace original src with secure URL
                $value = str_replace($src, $secureUrl, $value);
            }
        }
        return $value;
    }

    /**
     * Decode RFC 2047 MIME "encoded-words" (e.g. '=?utf-8?Q?...?=') in a mail header to UTF-8.
     * Plain text passes through unchanged; handles folded/multiple encoded-words and mixed charsets.
     *
     * @param string $value
     * @return string
     */
    public static function decodeMimeHeader(string $value): string {
        if ($value === '') {
            return '';
        }
        if (function_exists('iconv_mime_decode')) {
            $decoded = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8');
            if ($decoded !== false) {
                return $decoded;
            }
        }
        return mb_decode_mimeheader($value);
    }
}

