<?php

namespace IMATHUZH\Qfq\Core\Form\FormElement;

use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
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 TYPO3\CMS\Extensionmanager\Utility\Repository\Helper;

class InputFormElement extends AbstractFormElement {
    /**
     * Input or Textarea (if multi-line)
     */
    public string $htmlTag = "input";
    protected string $typeAheadUrlParams = "";
    public array $characterCountCssClasses = array();
    public array $characterCountHtmlAttributes = array();

    /**
     * Filter count for text inputs (from sql1 with 'count' column)
     */
    public ?int $filterCount = null;

    /**
     * @param $attributes
     * @param Form|null $form
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct($attributes, ?Form $form = null) {
        parent::__construct($attributes, $form);

        $this->handleCharacterCount();
        $this->handleHideZero();
        $this->handleDecimalFormatting();
        $this->handleClearMe();
        $this->handleTypeAheadTag();
        $this->handleAiAssist();
        $this->handleMinMax();
        $this->handleInputOrTextarea();
        $this->handleCheckPattern();
        $this->handleHtmlAttributes();
        $this->handleFilterCount();
    }

    /**
     * Extract filter count from sql1 if available.
     * For text inputs in filter context, sql1 can return a single 'count' value.
     */
    private function handleFilterCount(): void {
        if (!isset($this->attributes[FE_SQL1]) || !is_array($this->attributes[FE_SQL1])) {
            return;
        }

        // sql1 should have one row with a 'count' column
        if (count($this->attributes[FE_SQL1]) > 0 && isset($this->attributes[FE_SQL1][0]['count'])) {
            $this->filterCount = (int)$this->attributes[FE_SQL1][0]['count'];
        }
    }


    /**
     * @return void
     */
    private function handleCharacterCount(): void {
        // Check if there are \r\n > those should be counted as one char, not two. #13030
        // Value as array is a special case used in templateGroup.
        if ($this->attributes[FE_MAX_LENGTH] > 0 && $this->attributes[FE_VALUE] !== '' && !isset($this->attributes[FE_TYPEAHEAD_SQL]) && !is_array($this->attributes[FE_VALUE])) {
            $crlf = substr_count($this->attributes[FE_VALUE], "\r\n");
            $max = $this->attributes[FE_MAX_LENGTH] + $crlf;
            // crop string only if it's not empty (substr returns false on empty strings)
            $this->attributes[FE_VALUE] = mb_substr($this->attributes[FE_VALUE], 0, $max);
        }

        // CharacterCountWrap
        if (isset($this->attributes[FE_CHARACTER_COUNT_WRAP])) {
            array_push($this->cssClasses, CLASS_CHARACTER_COUNT);
            $this->htmlAttributes[DATA_CHARACTER_COUNT_ID] = $this->attributes[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT;

            $this->characterCountHtmlAttributes[FE_ID] = $this->attributes[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT;
            array_push($this->characterCountCssClasses, FE_CHARACTER_COUNT_CLASS);
        }
    }

    /**
     * @return void
     */
    private function handleHideZero(): void {
        // HideZero (empty TypeAhead)
        if ($this->attributes[FE_HIDE_ZERO] != '0' && $this->value == '0') {
            $this->value = '';
        }
    }

    /**
     * @return void
     */
    private function handleDecimalFormatting(): void {
        // Decimal Formatting
        if ($this->attributes[FE_DECIMAL_FORMAT] !== '') {
            if ($this->attributes[FE_VALUE] !== '') { // empty string causes exception in number_format()
                $decimalScale = explode(',', $this->attributes[FE_DECIMAL_FORMAT])[1]; // scale: Nachkommastellen
                $this->attributes[FE_VALUE] = number_format($this->attributes[FE_VALUE], $decimalScale, '.', '');
            }
        }
    }

    /**
     * @return void
     */
    private function handleClearMe(): void {
        // qfq-clear-me (with or without multiform vertical)
        if (isset($this->attributes[F_FE_INPUT_CLEAR_ME]) && $this->attributes[F_FE_INPUT_CLEAR_ME] != '0' && $this->attributes[FE_MODE] !== FE_MODE_READONLY) {
            if ($this->form != null && $this->form->specFinal[F_MULTI_MODE] !== F_MULTI_MODE_NONE) {
                array_push($this->cssClasses, 'qfq-clear-me-multiform');
            } else {
                array_push($this->cssClasses, 'qfq-clear-me');
            }
        }
    }

    /**
     * @return void
     */
    private function handleMinMax(): void {
        // 'maxLength' needs an upper 'L': naming convention for DB tables!
        if ($this->attributes[FE_MAX_LENGTH] > 0) {
            $this->htmlAttributes['maxlength'] = $this->attributes[FE_MAX_LENGTH];
        }

        // In case the user specifies MIN & MAX with numbers, the html tag 'type' has to be 'number', to make the range check work in the browser.
        if (empty($this->attributes[FE_INPUT_TYPE]) && !empty($this->attributes[FE_MIN]) && !empty($this->attributes[FE_MAX]) &&
            is_numeric($this->attributes[FE_MIN]) && is_numeric($this->attributes[FE_MAX])) {
            $this->attributes[FE_INPUT_TYPE] = 'number';
        }
    }

    /**
     * @return void
     * @throws \CodeException
     */
    private function handleCheckPattern(): void {
        // Do Check Pattern
        $this->attributes[FE_CHECK_PATTERN] = Sanitize::getInputCheckPattern($this->attributes[FE_CHECK_TYPE], $this->attributes[FE_CHECK_PATTERN]
            , $this->attributes[FE_DECIMAL_FORMAT], $sanitizeMessage);
        if ($this->attributes[FE_CHECK_PATTERN] !== '') {
            Support::addAttributeToArray($this->htmlAttributes, ATTRIBUTE_PATTERN, $this->attributes[FE_CHECK_PATTERN], true, ESCAPE_WITH_BACKSLASH);

            // Use system specific message only if no custom message is set.
            if ($this->attributes[F_FE_DATA_PATTERN_ERROR] == $this->attributes[F_FE_DATA_PATTERN_ERROR_SYSTEM]) {
                $this->attributes[F_FE_DATA_PATTERN_ERROR] = $sanitizeMessage;
            }
            Support::addAttributeToArray($this->htmlAttributes, F_FE_DATA_PATTERN_ERROR, $this->attributes[F_FE_DATA_PATTERN_ERROR], true, ESCAPE_WITH_BACKSLASH);
        }
    }

    /**
     * @return void
     * @throws \CodeException
     */
    protected function handleHtmlAttributes(): void {
        Support::addAttributeToArray($this->htmlAttributes, ATTRIBUTE_DATA_REFERENCE, $this->attributes[FE_DATA_REFERENCE] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_INPUT_AUTOCOMPLETE, $this->attributes[FE_INPUT_AUTOCOMPLETE] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_AUTOFOCUS, $this->attributes[FE_AUTOFOCUS] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_PLACEHOLDER, $this->attributes[FE_PLACEHOLDER] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, F_FE_DATA_MATCH_ERROR, $this->attributes[F_FE_DATA_MATCH_ERROR] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, F_FE_DATA_ERROR, $this->attributes[F_FE_DATA_ERROR] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_MIN, $this->attributes[FE_MIN] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_MAX, $this->attributes[FE_MAX] ?? '');
        Support::addAttributeToArray($this->htmlAttributes, FE_STEP, $this->attributes[FE_STEP] ?? '');
        // Dynamic Update, Tooltip...
        Support::addAttributeToArray($this->htmlAttributes, FE_DATA_LOAD, ($this->attributes[FE_DYNAMIC_UPDATE] ?? '') === 'yes' ? 'data-load' : '');
        Support::addAttributeToArray($this->htmlAttributes, F_TITLE, $this->attributes[FE_TOOLTIP] ?? '');
        // What is this attribute for?
        if (isset($this->attributes[FE_RETYPE_SOURCE_NAME])) {
            $htmlFormElementNamePrimary = str_replace(FE_RETYPE_NAME_EXTENSION, '', $this->htmlAttributes[HTML_ATTR_NAME]);
            $this->htmlAttributes[FE_DATA_MATCH] = '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']';
        }
        // Value
        if ($this->htmlTag == "textarea") {
            $this->htmlAttributes[FE_VALUE] = htmlentities($this->value);
        } else {
            // Typeahead prefetch needs key as attribute. Key/value pair is needed later on client side.
            if (isset($this->attributes[FE_TYPEAHEAD_SQL_PREFETCH]) && is_array($this->value)) {
                $attributeValue = OnArray::getFirstValueFromArray($this->attributes[FE_VALUE], true);
            } else {
                $attributeValue = $this->value;
            }
            $this->htmlAttributes[FE_VALUE] = htmlentities($attributeValue);
        }
    }

    /**
     * Prepare Information for the Renderer (Input vs Textarea)
     *
     * @return void
     * @throws \CodeException
     */
    private function handleInputOrTextarea(): void {
        // when size empty but value contains \n then set auto multi line
        if (!isset($this->attributes[FE_TYPEAHEAD_SQL])) {
            if (strpos($this->value, "\n") == true && empty($formElement[FE_SIZE])) {
                $this->attributes[FE_SIZE] = '50,2';
            }
        }
        // Check for input type 'textarea'. Possible notation: a) '', b) '<width>', c) '<width>,<height>', d) '<width>,<min-height>,<max-height>'
        $colsRows = explode(',', $this->attributes[FE_SIZE], 3);
        if (empty($colsRows[1])) {
            $colsRows[1] = 1;
        }
        // Active Textarea if min-height>1 (rows) or third parameter max-height>0
        $flagTextarea = ($colsRows[1] > 1 || ($colsRows[2] ?? '') > 0);

        if ($flagTextarea) {
            $this->htmlTag = "textarea";

            $this->htmlAttributes['cols'] = $colsRows[0];
            $this->htmlAttributes['rows'] = $colsRows[1];

            // Check if $colsRows[2] != 0 >> enable auto-grow (if nothing is defined: enable auto-grow)
            $maxHeight = $colsRows[2] ?? '350';
            if ($maxHeight != '0') {
                array_push($this->cssClasses, CLASS_FORM_ELEMENT_AUTO_GROW);
                $this->htmlAttributes[ATTRIBUTE_DATA_MAX_HEIGHT] = $maxHeight;
            }

        } else {
            if (!empty($this->attributes[FE_INPUT_TYPE])) {
                $this->attributes[FE_TYPE] = $this->attributes[FE_INPUT_TYPE];
                // TypeAhead tag elements needs to be hidden
                if (HelperFormElement::booleParameter($this->attributes[FE_TYPEAHEAD_TAG] ?? '-')) {
                    $this->attributes[FE_TYPE] = 'hidden';
                }
            }
            // If type password is selected then type text with own class will be taken to fake password over CSS
            if ($this->attributes[FE_TYPE] === 'password') {
                $this->attributes[FE_TYPE] = 'text';
                array_push($this->cssClasses, 'qfq-password');
            }

            $this->htmlAttributes[FE_TYPE] = $this->attributes[FE_TYPE];
            $this->htmlAttributes[FE_SIZE] = $this->attributes[FE_SIZE];
        }

    }

    /**
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function handleTypeAheadTag(): void {
        // Check if typeAhead[Tag] needs to build
        $this->typeAheadUrlParams = $this->typeAheadBuildParam();
        if ($this->typeAheadUrlParams == "") return;

        // Build typeAhead[Tag]
        array_push($this->cssClasses, CLASS_TYPEAHEAD);
        if (empty($this->attributes[FE_INPUT_TYPE])) {
            $this->attributes[FE_INPUT_TYPE] = FE_TYPE_SEARCH; // typeahead behaves better with 'search' instead of 'text'
        }

        if (empty($this->attributes[FE_INPUT_AUTOCOMPLETE])) {
            $this->attributes[FE_INPUT_AUTOCOMPLETE] = time(); // typeahead behaves better with 'autocomplete'='...'
        }

        // Collect typeAhead initial suggestion
        if (!empty($this->attributes[FE_TYPEAHEAD_INITIAL_SUGGESTION])) {
            $arr = $this->evaluate->parse($this->attributes[FE_TYPEAHEAD_INITIAL_SUGGESTION]);
            $arr = $this->databaseManager->getDataDb()->makeArrayDict($arr, TYPEAHEAD_SQL_KEY_NAME, API_TYPEAHEAD_VALUE, API_TYPEAHEAD_KEY, API_TYPEAHEAD_VALUE);
            $this->htmlAttributes[DATA_TYPEAHEAD_INITIAL_SUGGESTION] = json_encode($arr);
        }

        $dataSip = $this->sip->queryStringToSip($this->typeAheadUrlParams, RETURN_SIP);
        $this->htmlAttributes[DATA_TYPEAHEAD_SIP] = $dataSip;
        $this->htmlAttributes[DATA_TYPEAHEAD_LIMIT] = $this->attributes[FE_TYPEAHEAD_LIMIT];
        $this->htmlAttributes[DATA_TYPEAHEAD_MINLENGTH] = $this->attributes[FE_TYPEAHEAD_MINLENGTH];


        if (HelperFormElement::booleParameter($this->attributes[FE_TYPEAHEAD_PEDANTIC] ?? '-')) {
            $this->htmlAttributes[DATA_TYPEAHEAD_PEDANTIC] = "true";
        }

        // Tag
        if (HelperFormElement::booleParameter($this->attributes[FE_TYPEAHEAD_TAG] ?? '-')) {
            $this->htmlAttributes[DATA_TYPEAHEAD_TAG] = 'true';

            // Default: tab, comma
            $kk = '[' . ($this->attributes[FE_TYPEAHEAD_TAG_DELIMITER] ?? '9,44') . ']';
            $this->htmlAttributes[DATA_TYPEAHEAD_TAG_DELIMITER] = $kk;
            $this->attributes[FE_INPUT_TYPE] = 'hidden';

            // Client: TAG handling expects the '$value' as a JSON string.
            $arr = KeyValueStringParser::parse($this->value, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, KVP_IF_VALUE_EMPTY_COPY_KEY);
            $this->value = OnArray::arrayToQfqJson($arr);
        }
        // Give typeAhead url for ability to initialize dynamic html elements
        $this->htmlAttributes[DATA_TYPEAHEAD_URL] = Path::urlApi(API_TYPEAHEAD_PHP);

    }


    /**
     * Check $formElement for FE_TYPE_AHEAD_SQL or FE_TYPE_AHEAD_LDAP_SERVER.
     * If one of them is given: build $urlParam with typeAhead Params.
     * Additionally set some parameter for later outside use, especially FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_MINLENGTH
     *
     * @param array $formElement
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function typeAheadBuildParam(): string {

        $arr = [];

        $this->attributes[FE_TYPEAHEAD_LIMIT] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_LIMIT, TYPEAHEAD_DEFAULT_LIMIT);
        $formElement[FE_TYPEAHEAD_MINLENGTH] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_MINLENGTH, 2);

        if (isset($this->attributes[FE_TYPEAHEAD_SQL])) {
            $sql = $this->checkSqlAppendLimit($this->attributes[FE_TYPEAHEAD_SQL], $this->attributes[FE_TYPEAHEAD_LIMIT]);

            $arr = [
                FE_TYPEAHEAD_SQL => $sql,
                FE_TYPEAHEAD_SQL_PREFETCH => $this->attributes[FE_TYPEAHEAD_SQL_PREFETCH] ?? '',
                PARAM_DB_INDEX_DATA => $this->form->specFinal[F_DB_INDEX]
            ];
        } elseif (isset($this->attributes[FE_TYPEAHEAD_LDAP])) {
            $this->attributes[FE_LDAP_SERVER] = Support::setIfNotSet($this->attributes, FE_LDAP_SERVER);
            $this->attributes[FE_LDAP_BASE_DN] = Support::setIfNotSet($this->attributes, FE_LDAP_BASE_DN);
            $this->attributes[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_LDAP_SEARCH);
            $this->attributes[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH);
            $this->attributes[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_LDAP_VALUE_PRINTF);
            $this->attributes[FE_TYPEAHEAD_LDAP_ID_PRINTF] = Support::setIfNotSet($this->attributes, FE_TYPEAHEAD_LDAP_ID_PRINTF);
            $this->attributes[FE_LDAP_USE_BIND_CREDENTIALS] = Support::setIfNotSet($this->attributes, FE_LDAP_USE_BIND_CREDENTIALS);

            foreach ([FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_TYPEAHEAD_LDAP_SEARCH] as $key) {
                if ($this->attributes[$key] == '') {
                    throw new \UserFormException('Missing definition: ' . $key, ERROR_MISSING_DEFINITON);
                }
            }

            if ($this->attributes[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] . $this->attributes[FE_TYPEAHEAD_LDAP_ID_PRINTF] == '') {
                throw new \UserFormException('Missing definition: ' . FE_TYPEAHEAD_LDAP_VALUE_PRINTF . ' or ' . FE_TYPEAHEAD_LDAP_ID_PRINTF, ERROR_MISSING_DEFINITON);
            }

            $arr = [
                FE_LDAP_SERVER => $this->attributes[FE_LDAP_SERVER],
                FE_LDAP_BASE_DN => $this->attributes[FE_LDAP_BASE_DN],
                FE_TYPEAHEAD_LDAP_SEARCH => $this->attributes[FE_TYPEAHEAD_LDAP_SEARCH],
                FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH => $this->attributes[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH],
                FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $this->attributes[FE_TYPEAHEAD_LDAP_VALUE_PRINTF],
                FE_TYPEAHEAD_LDAP_ID_PRINTF => $this->attributes[FE_TYPEAHEAD_LDAP_ID_PRINTF],
                FE_TYPEAHEAD_LIMIT => $this->attributes[FE_TYPEAHEAD_LIMIT],
            ];

            if (isset($this->attributes[FE_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN])) {
                $arr[FE_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN] = '1';
            }

            if ($this->attributes[FE_LDAP_USE_BIND_CREDENTIALS] == '1') {
                $arr[SYSTEM_LDAP_1_RDN] = $this->store->getVar(SYSTEM_LDAP_1_RDN, STORE_SYSTEM);
                $arr[SYSTEM_LDAP_1_PASSWORD] = $this->store->getVar(SYSTEM_LDAP_1_PASSWORD, STORE_SYSTEM);
            }

        }

        return OnArray::toString($arr);
    }


    /**
     * TODO: This does not belong in this class, it's more of an sql helper
     * Checks if $sql contains a SELECT statement.
     * Check for existence of a LIMIT Parameter. If not found add one.
     *
     * @param string $sql
     * @param int $limit
     *
     * @return string   Checked and maybe extended $sql statement.
     * @throws \UserFormException
     */
    private function checkSqlAppendLimit($sql, $limit): string {
        $sqlTest = '';

        $sql = trim($sql);
        // If exist, unwrap  '{{!', '}}'
        if (substr($sql, 0, 2) == '{{') {
            $sql = trim(substr($sql, 2, strlen($sql) - 4));

            if ($sql[0] ?? '' == '!') {
                $sql = trim(substr($sql, 1));
            }
        }

        if ($sql[0] == '[') {
            // Remove optional existing dbIndex token.
            $pos = strpos($sql, ']');
            $sqlTest = substr($sql, $pos + 1);
        } else {
            $sqlTest = $sql;
        }

        if (false === stristr(substr($sqlTest, 0, 7), 'SELECT ')) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => '"Expect a SELECT statement', ERROR_MESSAGE_TO_DEVELOPER => "Expect a SELECT statement in " . FE_TYPEAHEAD_SQL . " - got: " . $sqlTest]),
                ERROR_BROKEN_PARAMETER);

        }

        if (false === stristr($sql, ' LIMIT ')) {
            $sql .= " LIMIT $limit";
        }

        return $sql;
    }
}