<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/1/16
 * Time: 6:51 PM
 */

namespace IMATHUZH\Qfq\Core\Store;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/*
 * Stores:
 * - SIP
 * - webVar
 * - record
 * - form
 * - formElement
 */

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

    /**
     * @var Store Instance of class Store. There should only be one class 'Store' at a time.
     */
    private static $instance = null;

    /**
     * @var Sip Instance of class SIP
     */
    public static $sip = null;

    /**
     * @var Database Instance of class SIP
     */
    public static $db = null;

    /**
     * @var Session Instance of class Session
     */
//    private static $session = null;

    /**
     * @var array Stores all indiviudal stores with the variable raw values
     *
     * $raw['D']['id'] = 0  - Defaultvalues from Tabledefinition
     * ...
     * $raw['S']['r'] = 1234 - record ID from current SIP identifier
     * ...
     * $raw['C']['HTTP_SERVER'] = 'qfq' - Servername
     * $raw['C']['s'] = 'badcaffee1234' - recent SIP
     */
    private static $raw = array();

    /**
     * A list of keys of the system store that cannot be overwritten
     * by dynamic variables.
     * @var array
     */
    private static $storeSystemProtectedKeys = array();

    /**
     * @var array Default sanitize classes.
     */
    private static $sanitizeClass = array();

    /**
     * $sanitizeClass['S'] = false
     * $sanitizeClass['C'] = true
     * ...
     *
     * @var array each entry with true/false - depending if store needs to be sanitized.
     */
    private static $sanitizeStore = array();

    private static $phpUnit = false;

    /**
     * The list of stores used by the dev panel. These stores are saved to the session.
     * @var array
     */
    private static $devPanelStores = [STORE_CLIENT, STORE_RECORD, STORE_SIP, STORE_SYSTEM, STORE_TYPO3, STORE_USER];

    /**
     * @param string $bodytext
     * @param string $PhpUnitOverloadAbsoluteConfigFilePath
     *
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function __construct($bodytext = '', $PhpUnitOverloadAbsoluteConfigFilePath = '') {

//        self::$session = Session::getInstance(self::$phpUnit);

        // This check is critical for some unwanted exception recursion during startup.
        if (!function_exists('normalizer_normalize')) {
            throw new \CodeException("Function normalizer_normalize() not found - Please install 'php-intl'", ERROR_MISSING_INTL);
        }

        self::$sanitizeClass = [
//            TYPO3_DEBUG_LOAD => SANITIZE_ALLOW_DIGIT,
//            TYPO3_DEBUG_SAVE => SANITIZE_ALLOW_DIGIT,
//            TYPO3_FORM => SANITIZE_ALLOW_ALNUMX,
//            TYPO3_FE_USER => SANITIZE_ALLOW_ALNUMX,
//            TYPO3_FE_USER_UID => SANITIZE_ALLOW_DIGIT,
//            TYPO3_FE_USER_GROUP => SANITIZE_ALLOW_ALNUMX,

            CLIENT_SIP => SANITIZE_ALLOW_ALNUMX,
            CLIENT_TYPO3VARS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_RECORD_ID => SANITIZE_ALLOW_DIGIT,
            CLIENT_PAGE_ID => SANITIZE_ALLOW_DIGIT,
            CLIENT_PAGE_TYPE => SANITIZE_ALLOW_DIGIT,
            CLIENT_PAGE_LANGUAGE => SANITIZE_ALLOW_DIGIT,
            CLIENT_FORM => SANITIZE_ALLOW_ALNUMX,

            // Part of $_SERVER. Missing vars must be requested individual with the needed sanitize class.
            CLIENT_SCRIPT_URL => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SCRIPT_URI => SANITIZE_ALLOW_ALNUMX,
            CLIENT_HTTP_HOST => SANITIZE_ALLOW_ALNUMX,
            CLIENT_HTTP_USER_AGENT => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SERVER_NAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SERVER_ADDRESS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SERVER_PORT => SANITIZE_ALLOW_DIGIT,
            CLIENT_REMOTE_ADDRESS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_HTTP_X_REAL_IP => SANITIZE_ALLOW_ALNUMX,
            CLIENT_REQUEST_SCHEME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_REQUEST_METHOD => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SCRIPT_FILENAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_QUERY_STRING => SANITIZE_ALLOW_ALL,
            CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL,
            CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX,
//            CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT,

//            SYSTEM_DBUSER => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBSERVER => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBPW => SANITIZE_ALLOW_ALL,
//            SYSTEM_DB => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_TESTDB => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_SESSIONNAME => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBH => SANITIZE_ALLOW_ALL,

//            SYSTEM_SQL_RAW => SANITIZE_ALLOW_ALL,
//            SYSTEM_SQL_FINAL => SANITIZE_ALLOW_ALL,
//            SYSTEM_SQL_COUNT => SANITIZE_ALLOW_DIGIT,
//            SYSTEM_SQL_PARAM_ARRAY => SANITIZE_ALLOW_ALL,

//            SIP_SIP => SANITIZE_ALLOW_ALNUMX,
//            SIP_RECORD_ID => SANITIZE_ALLOW_DIGIT,
//            SIP_FORM => SANITIZE_ALLOW_ALNUMX,
//            SIP_URLPARAM => SANITIZE_ALLOW_ALL

        ];

        self::$sanitizeStore = [
            STORE_FORM => true,
            STORE_SIP => true,
            STORE_RECORD => true,
            STORE_BEFORE => false,
            STORE_PARENT_RECORD => false,
            STORE_TABLE_DEFAULT => false,
            STORE_TABLE_COLUMN_TYPES => false,
            STORE_CLIENT => true,
            STORE_TYPO3 => false,
            STORE_VAR => true,
            STORE_ZERO => false,
            STORE_EMPTY => false,
            STORE_ACCESS => true,
            STORE_WEB => true,
            STORE_SYSTEM => false,
            STORE_EXTRA => false,
            STORE_USER => true,
            STORE_LDAP => false,
            STORE_ADDITIONAL_FORM_ELEMENTS => false,
        ];

        if (Session::get(DEV_PANEL_SESSION_FLAG_CHANGES) === true) {
            // If via DevPanel triggered reload happens the stores that have been Saved in Session are used.
            self::fillStoresFromSession();
            self::fillStorePhpSession(STORE_EXTRA);
            self::fillStorePhpSession(STORE_ACCESS);
            self::fillStorePhpSession(STORE_WEB);
        } else {
            self::fillStoreTypo3($bodytext);  // should be filled before fillStoreSystem() to offer T3 variables, especially 'feUser'
            self::fillStoreClient();  // should be filled before fillStoreSystem() to offer Client variables
            self::fillStorePhpSession(STORE_EXTRA); // should be filled before fillStoreSystem() to restore variables like SYSTEM_SHOW_DEBUG_INFO
            self::fillStorePhpSession(STORE_USER); // should be filled before fillStoreSystem() to restore variables like SYSTEM_SHOW_DEBUG_INFO
            self::fillStorePhpSession(STORE_ACCESS);
            self::fillStorePhpSession(STORE_WEB);
            self::fillStoreSystem($PhpUnitOverloadAbsoluteConfigFilePath);
            self::fillStoreSip();
        }
    }

    /**
     * If not already open: open database.
     *
     * @return void
     */
    private static function getDatabase() {
        if (self::$db == null) {
            self::$db = new Database();
        }
    }

    /**
     * Fill the internal `$raw` store array with values from the session.
     *
     * Only specific store types (SIP, Client, User, System, Typo3) are allowed and retrieved.
     * If a session value exists for a given store, it is added to `self::$raw`.
     * This allows the application to restore / change state or context from Session.
     *
     * @return void
     */
    private static function fillStoresFromSession(): void {
        self::$sip = new Sip(self::$phpUnit);
        foreach (self::$devPanelStores as $store) {
            if ($store === STORE_RECORD) {
                // Do not load STORE_RECORD from the session
                continue;
            }
            self::$raw[$store] = self::loadFromSession($store);
        }
    }

    /**
     * @param $storeName
     * @return bool - true if store accepts sanitize class
     */
    public function getSanitizeStore($storeName) {
        return self::$sanitizeStore[$storeName];
    }

    /**
     * Returns a pointer to this Class.
     *
     * @param string $bodytext
     * @param bool|false $phpUnit
     * @param string $PhpUnitOverloadAbsoluteConfigFilePath
     *
     * @return null|Store
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function getInstance($bodytext = '', $phpUnit = false, $PhpUnitOverloadAbsoluteConfigFilePath = '') {

        if (defined('PHPUNIT_QFQ')) {
            self::$phpUnit = true;
        }

        if (self::$phpUnit) {

            // If $qfqConfigPhpUnit is given: clean STORE
//            if (self::$instance !== null && $qfqConfigPhpUnit != '') {
            if ($PhpUnitOverloadAbsoluteConfigFilePath != '') {

                if ($PhpUnitOverloadAbsoluteConfigFilePath == 'init') {
                    $PhpUnitOverloadAbsoluteConfigFilePath = '';
                }
                // fake to have a clean environment for the next test.
                self::unsetStore(STORE_TYPO3);
                self::fillStoreTypo3($bodytext);

                self::unsetStore(STORE_CLIENT);
                self::fillStoreClient();
            }

            // Testing different config files means initialize completely
            if ($PhpUnitOverloadAbsoluteConfigFilePath != '') {
                self::$instance = null;
            }
        }

        // Design Pattern: Singleton
        if (self::$instance === null) {
//            self::$phpUnit = $phpUnit;

            self::$instance = new self($bodytext, $PhpUnitOverloadAbsoluteConfigFilePath);
        } else {
            // Class Store seems to be persistent over multiple QFQ instantiation. Set bodytext again, with every new request (if bodytext is given).
            if ($bodytext !== '') {
                self::fillStoreTypo3($bodytext);
                self::unsetStore(STORE_RECORD); // At least in Multi DB Setup: STORE_RECORD of former  tt-content QFQ Records interfere with loading a form.
            }
        }

        // Disable TYPO3_DEBUG_SHOW_BODY_TEXT=1 if SYSTEM_SHOW_DEBUG_INFO!='yes'
        if (self::getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === '1' &&
            !Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))
        ) {
            self::setVar(TYPO3_DEBUG_SHOW_BODY_TEXT, '0', STORE_TYPO3);
        }

        return self::$instance;
    }

    /**
     * Set or overwrite a complete store.
     *
     * @param array $dataArray
     * @param string $store
     * @param bool|false $flagOverwrite
     *
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function setStore(array $dataArray, $store, $flagOverwrite = false) {

        // Check valid store name
        if (!isset(self::$sanitizeStore)) {
            throw new \UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
        } elseif ($store === STORE_ZERO || $store === STORE_EMPTY) {
            throw new \CodeException("setVarArray() for STORE_ZERO/STORE_EMPTY is impossible - there are no values.", ERROR_SET_STORE_ZERO);
        } elseif (!$flagOverwrite && !empty(self::$raw[$store] ?? false)) {
            throw new \CodeException("Raw values already been copied to store '$store'. Do this only one time.", ERROR_STORE_VALUE_ALREADY_CODPIED);
        }

        self::$raw[$store] = $dataArray;
        if ($store === STORE_EXTRA || $store === STORE_USER || $store === STORE_ACCESS || $store === STORE_WEB) {
            self::saveInSession($store, $dataArray);
        }
    }

    /**
     * Saves the content of a store in the current PHP session.
     *
     * @param $store
     * @param array $data
     * @return void
     */
    public static function saveInSession($store, array $data): void {
        Session::set(SESSION_STORE_PREFIX . '-' . $store, serialize($data));
    }

    /**
     * Loads store content from the current PHP session.
     * Returns null if no data has been found.
     *
     * @param $store
     * @return array|null
     */
    public static function loadFromSession($store): array {
        $storeKey = SESSION_STORE_PREFIX . '-' . $store;
        $loaded = Session::get($storeKey);
        if (is_string($loaded)) {
            // unserialize triggers E_NOTICE when the string cannot be unserialized
            try {
                $content = unserialize($loaded);
                if (is_array($content)) {
                    return $content;
                }
            } catch (\Throwable $e) {
                // Corrupted store data - ignore and clear
                // We may want to log this!
                Session::unsetItem($storeKey);
            }
        }
        // Return an empty array if no value has been found.
        return [];
    }

    /**
     * Deletes a store assigning a new empty array to it.
     *
     * @param $store
     *
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function unsetStore($store) {
        self::setStore(array(), $store, true);
    }

    /**
     * Returns a complete $store.
     *
     * @param string $store STORE_SYSTEM, ...
     *
     * @return array
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function getStore($store) {
        $vars = array();

        // Check valid store name
        if (!isset(self::$sanitizeStore[$store])) {
            throw new \UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
        }

        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
            throw new \CodeException("getStore() for $store is impossible - there are no values saved.", ERROR_GET_INVALID_STORE);
        }

        if (isset(self::$raw[$store])) {
            $vars = self::$raw[$store];
            if ($store == STORE_SIP) {
                $vars = self::checkDecodeBase64Arr($vars);
            }
        }

        return $vars;
    }

    /**
     * Sets the initial content of a store. Currently, the following stores use initialization:
     * - STORE_USER, STORE_ACCESS: after a fe user has changed
     * - STORE_TYPO3: recreated each time an instance of Store is requested
     *
     * @param array $dataArray
     * @param string $store
     * @return void
     */
    public static function setInitialStore(array $dataArray, string $store): void {
        if (!isset(self::$raw[STORE_EXTRA][STORE_EXTRA_INITIAL_STORE])) {
            self::$raw[STORE_EXTRA][STORE_EXTRA_INITIAL_STORE] = [];
        }
        self::$raw[STORE_EXTRA][STORE_EXTRA_INITIAL_STORE][$store] = $dataArray;
    }

    /**
     * Returns the initial content of a store.
     *
     * @param string $store
     * @return array
     */
    public static function getInitialStore(string $store): array {
        return self::$raw[STORE_EXTRA][STORE_EXTRA_INITIAL_STORE][$store] ?? [];
    }


    /**
     * Fill the system store by reading config.qfq.ini. Also setup config defaults.
     *
     * @param string $PhpUnitOverloadAbsoluteConfigFilePath
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private static function fillStoreSystem($PhpUnitOverloadAbsoluteConfigFilePath = '') {

        $config = Config::getConfigArray($PhpUnitOverloadAbsoluteConfigFilePath);
        $config[SYSTEM_QFQ_PROJECT_PATH] = Path::absoluteProject();

        // Get qfq version
        $extVersion = Config::getExtVersion();
        $config[SYSTEM_QFQ_VERSION] = $extVersion[EXT_KEY];
        $config[SYSTEM_DOCUMENTATION_QFQ] = SYSTEM_DOCUMENTATION_QFQ_URL . '/v' . $extVersion[EXT_KEY];

        // Set baseUrlLang

        $baseLang = T3Info::getBaseUrlLang();
        $config[SYSTEM_BASE_URL_LANG] = is_null($baseLang) ? $config[SYSTEM_BASE_URL] : $baseLang;

        self::setStore($config, STORE_SYSTEM, true);
        self::$storeSystemProtectedKeys = array_flip(array_keys($config));
    }

    /**
     * Copy the BodyText as well as some T3 specific vars to STORE_TYPO3.
     * Attention: if called through API, there is no T3 environment. The only values which are available are fe_user
     * and fe_user_uid.
     *
     * @param $bodytext
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private static function fillStoreTypo3($bodytext) {

        if (isset($GLOBALS["TSFE"])) {
            $arr = T3Info::getVars();
        } elseif (isset($_SESSION[SESSION_NAME])) {
            // No T3 environment (called by API): restore from SESSION
            $arr = array_intersect_key(
                $_SESSION[SESSION_NAME],
                array_flip([SESSION_FE_USER, SESSION_FE_USER_UID, SESSION_FE_USER_GROUP, SESSION_BE_USER, SESSION_PAGE_LANGUAGE, SESSION_PAGE_LANGUAGE_PATH, SESSION_PAGE_ID]),
            );
        } else {
            $arr = [];
        }

        $arr = array_merge(
        // Add initial values
            self::getInitialStore(STORE_TYPO3),
            // form=, showDebugBodyText=, 10.20..
            KeyValueStringParser::parse($bodytext, "=", "\n"),
            // T3 values
            $arr
        );

        self::setStore($arr, STORE_TYPO3, true);
    }

    /**
     * Fills the STORE_CLIENT
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private static function fillStoreClient() {

        $arr = Client::getParam();

        self::setStore($arr, STORE_CLIENT, true);

    }

    /**
     * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP.
     *
     * @param bool|string $s
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function fillStoreSip($s = false) {

        if (self::$sip === null) {
            self::$sip = new Sip(self::$phpUnit);
        }

        if ($s === false) {
            $s = self::getVar(CLIENT_SIP, STORE_CLIENT);
        }

        if ($s !== false) {
            // if session is given, copy values to store
            $param = self::$sip->getVarsFromSip($s);
            $param[SIP_SIP] = $s;
            $param[SIP_URLPARAM] = self::$sip->getQueryStringFromSip($s);

//            self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP);
            self::setStore($param, STORE_SIP, true);
        }
    }

    /**
     * Fills the STORE_EXTRA | STORE_USER | STORE_ACCESS.
     *
     * @param string $storeName STORE_EXTRA, STORE_USER, STORE_ACCESS
     * @throws \CodeException
     * @throws \UserFormException
     */
    private static function fillStorePhpSession($storeName): void {
        self::setStore(self::loadFromSession($storeName), $storeName, true);
    }

    /**
     * Fills STORE_TABLE_DEFAULT and STORE_TABLE_COLUMN_TYPES
     *
     * @param array $tableDefinition
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function fillStoreTableDefaultColumnType(array $tableDefinition) {

        // Defaults: empty strings are given as "''" > fix them to "".
        foreach ($tableDefinition as $key => $value) {
            if (($tableDefinition[$key]['Default'] ?? '') == "''") {
                $tableDefinition[$key]['Default'] = '';
            }
        }
        self::setStore(array_column($tableDefinition, 'Default', 'Field'), STORE_TABLE_DEFAULT, true);
        self::setStore(array_column($tableDefinition, 'Type', 'Field'), STORE_TABLE_COLUMN_TYPES, true);
    }

    /**
     * Set's a single $key/$value pair $store.
     *
     * @param string $key
     * @param string|array $value
     * @param string $storeName
     * @param bool|true $overWrite
     *
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function setVar($key, $value, $storeName, $overWrite = true) {
        // Check valid Store name
        if (!isset(self::$sanitizeStore)) {
            throw new \UserFormException("Unknown Store: $storeName", ERROR_UNNOWN_STORE);
        }

        if ($storeName === STORE_ZERO) {
            throw new \CodeException("setVar() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO);
        }

        // Complain if already set and different.
        if ($overWrite === false && isset(self::$raw[$storeName][$key]) && self::$raw[$storeName][$key] != $value) {
            throw new \UserFormException("Value of '$key' already set in store '$storeName'.", ERROR_STORE_KEY_EXIST);
        }

        self::$raw[$storeName][$key] = $value;

        // The STORE_EXTRA / STORE_USER saves arrays and is persistent
        if ($storeName === STORE_EXTRA || $storeName === STORE_USER || $storeName === STORE_ACCESS || $storeName === STORE_WEB) {

            $data = self::loadFromSession($storeName);

            // Switch FeUser: log this to qfq.log
            if ($storeName === STORE_USER && $key == TYPO3_FE_USER) {
                $qfqLogPathAbsolute = Path::absoluteQfqLogFile();
                $feUserOld = isset($data[$key]) ? $data[$key] : self::getVar($key, STORE_TYPO3 . STORE_EMPTY);
                if ($feUserOld !== $value) {
                    Logger::logMessage(date('Y.m.d H:i:s ') . ": Switch feUser '$feUserOld' to '$value'", $qfqLogPathAbsolute);
                }
            }

            $data[$key] = $value;
            self::saveInSession($storeName, $data);
        } elseif ($storeName === STORE_SYSTEM) {
            // Preserve this variable from being cleared
            self::$storeSystemProtectedKeys[$key] = 1;
        }
    }

    /**
     * Unset a variable in store.
     *
     * @param $key
     * @param $store
     */
    public static function unsetVar($key, $store) {

        if (isset(self::$raw[$store][$key])) {
            unset(self::$raw[$store][$key]);
            if ($store === STORE_SYSTEM && isset(self::$storeSystemProtectedKeys[$key])) {
                unset(self::$storeSystemProtectedKeys[$key]);
            }
        }
    }

    /**
     * Cycles through all stores in $useStore.
     * First match will return the found value.
     * During cycling: fill cache with requested value and sanitize raw value.
     *
     * @param string $key
     * @param string $useStores f.e.: 'FSRVD'
     * @param string $sanitizeClass
     * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found,
     *                             return ''.
     *
     * @param string $typeMessageViolate
     * @return string|array a) if found: value, b) false. STORE_EXTRA returns an array for the given key.
     * @throws \CodeException
     * @throws \UserFormException    SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO | SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY | SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS
     * @throws \UserReportException
     * @throws \DbException
     */
    public static function getVar($key, $useStores = STORE_USE_DEFAULT, $sanitizeClass = '', &$foundInStore = '',
                                  $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS, $default = '') {

        // no store specified?
        if ($useStores === "" || $useStores === null) {
            $useStores = STORE_USE_DEFAULT;
        }


        $len = strlen(SIP_PREFIX_BASE64);

        while ($useStores !== false && $useStores !== '') {
            $store = $useStores[0]; // current store

            // Deprecation check: Throw exception if pageAlias is used
            if ($key === TYPO3_PAGE_ALIAS && $store === STORE_TYPO3 && T3Handler::useSlugsInsteadOfPageAlias()) {
                Thrower::userReportException('Deprecated functionality used.', 'Tried to access the variable ' . TYPO3_PAGE_ALIAS
                    . ' from Typo3 store. Since Typo3 version 9 page slugs are used instead of page aliases. Please use ' . TYPO3_PAGE_SLUG
                    . ' instead. To enable the page slug migration check you can set the config variable '
                    . FORCE_RUN_PAGE_SLUG_MIGRATION_CHECK . ' to "true" in ' . Path::absoluteConf(CONFIG_QFQ_JSON));
            }

            $finalKey = $key;
            if ($store == STORE_LDAP) {
                $finalKey = strtolower($key); // in STORE_LDAP all keys are lowercase
            }

            $foundInStore = $store;
            $useStores = substr($useStores, 1); // shift left remaining stores

            if (!isset(self::$raw[$store][$finalKey])) {
                switch ($store) {
                    case STORE_ZERO:
                        return 0;
                    case STORE_EMPTY:
                        return '';
                    case STORE_VAR:
                        if ($finalKey === VAR_RANDOM) {
                            return Support::randomAlphaNum(RANDOM_LENGTH);
                        } else {
                            continue 2;  // no value provided, continue with while loop
                        }
                        break;
                    default:
                        continue 2; // no value provided, continue with while loop
                        break;
                }
            }

            // no sanitizeClass specified: take predefined (if exist) or default.
            if ($sanitizeClass === '' || $sanitizeClass === null) {

                switch ($store) {
                    case STORE_CLIENT:
                    case STORE_FORM:
                        $sanitizeClassFinal = self::$sanitizeClass[$key] ?? SANITIZE_DEFAULT_OF_STORE[$store];
                        break;
                    default:
                        $sanitizeClassFinal = SANITIZE_DEFAULT_OF_STORE[$store];
                        break;
                }
            } else {
                $sanitizeClassFinal = $sanitizeClass;
            }

            $rawVal = self::$raw[$store][$finalKey] ?? null;
            if (self::$sanitizeStore[$store] && $sanitizeClassFinal != '') {
                if ($sanitizeClassFinal == SANITIZE_ALLOW_PATTERN) {
                    // We do not have any pattern at this point. For those who be affected, they already checked earlier. So set 'no check'
                    $sanitizeClassFinal = SANITIZE_ALLOW_ALL;
                }

                return Sanitize::sanitize($rawVal, $sanitizeClassFinal, '', '', SANITIZE_EMPTY_STRING, '', $typeMessageViolate);
            } else {
                if ($store == STORE_SIP && (substr($key, 0, $len) == SIP_PREFIX_BASE64)) {
                    $rawVal = base64_decode($rawVal);
                }

                return $rawVal;
            }
        }
        $foundInStore = '';

        return false;
    }

    /**
     * Create a SIP after a form load. This is necessary on forms without a sip and on forms with r=0 (new record).
     *
     * @param $formName
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function createSipAfterFormLoad($formName) {

        $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT);
        if ($recordId === false) {
            $recordId = 0;
        }

        // If there are existing SIP param, keep them by copying to the new SIP Param Array
        $tmpParam = self::getNonSystemSipParam();

        // Preserve dbIndexData - getNonSystemSipParam() filters '_'-prefixed keys, but this one is needed for multi-DB forms.
        $dbIndexData = self::getVar(PARAM_DB_INDEX_DATA, STORE_SIP);
        if ($dbIndexData !== false) {
            $tmpParam[PARAM_DB_INDEX_DATA] = $dbIndexData;
        }

        $tmpParam[SIP_RECORD_ID] = $recordId;
        $tmpParam[SIP_FORM] = $formName;

        if ($recordId == 0) {
            // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter
            $tmpParam[SIP_MAKE_URLPARAM_UNIQ] = uniqid();
        }

        // Construct fake urlparam
        $tmpUrlparam = OnArray::toString($tmpParam);

        // Create a fake SIP which has never been passed by URL - further processing might expect this to exist.
        $sip = self::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP);
        self::setVar(CLIENT_SIP, $sip, STORE_CLIENT);

        // Store in SIP Store (cause it's empty until now).
        $tmpParam[SIP_SIP] = $sip;
        self::setStore($tmpParam, STORE_SIP, true);

    }

    /**
     * Return an array with non system SIP parameter. Take the whole STORE_SIP and search for non system parameter.
     *
     * @return array
     * @throws \UserFormException
     * @throws \CodeException
     */
    public static function getNonSystemSipParam() {
        $tmpParam = array();

        $sipArray = self::getStore(STORE_SIP);

        foreach ($sipArray as $key => $value) {

            if ($key[0] === '_') {
                continue;
            }

            switch ($key) {
                case SIP_SIP:
                case SIP_RECORD_ID:
                case SIP_FORM;
                case SIP_URLPARAM:
                    continue 2;
                    break;
                default:
                    $tmpParam[$key] = $value;
                    break;
            }
        }

        return $tmpParam;
    }

    /**
     * Iterate over an array and look for keynames starting with SIP_PREFIX_BASE64.
     * Found elements will be base64_decode().
     *
     * @param array $vars
     *
     * @return array - incl. decoded base64 vars.
     */
    private static function checkDecodeBase64Arr(array $vars) {

        $len = strlen(SIP_PREFIX_BASE64);

        foreach ($vars as $key => $value) {
            if (substr($key, 0, $len) == SIP_PREFIX_BASE64) {
                $vars[$key] = base64_decode($value);
            }
        }

        return $vars;
    }

    /**
     * Returns a pointer to this class.
     *
     * @return null|Sip
     */
    public static function getSipInstance() {
        return self::$sip;
    }

    /**
     * Saves a subset of STORE_TYPO3 vars as a SIP. The SIP will be transmitted as hidden form element.
     *
     * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls'
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function copyT3VarsToSip() {

        $tempArray = self::getStore(STORE_TYPO3);

        foreach ([TYPO3_FE_USER, TYPO3_FE_USER_UID, TYPO3_FE_USER_GROUP, TYPO3_FE_USER_EMAIL,
                     TYPO3_BE_USER, TYPO3_BE_USER_UID, TYPO3_BE_USER_EMAIL,
                     TYPO3_TT_CONTENT_UID, T3DATA_UID,
                     TYPO3_PAGE_ID, TYPO3_PAGE_ALIAS, TYPO3_PAGE_SLUG, TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE, TYPO3_PAGE_LANGUAGE_PATH] as $key) {
            if (isset($tempArray[$key])) {
                $t3varsArray[$key] = $tempArray[$key];
            }
        }

        $t3varsArray[TYPO3_BE_USER_LOGGED_IN] = T3Info::beUserLoggedIn() ? 'yes' : 'no';

        $t3varsString = KeyValueStringParser::unparse($t3varsArray, '=', '&');

        return self::$sip->queryStringToSip($t3varsString, RETURN_SIP);
    }

    /**
     * Get stored STORE_TYPO3 vars from SIP and restore the store.
     *
     * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls'
     *
     * @param string $sipTypo3Vars
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function fillTypo3StoreFromSip($sipTypo3Vars) {
        $t3vars = self::getStore(STORE_TYPO3);

        // Do nothing if STORE_TYPO3 is already filled
        if (isset($t3vars[TYPO3_TT_CONTENT_UID]) && $t3vars[TYPO3_TT_CONTENT_UID] != '0') {
            return;
        }

        $typo3VarsArray = self::$sip->getVarsFromSip($sipTypo3Vars);

        self::setStore($typo3VarsArray, STORE_TYPO3, true);

        $value = self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM);
        $flag = isset($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN]) && ($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN] == 'yes');
        $value = Config::adjustConfigDebugInfoAuto($value, $flag);
        self::setVar(SYSTEM_SHOW_DEBUG_INFO, $value, STORE_SYSTEM);
    }

    /**
     * Load $recordId from $tableName using $db and saves it in $store.
     *
     * @param $tableName
     * @param $recordId
     * @param Database $db
     * @param $primaryKey
     * @param $store
     * @param $specificMessage
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public static function fillStoreWithRecord($tableName, $recordId, Database $db, $primaryKey = F_PRIMARY_KEY_DEFAULT,
                                               $store = STORE_RECORD, $specificMessage = '') {
        if ($recordId !== false && $recordId > 0 &&
            is_string($tableName) && $tableName !== '') {
            if (!$primaryKey) {
                $primaryKey = F_PRIMARY_KEY_DEFAULT;
            }

            $record = $db->sql("SELECT * FROM `$tableName` WHERE `$primaryKey` = ?", ROW_EXPECT_1, [$recordId], $specificMessage);
            self::setStore($record, $store, true);
        }
    }

    /**
     * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_1|2|3 from SYSTEM_STORE and if set:
     * a) fire the SQL
     * b) merge all columns to STORE_SYSTEM
     *
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function fillStoreSystemBySql() {

        self::getDatabase();
        $flagDirty = false;
        $storeSystem = self::getStore(STORE_SYSTEM);

        for ($ii = 1; $ii <= 3; $ii++) {
            if (empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"])) {
                continue;
            }

            $errMsg = "More than 1 record found. " . CONFIG_QFQ_PHP . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii";
            $mode = ROW_EXPECT_0_1;

            // If there is an error message defined, this means there should be exactly one record.
            if (!empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"])) {
                $mode = ROW_EXPECT_1;
                $errMsg = $storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"];
            }

            $storeSystemAdd = self::$db->sql($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"], $mode, array(), $errMsg);
            $storeSystemAdd = OnArray::keyNameRemoveLeadingUnderscore($storeSystemAdd);
            $storeSystem = array_merge($storeSystemAdd, $storeSystem);
            $flagDirty = true;
        }

        if ($flagDirty) {
            self::setStore($storeSystem, STORE_SYSTEM, true);
        }
    }

    /**
     * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW from SYSTEM_STORE and if set:
     * a) fire the SQL
     * b) merge all columns to STORE_SYSTEM
     *
     * SQL: SELECT '..' AS name, '...' AS value ...
     *
     * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW_VARIABLE from SYSTEM_STORE and if set:
     * a) fire the SQL also can parse variable
     * b) merge all columns to STORE_SYSTEM
     *
     * SQL: SELECT 'pIdUser' AS name , pId AS value ...FROM ... WHERE p.account = '{{feUser:T}}'
     *
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function fillStoreSystemBySqlRow(Evaluate $evaluate = null) {

        self::getDatabase();

        $storeSystem = self::getStore(STORE_SYSTEM);

        // Determine the key based on whether an Evaluate object is provided
        $sqlKey = $evaluate ? SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW_VARIABLE : SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW;

        if (empty($storeSystem[$sqlKey])) {
            return;
        }

        // Determine the error message key based on whether an Evaluate object is provided
        $errMsgKey = $evaluate ? SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW_VARIABLE_ERROR_MSG : SYSTEM_FILL_STORE_SYSTEM_BY_SQL_ROW_ERROR_MSG;

        if (empty($storeSystem[$errMsgKey])) {
            $errMsg = "Broken SQL defined in config: " . $sqlKey;
            $mode = ROW_REGULAR;
        } else {
            $errMsg = $storeSystem[$errMsgKey];
            $mode = ROW_EXPECT_GE_1;
        }

        // If an Evaluate object is provided, parse the SQL statement through it
        $statement = $evaluate ? $evaluate->parse($storeSystem[$sqlKey]) : $storeSystem[$sqlKey];

        $storeSystemAdd = self::$db->sql($statement, $mode, array(), $errMsg);


        // Check existence of 'name' and 'value'
        $row = $storeSystemAdd[0] ?? null;
        if ($row != null && (!isset($row[SYSTEM_FILL_STORE_SYSTEM_KEYWORD_NAME]) || !isset($row[SYSTEM_FILL_STORE_SYSTEM_KEYWORD_VALUE]))) {
            throw new \CodeException("Invalid " . $sqlKey . " in config. Missing column 'name' and/or 'value'.", ERROR_MISSING_KEY_VALUE);
        }

        // Convert [ [ name => 'name1', value => 'value1' ], [ name => 'name2', value => 'value2' ], ... ]
        // to [ name1 => value1, name2 => value2, ... ]
        $arr = array();
        foreach ($storeSystemAdd as $row) {
            $arr[$row[SYSTEM_FILL_STORE_SYSTEM_KEYWORD_NAME]] = $row[SYSTEM_FILL_STORE_SYSTEM_KEYWORD_VALUE];
        }

        // In case App Developer names the keys with leading underscore: remove the underscore.
        $arr = OnArray::keyNameRemoveLeadingUnderscore($arr);

        self::setStore(array_merge($arr, $storeSystem), STORE_SYSTEM, true);
    }

    /**
     * Append an array (in case of 'array of array': the first row of array) to store $storeName.
     * Existing values will be overwritten.
     *
     * @param array $dataArray - in special cases it might be an empty string -therefore no type definition to 'array'.
     * @param $storeName
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function appendToStore($dataArray, $storeName) {

        if (empty($dataArray) || !is_array($dataArray)) {
            return;
        }

        if (isset($dataArray[0]) && is_array($dataArray[0])) {
            $append = $dataArray[0];
        } else {
            $append = $dataArray;
        }

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

        $store = Store::getInstance();
        $store->setStore(array_merge($store->getStore($storeName), $append), $storeName, true);
    }

    /** Set the dynamic value of SYSTEM_SHOW_HISTORY in STORE_SYSTEM.
     *
     * @throws \UserFormException
     * @throws \CodeException
     * @throws \UserReportException
     * @throws \DbException
     */
    public static function setShowHistory(Evaluate $evaluate = null): void {
        self::getDatabase();

        $showHistoryParam = self::getVar(SYSTEM_SHOW_HISTORY, STORE_SYSTEM);
        $showHistoryParam = $showHistoryParam ?? 0;

        if ($showHistoryParam != 0 && $showHistoryParam != 1) {
            $statement = $evaluate->parse($showHistoryParam);
            $showHistoryResult = self::$db->sql($statement, ROW_EXPECT_0_1);
            if (!empty($showHistoryResult)) {
                self::setVar(SYSTEM_SHOW_HISTORY, reset($showHistoryResult), STORE_SYSTEM);
            } else {
                self::setVar(SYSTEM_SHOW_HISTORY, 0, STORE_SYSTEM);
            }
        }
    }

    /**
     * Stores the current raw store state into the session.
     *
     * Iterates over the internal static `$raw` store array and writes each entry to the session.
     * This is useful for persisting the current in-memory store state across page reloads or requests.
     *
     * @return void
     */
    public function setStoreStateToSession(): void {
        foreach (self::$raw as $storeKey => $storeValue) {
            self::saveInSession($storeKey, $storeValue);
        }
    }

    /**
     * Retrieves a subset of store states from the session.
     *
     * This method pulls specific store keys (record, client, sip, user, typo3, system)
     * from the session and returns them in an associative array, only including
     * those that exist in the session.
     *
     * @return array An associative array of store keys and their corresponding session values
     */
    public function getStoreStateFromSession(): array {
        $data = [];

        foreach (self::$devPanelStores as $store) {
            $data[$store] = self::loadFromSession($store);
        }

        return $data;
    }

    public static function clearDynamicVariablesFromStoreSystem(): void {
        self::$raw[STORE_SYSTEM] = array_intersect_key(
            self::$raw[STORE_SYSTEM],
            self::$storeSystemProtectedKeys
        );
    }

    /**
     * Clears user-related data from the store
     * @return void
     */
    public function clearUserData(bool $initialize = false) {
        if ($initialize) {
            $this->setStore($this->getInitialStore(STORE_USER), STORE_USER, true);
            $this->setStore($this->getInitialStore(STORE_ACCESS), STORE_ACCESS, true);
        } else {
            $this->unsetStore(STORE_USER);
            $this->unsetStore(STORE_ACCESS);
        }
    }
}
