<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 3/23/16
 * Time: 1:31 PM
 */

namespace IMATHUZH\Qfq\Core\Store;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\DateTime;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;

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

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

    /**
     * @var Database[] - Array of Database instantiated class
     */
    private $dbArray = array();

    private $dbIndexData = false;
    private $dbIndexQfq = false;

    /**
     * @var array
     */
    private $feSpecNative = array();

    /**
     * @var Evaluate
     */
    private $evaluate = null;

    /**
     * FillStoreForm constructor.
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct() {

        $this->store = Store::getInstance();

        $this->dbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP);
        if ($this->dbIndexData === false) {
            $this->dbIndexData = DB_INDEX_DEFAULT; // Fallback for FORMs which are not called via SIP;
        }
        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);

        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
        if ($this->dbIndexQfq != $this->dbIndexData) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
        }

        $this->feSpecNative = $this->loadFormElementsBasedOnSIP();

        $form = $this->store->getVar(SIP_FORM, STORE_SIP, SANITIZE_ALLOW_ALNUMX);
        if (!empty($form) && !defined('PHPUNIT_QFQ')) {
            // To make STORE_RECORD available at a very early stage.
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP, SANITIZE_ALLOW_DIGIT);
            $tableFromFormSql = "SELECT `tableName`, `primaryKey` FROM `Form` WHERE `name`=?";
            $form = $this->dbArray[$this->dbIndexQfq]->sql($tableFromFormSql, ROW_EXPECT_1, [$form]);

            if (empty($form[F_PRIMARY_KEY])) {
                $form[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT;
            }
            $this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]);
        }

        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
    }

    /**
     * Loads a minimal definition of FormElement of the form specified in SIP.
     *
     * @return array
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function loadFormElementsBasedOnSIP() {

        $formName = $this->store->getVar(SIP_FORM, STORE_SIP);

        // Preparation for Log, Debug
        $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM);

        $feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER, ROW_REGULAR, [$formName],
            'Form or FormElements not found: ' . ERROR_FORM_NOT_FOUND);
        HelperFormElement::explodeParameterInArrayElements($feSpecNative, FE_PARAMETER);

        $feSpecTemplateGroup = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_CONTAINER_TEMPLATE_GROUP, ROW_REGULAR, [$formName]);
        HelperFormElement::explodeParameterInArrayElements($feSpecTemplateGroup, FE_PARAMETER);

        $feSpecNative = $this->expandTemplateGroupFormElement($feSpecTemplateGroup, $feSpecNative);
        $feSpecNative = HelperFormElement::cleanFormElements($feSpecNative);

        return $feSpecNative;
    }

    /**
     * Checks if there are templateGroups defined. If yes, expand them. Return expanded feSpecNative array.
     *
     * @param array $feSpecTemplateGroup
     * @param array $feSpecNative
     *
     * @return array
     */
    private function expandTemplateGroupFormElement(array $feSpecTemplateGroup, array $feSpecNative) {
        $expanded = array();

        if (count($feSpecTemplateGroup) == 0) {
            return $feSpecNative; // No templateGroups >> nothing to do >> just return
        }

        // Iterate over all 'FormElements': part of a templateGroup?
        foreach ($feSpecNative as $fe) {
            $flagCopied = false;

            if ($fe[FE_ID_CONTAINER] > 0) {
                // Search for a corresponding template group.
                foreach ($feSpecTemplateGroup as $templateGroup) {
                    if ($fe[FE_ID_CONTAINER] == $templateGroup[FE_ID]) {

                        $flagCopied = true;

                        // Get max copies per template group
                        $maxCopies = HelperFormElement::tgGetMaxLength($templateGroup[FE_MAX_LENGTH]);

                        // Copy each native FormElement
                        $template = $fe[FE_NAME];
                        for ($ii = 1; $ii <= $maxCopies; $ii++) {
                            $fe[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $template);
                            $expanded[] = $fe;
                        }
                    }
                }
            }

            if (!$flagCopied) {
                $expanded[] = $fe;
            }
        }

        return $expanded;
    }

    /**
     * Copies all current form parameter from STORE_CLIENT to STORE_FORM. Checks the values against FormElement
     * definition and throws an exception if check fails. FormElements.type=hidden will be taken from STORE_SIP.
     *
     * @param string $formMode
     *
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function process($formMode = FORM_SAVE) {

        // The following will never be used during load (fe.type='upload').
        // FYI: #18029 - fillStoreVar should be fired, to have them availbable in modeSql during fillStoreForm
        $skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
            FE_VALUE, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION, FE_TYPEAHEAD_GLUE_UPDATE];

        $html = '';
        $newValues = array();

        $clientValues = $this->store->getStore(STORE_CLIENT);
        $formModeGlobal = Support::getFormModeGlobal($this->formSpec[F_MODE_GLOBAL] ?? '');

        if ($formMode == FORM_UPDATE && $formModeGlobal == '') {
            # During 'update': fake all elements to be not 'required'.
            $formModeGlobal = F_MODE_REQUIRED_OFF;
        }

        // If called through 'api/...': get STORE_TYPO3 via SIP parameter.
        if (isset($clientValues[CLIENT_TYPO3VARS]) && $formMode != FORM_REST) {
            $this->store->fillTypo3StoreFromSip($clientValues[CLIENT_TYPO3VARS]);
        }

        // Retrieve SIP vars, e.g. for HIDDEN elements.
        $sipValues = $this->store->getStore(STORE_SIP);

        // Copy SIP Values; not necessarily defined as a FormElement.
        foreach ($sipValues as $key => $value) {
            switch ($key) {
                case SIP_SIP:
                case SIP_RECORD_ID:
                case SIP_FORM:
                case SIP_TABLE:
                case SIP_URLPARAM:
                case 'id':
                    break;
                default:
                    $newValues[$key] = $value;
                    break;
            }
        }

        if ($formMode != FORM_REST && $formMode != FORM_DRAG_AND_DROP) {
            // Check if there is a 'new record already saved' situation:
            // yes: the names of the input fields are submitted with '<fieldname>:0' instead of '<fieldname>:<id>'
            // no: regular situation, take real 'recordid'
            $fakeRecordId = isset($sipValues[SIP_MAKE_URLPARAM_UNIQ]) ? 0 : ($sipValues[SIP_RECORD_ID]??0);
        }

        // If feId is given over sip than it should be handled separately
        if (isset($sipValues['feId'])) {
            return;
        }

        // Iterate over all FormElements. Sanitize values. Built an assoc array $newValues.
        foreach ($this->feSpecNative as $formElement) {

            // Never get a predefined 'id'
            if ($formElement[FE_NAME] === COLUMN_ID) {
                continue;
            }

            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

            // Evaluate current FormElement: e.g. FE_MODE_SQL
            $formElement = $this->evaluate->parseArray($formElement, $skip, $debugStack);

            // Get related formElement. Construct the field name used in the form.
            $clientFieldName = ($formMode == FORM_REST) ? $formElement[FE_NAME] : HelperFormElement::buildFormElementName($formElement, $fakeRecordId);
            // In case type chat the clientKey ends with -input
            if ($formElement[FE_TYPE] === FE_TYPE_CHAT) {
                $clientFieldName .= '-input';
            }
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement, [FE_MODE => $formModeGlobal]);

            if ($formElement[FE_TYPE] === FE_TYPE_EXTRA) {
                // Extra elements will be transferred by SIP
                if (!isset($sipValues[$formElement[FE_NAME]])) {
                    # Check for reserved names.
                    if ($formElement[FE_NAME] == CLIENT_PAGE_ID || $formElement[FE_NAME] == CLIENT_PAGE_TYPE || $formElement[FE_NAME] == CLIENT_PAGE_LANGUAGE) {
                        throw new \UserFormException(
                            json_encode(
                                [ERROR_MESSAGE_TO_USER => 'Reserved name "' . $formElement[FE_NAME] . '" in FormElement.',
                                    ERROR_MESSAGE_TO_DEVELOPER => 'FE_TYPE="extra" should not use ' . CLIENT_PAGE_ID . ',' . CLIENT_PAGE_TYPE . ',' . CLIENT_PAGE_LANGUAGE]), ERROR_FORM_RESERVED_NAME);
                    }
                    throw new \CodeException("Missing the " . FE_TYPE_EXTRA . " field '" . $formElement[FE_NAME] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP);
                }

                $newValues[$formElement[FE_NAME]] = $sipValues[$formElement[FE_NAME]] ?? '';
                continue;
            }

            switch ($formElement[FE_TYPE]) {
                case FE_TYPE_CHECKBOX:
                    // Checkbox Multi: collect values
                    $val = $this->collectCheckBoxValues($clientFieldName, $clientValues, $formElement[FE_CHECKBOX_CHECKED] ?? '');
                    if ($val !== false) {
                        $clientValues[$clientFieldName] = $val;
                    }
                    break;
                case FE_TYPE_SELECT:
                    if (isset($formElement[FE_SIZE]) && $formElement[FE_SIZE] > 1) {
                        $val = $this->collectCheckBoxValues($clientFieldName, $clientValues, $formElement[FE_CHECKBOX_CHECKED] ?? '');
                        if ($val !== false) {
                            $clientValues[$clientFieldName] = $val;
                        }
                    }
                case FE_TYPE_ANNOTATE:
                    $formElement[FE_ENCODE] = FE_ENCODE_NONE;
                    break;
                case FE_TYPE_COLOR_PICKER:
                    $formElement[FE_CHECK_TYPE] = SANITIZE_ALLOW_ALL;
                    break;
                default:
                    break;
            }

            // Bug #5077 / 'Required' FormElement with Dynamic Update - required FE will be checked later - at this point there is no F, R store.
//            if ($formElement[FE_MODE] === FE_MODE_REQUIRED) {
//                if (!isset($clientValues[$clientFieldName]) || ($clientValues[$clientFieldName] === '')) {
//                    throw new \UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY);
//                }
//            }


            // FORM_REST: typically form elements are filled and created on form load. This does not exist for REST Forms.
            // If a FE.value is defined, this has precedence over client supplied content.
            if ($formMode == FORM_REST && $formElement[FE_VALUE] != '') {
                $clientValues[$clientFieldName] = $this->evaluate->parse($formElement[FE_VALUE]);
            }

            // copy value to $newValues
            if (isset($clientValues[$clientFieldName])) {

                if ($formElement[FE_DYNAMIC_UPDATE] === 'yes' ||
                    $formElement[FE_MODE] === FE_MODE_REQUIRED ||
                    $formElement[FE_MODE] === FE_MODE_SHOW_REQUIRED ||
                    $formElement[FE_MODE] === FE_MODE_SHOW ||
                    (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] != '0')) {

                    // Dynamically changed fe mode from show or other to readonly is not fetched here (not yet stored in db).
                    // For this reason there is no included check for fe mode = readonly.
                    // Theoretically processReadOnly can be used in formElements with other fe modes too.
                    if (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] == '0') {
                        continue;
                    }

                    if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
                        // TypeAhead Tags received as JSON key/value
                        $cntNew = 0;

                        // TYPEAHEAD_TAG will be delivered as JSON. Check and sanitize every key/value pair.
                        $arr = json_decode($clientValues[$clientFieldName], true);
                        $arrTmp = array();
                        foreach ($arr as $row) {

                            $arrKey = $this->doValue($formElement, $formMode, $row['key']);
                            // New Tags always get '0' >> make them unique by adding a counter.
                            if ($arrKey == "0") {
                                $arrKey = "0-" . ++$cntNew;
                            }
                            $arrValue = $this->doValue($formElement, $formMode, $row['value']);
                            $arrTmp[$arrKey] = $arrValue;
                        }
                        $val = KeyValueStringParser::unparse($arrTmp, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, true);

                    } else {
                        // Single Value
                        $val = $this->doValue($formElement, $formMode, $clientValues[$clientFieldName]);
                    }
                    $newValues[$formElement[FE_NAME]] = $val;

                    if ($formElement[FE_TYPE] == FE_TYPE_IMAGE_CUT) {
                        $newValues[HTML_NAME_TARGET_IMAGE . $formElement[FE_NAME]] = $clientValues[HTML_NAME_TARGET_IMAGE . $clientFieldName];
                    }
                    // empty select, radio and checkbox elements used to be 0 for correct handling
                    if (($formElement[FE_TYPE] === FE_TYPE_SELECT || $formElement[FE_TYPE] === FE_TYPE_RADIO || $formElement[FE_TYPE] === FE_TYPE_CHECKBOX) && $val == '') {
                        $newValues[$formElement[FE_NAME]] = 0;
                    }
                }
            }
        }

        $this->store->setStore($newValues, STORE_FORM, true);

    }

    /**
     * @param $formElement
     * @param $value
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function doValue($formElement, $formMode, $value) {
        // Trim input
        if (empty($formElement[FE_TRIM])) {
            $value = trim($value);
        } elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) {
            $value = trim($value, $formElement[FE_TRIM]);
        }

        switch ($formElement[FE_TYPE]) {
            case FE_TYPE_DATE:
            case FE_TYPE_DATETIME:
            case FE_TYPE_TIME:
                if (!isset($value) || $value == '') {
                    if ($formElement[FE_TYPE] == FE_TYPE_TIME) {
                        $value = "00:00:00";
                    } else {
                        # This is fine for date and datetime
                        $value = "0000-00-00";
                    }
                } else {
                    $value = DateTime::doDateTime($formElement, $value);
                }
                break;

            default:
                if ($formElement[FE_TYPE] == FE_TYPE_EDITOR) {

//This is probably not needed anymore. Breaks if: <p>...</p><p>....</p>.
                    // Tiny MCE always wrap a '<p>' around the content. Remove it before saving.
//                    $value = Support::unWrapTag('<p>', $value);
                }

                // Check only if there is something.
                if ($value !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
                    $value = Sanitize::sanitize($value, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN],
                        $formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? '');

                    switch ($formElement[FE_ENCODE]) {
                        case FE_ENCODE_SPECIALCHAR:
                            $value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
                            break;
                        case FE_ENCODE_SINGLE_TICK:
                            $value = OnString::escapeSingleTickInHtml($value);
                            break;
                        default:
                            break;
                    }

                }
                break;
        }

        if ($value !== '') {
            $value = Sanitize::checkMinMax($value, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION);
        }

        return $value;
    }

    /**
     * Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma
     * separated string (MYSQL ENUM type). If there is no element '_h_${clientFieldName}', than there are no multi
     * values - return the already given `$clientValues[$clientFieldName]`.
     *
     * @param       $clientFieldName
     * @param array $clientValues
     *
     * @return string
     */
    private function collectCheckBoxValues($clientFieldName, array $clientValues, $unchecked) {

        // Check for Single
//        $checkboxKey = HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '', false);

        if (isset($clientValues[$clientFieldName])) {

            if (is_array($clientValues[$clientFieldName])) {
                return implode(',', $clientValues[$clientFieldName]);
            }

            return $clientValues[$clientFieldName];
        }

        return $unchecked;


        // For templateGroups: all expanded FormElements will be tried to collect - this fails for not submitted fields.
        // Therefore skip not existing clientvalues.
        if (!isset($clientValues[$checkboxKey])) {
            return false;
        }

        // Check if there is a hidden value with naming in checkbox multi syntax
        if (isset($clientValues[$checkboxKey])) {
            $checkboxValue = $clientValues[$checkboxKey];

            $pattern = '/' . HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '\d+') . '/';
            foreach ($clientValues as $key => $value) {
                if (1 === preg_match($pattern, $key)) {
                    $checkboxValue .= ',' . $value;
                }
            }

            if (isset($checkboxValue[0]) && $checkboxValue[0] === ',') {
                $checkboxValue = substr($checkboxValue, 1);
            }

            $clientValues[$clientFieldName] = $checkboxValue;
        }

        return $clientValues[$clientFieldName];
    }
}