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

namespace IMATHUZH\Qfq\Core;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Form\Chat;
use IMATHUZH\Qfq\Core\Form\Checkbox;
use IMATHUZH\Qfq\Core\Form\FormAsFile;
use IMATHUZH\Qfq\Core\Form\TypeAhead;
use IMATHUZH\Qfq\Core\Helper\DateTime;
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\Ldap;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Report\Report;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/**
 * Class AbstractBuildForm
 * @package qfq
 */
abstract class AbstractBuildForm {
    /**
     * @var array
     */
    protected $formSpec = array();  // copy of the loaded form
    /**
     * @var array
     */
    protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
    /**
     * @var array
     */
    protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
    /**
     * @var array
     */
    protected $buildElementFunctionName = array();
    /**
     * @var array
     */
    protected $pattern = array();
    /**
     * @var array
     */
    protected $wrap = array();
    /**
     * @var array
     */
    protected $symbol = array();
    /**
     * @var bool
     */
    protected $showDebugInfoFlag = false;

//    protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]">

    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
    /**
     * @var string
     */
    private $formId = null;
    /**
     * @var Sip
     */
    private $sip = null;
    /**
     * @var Link
     */
    protected $link = null;
    /**
     * @var Report
     */
    private $report = null;
    /**
     * @var BodytextParser
     */
    private $bodytextParser = null;

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

    /**
     * @var bool|mixed
     */
    protected $dbIndexData = false;
    /**
     * @var bool|string
     */
    protected $dbIndexQfq = false;
    /**
     *  counter to check if id is 0 then add a new index example-0-1, example-0-2
     * @var int
     */
    private $counter = 0;

    /**
     * @var int
     */
    public $recordId = 0;

    /**
     *
     */
    private $flagMultiForm = false;

    /**
     * @var string
     * During processing of FE, collects all JS content with replaced variables.
     */
    protected $clientJsAll = '';

    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     * @param array Database $db
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db = null) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
//        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexData = $formSpec[F_DB_INDEX];
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);

        $this->dbArray = $db;
        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
        $this->showDebugInfoFlag = Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM));

        $this->sip = $this->store->getSipInstance();

        $this->link = new Link($this->sip, $this->dbIndexData, '', $formSpec);

        // render mode specific
        $this->fillWrap();

        $this->buildRowName = [
            FE_TYPE_CHECKBOX => 'Native',
            FE_TYPE_DATE => 'Native',
            FE_TYPE_DATETIME => 'Native',
            'email' => 'Native',
            FE_TYPE_EXTRA => 'Native',
            FE_TYPE_TEXT => 'Native',
            FE_TYPE_EDITOR => 'Native',
            FE_TYPE_TIME => 'Native',
            FE_TYPE_NOTE => 'Native',
            FE_TYPE_PASSWORD => 'Native',
            FE_TYPE_RADIO => 'Native',
            FE_TYPE_SELECT => 'Native',
            FE_TYPE_SUBRECORD => 'Subrecord',
            FE_TYPE_UPLOAD => 'Native',
            FE_TYPE_ANNOTATE => 'Native',
            FE_TYPE_IMAGE_CUT => 'Native',
            FE_TYPE_CHAT => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill',
            'templateGroup' => 'TemplateGroup',
        ];

        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";

        $this->ttContentUid = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
        $this->formId = $this->formSpec[F_ID] ?? 0;
        $this->recordId = $this->store->getVar(CLIENT_RECORD_ID, STORE_SIP . STORE_RECORD . STORE_TYPO3 . STORE_ZERO);

        if (!empty($formSpec[F_MULTI_SQL])) {
            $this->flagMultiForm = true;
        }

    }

    abstract public function fillWrap();


    /**
     * @param $filter
     * @param $modeCollectFe
     * @param array $rcJson
     * @return string
     * @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
     */
    protected function buildMultiForm($filter, $modeCollectFe, array &$rcJson): string {

        $htmlElements = '';
        $rcJson = array();
        $flagTable = true;

        // Author: Zhoujie Li
        // if parameter multiFormAddRow === 1 then true else false
        $addRow = isset($this->formSpec[F_MULTIFORM_ADD_ROW]) && $this->formSpec[F_MULTIFORM_ADD_ROW] == '1';
        // if parameter multiFormDeleteRow === 1 then true else false
        $deleteRow = isset($this->formSpec[F_MULTIFORM_DELETE_ROW]) && $this->formSpec[F_MULTIFORM_DELETE_ROW] == '1';
        $finalDeleteBtn = '';
        // End from author

        $counter = 0;

        if (!isset($this->formSpec[F_MULTI_WRAP])) {
            $this->formSpec[F_MULTI_WRAP] = '';
        }
        if (!isset($this->formSpec[F_MULTI_CELL_WRAP])) {
            $this->formSpec[F_MULTI_CELL_WRAP] = '';
        }
        if ($this->formSpec[F_MULTI_WRAP] == '') {
            $this->formSpec[F_MULTI_WRAP] = '<table class="table table-multi-form qfq-table-100">';
            $this->formSpec[F_MULTI_CELL_WRAP] = '<td>';
        } else {
            $flagTable = false;
            if ($this->formSpec[F_MULTI_CELL_WRAP] == '') {
                $this->formSpec[F_MULTI_CELL_WRAP] = '<div>';
            }
        }

        $parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR);
        // Check for 'id' or '_id' as column name
        $idName = isset($parentRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID;

        // Author: Zhoujie Li
        // if addRow is true add 1 additional element to $parentRecord.
        if ($addRow) {
            HelperFormElement::addGenericElements($parentRecords, 1, $idName);
        }
        // Get get the Total records need to checkt the last element with id 0
        $totalRecords = count($parentRecords);
        // count the current index
        $currentRecordIndex = 0;


        // If form.parameter.processRow is set, checkboxes are added to parentRecords
        if (isset($this->formSpec[F_PROCESS_ROW])) {
            // check or uncheck processRow
            $this->processRow($parentRecords, $idName, $counter);
        }
        // End from author
//        // Important to destroy the reference (further usage)
//        unset ($array);


        // No rows: nothing to do.
        if (empty($parentRecords)) {
            return $this->formSpec[F_MULTI_MSG_NO_RECORD];
        }

        // Check that a column 'id' is given
        if (!isset($parentRecords[0][$idName])) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Missing column "_' . F_MULTI_COL_ID . '"', ERROR_MESSAGE_TO_DEVELOPER => $this->formSpec[F_MULTI_SQL]]),
                ERROR_INVALID_OR_MISSING_PARAMETER);
        }

        // This is a dirty workaround for formSave: clear FORM STORE (already outdated values).
        // Otherwise those outdated values will be taken to fill non primary FE in multiForm (which are garbage).
        // Better solution would be to have FORM_STORE in sync.
        $this->store::unsetStore(STORE_FORM);

        $storeVarBase = $this->evaluate->parse($this->formSpec[FE_FILL_STORE_VAR]);
        if (isset($storeVarBase[0])) {
            // FE_FILL_STORE_VAR might return multiple rows (not useful), nevertheless the first row should become the only one.
            $storeVarBase = $storeVarBase[0];
        }
        if (!is_array($storeVarBase)) {
            $storeVarBase = array();
        }

        // Per row, iterate over all form elements
        foreach ($parentRecords as $row) {
            // increment currentRecordIndexs
            $currentRecordIndex++;
            // Always start with a clean STORE_VAR
            $this->store->setStore($storeVarBase, STORE_VAR, true);

            $this->store->setStore(OnArray::keyNameRemoveLeadingUnderscore($row), STORE_PARENT_RECORD, true);
            $this->store->setVar(F_MULTI_COL_ID, $row[$idName], STORE_PARENT_RECORD); // In case '_id' is used, both '_id' and 'id' should be accessible.

            // Main record to load in STORE_RECORD
            $record = $this->dbArray[$this->dbIndexData]->sql('SELECT * FROM `' . $this->formSpec[F_TABLE_NAME] . '` WHERE `id`= ?',
                ROW_EXPECT_0_1, [$row[$idName]]);
            $this->store->setStore($record, STORE_RECORD, true);

            $jsonTmp = array();
            $feTmp = $this->feSpecNative;

            $leftColumns = $this->buildMultiFormLeftColumns($row, $this->formSpec[F_MULTI_CELL_WRAP]);
            $rightInputs = $this->elements($row[$idName], $filter, 0, $jsonTmp, $modeCollectFe,
                false, STORE_USE_DEFAULT, FORM_LOAD, true);


            // Author: Zhoujie Li
            if ($deleteRow) {
                $disable = $row[$idName] === 0 ? '' : 'disabled';
                $deleteRender = '';

                $strDefault = TOKEN_ACTION_DELETE . ':' . TOKEN_ACTION_DELETE_AJAX
                    . '|' . TOKEN_GLYPH . ':' . GLYPH_ICON_DELETE
                    . '|' . TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . $deleteRender
                    . '|' . TOKEN_URL_PARAM . ':form=' . $this->formSpec[F_NAME] . '&r=' . $row[$idName] . '&' . FLAG_MULTIFORM . '=true'
                    . '|' . TOKEN_TOOL_TIP . ':Delete';

                $deleteBtn = $row[$idName] === 0 ? $this->buildButtonCode(MULTIFORM_DELETE_ROW_BTN, '', 'delete row', GLYPH_ICON_DELETE, '', '', 'btn btn-default ' . MULTIFORM_DELETE_ROW_BTN) : $this->link->renderLink($strDefault);

                $finalDeleteBtn = '<td style="text-align: center;">' . $deleteBtn . '</td>';
            }
            if ($flagTable) {
                $recordClass = 'record';
                $useRecordClass = ($row[$idName] != 0) ? Support::doAttribute(HTML_ATTR_CLASS, $recordClass, true) : '';
                $htmlId = Support::doAttribute(HTML_ATTR_ID, $this->getFormId($this->ttContentUid, $this->formSpec[F_ID], $this->recordId) . '-' . $row[$idName]);
                // If it's the last record and it is empty also addRow is true, add 'dummy-row' class; otherwise, use a regular 'tr' tag
                $tRowTag = ($totalRecords === $currentRecordIndex && empty($record) && $addRow) ? 'class="dummy-row"' : $useRecordClass . $htmlId;

                $htmlElements .= Support::wrapTag("<tr $tRowTag>", $leftColumns . $rightInputs . $finalDeleteBtn);
            } else {
                $htmlElements .= $rightInputs;
            }

            // Clean for the next round
            $this->feSpecNative = $feTmp;
            $this->store::unsetStore(STORE_RECORD);

            $rcJson = array_merge($rcJson, $jsonTmp);
        }

        // Author: Zhoujie Li
        if ($flagTable) {
            $html = Support::wrapTag('<thead><tr>', $this->buildMultiFormTableHead($parentRecords[0], $deleteRow)) .
                '<tbody>' . $htmlElements . '</tbody>';
            // End from author
        } else {
            $html = $htmlElements;
        }


        $multiFormTable = Support::wrapTag($this->formSpec[F_MULTI_WRAP], $html);

        // Author: Zhoujie Li
        // render Button if $addRow is true
        if ($addRow) {
            // append button to html
            $multiFormTable .= $this->buildButtonCode(MULTIFORM_ADD_ROW_BTN, '', 'add new row', GLYPH_ICON_NEW, '', '', $this->formSpec[F_SAVE_BUTTON_CLASS] . ' addRowButton');
        }
        // End from author
        return $multiFormTable;
    }

    /**
     * Processes parent records to add a process row checkbox, set as checked or unchecked.
     *
     * @param array &$parentRecords
     * @param string $idName
     * @param int &$counter
     * @param array $formSpec
     * @return array|string
     */
    protected function processRow(&$parentRecords, $idName, &$counter): void {

        // Retrieve the title from the form specification
        $processRowTitle = $this->formSpec[F_PROCESS_ROW];
        // Define the key with a checkbox for the table header
        $processRowKey = '<label class="checkbox process-row-all process-row-label-header"><input type="checkbox"><span>' . $processRowTitle . '</span></label>';
        // Loop through each record to apply processing
        foreach ($parentRecords as &$array) {
            // Check if the checkbox should be marked as checked
            $checked = empty($array[F_PROCESS_ROW_COLUMN]) ? '' : 'checked="checked"';
            // if a new process counter should be used
            $processCounter = ($array[$idName] == 0) ? $counter : null;

            // Build the name for the form element
            $processRowName = HelperFormElement::buildFormElementName([FE_NAME => F_PROCESS_ROW_COLUMN], $array[$idName], $processCounter);
            // Increment counter
            if ($array[$idName] == 0) {
                $counter++;
            }
            // Create the process row value with a checkbox
            $processRowValue = '<label class="checkbox"><input name="' . $processRowName . '" type="checkbox" ' . $checked . '></label>';

            // Build the process row array and prepend it to the record
            $processRow = [$processRowKey => $processRowValue];
            $array = $processRow + $array;
        }
        // Important to destroy the reference (further usage)
        unset($array);

    }


    /**
     * Creates a button with the given attributes. If there is no $icon given, render the button without glyph.
     *
     * @param string $buttonHtmlId
     * @param string $text
     * @param string $tooltip
     * @param string $icon
     * @param string $disabled
     *
     * @param string $buttonOnChangeClass
     * @param string $class
     * @return string
     * @throws \CodeException
     */
    public function buildButtonCode($buttonHtmlId, $text, $tooltip, $icon, $disabled = '', $buttonOnChangeClass = '', $class = ''): string {

        if ($icon === '') {
            $element = $text;
        } else {
            $element = "<span class='glyphicon $icon'></span>" . ' ' . $text;
        }

        $class = Support::doAttribute('class', $class);
        $dataClassOnChange = Support::doAttribute('data-class-on-change', $buttonOnChangeClass);
        $tooltip = Support::doAttribute('title', $tooltip);
        $formId = $this->getFormId($this->ttContentUid, $this->formSpec[F_ID], $this->recordId);

        return "<button id='$buttonHtmlId-$formId' type='button' $class $dataClassOnChange $tooltip $disabled>$element</button>";
    }


    /**
     * @param array $row
     * @return string
     */
    private function buildMultiFormLeftColumns(array $row, $multiFormCellWrap): string {
        $line = '';

        // Collect columns
        foreach ($row as $key => $value) {
            if (($key[0] ?? '') != '_') {
                $line .= Support::wrapTag($multiFormCellWrap, $value);
            }
        }

        return $line;
    }

    /**
     * @param array $row
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function buildMultiFormTableHead(array $row, $deleteRow): string {
        $line = '';

        // Collect columns
        foreach ($row as $key => $value) {
            if (($key[0] ?? '') != '_') {
                $line .= "<th>$key</th>";
            }
        }

        // Collect label from FormElements
        foreach ($this->feSpecNative as $formElement) {
            $editFeHtml = '';

            // debugStack as Tooltip
            if ($this->showDebugInfoFlag) {
                // Build 'FormElement' Edit symbol
                $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
                $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
                $editFeHtml = ' ' . Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
            }

            $line .= '<th>' . $formElement[FE_LABEL] . $editFeHtml . '</th>';
        }

        // Author: Zhoujie Li
        $line .= $deleteRow ? '<th></th>' : '';
        // End from author
        return $line;
    }

    /**
     * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
     *
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @param bool $htmlElementNameIdZero
     * @param array $latestFeSpecNative
     * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
     *                        formElement.dynamicUpdate-yes  values/states
     * @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 process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()): array|string {
        $htmlHead = '';
        $htmlTail = '';
        $htmlT3vars = '';
        $htmlElements = '';
        $json = array();

        // After action 'afterSave', it's necessary to reinitialize the FeSpecNative
        if (!empty($latestFeSpecNative)) {
            $this->feSpecNative = $latestFeSpecNative;
        }

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

        if ($mode === FORM_SAVE) {
            $modeCollectFe = FLAG_ALL;
            $storeUse = STORE_RECORD . STORE_TABLE_DEFAULT;
        }

        // <form>
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }

        $filter = $this->getProcessFilter();

        // Form type
        if ($this->formSpec[F_MULTI_SQL] === '') {

            // Regular Form
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
            if (!($recordId == '' || is_numeric($recordId))) {
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]),
                    ERROR_INVALID_VALUE);
            }

            // Build FormElements
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);

            $formArray[F_TITLE] = $this->formSpec[F_UNEVALUATED_TITLE];
            $evaluatedTitle = $this->evaluate->parseArray($formArray);
            // If form title has changed, add new title to JSON
            if ($this->formSpec[F_TITLE] != $evaluatedTitle[F_TITLE]) {
                $element = array(
                    'form-element' => 'qfq-form-title',
                    'value' => $evaluatedTitle[F_TITLE]
                );
                $json[] = $element;
            }

            if ($mode === FORM_SAVE && $recordId != 0) {

                // element-update: with 'value'
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
                $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);

                // Via 'element-update'
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
            }

        } else {
            // Multi Form
            if ($mode === FORM_LOAD || $mode === FORM_SAVE) {
                $htmlElements = $this->buildMultiForm($filter, $modeCollectFe, $json);
            }
        }

        // <form>
        if ($mode === FORM_LOAD) {
            $htmlT3vars = $this->prepareT3VarsForSave();
            $htmlTail = $this->tail();
        }
        $htmlHidden = $this->buildAdditionalFormElements();

        $htmlSip = $this->buildHiddenSip($json);

        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail : $json;
    }

    /**
     * Build the head area of the form.
     *
     * @param string $mode
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function head($mode = FORM_LOAD): string {
        $html = '';

        $html .= '<div ' . Support::doAttribute('class', $this->formSpec[F_CLASS], true) . '>'; // main <div class=...> around everything

        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM, $this->formSpec[F_ID]);

        $html .= "<p><a " . Support::doAttribute('href', $formEditUrl) . ">Edit</a> <small>[$sipParamString]</small></p>";

        $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec[F_TITLE], true);

        $html .= $this->getFormTag();

        return $html;
    }

    /**
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
     *
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
     * @param int $recordId id of form or formElement
     * @param array $param
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function createFormEditorUrl($form, $recordId, array $param = array()): string {

        if (!$this->showDebugInfoFlag) {
            return '';
        }

        $queryStringArray = [
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
            'form' => $form,
            'r' => $recordId,
            PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
        ];

        if (T3Handler::useSlugsInsteadOfPageAlias()) {
            unset($queryStringArray['id']);
        }

        $queryStringArray = array_merge($queryStringArray, $param);

        $queryString = Support::arrayToQueryString($queryStringArray);

        if (T3Handler::useSlugsInsteadOfPageAlias()) {
            $queryString = Path::urlApp($this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM)) . '?' . $queryString;
        }

        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);

        return $url;
    }

    /**
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
     * @param string $item
     * @param string $value
     * @param bool|false $flagOmitEmpty
     *
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false): string {

        if ($flagOmitEmpty && $value === "") {
            return '';
        }

        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
     * Returns '<form ...>'-tag with various attributes.
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function getFormTag(): string {

        $attribute = $this->getFormTagAttributes();
        $honeypot = $this->getHoneypotVars();
        $md5 = $this->buildInputRecordHashMd5();

        return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>' . $honeypot . $md5;
    }

    /**
     * Build MD5 from the current record. Return HTML Input element.
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function buildInputRecordHashMd5(): string {

        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);

        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='hidden' value='$md5'>";

//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
     * @param string $primaryKey
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT): string {
        $record = array();

        if ($recordId != 0) {
            $record = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM `$tableName` WHERE `$primaryKey`=?", ROW_EXPECT_1, [$recordId], "Record to load not found. "
                . (FEATURE_FORM_FILE_SYNC ? FormAsFile::errorHintFormImport($tableName) : ''));
        }

        if (isset($record[F_FILE_STATS])) {
            // why: The column "fileStats" in the Form table is modified when a form is exported to a file but nothing else changes.
            unset($record[F_FILE_STATS]);
        }

        return OnArray::getMd5($record);
    }

    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function getHoneypotVars(): string {
        $html = '';

        $vars = $this->store->getVar(SYSTEM_SECURITY_VARS_HONEYPOT, STORE_SYSTEM);

        // Iterate over all fake vars
        $arr = explode(',', $vars);
        foreach ($arr as $name) {
            $name = trim($name);
            if ($name === '') {
                continue;
            }
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }


    /**
     * Build an assoc array with standard form attributes.
     *
     * @return array
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function getFormTagAttributes(): array {

        $attribute['id'] = $this->getFormId($this->ttContentUid, $this->formId, $this->recordId);
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
        $attribute['enctype'] = $this->getEncType();
        $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
        $attribute['data-activate-first-required-tab'] = $this->formSpec[F_ACTIVATE_FIRST_REQUIRED_TAB] == '1' ? "true" : "false"; // attribute meaning is inverted

        return $attribute;
    }

    /**
     * Return a uniq form id
     *
     * @return string
     */
    public static function getFormId(string $ttContentUid, string $formId, string $recordId): string {
        //            $this->formId = uniqid('qfq-form-');
//      $this->formId = 'qfq-form-' . $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3) . '-'
//                . $this->formSpec[F_ID] . '-' . $this->store->getVar(CLIENT_RECORD_ID, STORE_SIP . STORE_RECORD . STORE_TYPO3 . STORE_ZERO);


        //return $this->formId;
        return 'qfq-form-' . $ttContentUid . '-' . $formId . '-' . $recordId;
    }

    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
     * Notice: the SIP will be transferred as POST Parameter.
     *
     * @return string
     */
    public function getActionUrl(): string {

        return Path::urlApi(API_SAVE_PHP);
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function getEncType() {

        $result = $this->dbArray[$this->dbIndexQfq]->sql("SELECT id FROM FormElement AS fe WHERE fe.formId=? AND fe.type='upload' LIMIT 1", ROW_REGULAR, [$this->formSpec['id']], 'Look for Formelement.type="upload"');

        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }

    /**
     * @return mixed
     */
    abstract public function getProcessFilter();

    /**
     * @param array|string $value
     *
     * @return array|string
     * @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
     */
    private function processReportSyntax($value) {

        if (is_array($value)) {
            $new = array();

            //might happen for e.g Template Groups
            foreach ($value as $item) {
                $new[] = $this->processReportSyntax($item);
            }

            return $new;
        }

        $value = trim($value);
        if (substr($value, 0, 8) == SHEBANG_REPORT) {
            if ($this->report === null) {
                $this->report = new Report(array(), $this->evaluate, false);
            }

            if ($this->bodytextParser === null) {
                $this->bodytextParser = new BodytextParser();
            }

            $storeRecord = $this->store->getStore(STORE_RECORD);
            $value = $this->report->process($this->bodytextParser->process($value));
            $this->store->setStore($storeRecord, STORE_RECORD, true);
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
        }

        return $value;
    }


    /**
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
     *
     * @param int $recordId
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
     * @param int $feIdContainer
     * @param array $json
     * @param string $modeCollectFe
     * @param bool $htmlElementNameIdZero
     * @param string $storeUseDefault
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     * @param bool $flagMulti
     *
     * @return string
     * @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 elements($recordId, $filter, $feIdContainer, array &$json,
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD, $flagMulti = false) {
        $html = '';

        // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
        $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
            FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_GLUE_UPDATE,
            FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION];

        // get current data record
        $primaryKey = $this->formSpec[F_PRIMARY_KEY];
        if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
            $tableName = $this->formSpec[F_TABLE_NAME];
            $row = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM `$tableName` WHERE `$primaryKey` = ?", ROW_EXPECT_1,
                array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$primaryKey'='$recordId' from table '" .
                $this->formSpec[F_TABLE_NAME] . "'.");
            $this->store->setStore($row, STORE_RECORD);
        }

        $this->checkAutoFocus();

        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

        // Pre calculate HTML_ID for all FE
        $formIdExtended = $this->getFormId($this->ttContentUid, $this->formId, $this->recordId);
        $storeVarAdd = array();
        foreach ($this->feSpecNative as $key => $fe) {
//            $this->feSpecNative[$key][FE_HTML_ID] = HelperFormElement::buildFormElementId($formIdExtended, $fe[FE_ID], $fe[FE_TG_INDEX]);
//            $storeVarAdd['&' . $fe[FE_NAME]] = $this->feSpecNative[$key][FE_HTML_ID];
            $storeVarAdd['&' . $fe[FE_NAME]] = HelperFormElement::buildFormElementName($fe, ($htmlElementNameIdZero) ? 0 : $recordId);
        }
        // Add all HTML_ID elements to STORE_VAR to make them accessible via {{&<name>:V}}
        $this->store->appendToStore($storeVarAdd, STORE_VAR);

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
            $storeUse = $storeUseDefault;


            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
            ) {
                continue; // skip this FE
            }

            // #7705. Dirty fix: 'form_save and fresh created tg element can't be updated, cause the HTML id is unknown: skip those.
            if ($mode == FORM_SAVE && false != stripos($fe[FE_NAME], '%d')) {
                continue; // skip this FE
            }
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not be displayed and not transmitted to the form.

            $debugStack = array();

            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);

            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);

            if (isset($fe[FE_FILL_STORE_VAR])) {
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug
                $fe[FE_FILL_STORE_VAR] = $this->evaluate->parse($fe[FE_FILL_STORE_VAR], ROW_EXPECT_0_1);
                $this->store->appendToStore($fe[FE_FILL_STORE_VAR], STORE_VAR);
            }

            // If given, slaveId will be copied to STORE_VAR
            if (!empty($fe[FE_SLAVE_ID])) {
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
            }

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug
            $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]);

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_NOTE, STORE_SYSTEM); // debug
            $fe[FE_NOTE] = $this->processReportSyntax($fe[FE_NOTE]);

            // ** evaluate current FormElement **
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);

            // Some Defaults
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);

//            // Copy global readonly mode.
//            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
//                $fe[FE_MODE] = FE_MODE_READONLY;
//                $fe[FE_MODE_SQL] = '';
//            }

            if ($flagOutput === true && !$flagMulti) {
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
            }

            Support::setIfNotSet($formElement, FE_VALUE);

            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE]) > 0) ? current($formElement[FE_VALUE][0]) : '';
            }

            $value = $formElement[FE_VALUE];
            // In chat case the value could be stored in Form Store
            if ($formElement[FE_TYPE] === FE_TYPE_CHAT && $value === '') {
                $value = $this->store::getVar($formElement[FE_NAME], STORE_FORM . STORE_EMPTY);
            }
            $prefetchTypeAheadArray = array();
            if ($value === '') {
                // #2064 / Only take the default, if the FE is a real tablecolumn.
                // #3426 / Dynamic Update: Inputs loose the new content and shows the old value.
                if ($storeUse == STORE_USE_DEFAULT && $this->store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) === false) {
                    $storeUse = str_replace(STORE_TABLE_DEFAULT, '', $storeUse); // Remove STORE_DEFAULT
                }

                // In case the current element is a 'RETYPE' element: take the element name of the source FormElement. Needed to retrieve the default value later.
                $name = (isset($formElement[FE_RETYPE_SOURCE_NAME])) ? $formElement[FE_RETYPE_SOURCE_NAME] : $formElement[FE_NAME];

                // Retrieve value via FSRVD
                $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
                $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);

                // For typeAhead fields: perform prefetch to display description instead of key (#5444)
                if ($mode == FORM_SAVE && isset($fe[FE_TYPEAHEAD_SQL_PREFETCH])) {
                    // Get new record store if value is empty. In case if a previous formElement has modified data from actual used record and a new value exists.
                    $tableName = $this->dbArray[$this->dbIndexQfq]->getTableNameById($fe[FE_FORM_ID])[0]['tableName'];
                    if ($value === '' || $value === 0) {
                        $this->store::fillStoreWithRecord($tableName, $recordId, $this->dbArray[$this->dbIndexData]);
                        $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);
                    }

                    $config = [FE_TYPEAHEAD_SQL_PREFETCH => $fe[FE_TYPEAHEAD_SQL_PREFETCH]];
                    $value = TypeAhead::typeAheadSqlPrefetch($config, $value, $this->dbArray[$this->dbIndexData]);
                    $prefetchTypeAheadArray = $value;
                    $value = OnArray::getFirstValueFromArray($value);
                }
            }

            // Typehead might deliver an array, which is unwanted. Except typeahead prefetch, which needs key/value pair to show refreshed data.
            if (is_array($value) && (isset($value[0][API_TYPEAHEAD_VALUE]) || empty($value))) {
                $value = OnArray::getFirstValueFromArray($value);
            }

            // Decrypt value if its encrypted
            if (EncryptDecrypt::checkForEncryptedValue($value)) {
                $encryptionKey = $this->store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
                $value = EncryptDecrypt::getPlaintext($value, $encryptionKey);
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
                $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
            } elseif ($formElement[FE_ENCODE] === FE_ENCODE_SINGLE_TICK) {
                $value = OnString::escapeSingleTickInHtml($value);
            }

            $counter = ($recordId === 0 && $flagMulti) ? $this->counter : null;


            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId, $counter);

            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->getFormId($this->ttContentUid, $this->formId, $this->recordId), $formElement[FE_ID], $formElement[FE_TG_INDEX]);

            $jsonElement = array();
            $elementExtra = '';

            // Backup array from typeAheadPrefetch if exists before json is built.
            if (!empty($prefetchTypeAheadArray)) {
                OnArray::setFirstValueInArray($prefetchTypeAheadArray, 'value', $value);
                $value = $prefetchTypeAheadArray;
            }

            switch ($formElement[FE_TYPE]) {
                case FE_TYPE_CHECKBOX:
                    $elementHtml = $this->buildCheckbox($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_DATE:
                case FE_TYPE_DATETIME:
                case FE_TYPE_TIME:
                    $elementHtml = DateTime::buildDateTime($formElement, $htmlFormElementName, $value, $jsonElement, $this->formSpec, $this->store, $mode, $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS]);
                    //needed for datepicker to be positioned correctly
                    if ($flagMulti == true) {
                        $elementHtml = Support::wrapTag('<div class="col-d-12 col-lg-12">', $elementHtml);
                    }
                    break;
                case FE_TYPE_TEXT:
                case FE_TYPE_PASSWORD:
                case 'email':
                    $elementHtml = $this->buildInput($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_EXTRA:
                    $elementHtml = $this->buildExtra($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_EDITOR:
                    $elementHtml = $this->buildEditor($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_NOTE:
                    $elementHtml = $this->buildNote($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_RADIO:
                    $elementHtml = $this->buildRadio($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_SELECT:
                    $elementHtml = $this->buildSelect($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_SUBRECORD:
                    $elementHtml = $this->buildSubrecord($formElement);
                    break;
                case FE_TYPE_UPLOAD:
                    $elementHtml = $this->buildFile($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_CHAT:
                    $elementHtml = $this->buildChat($formElement, $htmlFormElementName, $value, $jsonElement);
                    break;
                case FE_TYPE_ANNOTATE:
                    $elementHtml = $this->buildAnnotate($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_IMAGE_CUT:
                    $elementHtml = $this->buildImageCut($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_FIELDSET:
                    $elementHtml = $this->buildFieldset($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_PILL:
                    $elementHtml = $this->buildPill($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                case FE_TYPE_TEMPLATE_GROUP:
                    $elementHtml = $this->buildTemplateGroup($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
                    break;
                default:
                    throw new \UserFormException('Unknown or deprecated FormElement type: ' . $formElement[FE_TYPE], ERROR_UNKNOWN_MODE);
            }

            $this->clientJsAll .= $formElement[FE_CLIENT_JS];

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
                // for non-container elements: just add the current json status
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) {
                    if (isset($jsonElement[0]) && is_array($jsonElement[0])) {
                        // Checkboxes are delivered as array of arrays: unnest them and append them to the existing json array.
                        $json = array_merge($json, $jsonElement);
                    } else {
                        $json[] = $jsonElement;
                    }
                }
            }

            if ($flagOutput) {
                // debugStack as Tooltip
                if ($this->showDebugInfoFlag) {

                    if (count($debugStack) > 0) {
                        $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
                    }

                    if (!$flagMulti) {
                        // Build 'FormElement' Edit symbol. MultiForms: Edit symbol is in thead.
                        $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
                        $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                        $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
                        $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
                    }
                }

                if ($flagMulti) {
                    $html .= Support::wrapTag($this->formSpec[F_MULTI_CELL_WRAP], $elementHtml);
                } else {
                    // Construct Marshaller Name: buildRow...
                    $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
                    $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName);
                }
            }
        }

        if ($recordId === 0) {
            $this->counter++;
        }

        $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, '', STORE_SYSTEM); // debug

        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);
        $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, 0, STORE_SYSTEM);

        return $html;
    }

    /**
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
     *
     * @return array
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function prepareFillStoreFireLdap(array $formElement) {

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
            $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES,
                F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN, F_TYPEAHEAD_LDAP_SEARCH_PREFETCH,
                F_TYPEAHEAD_LIMIT, F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF,
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
            $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
        } else {
            return $formElement; // nothing to do.
        }

        if (isset($formElement[FE_FILL_STORE_LDAP])) {

            // Extract necessary elements
            $config = OnArray::getArrayItems($formElement, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]);

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN,
                FE_LDAP_SERVER . ',' . FE_LDAP_BASE_DN . ',' . FE_LDAP_SEARCH . ',' . FE_LDAP_ATTRIBUTES, STORE_SYSTEM);
            $config = $this->evaluate->parseArray($config);

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

            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

        return $formElement;
    }

    /**
     * Check if there is an explicit 'autofocus' definition in at least one FE.
     * Found: do nothing, it will be rendered at the correct position.
     * Not found: set 'autofocus' on the first FE.
     *
     * Accepted misbehaviour on forms with pills: if there is at least one editable element on the first pill,
     *   the other pills are not checked - independent if there was a definition on the first pill or not.
     *   Reason: checks happens per pill - if there is no explicit definition on the first pill, take the first
     *   editable element of that pill.
     */
    private function checkAutoFocus() {
        static $found = false;
        $idx = false;

        if ($found) {
            return;
        }

        // Search if there is an explicit autofocus definition.
        for ($i = 0; $i < count($this->feSpecNative); ++$i) {
            // Only check native elements which will be shown
            if ($this->feSpecNative[$i][FE_CLASS] == FE_CLASS_NATIVE &&
                ($this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW
                    || $this->feSpecNative[$i][FE_MODE] == FE_MODE_REQUIRED
                    || $this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW_REQUIRED)
            ) {
                // Check if there is an explicit definition.
                if (isset($this->feSpecNative[$i][FE_AUTOFOCUS])) {
                    if ($this->feSpecNative[$i][FE_AUTOFOCUS] == '' || $this->feSpecNative[$i][FE_AUTOFOCUS] == '1') {
                        $this->feSpecNative[$i][FE_AUTOFOCUS] = '1'; // fix to '=1'
                    } else {
                        unset($this->feSpecNative[$i][FE_AUTOFOCUS]);
                    }
                    $found = true;

                    return;
                }

                if ($idx === false) {
                    $idx = $i;
                }
            }
        }

        // No explicit definition found: take the first found editable element.
        if ($idx !== false) {
            $found = true;
            // No explicit definition found: set autofocus.
            $this->feSpecNative[$idx][FE_AUTOFOCUS] = '1';
        }
    }

    /**
     * @param $label
     * @param $input
     * @param $note
     */
    abstract public function fillWrapLabelInputNote($label, $input, $note);

    /**
     * Copy a subset of current STORE_TYPO3 variables to SIP. Set a hidden form field to submit the assigned SIP to
     * save/update.
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function prepareT3VarsForSave() {

        $t3VarsSip = $this->store->copyT3VarsToSip();

        return HelperFormElement::buildNativeHidden(CLIENT_TYPO3VARS, $t3VarsSip);

    }

    /**
     *
     */
    abstract public function tail();

    /**
     *
     */
    abstract public function doSubrecords();

    /**
     * Get all elements from STORE_ADDITIONAL_FORM_ELEMENTS and return them as a string.
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function buildAdditionalFormElements() {

        $data = $this->store->getStore(STORE_ADDITIONAL_FORM_ELEMENTS);

        return is_array($data) ? implode('', $data) : '';
    }

    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
     * @param array $json
     *
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function buildHiddenSip(array &$json) {

        $sipArray = $this->store->getStore(STORE_SIP);

        // do not include system vars
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

        $queryString = Support::arrayToQueryString($sipArray);
        $sip = $this->store->getSipInstance();

        $sipValue = $sip->queryStringToSip($queryString, RETURN_SIP);

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json[] = $this->getFormElementForJson(CLIENT_SIP, $sipValue, [FE_MODE => FE_MODE_SHOW], $wrapSetupClass);

        return HelperFormElement::buildNativeHidden(CLIENT_SIP, $sipValue);
    }

    /**
     * Create an array with standard elements for 'mode' (hidden, disabled, required, readonly)
     * and add 'form-element', 'value'.
     * 'Generic Element Update': add via API_ELEMENT_UPDATE 'label' and 'note'.
     * All collected data as array - will be later converted to JSON.
     *
     * @param string $htmlFormElementName
     * @param string|array $value
     * @param array $formElement
     * @param string $wrap
     * @param int $optionIdx
     * @param string $optionClass
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function getFormElementForJson(string $htmlFormElementName, $value, array $formElement, $wrap = '', $optionIdx = 0, $optionClass = ''): array {
        $addClassRequired = array();

        $json = HelperFormElement::getJsonFeMode($formElement[FE_MODE]); // disabled, required

        $json[API_FORM_UPDATE_FORM_ELEMENT] = $htmlFormElementName;

        if (isset($formElement[FE_FLAG_ROW_OPEN_TAG]) && isset($formElement[FE_FLAG_ROW_CLOSE_TAG])) {
            $flagRowUpdate = ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]);
        } else {
            $flagRowUpdate = true;
        }

        $statusHidden = ($formElement[FE_MODE] == 'hidden');
        $pattern = null;
        if (isset($formElement[FE_CHECK_PATTERN]) && $formElement[FE_CHECK_PATTERN] != '') {
            $pattern = $statusHidden ? false : $formElement[FE_CHECK_PATTERN];
        }

        // 'value' update via 'form-update' on the full row: only if there is no other FE in that row
        if ($flagRowUpdate) {
            $json[API_FORM_UPDATE_VALUE] = $value;

//CR            if ($pattern !== null) {
//                $json['pattern'] = $pattern;
//            }
        }

        if (($formElement[FE_MODE] == FE_MODE_REQUIRED || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED)
            && $formElement[FE_INDICATE_REQUIRED] == '1') {
            $addClassRequired = HelperFormElement::getRequiredPositionClass($formElement[F_FE_REQUIRED_POSITION]);
        }

        // Label
        if (isset($formElement[FE_LABEL])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL;
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = HelperFormElement::buildLabel($htmlFormElementName, $formElement[FE_LABEL], $addClassRequired[FE_LABEL] ?? '');
        }

        // Note
        if (isset($formElement[FE_NOTE])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE;
            if (!empty($addClassRequired[FE_NOTE])) {
                $formElement[FE_NOTE] = Support::wrapTag('<span class="' . $addClassRequired[FE_NOTE] . '">', $formElement[FE_NOTE]);
            }
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $formElement[FE_NOTE];
        }

        // Input
        if (isset($formElement[FE_TYPE])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT;

            // For FE.type='note': update the column 'input'
            if ($formElement[FE_TYPE] === FE_TYPE_NOTE) {
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $value;
            }

            // Check show/hide: only FE with FE_MODE_SQL given, might change.
            if (!empty($formElement[FE_MODE_SQL])) {
                $class = is_numeric($formElement[FE_BS_INPUT_COLUMNS]) ? ('col-md-' . $formElement[FE_BS_INPUT_COLUMNS]) : $formElement[FE_BS_INPUT_COLUMNS];
//                $class = 'col-md-' . $formElement[FE_BS_INPUT_COLUMNS] . ' ';

                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['required'] = ($formElement[FE_MODE] == 'required');
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['hidden'] = $statusHidden;
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['readonly'] = $json['disabled'] ? 'readonly' : 'false';

                // Checkbox: Copy attributes to every checkbox (appreciated by screen reader):
                if ($formElement[FE_TYPE] == FE_TYPE_CHECKBOX) {
                    $optionsId = $formElement[FE_HTML_ID] . '-' . $optionIdx;
                    $json[API_ELEMENT_UPDATE][$optionsId][API_ELEMENT_ATTRIBUTE] = $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE];
                    $json[API_ELEMENT_UPDATE][$optionsId][API_ELEMENT_ATTRIBUTE]['data-disabled'] = $json['disabled'] ? 'yes' : 'no';
                    $json[API_ELEMENT_UPDATE][$optionsId][API_ELEMENT_ATTRIBUTE]['data-required'] = $json['required'] ? 'yes' : 'no';

                    // Update label class (i.e.: 'qfq-disabled') of Checkbox/Radio (i.e. readonly on/off).
                    if (isset($formElement[FE_TMP_CLASS_OPTION])) {
                        $optionsLabelId = HelperFormElement::getCheckboxRadioOptionId($formElement[FE_HTML_ID], $optionIdx, HTML_ID_EXTENSION_LABEL);
                        $json[API_ELEMENT_UPDATE][$optionsLabelId][API_ELEMENT_ATTRIBUTE]['class'] = $formElement[FE_TMP_CLASS_OPTION];
                    }
                }

                $class .= ($formElement[FE_MODE] == FE_MODE_HIDDEN) ? ' hidden' : '';
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['class'] = $class;

                if ($pattern !== null) {
                    $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['pattern'] = $pattern;
                }

            }

            // #3647
//            if (!$flagRowUpdate) {

            // #4771 - temporary workaround: SELECT in 'multi FE row' won't updated after 'save' or with dynamic update.
            //TODO #5016 - exception for FE_TYPE_CHECKBOX should be removed ASAP
            if ($formElement[FE_TYPE] != FE_TYPE_SELECT && $formElement[FE_TYPE] != FE_TYPE_UPLOAD && $formElement[FE_TYPE] != FE_TYPE_CHECKBOX) {
                $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['value'] = $value;
                $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['required'] = ($formElement[FE_MODE] == 'required');
                $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['hidden'] = $statusHidden;

                if ($pattern !== null) {
                    $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['pattern'] = $pattern;
                }
            }
        }

        // Show / Hide the complete FormElement Row.
        if (isset($formElement[FE_HTML_ID])) { // HIDDEN_SIP comes without a real existing FE structure.
            // Activate 'show' or 'hidden' on the current FormElement via JSON 'API_ELEMENT_UPDATE'
            if ($optionClass != '') {
                $class = $optionClass;
            } else {
                $class = $wrap;
            }

            if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
                $class .= ' hidden';
            }

            if (!empty($addClassRequired[FE_INPUT])) {
                $class .= ' ' . $addClassRequired[FE_INPUT];
            }

            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_ROW;
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['class'] = $class;

            // Check if FE is upload and parameter downloadButton is set. Has to come after saving the Form. Avoids throwing error
            if ($formElement[FE_TYPE] == FE_TYPE_UPLOAD && isset($formElement[FE_FILE_DOWNLOAD_BUTTON]) && isset($_GET["submit_reason"])) {
                $json['type-file'] = true;
                $json['html-content'] = $formElement[FE_FILE_DOWNLOAD_BUTTON_HTML_INTERNAL] ?? '';
            }

//            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['buggy'] = $class;
        }

        return $json;
    }

    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @param $toolTipNew
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {

        $urlParam = $this->store->getNonSystemSipParam();
        $urlParam[SIP_RECORD_ID] = 0;
        $urlParam[SIP_FORM] = $this->formSpec[F_NAME];

        Support::appendTypo3ParameterToArray($urlParam);

        $sip = $this->store->getSipInstance();

        $url = $sip->queryStringToSip(OnArray::toString($urlParam));

        if ($this->showDebugInfoFlag) {
            //TODO: missing decoding of SIP
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }

        return $url;
    }

    /**
     * @param array $formElement
     * @param $elementHtml
     * @return mixed
     */
    abstract public function buildRowPill(array $formElement, $elementHtml);

    /**
     * @param array $formElement
     * @param $elementHtml
     * @return mixed
     */
    abstract public function buildRowFieldset(array $formElement, $elementHtml);

    /**
     * @param array $formElement
     * @param $elementHtml
     * @return mixed
     */
    abstract public function buildRowTemplateGroup(array $formElement, $elementHtml);

    /**
     * @param array $formElement
     * @param $elementHtml
     * @return mixed
     */
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);

    /**
     * Builds HTML 'input' element.
     * Format: <input name="$htmlFormElementName" <type="email|input|password|url" [autocomplete="autocomplete"]
     * [autofocus="autofocus"]
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json Return updates in this array - will be later converted to JSON.
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string complete rendered HTML input element.
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $textarea = '';
        $attribute = '';
        $class = 'form-control';
        $elementCharacterCount = '';

        if (isset($formElement[F_FE_INPUT_CLEAR_ME]) && $formElement[F_FE_INPUT_CLEAR_ME] != '0') {
            if (($this->formSpec[F_MULTI_MODE] ?? '') == F_MULTI_MODE_VERTICAL) {
                $class .= ' qfq-clear-me-multiform';
            } else {
                $class .= ' qfq-clear-me';
            }
        }

        if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '' && !isset($formElement[FE_TYPEAHEAD_SQL])) {
            // Check if there are \r\n > those should be counted as one char, not two. #13030
            $crlf = substr_count($value, "\r\n");
            $max = $formElement[FE_MAX_LENGTH] + $crlf;
            // crop string only if it's not empty (substr returns false on empty strings)
            $value = mb_substr($value, 0, $max);
        }

        if ($formElement[FE_HIDE_ZERO] != '0' && $value == '0') {
            $value = '';
        }

        if ($formElement[FE_DECIMAL_FORMAT] !== '') {
            if ($value !== '') { // empty string causes exception in number_format()
                $decimalScale = explode(',', $formElement[FE_DECIMAL_FORMAT])[1]; // scale: Nachkommastellen
                $value = number_format($value, $decimalScale, '.', '');
            }
        }

        // Check if typeAhead[Tag] needs to build
        if ('' != ($typeAheadUrlParam = $this->typeAheadBuildParam($formElement))) {

            if (empty($formElement[FE_INPUT_TYPE])) {
                $formElement[FE_INPUT_TYPE] = FE_TYPE_SEARCH; // typeahead behaves better with 'search' instead of 'text'
            }

            if (empty($formElement[FE_INPUT_AUTOCOMPLETE])) {
                $formElement[FE_INPUT_AUTOCOMPLETE] = 'off'; // typeahead behaves better with 'autocomplete'='off'
            }

            // Collect typeAhead initial suggestion
            if (!empty($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION])) {
//                $formElement[FE_TYPEAHEAD_MINLENGTH] = 0; // If a suggestion is defined: minLength becomes automatically 0
                $arr = $this->evaluate->parse($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION]);
                $arr = $this->dbArray[$this->dbIndexData]->makeArrayDict($arr, TYPEAHEAD_SQL_KEY_NAME, API_TYPEAHEAD_VALUE, API_TYPEAHEAD_KEY, API_TYPEAHEAD_VALUE);
                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_INITIAL_SUGGESTION, json_encode($arr));
            }

            $class .= ' ' . CLASS_TYPEAHEAD;
            $dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
            if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_PEDANTIC] ?? '-')) {
                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_PEDANTIC, 'true');
            }

            // Tag
            if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_TAG, 'true');

                // Default: tab, comma
                $kk = '[' . ($formElement[FE_TYPEAHEAD_TAG_DELIMITER] ?? '9,44') . ']';
                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_TAG_DELIMITER, $kk);
                $formElement[FE_INPUT_TYPE] = 'hidden';

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

        if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
            $class .= ' ' . CLASS_CHARACTER_COUNT;
            $attribute .= Support::doAttribute(DATA_CHARACTER_COUNT_ID, $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);
            $attributeCC = Support::doAttribute(FE_ID, $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);
            $classCC = ($formElement[FE_CHARACTER_COUNT_WRAP] == '') ? Support::doAttribute(FE_CLASS, FE_CHARACTER_COUNT_CLASS) : '';
            $elementCharacterCount = "<span $attributeCC $classCC></span>";

            if ($formElement[FE_CHARACTER_COUNT_WRAP] != '') {
                $arr = explode('|', $formElement[FE_CHARACTER_COUNT_WRAP], 2);
                $arr[] = '';
                $arr[] = ''; //skip check that at least 2 elements exist
                $elementCharacterCount = $arr[0] . $elementCharacterCount . $arr[1];
            }
        }

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        if (isset($formElement[FE_RETYPE_SOURCE_NAME])) {
            $htmlFormElementNamePrimary = str_replace(FE_RETYPE_NAME_EXTENSION, '', $htmlFormElementName);
            $attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']');
        }

        // 'maxLength' needs an upper 'L': naming convention for DB tables!
        if ($formElement[FE_MAX_LENGTH] > 0) {
            $attribute .= Support::doAttribute('maxlength', $formElement[FE_MAX_LENGTH], false);
        }

        // 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($formElement[FE_INPUT_TYPE]) && !empty($formElement[FE_MIN]) && !empty($formElement[FE_MAX]) &&
            is_numeric($formElement[FE_MIN]) && is_numeric($formElement[FE_MAX])
        ) {
            $formElement[FE_INPUT_TYPE] = 'number';
        }

        // when size empty but value contains \n then set auto multi line
        if (!isset($formElement[FE_TYPEAHEAD_SQL])) {
            if (strpos($value, "\n") == true && empty($formElement[FE_SIZE])) {
                $formElement[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(',', $formElement[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);

        $formElement = HelperFormElement::prepareExtraButton($formElement, !$flagTextarea);


        if ($flagTextarea) {
            $htmlTag = '<textarea';

            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);

            if (($formElement[FE_TEXTAREA_RESIZE] ?? '1') == '0') {
                $attribute .= Support::doAttribute('style', 'resize: none;');
            }
            // Check if $colsRows[2] != 0 >> enable auto-grow (if nothing is defined: enable auto-grow)
            $maxHeight = $colsRows[2] ?? '350';
            if ($maxHeight != '0') {
                $class .= ' ' . CLASS_FORM_ELEMENT_AUTO_GROW;
                $attribute .= Support::doAttribute(ATTRIBUTE_DATA_MAX_HEIGHT, $maxHeight);
            }
            $textarea = htmlentities($value) . '</textarea>';
        } else {
            $htmlTag = '<input';
            if (!empty($formElement[FE_INPUT_TYPE])) {
                $formElement[FE_TYPE] = $formElement[FE_INPUT_TYPE];
                // TypeAhead tag elements needs to be hidden
                if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
                    $formElement[FE_TYPE] = 'hidden';
                }
            }
            // If type password is selected then type text with own class will be taken to fake password over CSS
            if ($formElement[FE_TYPE] === 'password') {
                $formElement[FE_TYPE] = 'text';
                $class .= ' qfq-password';
            }
            $attribute .= HelperFormElement::getAttributeList($formElement, [FE_TYPE, 'size']);

            // Typeahead prefetch needs key as attribute. Key/value pair is needed later on client side.
            if (isset($formElement[FE_TYPEAHEAD_SQL_PREFETCH]) && is_array($value)) {
                $attributeValue = OnArray::getFirstValueFromArray($value, true);
            } else {
                $attributeValue = $value;
            }

            $attribute .= Support::doAttribute('value', htmlentities($attributeValue), false);

//            $attribute .= Support::doAttribute('value', htmlentities($value, ENT_QUOTES, 'UTF-8'), false);
        }

        $attribute .= HelperFormElement::getAttributeList($formElement, [FE_INPUT_AUTOCOMPLETE, 'autofocus', 'placeholder']);

        $formElement[FE_CHECK_PATTERN] = Sanitize::getInputCheckPattern($formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN]
            , $formElement[FE_DECIMAL_FORMAT], $sanitizeMessage);
        if ($formElement[FE_CHECK_PATTERN] !== '') {
            $attribute .= Support::doAttribute('pattern', $formElement[FE_CHECK_PATTERN], true, ESCAPE_WITH_BACKSLASH);

            // Use system specific message only,if no custom one is set.
            if ($formElement[F_FE_DATA_PATTERN_ERROR] == $formElement[F_FE_DATA_PATTERN_ERROR_SYSTEM]) {
                $formElement[F_FE_DATA_PATTERN_ERROR] = $sanitizeMessage;
            }
            $attribute .= Support::doAttribute(F_FE_DATA_PATTERN_ERROR, $formElement[F_FE_DATA_PATTERN_ERROR], true, ESCAPE_WITH_BACKSLASH);
        }

        if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
            $attribute .= Support::doAttribute(F_FE_DATA_REQUIRED_ERROR, $formElement[F_FE_DATA_REQUIRED_ERROR]);
        }

        if (empty($formElement[F_FE_DATA_MATCH_ERROR])) {
            $formElement[F_FE_DATA_REQUIRED_ERROR] = F_FE_DATA_REQUIRED_ERROR_DEFAULT;
        }

        $attribute .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR, FE_MIN, FE_MAX, FE_STEP]);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);

        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE], false);

        $attribute .= Support::doAttribute('class', $class);

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        $input = "$htmlTag $attribute>$textarea";

        if ($formElement[FE_TMP_EXTRA_BUTTON_HTML] !== '') {

            if ($flagTextarea) {
                $input .= $formElement[FE_TMP_EXTRA_BUTTON_HTML];
            } else {
                $input = Support::wrapTag('<div class="input-group">', $input . $formElement[FE_TMP_EXTRA_BUTTON_HTML]);
            }
        }

        $input .= HelperFormElement::getHelpBlock() . $elementCharacterCount;

        if (isset($formElement[FE_INPUT_EXTRA_BUTTON_INFO])) {
            $input .= $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
        }

        return $input;
    }

    /**
     * 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(array &$formElement) {

        $arr = [];

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

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

            $arr = [
                FE_TYPEAHEAD_SQL => $sql,
                FE_TYPEAHEAD_SQL_PREFETCH => $formElement[FE_TYPEAHEAD_SQL_PREFETCH] ?? ''
            ];
        } elseif (isset($formElement[FE_TYPEAHEAD_LDAP])) {
            $formElement[FE_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_LDAP_SERVER);
            $formElement[FE_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_LDAP_BASE_DN);
            $formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH);
            $formElement[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH);
            $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF);
            $formElement[FE_TYPEAHEAD_LDAP_ID_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_ID_PRINTF);
            $formElement[FE_LDAP_USE_BIND_CREDENTIALS] = Support::setIfNotSet($formElement, FE_LDAP_USE_BIND_CREDENTIALS);

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

            if ($formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] . $formElement[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 => $formElement[FE_LDAP_SERVER],
                FE_LDAP_BASE_DN => $formElement[FE_LDAP_BASE_DN],
                FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH],
                FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH_PREFETCH],
                FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF],
                FE_TYPEAHEAD_LDAP_ID_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_ID_PRINTF],
                FE_TYPEAHEAD_LIMIT => $formElement[FE_TYPEAHEAD_LIMIT],
            ];

            if (isset($formElement[FE_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN])) {
                $arr[FE_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN] = '1';
            }

            if ($formElement[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);
    }

    /**
     * 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) {
        $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;
    }

    /**
     * Builds HTML 'checkbox' element.
     *
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value
     * will be transferred first.
     *
     * Format: <input type="hidden" name="$htmlFormElementName" value="$valueUnChecked">
     *         <input name="$htmlFormElementName" type="checkbox" [autofocus="autofocus"]
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildCheckbox(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        $checkbox = new Checkbox;
        return $checkbox->build($formElement, $htmlFormElementName, $value, $json, $mode);

    }

    /**
     * For CheckBox's with only one checkbox: if no parameter:checked|unchecked is defined, take defaults:
     *
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
     *
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

        if (!isset($formElement[FE_CHECKBOX_CHECKED])) {
            if (isset($itemKey[0])) {
                // First element in $itemKey list
                $formElement[FE_CHECKBOX_CHECKED] = $itemKey[0];
            } else {
                // Take column default value
                $formElement[FE_CHECKBOX_CHECKED] = $this->store->getVar($formElement[FE_NAME], STORE_TABLE_DEFAULT);
            }
        }

        // unchecked
        if (!isset($formElement[FE_CHECKBOX_UNCHECKED])) {
            if (isset($itemKey[1])) {
                $formElement[FE_CHECKBOX_UNCHECKED] = ($itemKey[0] === $formElement[FE_CHECKBOX_CHECKED]) ? $itemKey[1] : $itemKey[0];
            } else {
                $formElement[FE_CHECKBOX_UNCHECKED] = '';
            }
        }

        if ($formElement[FE_CHECKBOX_CHECKED] === $formElement[FE_CHECKBOX_UNCHECKED]) {
            throw new \UserFormException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement[FE_CHECKBOX_CHECKED], ERROR_CHECKBOX_EQUAL);
        }

    }

    /**
     * Submit extra (hidden) values by SIP.
     *
     * Sometimes, it's usefull to precalculate values during formload and to submit them as hidden fields.
     * To avoid any manipulation on those fields, the values will be transferred by SIP.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function buildExtra(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        if ($mode === FORM_LOAD) {
            $this->store->setVar($formElement[FE_NAME], $value, STORE_SIP, true);
        }

        return;
    }

    /**
     * Build HTML 'radio' element.
     *
     * Format: <input type="hidden" name="$htmlFormElementName" value="$valueUnChecked">
     *         <input name="$htmlFormElementName" type="radio" [autofocus="autofocus"]
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildRadio(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        if (isset($formElement[FE_BUTTON_CLASS])) {

            if ($formElement[FE_BUTTON_CLASS] == '') {
                $formElement[FE_BUTTON_CLASS] = 'btn-default';
            }
            // BS
            $html = $this->constructRadioButton($formElement, $htmlFormElementName, $value, $json, $mode);
        } else {
            // Plain
            $html = $this->constructRadioPlain($formElement, $htmlFormElementName, $value, $json, $mode);
        }

        $formElement = HelperFormElement::prepareExtraButton($formElement, false);

        return $html . $formElement[FE_TMP_EXTRA_BUTTON_HTML] . HelperFormElement::getHelpBlock() . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];

    }

    /**
     * Build Bootstrap Button 'radio' element.
     *
     * <div class="btn-group" data-toggle="buttons">
     *
     *   <label class="btn btn-primary active">
     *     <input type="radio" name="options" id="option1" autocomplete="off" checked> Radio 1 (preselected)
     *   </label>
     *
     *   <label class="btn btn-primary">
     *     <input type="radio" name="options" id="option2" autocomplete="off"> Radio 2
     *   </label>
     *
     *   <label class="btn btn-primary">
     *     <input type="radio" name="options" id="option3" autocomplete="off"> Radio 3
     *   </label>
     *
     * </div>
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function constructRadioButton(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
        HelperFormElement::getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);

        $attributeBase = HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        $attributeBase .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
//        $attributeBase .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attributeBase .= Support::doAttribute('name', $htmlFormElementName);
        $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
        $attributeBase .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attributeBase .= Support::doAttribute(FE_INPUT_AUTOCOMPLETE, 'off');

        $attribute = $attributeBase;
        if (isset($formElement[FE_AUTOFOCUS])) {
            $attribute .= Support::doAttribute('autofocus', $formElement[FE_AUTOFOCUS]);
        }

        $html = '';
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $classActive = '';
            $classReadonly = '';

            $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID] . '-' . $ii);
            $attribute .= Support::doAttribute('value', $itemKey[$ii], false); // Always set value, even to '' - #3832
            $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE] . '-' . $ii);

            if ($itemKey[$ii] == $value) {
                $attribute .= Support::doAttribute('checked', 'checked');
                $classActive = ' active';
            }

            if ($formElement[FE_MODE] == FE_MODE_READONLY) {
                $classReadonly = ' noclick';
                if ($itemKey[$ii] != $value) {
                    $classReadonly .= ' disabled';
                }
            }

            if ($itemValue[$ii] == '') { // In case the value is empty, the rendered button looks bad. Set '&nbsp;'.
                $itemValue[$ii] = '&nbsp;';
            }

            $htmlElement = '<input ' . $attribute . '>' . $itemValue[$ii];

            $labelAttribute = Support::doAttribute('title', $formElement[FE_TOOLTIP]);
            $labelAttribute .= Support::doAttribute('class', 'btn ' . $formElement[FE_BUTTON_CLASS] . $classReadonly . $classActive);
            $htmlElement = Support::wrapTag("<label $labelAttribute>", $htmlElement);

            $html .= $htmlElement;

            // Init for the next round
            $attribute = $attributeBase;
        }

        $html = Support::wrapTag('<div class="btn-group" data-toggle="buttons">', $html);

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        return $html;
    }

    /**
     * Build plain HTML 'radio' element.
     *
     * Format: <input type="hidden" name="$htmlFormElementName" value="$valueUnChecked">
     *         <input name="$htmlFormElementName" type="radio" [autofocus="autofocus"]
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function constructRadioPlain(array $formElement, string $htmlFormElementName, $value, array &$json, string $mode = FORM_LOAD): string {
        $attributeBase = '';
        $html = '';

        if (isset($formElement[FE_BUTTON_CLASS])) {
            return $this->constructRadioButton($formElement, $htmlFormElementName, $value, $json, $mode);
        }

        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
        HelperFormElement::getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);

        $attributeBase .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        $attributeBase .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
        $attributeBase .= Support::doAttribute('name', $htmlFormElementName);
        $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
        $attributeBase .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');

        $attributeBaseLabel = Support::doAttribute('style', 'min-width: ' . $formElement[F_FE_MIN_WIDTH] . 'px; font-weight: 400;');

        $jj = 0;

        $orientation = ($formElement[FE_MAX_LENGTH] > 1) ? ALIGN_HORIZONTAL : ALIGN_VERTICAL;
        $radioClass = ($orientation === ALIGN_HORIZONTAL) ? 'radio-inline' : 'radio';
        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $radioClass .= ' qfq-disabled';
        }
//        $radioOuterTag = ($orientation === ALIGN_HORIZONTAL) ? "label $attributeBaseLabel" : 'label';
        $br = '';

        // Used in getFormElementForJson() for dynamic update.
        $formElement[FE_TMP_CLASS_OPTION] = $radioClass;

        $attribute = $attributeBase;
        if (isset($formElement[FE_AUTOFOCUS])) {
            $attribute .= Support::doAttribute('autofocus', $formElement[FE_AUTOFOCUS]);
        }

        $jsonTmp = array();
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $jj++;

            $optionId = HelperFormElement::getCheckboxRadioOptionId($formElement[FE_HTML_ID], $ii);
            $attribute .= Support::doAttribute('id', $optionId);
            $attribute .= Support::doAttribute('value', $itemKey[$ii], false); // Always set value, even to '' - #3832
            $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE] . '-' . $ii);

            if ($itemKey[$ii] == $value) {
                $attribute .= Support::doAttribute('checked', 'checked');
            }

            // '&nbsp;' - This is necessary to correctly align an empty input.
            $tmpValue = ($itemValue[$ii] === '') ? '&nbsp;' : $itemValue[$ii];

            $htmlElement = '<input ' . $attribute . '>' . $tmpValue;

            // With ALIGN_HORIZONTAL: the label causes some trouble: skip it
//            if (($orientation === ALIGN_VERTICAL)) {
//                $htmlElement = Support::wrapTag("<label>", $htmlElement);
//            }

            if ($formElement[FE_MAX_LENGTH] > 1) {

                if ($jj == $formElement[FE_MAX_LENGTH]) {
                    $jj = 0;
                    $br = '<br>';
                } else {
                    $br = '';
                }
            }

            $wrapAttribute = Support::doAttribute('title', $formElement[FE_TOOLTIP]);
            $wrapAttribute .= Support::doAttribute('class', $radioClass);

            $radioLabelId = HelperFormElement::getCheckboxRadioOptionId($formElement[FE_HTML_ID], $ii, HTML_ID_EXTENSION_LABEL);
            $htmlElement = Support::wrapTag("<label $wrapAttribute $attributeBaseLabel id='$radioLabelId'>", $htmlElement) . $br;

            $html .= $htmlElement;

            $attribute = $attributeBase;

            // Update label class (i.e.: 'qfq-disabled') of Checkbox/Radio (i.e. readonly on/off).
            $optionLabelId = HelperFormElement::getCheckboxRadioOptionId($formElement[FE_HTML_ID], $ii, HTML_ID_EXTENSION_LABEL);
            $jsonTmp[API_ELEMENT_UPDATE][$optionLabelId][API_ELEMENT_ATTRIBUTE]['class'] = $formElement[FE_TMP_CLASS_OPTION];

            $jsonTmp[API_ELEMENT_UPDATE][$optionId][API_ELEMENT_ATTRIBUTE]['required'] = ($formElement[FE_MODE] == FE_MODE_REQUIRED) ? 'required' : 'false';
        }

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = array_merge($this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass));
        $json[API_ELEMENT_UPDATE] = array_merge($json[API_ELEMENT_UPDATE], ($jsonTmp[API_ELEMENT_UPDATE] ?? array()));


        return $html;
    }

    /**
     * Builds a Selct (Dropdown) Box.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return mixed
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildSelect(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $itemKey = array();
        $itemValue = array();
        $attribute = '';

        if (!empty($formElement[FE_PLACEHOLDER])) {
            $formElement[FE_EMPTY_ITEM_AT_START] = '';
            if (isset($formElement[FE_EMPTY_ITEM_AT_END])) {
                unset($formElement[FE_EMPTY_ITEM_AT_END]);
            }
        }

        // Fill $itemKey & $itemValue
        HelperFormElement::getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
        $attribute .= Support::doAttribute('class', 'form-control');
        $attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
        $attribute .= HelperFormElement::getAttributeList($formElement, ['autofocus', F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR]);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        if (isset($formElement[FE_SIZE]) && $formElement[FE_SIZE] > 1) {
            $attribute .= Support::doAttribute('size', $formElement[FE_SIZE]);
            $attribute .= Support::doAttribute('multiple', 'multiple');
        }

        $option = '';
        $firstSelect = true;
        $jsonValues = array();
        $total = count($itemValue);
        for ($ii = 0; $ii < $total; $ii++) {
            $option .= '<option ';

            $option .= Support::doAttribute('value', $itemKey[$ii], false);

            if ($ii == 0 && $formElement[FE_PLACEHOLDER] != '') {
                $option .= 'disabled hidden ';
            }

            $jsonValues[] = [
                'value' => $itemKey[$ii],
                'text' => $itemValue[$ii],
                'selected' => ($itemKey[$ii] == $value && $firstSelect),
            ];

            if ($itemKey[$ii] == $value && $firstSelect) {
                $option .= 'selected';
                $firstSelect = false;
            }

            $option .= '>' . $itemValue[$ii] . '</option>';
        }

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $jsonValues, $formElement, $wrapSetupClass);

        $formElement = HelperFormElement::prepareExtraButton($formElement, false);
        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        if (isset($formElement[FE_DATALIST])) {
            if ($formElement[FE_DYNAMIC_UPDATE] === 'yes') {
                throw new \UserFormException("Datalist funktionert nicht mit dynamic update", ERROR_NOT_IMPLEMENTED);
            }
            $datalistId = $formElement[FE_HTML_ID] . '-datalist';
            $html = '<input ' . Support::doAttribute('list', $datalistId) . $attribute . '><datalist '
                . Support::doAttribute('id', $datalistId) . '>' . $option . '</datalist>';
        } else {
            $html = '<select ' . $attribute . '>' . $option . '</select>';
        }
        $html = $html . HelperFormElement::getHelpBlock() . $formElement[FE_TMP_EXTRA_BUTTON_HTML];
        return $html . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
    }

    /**
     * @param string $linkNew Complete Button, incl. SIP href
     * @param $flagDelete
     * @param $deleteTitle
     * @param array $firstRow First row of all subrecords to extract columntitles
     * @param array $control Array with <th> column names / format.
     *
     * @return string
     * @throws \UserFormException
     */
    private function subrecordHead($linkNew, $flagDelete, $deleteTitle, array $firstRow, array &$control) {

        $columns = $linkNew;

        if (!empty($firstRow)) {
            // construct column attributes
            $control = $this->getSubrecordColumnControl(array_keys($firstRow));

            // Subrecord: Column titles
            $columns .= '<th>' . implode('</th><th>', $control[SUBRECORD_COLUMN_TITLE]) . '</th>';
        }

        if ($flagDelete) {
            $columns .= "<th data-sorter='false' class='filter-false' data-priority='always' style='width: 40px;'>$deleteTitle</th>";
        }

        return Support::wrapTag('<thead><tr>', $columns);
    }

    /**
     * Construct a HTML table of the subrecord data.
     * Column syntax: http://docs.qfq.io/en/develop/Form.html#type-subrecord
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildSubrecord(array $formElement) {
        $subrecordSettings = array();
        // Set default flags, control, active dragAndDrop and nameColumnId in settings
        $this->setSubrecordSettings($subrecordSettings);
        $rcText = false;
//        $targetTableName = '';
        $linkNew = '';

        // Set first subrecord settings
        $this->setSubrecordSettings($subrecordSettings, SUBRECORD_PRIMARY_RECORD, $this->store->getStore(STORE_RECORD));
        $this->setSubrecordSettings($subrecordSettings, SUBRECORD_CSS_CLASS_COLUMN_ID, $this->store->getVar(SYSTEM_CSS_CLASS_COLUMN_ID, STORE_SYSTEM . STORE_EMPTY));

        if (!$this->prepareSubrecord($formElement, $rcText, $subrecordSettings[SUBRECORD_NAME_COLUMN_ID])) {
            return $rcText;
        }

        // New/Edit/Delete Link?
        if (isset($formElement[SUBRECORD_PARAMETER_FORM])) {

            Support::setIfNotSet($formElement, F_EXTRA_DELETE_FORM, '');
            $formElement[F_FINAL_DELETE_FORM] = $formElement[F_EXTRA_DELETE_FORM] != '' ? $formElement[F_EXTRA_DELETE_FORM] : $formElement[SUBRECORD_PARAMETER_FORM];

            // Decode settings in subrecordOption
            $this->setSubrecordSettings($subrecordSettings, SUBRECORD_FLAG_NEW, Support::findInSet(SUBRECORD_NEW, $formElement[FE_SUBRECORD_OPTION]) && ($formElement[FE_MODE] != FE_MODE_READONLY));
            $this->setSubrecordSettings($subrecordSettings, SUBRECORD_FLAG_EDIT, Support::findInSet(SUBRECORD_EDIT, $formElement[FE_SUBRECORD_OPTION]));
            $this->setSubrecordSettings($subrecordSettings, SUBRECORD_FLAG_DELETE, Support::findInSet(SUBRECORD_DELETE, $formElement[FE_SUBRECORD_OPTION]) && ($formElement[FE_MODE] != FE_MODE_READONLY));

            $editColumnTitle = $subrecordSettings[SUBRECORD_FLAG_NEW] ? $this->createFormLink($formElement, 0, $subrecordSettings[SUBRECORD_PRIMARY_RECORD], SYMBOL_NEW, 'New', $formElement[SUBRECORD_PARAMETER_NEW] ?? '') : '';
            $editColumnTitle .= ' ' . ($formElement[SUBRECORD_COLUMN_TITLE_EDIT] ?? '');

            if ($subrecordSettings[SUBRECORD_FLAG_NEW] || ($subrecordSettings[SUBRECORD_FLAG_EDIT] && !empty($formElement[FE_SQL1]))) {
                $linkNew = Support::wrapTag('<th data-sorter="false" class="filter-false" data-priority="always" style="width: 40px;">', $editColumnTitle);
            }
        }

        // Determine if DragAndDrop is active
        $orderColumn = $formElement[FE_ORDER_COLUMN] ?? DND_COLUMN_ORD;
        $dndTable = '';

        if (isset($formElement[FE_DND_TABLE])) {
            // Table is specified in parameter field
            $dndTable = $formElement[FE_DND_TABLE];
        } elseif (!empty($formElement[SUBRECORD_PARAMETER_FORM])) {
            // Read table from form specified in subrecord
            $formName = $formElement[SUBRECORD_PARAMETER_FORM];
            $form = $this->dbArray[$this->dbIndexQfq]->sql("SELECT * FROM `Form` AS f WHERE `f`.`" . F_NAME . "` LIKE ? AND `f`.`deleted`='no'",
                ROW_REGULAR, [$formName]);
            if (count($form) > 0) {
                $dndTable = $form[0][F_TABLE_NAME];
            }
        } else {
            throw new \UserFormException(json_encode(
                [ERROR_MESSAGE_TO_USER => 'Missing Parameter',
                    ERROR_MESSAGE_TO_DEVELOPER => FE_DND_TABLE . ' or ' . SUBRECORD_PARAMETER_FORM]),
                ERROR_MISSING_TABLE_NAME);
        }

        if ($dndTable) {
            $columns = $this->dbArray[$this->dbIndexData]->sql("SHOW COLUMNS FROM $dndTable");
            foreach ($columns as $column) {
                if ($column[COLUMN_FIELD] === $orderColumn) {
                    // DragAndDrop is active if the dndTable has the orderColumn
                    $subrecordSettings[SUBRECORD_HAS_DRAG_AND_DROP] = true;
                    break;
                }
            }
        }

        if (isset($formElement[FE_LABEL])) {
            $caption = $formElement[FE_LABEL] ?? '';
        }
        $subrecordEmpty = $this->store->getVar(SYSTEM_SUBRECORD_EMPTY, STORE_SYSTEM);
        if (isset($this->formSpec[F_SUBRECORD_EMPTY])) {
            $subrecordEmpty = $this->formSpec[F_SUBRECORD_EMPTY];
        }
        if (isset($formElement[FE_SUBRECORD_EMPTY])) {
            $subrecordEmpty = $formElement[FE_SUBRECORD_EMPTY];
        }
        $subrecordEmptyParams = explode(":", $subrecordEmpty);
        switch ($subrecordEmptyParams[0]) {
            case SUBRECORD_EMPTY_SHOW:
                if (empty($formElement[FE_SQL1]) || empty($subrecordSettings[SUBRECORD_PRIMARY_RECORD])) {
                    $noRecordText = ' - ' . Support::wrapTag("<small>", $subrecordEmptyParams[1] ?? 'Currently no records exist.');
                    $caption .= $noRecordText;
                }
                break;
            case SUBRECORD_EMPTY_HIDE:
                if ((!$subrecordSettings[SUBRECORD_FLAG_NEW] && empty($formElement[FE_SQL1])) || empty($subrecordSettings[SUBRECORD_PRIMARY_RECORD])) {
                    $caption = '';
                }
                break;
            case SUBRECORD_EMPTY_MUTE:
                if ((!$subrecordSettings[SUBRECORD_FLAG_NEW] && empty($formElement[FE_SQL1])) || empty($subrecordSettings[SUBRECORD_PRIMARY_RECORD])) {
                    $caption = Support::wrapTag('<div class="text-muted">', $caption);
                }
                break;
        }
        if ($subrecordEmptyParams[0] != SUBRECORD_EMPTY_HIDE || $subrecordSettings[SUBRECORD_FLAG_NEW] || !empty($formElement[FE_SQL1])) {
            $caption = '<caption class="qfq-subrecord-title">' . $caption . '</caption>';
        }

        $htmlHead = '';
        $htmlBody = '';
        // only the caption should be displayed when r=0
        if (!empty($subrecordSettings[SUBRECORD_PRIMARY_RECORD])) {
            // Build general html rows
            $firstRow = isset($formElement[FE_SQL1][0]) ? $formElement[FE_SQL1][0] : array();
            $htmlHead = $this->subrecordHead($linkNew, $subrecordSettings[SUBRECORD_FLAG_DELETE], ($formElement[SUBRECORD_COLUMN_TITLE_DELETE] ?? ''), $firstRow, $subrecordSettings[SUBRECORD_CONTROL]);
            $htmlBody = $this->buildSubrecordRowsHtml($formElement, $subrecordSettings);
        }

        // Build additional html rows. Because of new rows html append to the end of general rows, dragAndDrop compatibility is limited to general rows.
        if (!empty($formElement[FE_SUBRECORD_APPEND_SQL])) {
            $htmlBody .= $this->buildSubrecordRowsHtml($formElement, $subrecordSettings, true);
        }

        // Handle DragAndDrop
        $dndAttributes = '';
        if ($subrecordSettings[SUBRECORD_HAS_DRAG_AND_DROP]) {

            $orderInterval = $formElement[FE_ORDER_INTERVAL] ?? FE_ORDER_INTERVAL_DEFAULT;
            if ($orderInterval == '' || !is_numeric($orderInterval)) {
                $orderInterval = FE_ORDER_INTERVAL_DEFAULT;
            }

            $numColumns = 0;
            if (count($formElement[FE_SQL1]) > 0) {
                $numColumns = count($formElement[FE_SQL1][0]) + (int)$subrecordSettings[SUBRECORD_FLAG_DELETE] + (int)($subrecordSettings[SUBRECORD_FLAG_NEW] || $subrecordSettings[SUBRECORD_FLAG_EDIT]);
            }

            $dataDndApi = DND_SUBRECORD_ID . '=' . $formElement[FE_ID];
            $dataDndApi .= '&' . DND_SUBRECORD_FORM_ID . '=' . $this->store->getVar('id', STORE_RECORD);
            $dataDndApi .= '&' . DND_SUBRECORD_FORM_TABLE . '=' . $this->formSpec[F_TABLE_NAME];
            $dataDndApi .= '&' . FE_ORDER_INTERVAL . '=' . $orderInterval;
            $dataDndApi .= '&' . FE_ORDER_COLUMN . '=' . $orderColumn;
            $dataDndApi .= '&' . FE_DND_TABLE . '=' . $dndTable;
            $dataDndApi .= '&' . DND_DB_INDEX . '=' . $this->formSpec[F_DB_INDEX];

            $dndAttributes = Support::doAttribute('class', 'qfq-dnd-sort');
            $dndAttributes .= $this->evaluate->parse("{{ '$dataDndApi' AS _data-dnd-api }}") . ' ';
            $dndAttributes .= Support::doAttribute('data-columns', $numColumns);
        }

        $x = T3Handler::useSlugsInsteadOfPageAlias() ? $formElement[F_ID] : $this->store::getVar(TYPO3_PAGE_ALIAS, STORE_TYPO3);
        $tableSorterHtmlId = $x . '-' . $formElement[FE_ID];
        $attribute = Support::doAttribute('class', $formElement[FE_SUBRECORD_TABLE_CLASS]);
        $attribute .= Support::doAttribute('id', $tableSorterHtmlId);

        if (isset($formElement[FE_SUBRECORD_TABLE_ATTRIBUTE])) {
            $attribute .= $this->evaluate->parse($formElement[FE_SUBRECORD_TABLE_ATTRIBUTE]);
        }

        return Support::wrapTag("<table $attribute>",
            $caption . $htmlHead . Support::wrapTag("<tbody $dndAttributes>", $htmlBody), true);
    }

    /**
     * Prepare Subrecord:
     * - check if there is an SELECT statement for the subrecords.
     * - determine &$nameColumnId
     *
     * @param array $formElement
     * @param array $primaryRecord
     * @param string $rcNameColumnId
     *
     * @return bool
     * @throws \UserFormException
     */
    private function prepareSubrecord(array $formElement, &$rcText, &$rcNameColumnId) {

        if (!is_array($formElement[FE_SQL1])) {
            throw new \UserFormException('Missing \'sql1\' query', ERROR_MISSING_SQL1);
        }

        // No records?
        if (count($formElement[FE_SQL1]) == 0) {
            $rcText = '';

            return true;
        }

        // Check if $nameColumnId column exist.
        if (!isset($formElement[FE_SQL1][0][$rcNameColumnId])) {
            // no: try fallback.
            $rcNameColumnId = '_' . $rcNameColumnId;
            if (!isset($formElement[FE_SQL1][0][$rcNameColumnId])) {
                throw new \UserFormException(
                    json_encode([ERROR_MESSAGE_TO_USER => "Missing column $rcNameColumnId in subrecord query", ERROR_MESSAGE_TO_DEVELOPER => '']),
                    ERROR_SUBRECORD_MISSING_COLUMN_ID);

            }

        }

        return true;
    }

    /**
     * Build html for subrecord rows.
     * Customizable with own settings array.
     * <tr ...><td>...</td></tr><tr><td>...</td></tr>...
     *
     * @param array $formElement
     * @param array $rowSettings
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildSubrecordRowsHtml(array $formElement, array $rowSettings, $additionalRow = false): string {
        $htmlBody = '';
        $rowHtml = '';
        $subrecordToken = FE_SQL1;

        // Configure formElement array for additional rows.
        if ($additionalRow) {
            $formElement = $this->createFeSubrecordAdditionalRow($formElement);
            $subrecordToken = FE_SUBRECORD_APPEND_SQL;
        }

        foreach ($formElement[$subrecordToken] as $row) {
            $rowHtml = '';
            if ($formElement[FE_MODE] == FE_MODE_READONLY) {
                $editToolTip = 'Show';
                $editSymbol = SYMBOL_SHOW;
                $deleteRender = '|r:3';
            } else {
                $editToolTip = 'Edit';
                $editSymbol = SYMBOL_EDIT;
                $deleteRender = '';
            }

            if ($rowSettings[SUBRECORD_FLAG_EDIT]) {
                $rowHtml .= Support::wrapTag('<td style="vertical-align: middle;">',
                    $this->createFormLink($formElement, $row[$rowSettings[SUBRECORD_NAME_COLUMN_ID]], $rowSettings[SUBRECORD_PRIMARY_RECORD], $editSymbol, $editToolTip, $row[SUBRECORD_COLUMN_ROW_EDIT] ?? '', $row));
            } elseif ($rowSettings[SUBRECORD_FLAG_NEW]) {
                // Fake column
                $rowHtml .= Support::wrapTag('<td style="vertical-align: middle;">', $rowHtml, false);
            }

            // All columns
            foreach ($row as $columnName => $value) {
                if (isset($rowSettings[SUBRECORD_CONTROL][SUBRECORD_COLUMN_TITLE][$columnName])) {
                    $rowHtml .= Support::wrapTag("<td>", $this->renderCell($rowSettings[SUBRECORD_CONTROL], $columnName, $value, $rowSettings[SUBRECORD_CSS_CLASS_COLUMN_ID]));
                }
            }

            if ($rowSettings[SUBRECORD_FLAG_DELETE]) {
                // $strLink = 'x:a|b|s|U:form=' . $formName . '&r:' . $recordId;
                $strDefault = TOKEN_ACTION_DELETE . ':' . TOKEN_ACTION_DELETE_AJAX
                    . '|' . TOKEN_GLYPH . ':' . GLYPH_ICON_DELETE
                    . '|' . TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . $deleteRender
                    . '|' . TOKEN_URL_PARAM . ':form=' . $formElement[F_FINAL_DELETE_FORM] . '&r=' . $row[$rowSettings[SUBRECORD_NAME_COLUMN_ID]]
                    . '|' . TOKEN_TOOL_TIP . ':Delete';
                $rowHtml .= Support::wrapTag('<td style="vertical-align: middle;">', $this->link->renderLink($row[SUBRECORD_COLUMN_ROW_DELETE] ?? '', $strDefault));
            }

            Support::setIfNotSet($row, SUBRECORD_COLUMN_ROW_CLASS);
            $rowClass = 'record ';
            $rowClass .= $row[SUBRECORD_COLUMN_ROW_CLASS];
            $defaultAdditionalRowClass = 'qfq-subrecord-additional';

            if ($additionalRow) {
                if (isset($formElement[FE_SUBRECORD_APPEND_CLASS])) {
                    $defaultAdditionalRowClass = $formElement[FE_SUBRECORD_APPEND_CLASS];
                }
                $rowClass .= ' ' . $defaultAdditionalRowClass;
            }

            $rowTooltip = '';
            if (isset($row[SUBRECORD_COLUMN_ROW_TOOLTIP])) {
                $rowTooltip = $row[SUBRECORD_COLUMN_ROW_TOOLTIP];
            } elseif (isset($row[SUBRECORD_COLUMN_ROW_TITLE])) { // backward compatibility
                $rowTooltip = $row[SUBRECORD_COLUMN_ROW_TITLE];
            }
            $rowAttribute = Support::doAttribute('class', $rowClass);
            $rowAttribute .= Support::doAttribute('title', $rowTooltip);
            if ($rowSettings[SUBRECORD_HAS_DRAG_AND_DROP]) {
                $rowAttribute .= Support::doAttribute('id', $formElement[FE_HTML_ID] . '-' . $row[$rowSettings[SUBRECORD_NAME_COLUMN_ID]]);
                $rowAttribute .= Support::doAttribute('data-dnd-id', $row[$rowSettings[SUBRECORD_NAME_COLUMN_ID]]);
            }
            $htmlBody .= Support::wrapTag("<tr $rowAttribute>", $rowHtml, true);
        }
        if (empty($formElement[$subrecordToken])) {
            $rowHtml .= Support::wrapTag("<td>", $formElement[SUBRECORD_EMPTY_TEXT] ?? SUBRECORD_DEFAULT_EMPTY_TEXT);
            $htmlBody .= Support::wrapTag("<tr>", $rowHtml, true);
        }
        return $htmlBody;
    }

    /**
     * Create own formElement for subrecord additional rows to handle it separately.
     * Possible that additional rows have own form or delete form.
     *
     * @param array $formElement
     * @return array
     */
    private function createFeSubrecordAdditionalRow(array $formElement): array {
        $formElementAppendRows = array();
        $formElementAppendRows[FE_HTML_ID] = $formElement[FE_HTML_ID];
        $formElementAppendRows[FE_MODE] = $formElement[FE_MODE];
        $formElementAppendRows[FE_SUBRECORD_APPEND_SQL] = $formElement[FE_SUBRECORD_APPEND_SQL];

        // Using own class possible. If isset but empty, default class will be deactivated.
        if (isset($formElement[FE_SUBRECORD_APPEND_CLASS])) {
            $formElementAppendRows[FE_SUBRECORD_APPEND_CLASS] = $formElement[FE_SUBRECORD_APPEND_CLASS];
        }

        if (isset($formElement[SUBRECORD_PARAMETER_PAGE])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_PAGE] = $formElement[SUBRECORD_PARAMETER_PAGE];
        }

        if (isset($formElement[SUBRECORD_PARAMETER_DETAIL])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_DETAIL] = $formElement[SUBRECORD_PARAMETER_DETAIL];
        }

        // Using own form possible.
        if (isset($formElement[FE_SUBRECORD_APPEND_FORM])) {
            $formElementAppendRows[SUBRECORD_PARAMETER_FORM] = $formElement[FE_SUBRECORD_APPEND_FORM];
        } else {
            $formElementAppendRows[SUBRECORD_PARAMETER_FORM] = $formElement[SUBRECORD_PARAMETER_FORM];
        }

        // Using own delete form possible.
        if (isset($formElement[FE_SUBRECORD_APPEND_EXTRA_DELETE_FORM])) {
            $formElementAppendRows[F_FINAL_DELETE_FORM] = $formElement[FE_SUBRECORD_APPEND_EXTRA_DELETE_FORM];
        } else {
            $formElementAppendRows[F_FINAL_DELETE_FORM] = $formElementAppendRows[SUBRECORD_PARAMETER_FORM];
        }

        return $formElementAppendRows;
    }

    /**
     * Set subrecord settings or change values from it.
     * Useful for additional rows.
     *
     * @param array $additionalRowSettings
     * @param string $key
     * @param mixed $value
     * @return void
     */
    private function setSubrecordSettings(array &$additionalRowSettings, string $key = '', $value = '') {
        if (empty($key) && empty($value)) {
            $additionalRowSettings = array(
                SUBRECORD_NAME_COLUMN_ID => 'id',
                SUBRECORD_PRIMARY_RECORD => array(),
                SUBRECORD_FLAG_EDIT => false,
                SUBRECORD_FLAG_NEW => false,
                SUBRECORD_FLAG_DELETE => false,
                SUBRECORD_CSS_CLASS_COLUMN_ID => '',
                SUBRECORD_HAS_DRAG_AND_DROP => false,
                SUBRECORD_CONTROL => array()
            );
        } else {
            $additionalRowSettings[$key] = $value;
        }
    }

    /**
     * Renders an Link with a symbol (edit/new) and register a new SIP to grant permission to the link.
     *
     * Returns <a href="<Link>">[icon]</a>
     *
     * Link: <page>?s=<SIP>&<standard typo3 params>
     * SIP: form = $formElement['form'] (provided via formElement[FE_PARAMETER])
     *      r = $targetRecordId
     *      Parse  $formElement['detail'] with possible key/value pairs. E.g.: detail=id:gr_id,#{{a}}:p_id,#12:x_id
     *        gr_id = <<primarytable.id>>
     *        p_id = <<variable defined in SIP or Client>>
     *        x_id = 12 (constant)
     *
     * $strLink - Optional. Only used if custom parameter (e.g. in subrecord '_rowEdit', or fe.parameter.new) should be set.
     *                      Will overwrite all other settings (exception: url default/explicit params are merged).
     *
     * @param array $formElement
     * @param $targetRecordId
     * @param array $primaryRecord
     * @param $symbol
     * @param $toolTip
     * @param $strLink - Optional. Only used if custom parameter should be set. Will overwrite all other settings. Exception: url params are merged
     * @param $currentRow
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     *
     */
    private function createFormLink(array $formElement, $targetRecordId, array $primaryRecord, $symbol, $toolTip, $strLink, $currentRow = array()): string {

        $queryStringArray = [
            SIP_FORM => $formElement[SUBRECORD_PARAMETER_FORM],
            SIP_RECORD_ID => $targetRecordId,
            PARAM_DB_INDEX_DATA => $this->formSpec[F_DB_INDEX],
        ];

        // Inherit current F_MODE:
        if ($this->formSpec[F_MODE_GLOBAL] != '') {
            $queryStringArray[F_MODE_GLOBAL] = $this->formSpec[F_MODE_GLOBAL];
        }

        // In case the subrecord FE is set to 'readonly': subforms will be called with formModeGlobal=readonly
        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $queryStringArray[F_MODE_GLOBAL] = FE_MODE_READONLY;
        }

        // Add custom query parameter
        if (isset($formElement[SUBRECORD_PARAMETER_DETAIL])) {

            $detailParam = KeyValueStringParser::parse($formElement[SUBRECORD_PARAMETER_DETAIL]);

            foreach ($detailParam as $src => $dest) {
                // Constants
                if ($src[0] == '&') {
                    $queryStringArray[$dest] = substr($src, 1);
                    continue;
                }
                // Form record values or parameter
                if (isset($primaryRecord[$src])) {
                    $queryStringArray[$dest] = $primaryRecord[$src];
                    continue;
                }

                // Current row - check '$src' and  '_$src' )
                foreach (['', '_'] as $pre) {
                    if (isset($currentRow[$pre . $src])) {
                        $queryStringArray[$dest] = $currentRow[$pre . $src];
                        continue 2;
                    }
                }

                // None of the above matched: poor solution of error reporting
                $queryStringArray[$dest] = ERROR_SUBRECORD_DETAIL_COLUMN_NOT_FOUND;
            }
        }

        // Check if detail contains Typo3 reserved keywords. Especially 'id' is forbidden at this state
        if (isset($queryStringArray[CLIENT_PAGE_ID]) || isset($queryStringArray[CLIENT_PAGE_TYPE]) || isset($queryStringArray[CLIENT_PAGE_LANGUAGE])) {
            throw new \UserFormException("Reserved Typo3 keyword (id, type, L) in formElement.parameter.detail - please use something else.");
        }

        Support::appendTypo3ParameterToArray($queryStringArray);
        // If there is a specific targetpage defined, take it.
        if (isset($formElement[SUBRECORD_PARAMETER_PAGE]) && $formElement[SUBRECORD_PARAMETER_PAGE] !== '') {
            $queryStringArray[CLIENT_PAGE_ID] = $formElement[SUBRECORD_PARAMETER_PAGE];
        }

        $queryString = Support::arrayToQueryString($queryStringArray);

        // Define defaults
        $strDefault = 'p:' . $queryString . '|b|s|o:' . $toolTip . '|G:' . $symbol;

        // In case $strLink is given: copy computed urlParam to $strLink, than the App-Developer do not have to implent
        // subrecord.detail manually in subrecord.sql1
        $strLink = $this->copyDefaultUrlParamToExplicit($queryStringArray, $strLink);

        return $this->link->renderLink($strLink, $strDefault);
    }

    /**
     * If exist" get TOKEN_PAGE from $strLink, explode and merge with $queryStringArray. Return result as string.
     *
     * @param $queryStringArray
     * @param $strLink
     * @return string
     * @throws \UserFormException
     */
    private function copyDefaultUrlParamToExplicit($queryStringArray, $strLink) {

        // If it is empty, than the default will be used: nothing to do here.
        if (empty($strLink)) {
            return '';
        }

        $arrStrLink = KeyValueStringParser::parse($strLink, PARAM_KEY_VALUE_DELIMITER, PARAM_DELIMITER);
        // If it is empty, than the default will be used: nothing to do here.
        if (empty($arrStrLink[TOKEN_PAGE])) {
            return $strLink;
        }

        // Explode explicit given url params: id=123&pId=45&r=56&form=address
        $arr = KeyValueStringParser::parse($arrStrLink[TOKEN_PAGE], '=', '&');

        // Merge default and explicit given. Explicit should overwrite default
        $arrStrLink[TOKEN_PAGE] = Support::arrayToQueryString(array_merge($queryStringArray, $arr));

        // Implode $strLink again.
        return KeyValueStringParser::unparse($arrStrLink, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
    }

    /**
     * Get the name for the given form $formName. If not found, return ''.
     *
     * @param string $formName
     *
     * @return string   tableName for $formName
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    private function getFormTable($formName) {
        $row = $this->dbArray[$this->dbIndexQfq]->sql("SELECT `" . F_TABLE_NAME . "` FROM `Form` AS f WHERE `f`.`name` = ?", ROW_EXPECT_0_1, [$formName]);
        if (isset($row[F_TABLE_NAME])) {
            return $row[F_TABLE_NAME];
        }

        return '';
    }

    /**
     * Get various column format information based on the 'raw' column title. The attributes are separated by '|'
     * and specified as 'key' or 'key=value'.
     *
     * - Return all parsed values as an assoc array.
     * - For regular columns: If there is no 'width' specified, take the default 'SUBRECORD_COLUMN_WIDTH'
     * - For 'icon /  url / mailto': no width limit.
     *
     * Returned assoc array:
     *  title      Only key. Element is non numeric, which is not a keyword 'width/nostrip/icon/url/mailto'
     *  maxLength  Key/Value Pair. Not provided for 'icon/url/mailto'.
     *  nostrip    Only key. Do not strip HTML Tags from the content.
     *  icon       Only key. Value will rendered (later) as an image.
     *  url        Only key. Value will rendered (later) as a 'href'
     *  mailto     Only key. Value will rendered (later) as a 'href mailto'
     *
     *
     * @param array $titleRaw
     *
     * @return array
     * @throws \UserFormException
     */
    private function getSubrecordColumnControl(array $titleRaw) {
        $control = array();

        foreach ($titleRaw as $columnName) {

            switch ($columnName) {
                case SUBRECORD_COLUMN_ROW_CLASS:
                case SUBRECORD_COLUMN_ROW_TOOLTIP:
                case SUBRECORD_COLUMN_ROW_TITLE: // Backward compatibility
                    continue 2;
                default:
                    break;
            }

            $flagWidthLimit = true;
            $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = SUBRECORD_COLUMN_DEFAULT_MAX_LENGTH;

            // a) 'City|maxLength=40', b) 'Status|icon', c) 'Mailto@maxLength=80|nostrip'
            $arr = KeyValueStringParser::parse($columnName, '=', '|', KVP_IF_VALUE_EMPTY_COPY_KEY);
            foreach ($arr as $attribute => $value) {
                switch ($attribute) {
                    case SUBRECORD_COLUMN_MAX_LENGTH:
                    case SUBRECORD_COLUMN_NO_STRIP:
                    case SUBRECORD_COLUMN_TITLE:
                    case SUBRECORD_COLUMN_LINK:
                        break;
                    case SUBRECORD_COLUMN_ICON:
                    case SUBRECORD_COLUMN_URL:
                    case SUBRECORD_COLUMN_MAILTO:
                        $flagWidthLimit = false;
                        break;
                    default:
                        $attribute = is_numeric($value) ? SUBRECORD_COLUMN_MAX_LENGTH : SUBRECORD_COLUMN_TITLE;
                        break;
                }
                $control[$attribute][$columnName] = $value;
            }

            if (!isset($control[SUBRECORD_COLUMN_TITLE][$columnName])) {
                $control[SUBRECORD_COLUMN_TITLE][$columnName] = ''; // Fallback:  Might be wrong, but better than nothing.
            }

            // Don't render Columns starting with '_...'.
            if (substr($control[SUBRECORD_COLUMN_TITLE][$columnName], 0, 1) === '_') {
                unset($control[SUBRECORD_COLUMN_TITLE][$columnName]); // Do not render column later.
                continue;
            }

            // Limit title length
            $maxLength = $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName];
            if ($maxLength != 0) {
                $control[SUBRECORD_COLUMN_TITLE][$columnName] = mb_substr($control[SUBRECORD_COLUMN_TITLE][$columnName], 0, $maxLength);
            }
//            $control[SUBRECORD_COLUMN_TITLE][$columnName] = mb_substr($control[SUBRECORD_COLUMN_TITLE][$columnName], 0, $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName]);

            if (!$flagWidthLimit) {
                $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = false;
            }
        }

        return $control;
    }

    /**
     * Renders $value as specified in array $control
     *
     * nostrip: by default, HTML tags are removed. With this attribute, the value will be delivered as it is.
     * width: if there is a size limit - apply it.
     * icon: The cell will be rendered as an image. $value should contain the name of an image in
     * 'fileadmin/icons/'
     * mailto: The cell will be rendered as an <a> tag with the 'mailto' attribute.
     * url:  The cell will be rendered as an <a> tag. The value will be exploded by '|'. $value[0] = href, value[1]
     * = text. E.g. $value = 'www.math.uzh.ch/?id=45&v=234|Show details for Modul 123' >> <a
     * href="www.math.uzh.ch/?id=45&v=234">Show details for Modul 123</a>
     *
     * @param array $control
     * @param string $columnName
     * @param string $columnValue
     * @param string $cssClassColumnId
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function renderCell(array $control, $columnName, $columnValue, $cssClassColumnId) {

        switch ($columnName) {
            // Skip columns with special names
            case SUBRECORD_COLUMN_ROW_CLASS:
            case SUBRECORD_COLUMN_ROW_TITLE:
            case SUBRECORD_COLUMN_ROW_EDIT:
            case SUBRECORD_COLUMN_ROW_DELETE:
                return '';
            default:
                break;
        }

        $arr = explode('|', (string)$columnValue);
        if (count($arr) == 1) {
            $arr[1] = $arr[0];
        }

        if (isset($control[SUBRECORD_COLUMN_NO_STRIP][$columnName])) {
            $cell = $columnValue;
            $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] = false;
        } else {
            $cell = strip_tags((string)$columnValue);
        }

        if ($control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] !== false && $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName] != 0) {
            $cell = mb_substr($cell, 0, $control[SUBRECORD_COLUMN_MAX_LENGTH][$columnName]);
        }

        if (isset($control[SUBRECORD_COLUMN_ICON][$columnName])) {
            $cell = ($cell === '') ? '' : "<image src='" . Path::urlExt(Path::EXT_TO_PATH_ICONS, $cell) . "'>";
        }

        if (isset($control[SUBRECORD_COLUMN_MAILTO][$columnName])) {
            $cell = "<a " . Support::doAttribute('href', "mailto:$arr[0]") . ">$arr[1]</a>";
        }

        if (isset($control[SUBRECORD_COLUMN_URL][$columnName])) {
            $cell = "<a " . Support::doAttribute('href', $arr[0]) . ">$arr[1]</a>";
        }

        if (isset($control[SUBRECORD_COLUMN_LINK][$columnName])) {
            $cell = $this->link->renderLink($columnValue);
        }

        if (strcasecmp($columnName, 'id') == 0) {
            $cell = Support::wrapTag('<span class="' . $cssClassColumnId . '">', $cell, true);
        }
        return $cell;
    }

    /**
     * Create a link (incl. SIP) to delete the current record.
     *
     * @param string $formName if there is a form, specify that
     * @param int $recordId record to delete
     * @param string $mode
     *                          * mode=RETURN_URL: return complete URL
     *                          * mode=RETURN_SIP: returns only the sip
     *                          * mode=RETURN_ARRAY: returns array with url ('sipUrl') and all decoded and created
     *                          parameters.
     *
     * @return string String: "API_DIR/delete.php?sip=...."
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function createDeleteUrl($formName, $recordId, $mode = RETURN_URL) {

        $urlParam = $this->store->getNonSystemSipParam(); // especially 'periodId' from calling URL should be passed to delete.php to evaluate it later.
        Support::appendTypo3ParameterToArray($urlParam);
        $urlParam[SIP_RECORD_ID] = $recordId;
        $urlParam[SIP_FORM] = $formName;
        $urlParam[SIP_MODE_ANSWER] = MODE_JSON;
//        $urlParam[PARAM_DB_INDEX_DATA] = $this->formSpec[F_DB_INDEX];

        $queryString = Support::arrayToQueryString($urlParam);

        $sip = $this->store->getSipInstance();

        return $sip->queryStringToSip($queryString, $mode, Path::urlApi(API_DELETE_PHP));
    }

    /**
     * Build an Upload (File) Button.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildFile(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $attribute = '';
        $uploadType = $formElement[UPLOAD_TYPE] ?? UPLOAD_TYPE_DND;
        $sipDownloadKey = 'sip-' . $formElement[FE_ID];

        $this->store->appendToStore(HelperFile::pathinfo($value), STORE_VAR);

        if (empty($formElement[FE_FILE_MIME_TYPE_ACCEPT])) {
            $formElement[FE_FILE_MIME_TYPE_ACCEPT] = UPLOAD_DEFAULT_MIME_TYPE;
        }

        Support::setIfNotSet($formElement, FE_FILE_CAPTURE);
        if ($formElement[FE_FILE_CAPTURE] == FE_FILE_CAPTURE_CAMERA) {
            $formElement[FE_FILE_MIME_TYPE_ACCEPT] = 'image/*';
        }

        if ($formElement[FE_FILE_MIME_TYPE_ACCEPT] == '*' || $formElement[FE_FILE_MIME_TYPE_ACCEPT] == '*.*' || $formElement[FE_FILE_MIME_TYPE_ACCEPT] == '*/*') {
            $formElement[FE_FILE_MIME_TYPE_ACCEPT] = '';
        }

        # Build param array for uniq SIP
        $arr = array();
        $arr['fake_uniq_never_use_this'] = uniqid(); // make sure we get a new SIP. This is needed for multiple forms (same user) with r=0
        $arr[CLIENT_SIP_FOR_FORM] = $this->store->getVar(SIP_SIP, STORE_SIP);
        $arr[CLIENT_FE_NAME] = $formElement[FE_NAME];
        $arr[CLIENT_FORM] = $this->formSpec[F_NAME];
        $arr[CLIENT_RECORD_ID] = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $arr[CLIENT_PAGE_ID] = 'fake';
        $arr[EXISTING_PATH_FILE_NAME] = $value;
        $arr[FE_FILE_MIME_TYPE_ACCEPT] = $formElement[FE_FILE_MIME_TYPE_ACCEPT];
        $arr[UPLOAD_SIP_DOWNLOAD_KEY] = $sipDownloadKey;

        // Check Safari Bug #5578: in case Safari (Mac OS X or iOS) loads an 'upload element' with more than one file type, fall back to 'no preselection'.
        // Still do the file type check on the server side!
        if (strpos($formElement[FE_FILE_MIME_TYPE_ACCEPT], ',') !== false) {
            $ua = $this->store->getVar('HTTP_USER_AGENT', STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
            // Look for " Version/11.0 Mobile/15A5370a Safari/" or  " Version/9.0.2 Safari/"
            $rc = preg_match('; Version/.*Safari/;', $ua, $matches);
            // But not like " Version/4.0 Chrome/52.0.2743.98 Safari/"
            if ($rc == 1 && false === strpos($matches[0], ' Chrome/')) {
                $formElement[FE_FILE_MIME_TYPE_ACCEPT] = ''; // This only fakes the upload dialog. But the server file type check is still active due to $arr[FE_FILE_MIME_TYPE_ACCEPT]
            }
        }

        $arr[FE_FILE_MAX_FILE_SIZE] = empty($formElement[FE_FILE_MAX_FILE_SIZE]) ? $this->formSpec[FE_FILE_MAX_FILE_SIZE] : $formElement[FE_FILE_MAX_FILE_SIZE];
        $maxFileSize = Support::returnBytes($arr[FE_FILE_MAX_FILE_SIZE]);
        if ((Support::returnBytes(ini_get('post_max_size')) < $maxFileSize) ||
            (Support::returnBytes(ini_get('upload_max_filesize')) < $maxFileSize)
        ) {
            throw new \UserFormException("Configured 'maxFileSize'=" . $arr[FE_FILE_MAX_FILE_SIZE] .
                " bigger than at least of one of the php.ini setttings 'post_max_size'=" . ini_get('post_max_size') .
                " or 'upload_max_filesize'=" . ini_get('upload_max_filesize'), ERROR_MAX_FILE_SIZE_TOO_BIG);
        }
        $arr[FE_FILE_MAX_FILE_SIZE] = $maxFileSize;

        $sipUpload = $this->sip->queryStringToSip(OnArray::toString($arr), RETURN_SIP);

        $hiddenSipUpload = HelperFormElement::buildNativeHidden($htmlFormElementName, $sipUpload);

        // Below, $value and $formElement[FE_MODE]=FE_MODE_REQUIRED will be changed. JSON will be made later, therefore we will need those values unchanged
        $jsonValue = $value;
        $jsonFormElement = $formElement;

        if ($value === '' || $value === false) {
            $textDeleteClass = 'hidden';
            $uploadClass = '';
        } else {

            $textDeleteClass = '';
            $uploadClass = 'hidden';
            if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
                $formElement[FE_MODE] = FE_MODE_SHOW; // #4495 - Upload, which already has been uploaded should not marked as required
                $attribute .= Support::doAttribute(DATA_REQUIRED, FE_MODE_REQUIRED);
            }
//            $formElement[FE_MODE] = FE_MODE_HIDDEN; // #3876, CR did not understand why we need this here. Comment. If active, this element will be hide on each dynamic update.
        }

        $disabled = ($formElement[FE_MODE] == FE_MODE_READONLY) ? 'disabled' : '';

        Support::setIfNotSet($formElement, FE_FILE_BUTTON_TEXT, FE_FILE_BUTTON_TEXT_DEFAULT);

        switch ($uploadType) {
            case UPLOAD_TYPE_DND:
            case UPLOAD_TYPE_V2:

                $htmlInputFile = $this->createUploadElement($formElement, $value, $sipUpload, $sipDownloadKey, $disabled);
                break;
            case UPLOAD_TYPE_BUTTON:
            case UPLOAD_TYPE_V1:

                $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
                $attribute .= Support::doAttribute('name', $htmlFormElementName);
//        $attribute .= Support::doAttribute('class', 'form-control');
                $attribute .= Support::doAttribute('type', 'file');
                $attribute .= Support::doAttribute('title', $formElement[FE_TOOLTIP]);
                $attribute .= Support::doAttribute(FE_FILE_CAPTURE, $formElement[FE_FILE_CAPTURE], true);
                $attribute .= HelperFormElement::getAttributeList($formElement, [FE_AUTOFOCUS, FE_FILE_MIME_TYPE_ACCEPT]);
                $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
                $attribute .= Support::doAttribute('data-sip', $sipUpload);
                $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

                $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
                $attribute .= Support::doAttribute('class', $uploadClass, true);

//        $htmlInputFile = '<input ' . $attribute . '>' . HelperFormElement::getHelpBlock();

                // <input type="file"> with BS3: https://stackoverflow.com/questions/11235206/twitter-bootstrap-form-file-element-upload-button/25053973#25053973
                $attribute .= Support::doAttribute('style', "display:none;");
                $htmlInputFile = '<input ' . $attribute . '>';

                $attributeFileLabel = Support::doAttribute('for', $formElement[FE_HTML_ID]);
                $attributeFileLabel .= Support::doAttribute('class', 'btn btn-default ' . $uploadClass);

                $htmlInputFile = Support::wrapTag("<label $attributeFileLabel>", $htmlInputFile . $formElement[FE_FILE_BUTTON_TEXT]);

                // Check if a custom text right beside the trash symbol is given.
                $trashText = '';
                if (!empty($formElement[FE_FILE_TRASH_TEXT])) {
                    $trashText = ' ' . $formElement[FE_FILE_TRASH_TEXT];
                }

                if (!empty($value) && Support::isEnabled($formElement, FE_FILE_DOWNLOAD_BUTTON)) {
                    $testValue = file_exists($value);

                    // API calls don't recognize paths like '/fileadmin/protected/...'
                    if (!$testValue && isset($_GET["submit_reason"])) {
                        $value = $_SERVER["DOCUMENT_ROOT"] . '/' . $value;
                        $testValue = file_exists($value);
                    }

                    if (is_readable($value)) {
                        $link = new Link($this->sip, $this->dbIndexData);
                        $value = $link->renderLink($this->evaluate->parse($formElement[FE_FILE_DOWNLOAD_BUTTON]), 's|M:file|d|F:' . $value);
                        $jsonFormElement[FE_FILE_DOWNLOAD_BUTTON_HTML_INTERNAL] = $value;
                    } else {
                        $msg = "Already uploaded file not found.";
                        // In case debugging is off: showing download button means 'never show the real pathfilename'
                        if ($this->showDebugInfoFlag) {
                            $msg .= ' ShowDebugInfo=on >> Missing file is: ' . $value;
                        }
                        $value = $msg;
                    }
                }

                $deleteButton = '';
                $htmlFilename = Support::wrapTag("<span class='uploaded-file-name'>", $value, false);

                if (($formElement[FE_FILE_TRASH] ?? '1') == '1') {
                    $deleteButton = Support::wrapTag("<button type='button' class='btn btn-default delete-file $disabled' $disabled data-sip='$sipUpload' name='delete-$htmlFormElementName'>", $this->symbol[SYMBOL_DELETE] . $trashText);
                }
                $htmlTextDelete = Support::wrapTag("<div class='uploaded-file $textDeleteClass'>", $htmlFilename . ' ' . $deleteButton);

//        <button type="button" class="file-delete" data-sip="571d1fc9e6974"><span class="glyphicon glyphicon-trash"></span></button>
                break;

            default:
                throw new \UserFormException("Unknown " . UPLOAD_TYPE . ": '" . $formElement[UPLOAD_TYPE] . "'", ERROR_UNKNOWN_MODE);
                break;
        }

        // JSON Build
        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $jsonValue, $jsonFormElement, $wrapSetupClass); // Below, $formElement[FE_MODE]=FE_MODE_REQUIRED will be changed. Get the JSON unchanged

        $formElement = HelperFormElement::prepareExtraButton($formElement, false);

        $htmlOutputElement = $htmlInputFile . $hiddenSipUpload . $formElement[FE_TMP_EXTRA_BUTTON_HTML] . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
        if ($uploadType === UPLOAD_TYPE_BUTTON || $uploadType === UPLOAD_TYPE_V1) {
            $htmlOutputElement = $htmlTextDelete . $htmlInputFile . $hiddenSipUpload . $formElement[FE_TMP_EXTRA_BUTTON_HTML] . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
        }

        return $htmlOutputElement;
    }

    /** Build a chat box with input element
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildChat(array $formElement, $htmlFormElementName, $value, array &$json): string {
        $chatConfig = Chat::createChatConfig($formElement);
        $dbColumnNames = $chatConfig[CHAT_DB_COLUMN_NAMES];
        $doneResetParam = '&resetDone=false';
        if ($chatConfig[CHAT_PARAMETER_OPTION_TAG_DONE_RESET]) {
            $doneResetParam = '&resetDone=true';
        }

        $chatConfigJson = json_encode($chatConfig, JSON_UNESCAPED_SLASHES);
        $websocketUrl = $this->store::getVar(SYSTEM_WEBSOCKET_URL, STORE_SYSTEM) ?? '';
        $typeAheadUrl = $this->store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM) . 'typo3conf/ext/qfq/Classes/Api/typeahead.php';

        // Part 1: Build fieldset for chat
        $formElement[FE_HTML_ID] = $formElement[FE_HTML_ID] ?? '';
        $fieldsetAttribute = '';
        $fieldsetAttribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $fieldsetAttribute .= Support::doAttribute('data-chat', 1);
        $fieldsetAttribute .= Support::doAttribute('name', $htmlFormElementName);
        $fieldsetAttribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $fieldsetAttribute .= Support::doAttribute('class', $formElement[F_FE_FIELDSET_CLASS] . ' qfq-chat qfq-skip-dirty');
        $fieldsetAttribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $fieldsetAttribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE], false);

        $chatFieldsetStart = '<fieldset ' . $fieldsetAttribute . '>';

        if ($formElement[FE_LABEL] !== '' && $formElement[FE_BS_LABEL_COLUMNS] == 0) {
            $chatFieldsetStart .= '<legend>' . $formElement[FE_LABEL] . '</legend>';
        }
        $chatFieldsetEnd = '</fieldset>';


        // Part 2: Build chat chat window content
        $chatWindowAttribute = Support::doAttribute('name', $htmlFormElementName . '-chat');
        $chatWindowAttribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE] . '-chat');
        $chatWindowAttribute .= Support::doAttribute('data-chat-config', $chatConfigJson);
        $chatWindowAttribute .= Support::doAttribute('data-websocket-url', $websocketUrl);
        $chatWindowAttribute .= Support::doAttribute('data-typeahead-url', $typeAheadUrl);
        $chatWindowAttribute .= Support::doAttribute('data-load-api', $this->sip->queryStringToSip(Path::urlApi(API_LOAD_PHP) . '?' . 'mode=chat_load&chat_config=' . $chatConfigJson, RETURN_URL));
        $chatWindowAttribute .= Support::doAttribute('data-save-api', $this->sip->queryStringToSip(Path::urlApi(API_SAVE_PHP) . '?' . 'mode=chat_save&chat_config=' . $chatConfigJson . $doneResetParam, RETURN_URL));
        $chatWindowAttribute .= Support::doAttribute('data-toolbar-load-api', $this->sip->queryStringToSip(Path::urlApi(API_LOAD_PHP) . '?' . 'mode=chat_toolbar_load&chat_config=' . $chatConfigJson, RETURN_URL));
        $typeAheadParam = 'typeAheadSql=[1]SELECT ' . $dbColumnNames[CHAT_COLUMN_ID] . ',' . $dbColumnNames[CHAT_COLUMN_MESSAGE] . ' FROM ' . $chatConfig[CHAT_TABLE_NAME] . ' WHERE ' . $dbColumnNames[CHAT_COLUMN_TYPE] . ' = "' . CHAT_COLUMN_TYPE_TAG . '" AND ' . $dbColumnNames[CHAT_COLUMN_MESSAGE]
            . ' != "' . CHAT_TYPE_TAG_DONE . '" AND ' . $dbColumnNames[CHAT_COLUMN_MESSAGE] . ' LIKE ? GROUP BY ' . $dbColumnNames[CHAT_COLUMN_MESSAGE] . ' ORDER BY ' . $dbColumnNames[CHAT_COLUMN_MESSAGE] . ' LIMIT 10&typeAheadSqlPrefetch=';
        $chatWindowAttribute .= Support::doAttribute('data-typeahead-sip', $this->sip->queryStringToSip($typeAheadParam, RETURN_SIP));

        $chatHead = '<div class="qfq-chat-window" ' . $chatWindowAttribute . '><span class="fas fa-search chat-search-activate qfq-skip-dirty"></span><div class="chat-search"><input type="text" class="chat-search-input qfq-skip-dirty" placeholder="Search..."><button class="chat-search-btn qfq-skip-dirty">Search</button><span class="chat-search-info"></span></div><div class="chat-messages">';
        $chatTail = '</div></div>';

        $jsonChat = Chat::createChat($chatConfig, $this->dbArray[$this->dbIndexQfq], '', 'refresh', $this->store);

        if (empty($jsonChat[FE_TYPE_CHAT])) {
            $chatContent = '<div class="chat-no-message"><span class="label label-default">No messages found</span></div>';
        } else {
            $chatContent = '<div class="chat-loader-container" style="display: none;"><div class="chat-loader"></div></div>';
        }

        // Part 3: Build input field
        $colsRows = explode(',', $formElement[FE_SIZE], 3);
        if (empty($colsRows[1])) {
            $colsRows[1] = 1;
        }
        $textarea = '<textarea ';
        $inputClass = 'form-control chat-input-field qfq-skip-dirty';

        $textarea .= Support::doAttribute('cols', $colsRows[0]);
        $textarea .= Support::doAttribute('rows', $colsRows[1]);
        $textarea .= Support::doAttribute('id', $formElement[FE_HTML_ID] . '-chat-i');

        // Check if $colsRows[2] != 0 >> enable auto-grow (if nothing is defined: enable auto-grow)
        $maxHeight = $colsRows[2] ?? '350';
        if ($maxHeight != '0') {
            $inputClass .= ' ' . CLASS_FORM_ELEMENT_AUTO_GROW;
            $textarea .= Support::doAttribute(ATTRIBUTE_DATA_MAX_HEIGHT, $maxHeight);
        }

        $textarea .= Support::doAttribute('name', $htmlFormElementName . '-input');
        $textarea .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE] . '-input');
        $textarea .= Support::doAttribute('class', $inputClass);
        if ($formElement[FE_MAX_LENGTH] > 0) {
            $textarea .= Support::doAttribute('maxlength', $formElement[FE_MAX_LENGTH], false);
        }

        if (empty($formElement[F_FE_DATA_MATCH_ERROR])) {
            $formElement[F_FE_DATA_REQUIRED_ERROR] = F_FE_DATA_REQUIRED_ERROR_DEFAULT;
        }

        if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
            $textarea .= Support::doAttribute(F_FE_DATA_REQUIRED_ERROR, $formElement[F_FE_DATA_REQUIRED_ERROR]);
        }

        $textarea .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE], false);
        $textarea .= '>' . htmlentities($value) . '</textarea>';

        $chatSubmit = '<div class="chat-submit-button"><i class="fas fa-paper-plane"></i></div>';
        $inputContainer = '<div class="chat-input-container">' . $textarea . $chatSubmit . '</div>';

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';

        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        // Concat chat html elements and return
        return $chatFieldsetStart . $chatHead . $chatContent . $chatTail . $inputContainer . $chatFieldsetEnd;


    }

    /**
     * Create HTML upload element for filePond instance.
     *
     * @param array $formElement
     * @param string $disabled # '' not disabled, anything else disabled
     * @param string $value
     *
     */
    public function createUploadElement(array $formElement, string $value, string $sipUpload, string $sipDownloadKey, string $disabled): string {
        $defaultText = 'Drag & Drop or <span class="btn btn-default btn-small filepond--label-action"> ' . ($formElement[FE_FILE_BUTTON_TEXT] ?? '') . ' </span>';
        $projectPath = Path::absoluteApp();
        $tokenValue = '';
        // Check for upload type new or old and initialize json config for new upload type
        $jsonConfig = array();
        $preloadedFiles = '';

        $downloadButton = $formElement[FE_FILE_DOWNLOAD_BUTTON] ?? false;
        $tokens = KeyValueStringParser::parse($downloadButton, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
        $maxFileSizeInput = empty($formElement[FE_FILE_MAX_FILE_SIZE]) ? ($this->formSpec[FE_FILE_MAX_FILE_SIZE] ?? '') : $formElement[FE_FILE_MAX_FILE_SIZE];
        $maxFileSize = Support::returnBytes($maxFileSizeInput);
        $jsonConfig[UPLOAD_TOOL_TIP] = $this->evaluate->parse($tokens[TOKEN_TOOL_TIP] ?? '');

        $jsonConfig[UPLOAD_GLYPH_ICON] = $tokens[TOKEN_GLYPH] ?? '';
        $jsonConfig[UPLOAD_MIME_TYPE_ACCEPT] = $formElement[FE_FILE_MIME_TYPE_ACCEPT] ?? null;
        $jsonConfig[UPLOAD_MAX_FILE_SIZE] = $maxFileSize ?? null;
        $jsonConfig[UPLOAD_MULTI_UPLOAD] = false;
        $jsonConfig[UPLOAD_DELETE_OPTION] = false;
        if (($formElement[FE_FILE_TRASH] ?? '1') === '1' && $disabled === '') {
            $jsonConfig[UPLOAD_DELETE_OPTION] = true;
        }
        $jsonConfig[UPLOAD_IMAGE_EDITOR] = false;
        $jsonConfig[UPLOAD_ALLOW] = true;
        $jsonConfig[UPLOAD_DOWNLOAD_ALLOW] = true;
        $jsonConfig[UPLOAD_TEXT] = $defaultText;
        $jsonConfig[UPLOAD_MAX_FILES] = null;
        $jsonConfig[UPLOAD_ID] = 1;
        $jsonConfig[UPLOAD_GROUP_ID] = $groupId ?? 0;
        $jsonConfig[UPLOAD_DROP_BACKGROUND] = 'white';
        $jsonConfig[UPLOAD_DOWNLOAD_BUTTON] = $this->evaluate->parse($tokens[TOKEN_TEXT] ?? '');
        $jsonConfig[UPLOAD_TYPE_FORM] = true;
        $jsonConfig[UPLOAD_FORM_ID] = $formElement[FE_HTML_ID];
        $jsonConfig[UPLOAD_SIP_DOWNLOAD_KEY] = $sipDownloadKey;

        if (!isset($jsonConfig[UPLOAD_PATH_FILE_NAME])) {
            $jsonConfig[UPLOAD_PATH_FILE_NAME] = '';
            $jsonConfig[UPLOAD_PATH_DEFAULT] = 1;
        }

        $encodedJsonConfig = htmlspecialchars(json_encode($jsonConfig, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');
        $baseUrl = $this->store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM);

        // Check if fileDestination exists in extra Store
        $storeExtra = $this->store::getVar($sipDownloadKey, STORE_EXTRA . STORE_EMPTY);
        $sipFileDestination = $storeExtra[FE_FILE_DESTINATION] ?? '';
        if ($value !== '' || $sipFileDestination !== '') {
            $path = empty($value) ? $sipFileDestination : $value;
            $pathToCheck = $projectPath . '/' . $path;
            if (file_exists($pathToCheck)) {
                // Currently no information except path is stored for upload over form.
                $preloadedFiles = '[{"id":"1","pathFileName":"' . $projectPath . '/' . $path . '","size":"null","type":"null"}]';
                $link = new Link($this->sip, $this->dbIndexData);
                $sipDownload = $link->renderLink('', 's|M:file|d|r:8|F:' . $path);
                $this->store->setVar($sipDownloadKey, array(), STORE_EXTRA);

                // Fill extra store for downloadable upload after save
                if ($this->store::getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX) === API_SUBMIT_REASON_SAVE) {
                    $statusUpload[SIP_DOWNLOAD_PARAMETER] = 'F:' . $path;
                    $this->store->setVar($sipDownloadKey, $statusUpload, STORE_EXTRA);
                }
            }
        }

        $encodedPreloadFilesConfig = htmlspecialchars($preloadedFiles, ENT_QUOTES, 'UTF-8');

//        $apiUrls['upload'] = $baseUrl . 'typo3conf/ext/qfq/Classes/Api/file.php';
        $apiUrls['upload'] = Path::urlApi(API_FILE_PHP);
//        $apiUrls['download'] = $baseUrl . 'typo3conf/ext/qfq/Classes/Api/download.php';
        $apiUrls['download'] = Path::urlApi(API_DOWNLOAD_PHP);
        $encodedApiUrls = htmlspecialchars(json_encode($apiUrls, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');

        $sipValues['download'] = $sipDownload ?? '';
        $sipValues['delete'] = $sipUpload . '&action=delete';
        $sipValues['upload'] = $sipUpload . '&action=upload';
        $encodedSipValues = htmlspecialchars(json_encode($sipValues, JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8');

        //data-class="' . $uploadClass . '"
        return '<input class="fileupload single-upload form-upload" data-preloadedFiles="' . $encodedPreloadFilesConfig . '" data-api-urls="' . $encodedApiUrls . '" data-sips="' . $encodedSipValues . '" data-config="' . $encodedJsonConfig . '" type="file" ' . $disabled . '>';

    }

    /**
     * @param array $formElement
     * @param $htmlFormElementName
     * @param $value
     * @param array $json
     * @param string $mode
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function buildAnnotate(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        switch ($formElement[FE_ANNOTATE_TYPE] ?? FE_ANNOTATE_TYPE_GRAPHIC) {
            case FE_ANNOTATE_TYPE_GRAPHIC:
                $html = $this->buildAnnotateGraphic($formElement, $htmlFormElementName, $value, $json, $mode);
                break;

            case FE_ANNOTATE_TYPE_TEXT:
                $html = $this->buildAnnotateCode($formElement, $htmlFormElementName, $value, $json, $mode);
                break;
            default:
                throw new \UserFormException("Unknown " . FE_ANNOTATE_TYPE . ": '" . $formElement[FE_ANNOTATE_TYPE] . "'", ERROR_UNKNOWN_MODE);
        }
        return $html;
    }

    /**
     * @param array $formElement
     * @param $htmlFormElementName
     * @param $value
     * @param array $json
     * @param string $mode
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function buildAnnotateCode(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        //TODO: implement code annotation if there i s no file, but text, save in a specific column
        if (!isset($formElement[FE_TEXT_SOURCE])) {
            throw new \UserFormException("Missing parameter '" . FE_TEXT_SOURCE . "'", ERROR_IO_READ_FILE);
        }

#        if ($formElement[FE_TEXT_SOURCE] != '' && !is_readable($formElement[FE_TEXT_SOURCE])) {
#            throw new \UserFormException("Error reading file: " . $formElement[FE_TEXT_SOURCE], ERROR_IO_READ_FILE);
#        }

        if (!empty($value) && json_decode($value) === null) {
            throw new \UserFormException("Annotate data: JSON structure not valid", ERROR_BROKEN_JSON_STRUCTURE);
        }

        Support::setIfNotSet($formElement, FE_ANNOTATE_USER_UID);
        Support::setIfNotSet($formElement, FE_ANNOTATE_USER_NAME);
        Support::setIfNotSet($formElement, FE_ANNOTATE_USER_AVATAR);

        $dataHighlight = HelperFile::getFileTypeHighlight($formElement[FE_HIGHLIGHT] ?? '', $formElement[FE_TEXT_SOURCE]);


//        <div class="codeCorrection" data-uid='{"uid": 1, "name": "Reginald Commenter", "avatar": "http://www"}' data-file="../javascript/src/CodeCorrection.js" data-target="codeCorrection-output1">
//            </div>
//            <input id="codeCorrection-output1" name="correction-data" type="hidden"
//                   value='{ ... }'>

        $jsonDataUid = json_encode(['uid' => $formElement[FE_ANNOTATE_USER_UID]
            , 'name' => $formElement[FE_ANNOTATE_USER_NAME]
            , 'avatar' => $formElement[FE_ANNOTATE_USER_AVATAR]], JSON_UNESCAPED_SLASHES);

        $attributeDiv = Support::doAttribute('class', ANNOTATE_TEXT_CSS_CLASS);
//        $attributeDiv .= Support::doAttribute('data-uid', $jsonDataUid, true,ESCAPE_WITH_BACKSLASH);
        $attributeDiv .= Support::doAttribute('data-file', $this->fileToSipUrl($formElement[FE_TEXT_SOURCE]));
        $attributeDiv .= Support::doAttribute('data-target', $formElement[FE_HTML_ID]);
        $attributeDiv .= Support::doAttribute('data-highlight', $dataHighlight);
        $attributeDiv .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $attributeDiv .= Support::doAttribute('data-view-only', 'true');
        }

        $htmlAnnotate = Support::wrapTag('<div ' . $attributeDiv . ' data-uid=\'' . $jsonDataUid . '\' >', '', false);
//        $htmlAnnotate = Support::wrapTag('<div ' . $attributeDiv .'>', '', false);

        $attributeInput = Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attributeInput .= Support::doAttribute('name', $htmlFormElementName);
        $attributeInput .= Support::doAttribute('type', 'hidden');
        $attributeInput .= Support::doAttribute('value', htmlentities($value), false);
        $attributeInput .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $htmlInput = Support::wrapTag('<input ' . $attributeInput . ' >', '', false);

        $html = $htmlAnnotate . $htmlInput . HelperFormElement::getHelpBlock();

//        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement);

        return $html;
    }

    /**
     * Build a Fabric Annotate Element
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function buildAnnotateGraphic(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        if ($mode == FORM_LOAD) {
            if (!isset($formElement[FE_IMAGE_SOURCE])) {
                throw new \UserFormException("Missing parameter '" . FE_IMAGE_SOURCE . "'", ERROR_IO_READ_FILE);
            }

            if ($formElement[FE_IMAGE_SOURCE] != '' && !is_readable($formElement[FE_IMAGE_SOURCE])) {
                throw new \UserFormException("Error reading file: " . $formElement[FE_IMAGE_SOURCE], ERROR_IO_READ_FILE);
            }
        }

        if (!empty($value) && json_decode($value) === null) {
            throw new \UserFormException("Fabric data: JSON structure not valid", ERROR_BROKEN_JSON_STRUCTURE);
        }

//        $attributeImage = Support::doAttribute('id', 'qfq-fabric-image-1');
//        $attributeImage .= Support::doAttribute('src', $this->fileToSipUrl($formElement[FE_IMAGE_SOURCE]));
//        $attributeImage .= Support::doAttribute('class', 'qfq-fabric-image');
//        $htmlImage = Support::wrapTag('<img ' . $attributeImage . '>', '', false);

//        $attributeFabric = Support::doAttribute('id', 'fabric');

        $attributeFabric = Support::doAttribute('class', ANNOTATE_GRAPHIC_CSS_CLASS);
        $attributeFabric .= Support::doAttribute('data-background-image', $this->fileToSipUrl($formElement[FE_IMAGE_SOURCE]));
        $attributeFabric .= Support::doAttribute('data-control-name', $formElement[FE_HTML_ID]);
        $attributeFabric .= Support::doAttribute('data-buttons', Path::urlExt('Resources/Public/Json/fabric.buttons.json'));
        $attributeFabric .= Support::doAttribute('data-emojis', Path::urlExt('Resources/Public/Json/qfq.emoji.json'));
        $attributeFabric .= Support::doAttribute('data-fabric-color', HelperFormElement::penColorToHex($formElement));
        $attributeFabric .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $attributeFabric .= Support::doAttribute('data-view-only', 'true');
        }
        $htmlFabric = Support::wrapTag('<div ' . $attributeFabric . ' >', '', false);

        $attributeInput = Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attributeInput .= Support::doAttribute('name', $htmlFormElementName);
        $attributeInput .= Support::doAttribute('type', 'hidden');
        $attributeInput .= Support::doAttribute('value', htmlentities($value), false);
        $attributeInput .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $htmlInput = Support::wrapTag('<input ' . $attributeInput . ' >', '', false);

        $html = $htmlFabric . $htmlInput . HelperFormElement::getHelpBlock();

//        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement);

        return $html;
    }

    /**
     * Build a Fabric ImageCut Element
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function buildImageCut(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        // IF image file is given, check if it is readable.
        if ($mode == FORM_LOAD && !empty($formElement[FE_IMAGE_SOURCE])) {

            if (!is_readable($formElement[FE_IMAGE_SOURCE])) {
                return "Opps, Image can't be read.";
            }

            // KEEP_ORIGINAL=0 >> do not try to load orig file.
            if (($formElement[FE_IMAGE_KEEP_ORIGINAL] ?? '1') == '1') {
                // If there is already an orig file: take that one as source
                $origFileName = HelperFile::getQfqOrigHiddenFilename($formElement[FE_IMAGE_SOURCE]);
                if (is_readable($origFileName)) {
                    $formElement[FE_IMAGE_SOURCE] = $origFileName;
                }
            }
        }

        $imageFileName = $this->fileToSipUrl($formElement[FE_IMAGE_SOURCE]);

        // Sanatize FE_IMAGE_OUTPUT_FORMAT
        switch ($formElement[FE_IMAGE_OUTPUT_FORMAT] ?? '') {
            case '':
                $formElement[FE_IMAGE_OUTPUT_FORMAT] = 'jpeg';
                break;
            case 'jpeg':
            case 'png':
                break;
            default:
                throw new \UserFormException("Unknown output format: " . $formElement[FE_IMAGE_OUTPUT_FORMAT], ERROR_UNKNOWN_MODE);
        }

        if (($formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG] ?? '') == '') {
            $formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG] = '0.8';
        }

        // Check for valid decimal
        if (!is_numeric($formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG])
            || $formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG] < 0
            || 1 < $formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG]) {
            throw new \UserFormException("JPG quality should be 0.0 <= ? <= 1.0" . $formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG], ERROR_INVALID_DECIMAL_FORMAT);
        }

        if (($formElement[FE_IMAGE_OUTPUT_WIDTH] ?? '') == '') {
            $formElement[FE_IMAGE_OUTPUT_WIDTH] = '150';
        }
        if (($formElement[FE_IMAGE_OUTPUT_HEIGHT] ?? '') == '') {
            $formElement[FE_IMAGE_OUTPUT_HEIGHT] = '200';
        }

        // Check for valid decimal
        if (!is_numeric($formElement[FE_IMAGE_OUTPUT_WIDTH]) || !is_numeric($formElement[FE_IMAGE_OUTPUT_HEIGHT])) {
            throw new \UserFormException("Output width and height should be numeric.", ERROR_INVALID_DECIMAL_FORMAT);
        }

        if (($formElement[FE_IMAGE_DARKEN_MARGIN] ?? '') != '') {
            $result = json_decode($formElement[FE_IMAGE_DARKEN_MARGIN]);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \UserFormException(FE_IMAGE_DARKEN_MARGIN . ": invalid JSON" . $formElement[FE_IMAGE_DARKEN_MARGIN], ERROR_INVALID_VALUE);
            }
        }

        //<div class="qfq-image-adjust"
        //  data-image="https://cdn.pixabay.com/photo/2016/11/14/04/45/elephant-1822636_1280.jpg"
        //  data-base64-id="target-image-5"
        //  data-fabric-json-id="fabric-json-5"
        //  data-img-preview-id="image-5"
        //  data-output-format="jpeg"
        //  data-output-quality-jpeg="0.8"
        //  data-output-width="500"
        //  data-output-height="400"
        //  data-darken-margins='{"left": 50, "right": 50, "bottom": 80, "top": 20}'
        //  >
        //</div>

        $htmlFabricTargetImageId = $formElement[FE_HTML_ID] . HTML_ID_TARGET_IMAGE; /* data-fabric-json-id */

        $attributeFabric = Support::doAttribute('class', IMAGE_ADJUST_CSS_CLASS);
        $attributeFabric .= Support::doAttribute('data-image', $imageFileName);
        $attributeFabric .= Support::doAttribute('data-base64-id', $htmlFabricTargetImageId);
        $attributeFabric .= Support::doAttribute('data-fabric-json-id', $formElement[FE_HTML_ID]);
        if (($formElement[FE_IMAGE_PREVIEW_HTML_ID] ?? '') != '') {
            $attributeFabric .= Support::doAttribute('data-img-preview-id', $formElement[FE_IMAGE_PREVIEW_HTML_ID]);
        }
        $attributeFabric .= Support::doAttribute('data-output-format', $formElement[FE_IMAGE_OUTPUT_FORMAT]);
        if ($formElement[FE_IMAGE_OUTPUT_FORMAT] == 'jpeg') {
            $attributeFabric .= Support::doAttribute('data-output-quality-jpeg', $formElement[FE_IMAGE_OUTPUT_QUALITY_JPEG]);
        }
        $attributeFabric .= Support::doAttribute('data-output-width', $formElement[FE_IMAGE_OUTPUT_WIDTH]);
        $attributeFabric .= Support::doAttribute('data-output-height', $formElement[FE_IMAGE_OUTPUT_HEIGHT]);
        $attributeFabric .= Support::doAttribute('data-darken-margins', $formElement[FE_IMAGE_DARKEN_MARGIN] ?? '');

        $attributeFabric .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);

        $htmlFabric = Support::wrapTag('<div ' . $attributeFabric . ' >', '', false);

        // Fabric Meta Data (JSON): <input type="hidden" id="fabric-json-5" name="fabric-json">
        $attributeInput = Support::doAttribute('type', 'hidden');
        $attributeInput .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attributeInput .= Support::doAttribute('name', $htmlFormElementName);
        $valueEncode = rawUrlEncode($value);
        $attributeInput .= Support::doAttribute('value', $valueEncode, false, ESCAPE_WITH_BACKSLASH);
        $htmlInput = Support::wrapTag('<input ' . $attributeInput . ' >', '', false);
//        $htmlInput = Support::wrapTag('<textarea style="display:none;" ' . $attributeInput . '>', $value, false);

        // New image, data in base64: <input type="hidden" id="target-image-5" name="image-5">
        $attributeInput = Support::doAttribute('id', $htmlFabricTargetImageId);
        $attributeInput .= Support::doAttribute('name', HTML_NAME_TARGET_IMAGE . $htmlFormElementName);
//        $attributeInput .= Support::doAttribute('value', $value, false, ESCAPE_WITH_BACKSLASH);
        $htmlTargetImage = '<input type="hidden" ' . $attributeInput . '">';
//        $htmlTargetImage = Support::wrapTag('<textarea style="display:none;" ' . $attributeInput . '>', $value, false);

        $html = $htmlFabric . HelperFormElement::getHelpBlock() . $htmlInput . $htmlTargetImage;

        return $html;
    }

    /**
     * @param string $pathFileName
     * @return string SIP encoded URL
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function fileToSipUrl($pathFileName) {
        $param[DOWNLOAD_MODE] = DOWNLOAD_MODE_FILE;
        $param[SIP_DOWNLOAD_PARAMETER] = base64_encode(TOKEN_FILE . PARAM_TOKEN_DELIMITER . $pathFileName);

        $url = $this->sip->queryStringToSip(Path::urlApi(API_DOWNLOAD_PHP) . '?'
            . KeyValueStringParser::unparse($param, '=', '&'), RETURN_URL);

        return $url;
    }


    /**
     * Build an Editor input element: TinyMCE / CodeMirror
     *
     * @param array $formElement
     * @param $htmlFormElementName
     * @param $value
     * @param array $json
     * @param string $mode
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildEditor(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        if (empty($formElement[FE_EDITOR_TYPE])) {
            $formElement[FE_EDITOR_TYPE] = FE_EDITOR_TYPE_TINYMCE;
        }

        switch ($formElement[FE_EDITOR_TYPE]) {
            case FE_EDITOR_TYPE_TINYMCE:
                $html = $this->buildEditorTinyMCE($formElement, $htmlFormElementName, $value, $json, $mode);
                break;
            case FE_EDITOR_TYPE_CODEMIRROR:
                $html = $this->buildEditorCodeMirror($formElement, $htmlFormElementName, $value, $json, $mode);
                break;
            default:
                throw new \UserFormException("Unexpected editor type: '" . $formElement[FE_EDITOR_TYPE] . "'", ERROR_FORMELEMENT_EDITOR_TYPE);
        }

        return $html;
    }

    /**
     * Build a HTML 'textarea' element which becomes a TinyMCE Editor.
     * List of possible plugins: https://www.tinymce.com/docs/plugins/
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildEditorTinyMCE(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $attribute = '';

        if ($this->flagMultiForm) {
            $multiFormId = $this->store->getVar(F_ID, STORE_RECORD, '', $foundInStore, '', 0);
            // If the result of getVar is an empty string, use '0' instead
            $multiFormId = $multiFormId === false || $multiFormId == '' ? '0' : $multiFormId;
            $multiFormId = '-' . $multiFormId;
        } else {
            $multiFormId = '';
        }

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID] . $multiFormId);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
//        $attribute .= Support::doAttribute('id', $htmlFormElementName);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $attribute .= Support::doAttribute('class', 'qfq-tinymce');
        $attribute .= Support::doAttribute('data-control-name', "$htmlFormElementName");
        $attribute .= Support::doAttribute('data-placeholder', $formElement[FE_PLACEHOLDER]);
//        $attribute .= Support::doAttribute('data-autofocus', $formElement[FE_AUTOFOCUS]);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('data-title', $formElement[FE_TOOLTIP]);

        $formElement = $this->setEditorTinyMCEConfig($formElement, $htmlFormElementName);
        // $formElement['editor-plugins']='autoresize code'
        // $formElement['editor-contextmenu']='link image | cell row column'

        // Get standard upload filepath from user and convert to SIP
        if (isset($formElement[FE_EDITOR_FILE_UPLOAD_PATH]) && $formElement[FE_EDITOR_FILE_UPLOAD_PATH] != '') {
            $base64Param = base64_encode($formElement[FE_EDITOR_FILE_UPLOAD_PATH]);
            $urlParam = 'fileUploadPath=' . $base64Param;
            $sipParam = $this->sip->queryStringToSip($urlParam, RETURN_SIP);
            $completeUrl = '?action=imageUpload&s=' . $sipParam;
        } else {
            $completeUrl = '?action=imageUpload';
        }

        // Prepare maxLength and characterCount for tinyMce
        $maxLength = $formElement[FE_MAX_LENGTH];
        $elementCharacterCount = '';
        if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
            $attribute .= Support::doAttribute('data-character-count-id', $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);
            $attributeCC = Support::doAttribute('id', $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);
            $classCC = ($formElement[FE_CHARACTER_COUNT_WRAP] == '') ? Support::doAttribute('class', 'qfq-cc-style') : '';
            $elementCharacterCount = "<span $attributeCC $classCC></span>";

            if ($formElement[FE_CHARACTER_COUNT_WRAP] != '') {
                $arr = explode('|', $formElement[FE_CHARACTER_COUNT_WRAP], 2);
                $arr[] = '';
                $arr[] = ''; //skip check that at least 2 elements exist
                $elementCharacterCount = $arr[0] . $elementCharacterCount . $arr[1];
            }
        }

        // Get baseUrl for image upload. Path handling in T3 => V10 needs absolute path.
        $baseUrl = $this->store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM);

//TODO: static setup for TinyMCE ImagePlugin - needs to be activated / dynamically set by QFQ parameter.
        $preSettings = [
#            "plugins" => "image",
            "plugins" => "charmap",
            "maxLength" => $maxLength,
            "file_picker_types" => "file image media",
            "image_advtab" => true,
            "automatic_uploads" => true,
            "images_upload_url" => $baseUrl . Path::appToApi(API_FILE_PHP) . $completeUrl,
            "images_reuse_filename" => true,
            "paste_data_images" => true
        ];


        $json = $this->getPrefixedElementsAsJSON(FE_EDITOR_PREFIX, $formElement, $preSettings);
        $attribute .= Support::doAttribute('data-config', $json, true);


        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        $attribute .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        $html = Support::wrapTag("<textarea $attribute>", htmlentities($value), false);
        $formElement = HelperFormElement::prepareExtraButton($formElement, false);

        return $html . HelperFormElement::getHelpBlock() . $formElement[FE_TMP_EXTRA_BUTTON_HTML] . $elementCharacterCount . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
    }

    /**
     * Build a HTML 'textarea' element which becomes a CodeMirror Editor.
     * https://codemirror.net/doc/manual.html#overview
     *
     * @param array $formElement
     * @param $htmlFormElementName
     * @param $value
     * @param array $json
     * @param $mode
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function buildEditorCodeMirror(array $formElement, $htmlFormElementName, $value, array &$json, $mode) {
        $attribute = '';

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        $attribute .= Support::doAttribute('class', 'qfq-codemirror');
//        $attribute .= Support::doAttribute('data-control-name', "$htmlFormElementName");
//        $attribute .= Support::doAttribute('data-placeholder', $formElement[FE_PLACEHOLDER]);
//        $attribute .= Support::doAttribute('data-autofocus', $formElement[FE_AUTOFOCUS]);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
//        $attribute .= Support::doAttribute('data-title', $formElement[FE_TOOLTIP]);

        // Defaults: fits good to QFQ style programming
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_MODE] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_MODE] ?? CODEMIRROR_MODE_QFQ;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_LINE_NUMBERS] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_LINE_NUMBERS] ?? true;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_LINE_WRAPPING] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_LINE_WRAPPING] ?? true;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_TAB_SIZE] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_TAB_SIZE] ?? 2;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_STYLE_ACTIVE_LINE] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_STYLE_ACTIVE_LINE] ?? false;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_MATCH_BRACKETS] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_MATCH_BRACKETS] ?? true;
        $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_AUTO_CLOSE_BRACKETS] = $formElement[FE_CODEMIRROR_PREFIX . CODEMIRROR_AUTO_CLOSE_BRACKETS] ?? true;

        if ($formElement[FE_MODE] == FE_MODE_READONLY) {
            $formElement[FE_CODEMIRROR_PREFIX . FE_EDITOR_READ_ONLY] = true;
        }

        $json = $this->getPrefixedElementsAsJSON(FE_CODEMIRROR_PREFIX, $formElement);

        $attribute .= Support::doAttribute('data-config', $json, true);

        $minMax = explode(',', $formElement[FE_SIZE], 2);
        if (isset($minMax[0]) && $minMax[0] !== '') {
            $attribute .= Support::doAttribute('data-height', $minMax[1]);
            $attribute .= Support::doAttribute('data-width', $minMax[0]);
        }

        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE]);
        $attribute .= HelperFormElement::getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        $html = Support::wrapTag("<textarea $attribute>", htmlentities($value), false);
        $formElement = HelperFormElement::prepareExtraButton($formElement, false);

        return $html . HelperFormElement::getHelpBlock() . $formElement[FE_TMP_EXTRA_BUTTON_HTML] . $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
    }


    /**
     * Parse $formElement[FE_EDITOR_*] settings and build editor settings.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     *
     * @return array
     */
    private function setEditorTinyMCEConfig(array $formElement, $htmlFormElementName) {
        $flagMaxHeight = false;

        // plugins
        if (!isset($formElement[FE_EDITOR_PREFIX . 'plugins'])) {
            $formElement[FE_EDITOR_PREFIX . 'plugins'] = 'code link lists searchreplace table textcolor textpattern visualchars image,paste';
        }

        // toolbar: https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
        if (!isset($formElement[FE_EDITOR_PREFIX . 'toolbar'])) {
            $formElement[FE_EDITOR_PREFIX . 'toolbar'] = 'code searchreplace undo redo | ' .
                'styleselect link table | ' .
                'bullist numlist outdent indent | forecolor backcolor bold italic';
        }

        // menubar
        if (!isset($formElement[FE_EDITOR_PREFIX . 'menubar'])) {
            $formElement[FE_EDITOR_PREFIX . 'menubar'] = 'false';
        }

        // autofocus
        if (isset($formElement[FE_AUTOFOCUS]) && $formElement[FE_AUTOFOCUS] == 'yes') {
            $formElement[FE_EDITOR_PREFIX . 'auto_focus'] = $htmlFormElementName;
        }

        // valid elements
        // Set defaults for tinyMce
        $imgToken = 'img[longdesc|usemap|src|border|alt=|title|hspace|vspace|width|height|align]';
        $textDecoration = 'span[style]';
        $table = 'table[style|align|border],td[style],th[style],tr[style],tbody[style],thead[style]';
        $url = 'a[href|target|title]';
        $paragraphToken = 'p[align]';
        $strong = 'strong';

        $htmlAllowArray = array();

        if (isset($formElement[FE_HTML_ALLOW])) {
            $htmlAllowArray = explode(',', $formElement[FE_HTML_ALLOW]);
        }

        $formatDropdownElements = array();
        $customEditorToolbar = 'code |';

        // flags to prevent multiple same values
        $listFlag = false;
        $decorationFlag = false;
        $formatDropdownFlag = false;
        $customEditorToolbarFlags = array();

        foreach ($htmlAllowArray as $htmlToken => $value) {
            switch ($value) {
                case 'a':
                    $htmlAllowArray[$htmlToken] = $url;
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'link');
                    break;
                case 'table':
                    $htmlAllowArray[$htmlToken] = $table;
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'table');
                    break;
                case 'textDecoration':
                case 'u':
                case 'ins':
                case 's':
                case 'del':
                    if (!$decorationFlag) {
                        $htmlAllowArray[$htmlToken] = $textDecoration;
                        $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'textDecoration', 'underline strikethrough');
                        $decorationFlag = true;
                    }
                    break;
                case 'img':
                    $htmlAllowArray[$htmlToken] = $imgToken;
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'image');
                    break;
                case 'ul':
                    if (!$listFlag) {
                        $htmlAllowArray[$htmlToken] = 'ul,li';
                        $listFlag = true;
                    } else {
                        $htmlAllowArray[$htmlToken] = 'ul';
                    }
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'bullist');
                    break;
                case 'ol':
                    if (!$listFlag) {
                        $htmlAllowArray[$htmlToken] = 'ol,li';
                        $listFlag = true;
                    } else {
                        $htmlAllowArray[$htmlToken] = 'ol';
                    }
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'numlist');
                    break;
                case 'b':
                case 'strong':
                    $htmlAllowArray[$htmlToken] = $strong;
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'bold');
                    break;
                case 'i':
                case 'em':
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'italic');
                    break;
                case 'sub':
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'subscript');
                    break;
                case 'sup':
                    $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'supscript');
                    break;
                case 'h1':
                case 'h2':
                case 'h3':
                case 'h4':
                case 'h5':
                case 'h6':
                case 'p':
                case 'div':
                case 'pre':
                    if ($value === 'p') {
                        $htmlAllowArray[$htmlToken] = $paragraphToken;
                    }
                    $this->setTinymceBlockFormats($value, $formatDropdownElements);
                    if (!$formatDropdownFlag) {
                        $formatDropdownFlag = true;
                    }
                    break;
                default:
                    break;
            }
        }

        // set format dropdown at the end of the toolbar if its used
        if ($formatDropdownFlag) {
            $this->setTinymceEditorToolbarAttributes($customEditorToolbarFlags, $customEditorToolbar, 'formatselect');
        }

        // If htmlAllow is used: toolbar will be overwritten with the customized one
        if (!empty($customEditorToolbarFlags)) {
            $formElement[FE_EDITOR_PREFIX . 'toolbar'] = $customEditorToolbar;
        }

        // Set allowed values and corrected dropdown from formats
        if (isset($formElement[FE_HTML_ALLOW])) {
            $formElement[FE_EDITOR_PREFIX . 'valid_elements'] = implode(',', $htmlAllowArray);
            $formElement[FE_TINYMCE_DROPDOWN_FORMATS] = implode(';', $formatDropdownElements);
            $formElement[FE_EDITOR_PREFIX . 'block_formats'] = $formElement[FE_TINYMCE_DROPDOWN_FORMATS];
        }

        // Check for min_height, max_height
        $minMax = explode(',', $formElement[FE_SIZE], 2);
        if (isset($minMax[0]) && ctype_digit($minMax[0]) && !isset($formElement[FE_EDITOR_PREFIX . 'min_height'])) {
            $formElement[FE_EDITOR_PREFIX . 'min_height'] = $minMax[0];
        }
        if (isset($minMax[1]) && ctype_digit($minMax[1]) && !isset($formElement[FE_EDITOR_PREFIX . 'max_height'])) {
            $formElement[FE_EDITOR_PREFIX . 'max_height'] = $minMax[1];
            $flagMaxHeight = true;
        }

        // statusbar: disable if not user defined and if no max_height is given.
        //if (!$flagMaxHeight && !isset($formElement[FE_EDITOR_PREFIX . 'statusbar'])) {
        //    $formElement[FE_EDITOR_PREFIX . 'statusbar'] = 'false';
        //}

        return $formElement;
    }

    /**
     * Tinymce toolbar attributes needs to be defined separately
     *
     * @param $toolbarFlags
     * @param $attributeList
     * @param $attributeName
     * @param $specialAttributeName
     * @return void
     */
    private function setTinymceEditorToolbarAttributes(&$toolbarFlags, &$attributeList, $attributeName, $specialAttributeName = '') {
        if (!empty($toolbarFlags[$attributeName]) && !$toolbarFlags[$attributeName]) {
            if ($specialAttributeName === '') {
                $attributeList .= ' ' . $attributeName;
            } else {
                $attributeList .= ' ' . $specialAttributeName;
            }
            $toolbarFlags[$attributeName] = true;
        }
    }

    /**
     * Tinymce has individual strings for some html block configurations. They need to be set here.
     *
     * @param $format
     * @param $formatDropdownElements
     * @return void
     */
    private function setTinymceBlockFormats($format, &$formatDropdownElements) {

        switch ($format) {
            case 'h1':
                $formatDropdownElements[] = 'Heading 1=h1';
                break;
            case 'h2':
                $formatDropdownElements[] = 'Heading 2=h2';
                break;
            case 'h3':
                $formatDropdownElements[] = 'Heading 3=h3';
                break;
            case 'h4':
                $formatDropdownElements[] = 'Heading 4=h4';
                break;
            case 'h5':
                $formatDropdownElements[] = 'Heading 5=h5';
                break;
            case 'h6':
                $formatDropdownElements[] = 'Heading 6=h6';
                break;
            case 'p':
                $formatDropdownElements[] = 'Paragraph=p';
                break;
            case 'div':
                $formatDropdownElements[] = 'Div-Container=div';
                break;
            case 'pre':
                $formatDropdownElements[] = 'Preformat=pre';
                break;
            default:
                break;
        }
    }

    /**
     * Searches for '$prefix*' elements in $formElement. Collect all found elements, strip $prefix (=$keyName) and
     *   returns keys/values JSON encoded.
     * Only 'alpha' chars are allowed as keyName.
     * Empty $settings are ok.
     *
     * @param string $prefix
     * @param array $formElement
     *
     * @return string
     * @throws \UserFormException
     */
    private function getPrefixedElementsAsJSON($prefix, array $formElement, array $preSettings = array()) {
        $settings = array();
        $lengthPrefix = strlen($prefix);

        // E.g.: $key = editor-plugins
        foreach ($formElement as $key => $value) {
            if (substr($key, 0, $lengthPrefix) == $prefix) {

                $keyName = substr($key, $lengthPrefix);

                if ($keyName == '') {
                    throw new \UserFormException("Empty '" . $prefix . "*' keyname: '" . $keyName . "'", ERROR_INVALID_EDITOR_PROPERTY_NAME);
                }

                // $value might be boolean false, which should be used! Do not compare with ''.
                if (isset($value)) {
                    // real boolean are important for TinyMCE config 'statusbar', 'menubar', ...
                    if ($value === 'false') $value = false;
                    if ($value === 'true') $value = true;
                    $settings[$keyName] = $value;
                }
            }
        }

        return json_encode(array_merge($preSettings, $settings));
    }


    /**
     * Build Note.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     *
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     * @return mixed
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function buildNote(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        $attribute = Support::doAttribute('id', $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        return Support::wrapTag("<div $attribute class='" . CLASS_NOTE . "'>", $value);
    }

    /**
     * Build Pill.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     *
     * @return mixed
     */
    public function buildPill(array $formElement, $htmlFormElementName, $value, array &$json) {
        return $value;
    }

    /**
     * Build a HTML fieldset. Renders all assigned FormElements inside the fieldset.
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return mixed
     * @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 buildFieldset(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $attribute = '';

        // save parent processed FE's
        $tmpStore = $this->feSpecNative;

        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('class', [$formElement[F_FE_FIELDSET_CLASS], ($formElement[FE_MODE] == FE_MODE_HIDDEN) ? FE_MODE_HIDDEN : '']);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);
        $attribute .= HelperFormElement::getAttributeFeMode($formElement[FE_MODE], false, $formElement[FE_TYPE]);

        // <fieldset>
        $html = '<fieldset ' . $attribute . '>';

        if ($formElement[FE_LABEL] !== '') {
            $html .= '<legend>' . $formElement[FE_LABEL] . '</legend>';
        }

        $html .= $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_START];

        // child FEs
        $this->feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER,
            ['yes', $this->formSpec["id"], 'native,container', $formElement[FE_ID]], $this->formSpec);

        // child FEs set to required if fieldset is required
        if ($formElement[FE_MODE] == FE_MODE_REQUIRED) {
            foreach ($this->feSpecNative as $key => $value) {
                $this->feSpecNative[$key][FE_MODE] = FE_MODE_REQUIRED;
            }
        }

        $html .= $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
        $html .= $this->wrap[WRAP_SETUP_IN_FIELDSET][WRAP_SETUP_END];

        $html .= '</fieldset>';

        // restore parent processed FE's
        $this->feSpecNative = $tmpStore;

        $wrapSetupClass = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS] ?? '';
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement, $wrapSetupClass);

        return $html;
    }

    /**
     * @param array $formElementArr
     * @param int $tgMaxCopies
     *
     * @return array
     */
    private function fillFeSpecNativeCheckboxWithTgMax(array $formElementArr, $tgMaxCopies) {

        foreach ($formElementArr as $key => $formElement) {
            if ($formElement[FE_TYPE] == FE_TYPE_CHECKBOX) {
                $formElementArr[$key][NAME_TG_COPIES] = $tgMaxCopies;
            }
        }

        return $formElementArr;
    }

    /**
     * Build a 'templateGroup'. Renders all assigned FormElements of the templateGroup.
     * If there are already vlaues for the formElements, fill as much copies as values exist
     *
     * @param array $formElement
     * @param string $htmlFormElementName
     * @param string $value
     * @param array $json
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
     *
     * @return mixed
     * @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 buildTemplateGroup(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
        $attribute = '';
        $html = '';

        $addClass = Support::setIfNotSet($formElement, FE_TEMPLATE_GROUP_ADD_CLASS, 'btn btn-default');
        $addText = Support::setIfNotSet($formElement, FE_TEMPLATE_GROUP_ADD_TEXT, 'Add');
        $removeClass = Support::setIfNotSet($formElement, FE_TEMPLATE_GROUP_REMOVE_CLASS, 'btn btn-default');
        $removeText = Support::setIfNotSet($formElement, FE_TEMPLATE_GROUP_REMOVE_TEXT, 'Remove');
        $classCustom = Support::setIfNotSet($formElement, FE_TEMPLATE_GROUP_CLASS);

        $disabled = ($formElement[FE_MODE] == FE_MODE_READONLY) ? "disabled='disabled'" : '';

        // save parent processed FE's
        $feSpecNativeSave = $this->feSpecNative;

        $addButtonId = 'add_button_' . $formElement[FE_ID];
        $qfqFieldsName = 'qfq_fields_' . $formElement[FE_ID]; // ='qfq-fields'
        $templateName = 'template_' . $formElement[FE_ID]; // ='template'
        $targetName = 'target_' . $formElement[FE_ID]; // ='template'
        Support::setIfNotSet($formElement, FE_MAX_LENGTH, '5', '');
        $max = $formElement[FE_MAX_LENGTH];

        $codeJs = <<<EOT
<script type="text/javascript">
    $(function () {
        $(".$qfqFieldsName").each(
            function () {
                QfqNS.initializeFields(this);
            }
        );
    });
</script>
EOT;

        $htmlAdd = <<<EOT
<button type="button" id="$addButtonId" class="$addClass" $disabled onclick="QfqNS.addFields('#$templateName', '#$targetName', $max)">$addText</button>
EOT;

        $htmlDelete = <<<EOT
<div class="qfq-note-no-padding">
    <button type="button"  class="$removeClass" $disabled onclick="QfqNS.removeFields(this)">$removeText</button>
</div>
EOT;

        // child FE's
        $this->feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER,
            ['yes', $this->formSpec[F_ID], 'native,container', $formElement[FE_ID]], $this->formSpec);

        // Count defined FormElements in the current templateGroup
        $elementsTotal = count($this->feSpecNative);
        if ($elementsTotal < 1) {
            // Nothing to do: return.
            $this->feSpecNative = $feSpecNativeSave;

            return '';
        }

        $this->feSpecNative = $this->fillFeSpecNativeCheckboxWithTgMax($this->feSpecNative, $max);

        // If there are already elements filled, take them.
        $html = $this->templateGroupCollectFilledElements($max, $htmlDelete, $json);

        $attribute = Support::doAttribute('class', $qfqFieldsName);
        $attribute .= Support::doAttribute('id', $targetName);
        $attribute .= Support::doAttribute('data-qfq-line-template', '#' . $templateName);
        $attribute .= Support::doAttribute('data-qfq-line-add-button', '#' . $addButtonId);
        $attribute .= Support::doAttribute(ATTRIBUTE_DATA_REFERENCE, $formElement[FE_DATA_REFERENCE]);

        // Element where the effective FormElements will be copied to. The BS 'col-md-* Classes are inside the template, not here. This here should be pure data without wrapping.
        $html = Support::wrapTag("<div $attribute>", $html, false);

        // Add button, row below: The label & note of the FormElement 'templateGroup' will be used for the add button row.
        $tmpFe = $formElement;
        $tmpFe[FE_NAME] = '_add';
        $tmpFe[FE_LABEL] = '';
        $tmpFe[FE_NOTE] = '';
        $this->fillWrapLabelInputNote($tmpFe[FE_BS_LABEL_COLUMNS], $tmpFe[FE_BS_INPUT_COLUMNS], $tmpFe[FE_BS_NOTE_COLUMNS]);
        $html .= $this->buildRowNative($tmpFe, $htmlAdd, $htmlFormElementName);

        $html = $this->wrap[WRAP_SETUP_IN_TEMPLATE_GROUP][WRAP_SETUP_START] . $html . $this->wrap[WRAP_SETUP_IN_TEMPLATE_GROUP][WRAP_SETUP_END];

        // Append 'delete' Button after 'note' of the templateGroups last element.
        $this->feSpecNative[$elementsTotal - 1][FE_NOTE] .= $htmlDelete;

        // Get FE natives
        $template = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE, 0, $json);

        // Wrap the main html code
        $attribute = Support::doAttribute('class', ['qfq-line', $classCustom]);
        $template = Support::wrapTag("<div $attribute>", $template);
        $template = Support::wrapTag('<script id="' . $templateName . '" type="text/' . $templateName . '">', $template);

        $html .= $template . $codeJs;

        // restore parent processed FE's
        $this->feSpecNative = $feSpecNativeSave;

        //TODO: nicht klar ob das hier noetig ist.
//        $json = $this->getJsonElementUpdate($htmlFormElementName, $value, $formElement[FE_MODE]);

        return $html;

    }

    /**
     * Build real FormElements based on the current templateGroup FormElements in $this->feSpecNative.
     * Take a templateGroup 'copy', if at least one of the elements is filled with a non default value.
     * Stop filling elements if there are no further elements filled.
     *
     * @param int $max
     * @param string $htmlDelete
     * @param array $json
     *
     * @return string
     * @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
     */
    private function templateGroupCollectFilledElements($max, $htmlDelete, array &$json) {

        $record = $this->store->getStore(STORE_RECORD); // current values
        if ($record === false || count($record) === 0) {
            return '';
        }
        $default = $this->store->getStore(STORE_TABLE_DEFAULT); // current defaults

        // evaluate FE_VALUE on all templateGroup FormElements: After this call, in case of non-primary FEs, FE_VALUE
        // might contain an array of values for all copies of the current FE.
        $maxForeignRecords = $this->templateGroupDoValue();

        $lastFilled = 0; // Marker if there is at least one element per copy who is filled.
        $feSpecNativeCopy = array();
        for ($ii = 1; $ii <= $max; $ii++) {

            // Per copy, iterate over all templateGroup FormElements.
            foreach ($this->feSpecNative as $fe) {

                $fe[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_NAME]);
                $fe[FE_LABEL] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_LABEL]);
                $fe[FE_NOTE] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $fe[FE_NOTE]);
                $fe[FE_TG_INDEX] = $ii;
                $columnName = $fe[FE_NAME];

                // Column of primary table?
                if (isset($record[$columnName])) {
                    if ($record[$columnName] != $default[$columnName]) {
                        $lastFilled = max($ii, $lastFilled);
                    }

                } else {
                    $lastFilled = max($maxForeignRecords, $lastFilled);
                    if (is_array($fe[FE_VALUE]) && isset($fe[FE_VALUE][$ii - 1])) {
                        $fe[FE_VALUE] = current($fe[FE_VALUE][$ii - 1]); // replace array with current value
                    }
//                    $fe[FE_TEMPLATE_GROUP_CURRENT_IDX] = $ii;
                }

                $feSpecNativeCopy[$ii - 1][] = $fe; // Build array with current copy of templateGroup.
            }

            // Append $htmlDelete on the last element of all copies, but not the first.
            if ($ii > 1) {
                // Count defined FormElements in the current templateGroup
                $last = count($feSpecNativeCopy[$ii - 1]) - 1;
                // Append 'delete' Button at the note of the last element
                $feSpecNativeCopy[$ii - 1][$last][FE_NOTE] .= $htmlDelete;
            }
        }

        // Nothing found: return
        if (count($feSpecNativeCopy) == 0) {
            return '';
        }

        $feSpecNativeSave = $this->feSpecNative;

        $lastFilled = min($lastFilled, $max); // It's possible (external records) that there are more fetched values than the maximum allows - skip those.
        $html = '';
        for ($ii = 0; $ii < $lastFilled; $ii++) {
            $this->feSpecNative = $feSpecNativeCopy[$ii];
            $htmlCopy = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE, 0, $json);
            $htmlCopy = Support::wrapTag('<div class="qfq-line">', $htmlCopy);
            $html .= $htmlCopy;
        }

        $this->feSpecNative = $feSpecNativeSave;

        return $html;
    }

    /**
     * Evaluate for all FormElements of the current templateGroup the field FE_VALUE.
     * If the specific FormElement is not a real column of the primary table, than the value is probably a
     * '{{!SELECT ...' statement, that one will be fired. In case of an non-primary FE, the result array are the
     * values for the copies of the specific FE.
     *
     * @return int   Count of records in FormElement[FE_VALUE] over all FormElements.
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function templateGroupDoValue() {

        // Fire 'value' statement
        $tgMax = 0;
        foreach ($this->feSpecNative as $key => $fe) {

            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);

            $this->feSpecNative[$key][FE_VALUE] = $this->evaluate->parse($fe[FE_VALUE]);

            if (is_array($this->feSpecNative[$key][FE_VALUE])) {
                $cnt = count($this->feSpecNative[$key][FE_VALUE]);

                $tgMax = max($cnt, $tgMax);
            }
        }

        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);
        $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, 0, STORE_SYSTEM);

        return $tgMax;
    }

    /**
     * @param array $formElement
     * @param $htmlElement
     * @param $htmlFormElementName
     * @return mixed
     */
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementName);

}
