<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/26/16
 * Time: 11:25 AM
 */

namespace IMATHUZH\Qfq\Core\Helper;

use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Form\FormElement\AbstractFormElement;
use IMATHUZH\Qfq\Core\Store\Store;

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

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

    /**
     * Array containing all FE / F parameter and rules that they should follow.
     * Rule options
     * 1. NO_BRACE => The parameter value should not be wrapped by ether '{{! }}' or '{{ }}'
     * 2. DOUBLE_OPEN_BRACE => The parameter value must be wrapped by '{{ }}'
     * 3. DOUBLE_OPEN_BRACE_WITH_EXCLAMATION => The parameter value must be wrapped by '{{! }}'
     * @see Documentation-develop/NEW_PARAMETER.md
     * @var array
     */
    private static array $FORM_RULES = [
        FE_DATE_FORMAT_SAVE=> [NO_BRACE],
        FE_MODE_SQL => [DOUBLE_OPEN_BRACE],
        FE_SQL1 => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION, DOUBLE_OPEN_BRACE_WITH_SQUARE_BRACKET],
        FE_ACCEPT_ZERO_AS_REQUIRED => [NO_BRACE],
        FE_DATEPICKER_MONTH_YEAR => [NO_BRACE],
        FE_DATEPICKER_YEAR => [NO_BRACE],
        FE_DATEPICKER_MONTH => [NO_BRACE],
        FE_AUTOFOCUS => [NO_BRACE],
        FE_FILE_MIME_TYPE_ACCEPT => [NO_BRACE],
        FE_FILE_MAX_FILE_SIZE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_BUTTON_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_DESTINATION => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_TRASH => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_TRASH_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_REPLACE_MODE => [NO_BRACE],
        FE_FILE_NOTE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_NOTE_TARGET => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_AUTO_ORIENT => [NO_BRACE],
        FE_FILE_AUTO_ORIENT_CMD => [NO_BRACE],
        FE_DATE_TIME_PICKER_TYPE => [NO_BRACE],
        FE_LABEL_2 => [DOUBLE_OPEN_BRACE, NO_BRACE],
        CHAT_COLUMN_PID_CREATOR => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_COLUMN_USERNAME => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_COLUMN_CID_TOPIC => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_COLUMN_XID => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_COLUMN_PID_TEAM => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_COLUMN_X_GRID_STATUS => [DOUBLE_OPEN_BRACE, NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        CHAT_TABLE_NAME => [NO_BRACE],
        CHAT_COLUMN_MAP => [NO_BRACE],
        CHAT_PARAMETER_OPTION_TAG_DONE => [NO_BRACE],
        CHAT_PARAMETER_OPTION_TAG => [NO_BRACE],
        CHAT_PARAMETER_OPTION_REMINDER => [NO_BRACE],
        CHAT_PARAMETER_OPTION_TAG_DONE_RESET => [NO_BRACE],
        CHAT_PARAMETER_THREAD => [NO_BRACE],
        FE_IMAGE_OUTPUT_FORMAT => [NO_BRACE],
        FE_IMAGE_OUTPUT_QUALITY_JPEG => [NO_BRACE],
        FE_IMAGE_OUTPUT_WIDTH => [NO_BRACE],
        FE_IMAGE_OUTPUT_HEIGHT => [NO_BRACE],
        FE_IMAGE_DARKEN_MARGIN => [NO_BRACE],
        FE_IMAGE_PREVIEW_HTML_ID => [NO_BRACE],
        FE_FILE_AUTO_ORIENT_MIME_TYPE => [NO_BRACE],
        FE_FILE_CHMOD_FILE => [NO_BRACE],
        FE_FILE_CHMOD_DIR => [NO_BRACE],
        FE_SLAVE_ID => [DOUBLE_OPEN_BRACE, NO_BRACE],
        FE_SQL_BEFORE => [DOUBLE_OPEN_BRACE],
        FE_SQL_UPDATE => [DOUBLE_OPEN_BRACE],
        FE_SQL_INSERT => [DOUBLE_OPEN_BRACE],
        FE_SQL_DELETE => [DOUBLE_OPEN_BRACE],
        FE_SQL_AFTER => [DOUBLE_OPEN_BRACE],
        FE_IMPORT_TO_TABLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_IMPORT_TO_COLUMNS => [NO_BRACE],
        FE_IMPORT_FROM_COLUMNS => [NO_BRACE],
        FE_IMPORT_REGION => [NO_BRACE],
        FE_IMPORT_MODE => [NO_BRACE],
        FE_IMPORT_TYPE => [NO_BRACE],
        FE_IMPORT_NAMED_SHEETS_ONLY => [NO_BRACE],
        FE_IMPORT_READ_DATA_ONLY => [NO_BRACE],
        FE_IMPORT_LIST_SHEET_NAMES => [NO_BRACE],
        FE_CHECKBOX_MODE => [NO_BRACE],
        FE_ITEM_LIST => [NO_BRACE],
        FE_EMPTY_ITEM_AT_START => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_EMPTY_ITEM_AT_END => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_BUTTON_CLASS => [NO_BRACE],
        FE_DATE_FORMAT => [NO_BRACE],
        F_FE_DATA_PATTERN_ERROR => [NO_BRACE],
        F_FE_DATA_REQUIRED_ERROR => [NO_BRACE],
        F_FE_DATA_MATCH_ERROR => [NO_BRACE],
        F_FE_DATA_ERROR => [NO_BRACE],
        FE_DECIMAL_FORMAT => [NO_BRACE],
        FE_HTML_AFTER => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_HTML_BEFORE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_INPUT_EXTRA_BUTTON_LOCK => [NO_BRACE],
        FE_INPUT_EXTRA_BUTTON_PASSWORD => [NO_BRACE],
        FE_INPUT_EXTRA_BUTTON_INFO => [NO_BRACE],
        SYSTEM_EXTRA_BUTTON_INFO_CLASS => [NO_BRACE],
        FE_EDITOR_DEFAULT_IMAGE_CLASS => [NO_BRACE],
        FE_EDITOR_PLUGINS => [NO_BRACE],
        FE_EDITOR_TOOLBAR => [NO_BRACE],
        FE_EDITOR_STATUSBAR => [NO_BRACE],
        FE_EDITOR_FORCED_ROOT_BLOCK => [NO_BRACE],
        FE_EDITOR_EXTENDED_VALID_ELEMENTS => [NO_BRACE],
        FE_EDITOR_CONTENTS_CSS => [NO_BRACE],
        FE_EDITOR_RELATIVE_URLS => [NO_BRACE],
        FE_CODEMIRROR_MODE => [NO_BRACE],
        FE_CODEMIRROR_LINE_NUMBERS => [NO_BRACE],
        FE_CODEMIRROR_LINE_WRAPPING => [NO_BRACE],
        FE_CODEMIRROR_TAB_SIZE => [NO_BRACE],
        FE_CODEMIRROR_STYLE_ACTIVE_LINE => [NO_BRACE],
        FE_CODEMIRROR_MATCH_BRACKETS => [NO_BRACE],
        FE_CODEMIRROR_AUTO_CLOSE_BRACKETS => [NO_BRACE],
        FE_CODEMIRROR_KEYWORDS_QFQ_BASE => [NO_BRACE],
        FE_EDITOR_TYPE => [NO_BRACE],
        FE_FILL_STORE_VAR => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        SUBRECORD_PARAMETER_FORM => [NO_BRACE, DOUBLE_OPEN_BRACE],
        SUBRECORD_PARAMETER_PAGE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_EXTRA_DELETE_FORM => [NO_BRACE],
        SUBRECORD_PARAMETER_DETAIL => [NO_BRACE, DOUBLE_OPEN_BRACE],
        SUBRECORD_PARAMETER_NEW => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SUBRECORD_TABLE_CLASS => [NO_BRACE],
        FE_SUBRECORD_TABLE_ATTRIBUTE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        SUBRECORD_COLUMN_TH_CLASS => [NO_BRACE],
        SUBRECORD_COLUMN_TITLE_EDIT => [NO_BRACE],
        SUBRECORD_COLUMN_TITLE_DELETE => [NO_BRACE],
        FE_SUBRECORD_APPEND_SQL => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_SUBRECORD_APPEND_CLASS => [NO_BRACE],
        FE_SUBRECORD_APPEND_FORM => [NO_BRACE],
        FE_SUBRECORD_APPEND_EXTRA_DELETE_FORM => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_DRAG_AND_DROP_ORDER_SQL => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_ORDER_COLUMN => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ORDER_INTERVAL => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_DND_TABLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_MIN => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_MAX => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_PROCESS_READ_ONLY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_RETYPE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_RETYPE_LABEL => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_RETYPE_NOTE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_CHARACTER_COUNT_WRAP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_HIDE_ZERO => [NO_BRACE],
        FE_INPUT_TYPE => [NO_BRACE],
        FE_STEP => [NO_BRACE],
        FE_TEXTAREA_RESIZE => [NO_BRACE],
        FE_HTML_ALLOW => [NO_BRACE],
        FE_EMPTY_MEANS_NULL => [NO_BRACE],
        FE_SHOW_SECONDS => [NO_BRACE],
        FE_SHOW_ZERO => [NO_BRACE],
        FE_TIME_IS_OPTIONAL => [NO_BRACE],
        FE_TYPEAHEAD_LIMIT => [NO_BRACE],
        FE_TYPEAHEAD_INITIAL_SUGGESTION => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_TYPEAHEAD_MINLENGTH => [NO_BRACE],
        FE_TYPEAHEAD_SQL => [NO_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_TYPEAHEAD_SQL_PREFETCH => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_AI_ASSIST => [NO_BRACE],
        FE_AI_PROMPT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_AI_CONTEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_AI_MODEL => [NO_BRACE],
        FE_AI_TARGET_LANGUAGE => [NO_BRACE],
        FE_AI_RAG_SOURCE => [NO_BRACE],
        FE_AI_RAG_PROJECT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_AI_RAG_ACL => [NO_BRACE],
        FE_AI_RAG_K => [NO_BRACE],
        FE_AI_CHAT_ALLOW_GENERAL => [NO_BRACE],
        FE_AI_THINKING => [NO_BRACE],
        FE_AI_CHAT_CLASS => [NO_BRACE],
        FE_TYPEAHEAD_PEDANTIC => [NO_BRACE],
        FE_TYPEAHEAD_TAG => [NO_BRACE],
        FE_TYPEAHEAD_GLUE_INSERT => [DOUBLE_OPEN_BRACE],
        FE_TYPEAHEAD_GLUE_DELETE => [DOUBLE_OPEN_BRACE],
        FE_TYPEAHEAD_GLUE_UPDATE => [DOUBLE_OPEN_BRACE],
        FE_TYPEAHEAD_TAG_INSERT => [DOUBLE_OPEN_BRACE],
        FE_TYPEAHEAD_TAG_DELIMITER => [NO_BRACE],
        FE_WRAP_ROW => [NO_BRACE],
        FE_WRAP_LABEL => [NO_BRACE],
        FE_WRAP_INPUT => [NO_BRACE],
        FE_WRAP_NOTE => [NO_BRACE],
        FE_TRIM => [NO_BRACE],
        UPLOAD_TYPE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        UPLOAD_ID => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SQL_VALIDATE => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_EXPECT_RECORDS => [NO_BRACE],
        FE_MESSAGE_FAIL => [NO_BRACE],
        FE_ALERT => [NO_BRACE],
        FE_QFQ_LOG => [NO_BRACE],
        FE_REQUIRED_LIST => [NO_BRACE],
        FE_DATA_REFERENCE => [NO_BRACE],
        F_FE_REQUIRED_POSITION => [NO_BRACE],
        FE_INDICATE_REQUIRED => [NO_BRACE],
        F_FE_MIN_WIDTH => [NO_BRACE],
        F_FE_INPUT_CLEAR_ME => [NO_BRACE],
        FE_DEFAULT_VALUE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SQL_HONOR_FORM_ELEMENTS => [NO_BRACE],
        FE_SAVE_FORM_JSON => [NO_BRACE],
        FE_SAVE_FORM_JSON_NAME => [NO_BRACE],
        FE_SENDMAIL_TO => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_CC => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_BCC => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_FROM => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_SUBJECT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_REPLY_TO => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_ATTACHMENT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_FLAG_AUTO_SUBMIT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_GR_ID => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_X_ID => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_X_ID2 => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_X_ID3 => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SENDMAIL_BODY_MODE => [NO_BRACE],
        FE_SENDMAIL_SUBJECT_HTML_ENTITY => [NO_BRACE],
        FE_SENDMAIL_BODY_HTML_ENTITY => [NO_BRACE],
        FE_RECORD_SOURCE_TABLE => [NO_BRACE],
        FE_RECORD_DESTINATION_TABLE => [NO_BRACE],
        FE_TRANSLATE_ID_COLUMN => [NO_BRACE],
        F_FE_FIELDSET_CLASS => [NO_BRACE],
        F_MAX_VISIBLE_PILL => [NO_BRACE],
        F_UPLOAD_IDLE_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_UPLOAD_SUCCESS_MESSAGE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        OWNER_ID_KEY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FIELD_TYPE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FIELD_CITEKEY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FIELD_TARGETTABLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FILE_TYPE_KEY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        TABLE_PUBLICATION => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FIELD_DATE_MODE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MESSAGE => [NO_BRACE],
        FE_TEMPLATE_GROUP_ADD_CLASS => [NO_BRACE],
        FE_TEMPLATE_GROUP_REMOVE_CLASS => [NO_BRACE],
        FE_TEMPLATE_GROUP_REMOVE_TEXT => [NO_BRACE],
        FE_TEMPLATE_GROUP_CLASS => [NO_BRACE],
        FE_TEMPLATE_GROUP_ADD_TEXT => [NO_BRACE],
        FE_IMAGE_SOURCE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ANNOTATE_TYPE => [NO_BRACE],
        FE_DEFAULT_PEN_COLOR => [NO_BRACE],
        FE_TEXT_SOURCE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ANNOTATE_USER_NAME => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ANNOTATE_USER_UID => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ANNOTATE_USER_AVATAR => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_HIGHLIGHT => [NO_BRACE],
        FE_IMAGE_KEEP_ORIGINAL => [NO_BRACE],
        FE_DATALIST => [NO_BRACE],
        SUBRECORD_EMPTY_TEXT => [NO_BRACE],
        FE_FILE_UNZIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_DESTINATION_SPLIT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_SPLIT_OPTIONS => [NO_BRACE],
        FE_FILE_SPLIT => [NO_BRACE],
        F_CLASS_BODY => [NO_BRACE],
        F_DB_INDEX => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_TYPEAHEAD_LDAP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_LDAP_SERVER => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_TYPEAHEAD_LDAP_SEARCH => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_TYPEAHEAD_LDAP_VALUE_PRINTF => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_TYPEAHEAD_LDAP_ID_PRINTF => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_TYPEAHEAD_LDAP_SEARCH_PREFETCH => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_LDAP_USE_BIND_CREDENTIALS => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_LDAP_ATTRIBUTES => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_LDAP_SEARCH => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_COLOR_PICKER_CONFIG => [NO_BRACE],
        FE_FILE_SPLIT_TABLE_NAME => [NO_BRACE, DOUBLE_OPEN_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_FILE_DOWNLOAD_BUTTON => [NO_BRACE, DOUBLE_OPEN_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_FILL_STORE_LDAP => [NO_BRACE],
        F_LDAP_BASE_DN => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MULTI_MODE => [NO_BRACE],
        F_MULTIFORM_DELETE_ROW => [NO_BRACE],
        F_PROCESS_ROW => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_BTN_TOP_LEFT => [NO_BRACE],
        F_BTN_TOP_RIGHT => [NO_BRACE],
        F_BTN_TOP_WRAP => [NO_BRACE],
        FE_INPUT_TOGGLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_BTN_TOP_LEFT_WRAP => [NO_BRACE],
        F_BTN_TOP_RIGHT_WRAP => [NO_BRACE],
        F_BTN_TOP_LEFT_ORDER => [NO_BRACE],
        F_BTN_TOP_RIGHT_ORDER => [NO_BRACE],
        F_BTN_FOOTER_WRAP => [NO_BRACE],
        F_BTN_FOOTER_LEFT_WRAP => [NO_BRACE],
        F_BTN_FOOTER_LEFT_ORDER => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_BTN_FOOTER_RIGHT_ORDER => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_BS_COLUMNS => [NO_BRACE],
        FE_CLASS => [NO_BRACE],
        F_UPLOAD_PDF_PROTECTED_ACTION => [NO_BRACE],
        F_CLASS_TITLE => [NO_BRACE],
        F_CLASS_PILL => [NO_BRACE],
        F_BUTTON_ON_CHANGE_CLASS => [NO_BRACE],
        F_ACTIVATE_FIRST_REQUIRED_TAB => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_ENTER_AS_SUBMIT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SUBMIT_BUTTON_TEXT => [NO_BRACE],
        F_SAVE_BUTTON_ACTIVE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SAVE_BUTTON_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SAVE_BUTTON_TOOLTIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SAVE_BUTTON_CLASS => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SAVE_BUTTON_GLYPH_ICON => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SUBMIT_BUTTON_CLASS => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SUBMIT_BUTTON_GLYPH_ICON => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SUBMIT_BUTTON_TOOLTIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_CLOSE_BUTTON_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_CLOSE_BUTTON_TOOLTIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_CLOSE_BUTTON_CLASS => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_CLOSE_BUTTON_GLYPH_ICON => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_DELETE_BUTTON_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_DELETE_BUTTON_TOOLTIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_DELETE_BUTTON_GLYPH_ICON => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_NEW_BUTTON_TEXT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_NEW_BUTTON_TOOLTIP => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_NEW_BUTTON_CLASS => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_NEW_BUTTON_GLYPH_ICON => [NO_BRACE, DOUBLE_OPEN_BRACE],
        SYSTEM_SHOW_ID_IN_FORM_TITLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_FORM_SUBMIT_LOG_MODE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SESSION_TIMEOUT_SECONDS => [NO_BRACE],
        F_REMEMBER_LAST_PILL => [NO_BRACE],
        F_DO_NOT_LOG_COLUMN => [NO_BRACE],
        F_SHOW_HISTORY => [NO_BRACE],
        F_SHOW_HISTORY_TITLE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_SHOW_HISTORY_FE_USER_SET => [NO_BRACE, DOUBLE_OPEN_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        F_BTN_PREVIOUS_NEXT_SQL => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        F_BTN_PREVIOUS_NEXT_LOOP => [NO_BRACE],
        F_BTN_PREVIOUS_NEXT_WRAP => [NO_BRACE],
        FE_SUBRECORD_EMPTY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MULTI_SQL => [DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        F_MULTIFORM_ADD_ROW => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_FE_MAX_IMAGE_DIMENSION => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MULTI_WRAP => [NO_BRACE],
        FE_MODE => [NO_BRACE],
        F_MULTI_TABLE_CLASS => [NO_BRACE],
        SYSTEM_DELETE_BUTTON_CLASS => [NO_BRACE],
        F_RECENT_LOG => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MULTI_CELL_WRAP => [NO_BRACE],
        FE_FIELDSET_CSS => [NO_BRACE],
        FE_BUTTON_CUSTOM => [NO_BRACE],
        FE_EDITOR_FILE_UPLOAD_PATH => [NO_BRACE, DOUBLE_OPEN_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_EDITOR_READ_ONLY => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_TEMPLATE_GROUP_CURRENT_IDX => [NO_BRACE, DOUBLE_OPEN_BRACE, DOUBLE_OPEN_BRACE_WITH_EXCLAMATION],
        FE_CHECKBOX_CHECKED => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_CHECKBOX_UNCHECKED => [NO_BRACE, DOUBLE_OPEN_BRACE],
        SYSTEM_FILE_NAME_TO_LOWER => [NO_BRACE],
        FE_MULTI_CHECKBOX_ITEMS => [NO_BRACE],
        F_MODE_GLOBAL => [NO_BRACE, DOUBLE_OPEN_BRACE],
        F_MULTI_MSG_NO_RECORD => [NO_BRACE],
        FE_SUBRECORD_COPY_RULE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_SUBRECORD_COPY_RULE_IMPORT => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_ICJ_TEMPLATE => [NO_BRACE, DOUBLE_OPEN_BRACE],
        FE_FILE_KEY => [NO_BRACE],
    ];

    /**
     * Expand column $keyName to row array as virtual columns.
     * E.g.: [ 'id' => '1', 'name' => 'John', 'parameter' => 'detail=xId:grId\nShowEmptyAtStart=1' ] becomes:
     *  [ 'id' => '1', 'name' => 'John', 'parameter' => 'detail=xId:grId\nShowEmptyAtStart=1', 'detail' => 'xId:grId',
     *  'showEmptyAtStart'='1']
     *
     * @param array $elements
     * @param string $keyName Typically F_PARAMETER or FE_PARAMETER (both: 'parameter')
     *
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function explodeParameterInArrayElements(array &$elements, $keyName) {

        foreach ($elements as $key => $element) {
            self::explodeParameter($element, $keyName);
            $elements[$key] = $element;
        }
    }


    /**
     * Set default values for given FormElement.
     * Hint: to copy values from Form, copyAttributesToFormElements() is more appropriate.
     *
     * @param array $elements
     *
     * @param array $formSpec
     * @return array
     */
    public static function formElementSetDefault(array $elements, array $formSpec) {

        // Do not add FE_SLAVE_ID - it's necessary to detect if a value is given or not.
        $default = [FE_SQL_BEFORE => '', FE_SQL_INSERT => '', FE_SQL_UPDATE => '', FE_SQL_DELETE => '', FE_SQL_AFTER => ''];

        foreach ($elements as $key => $element) {
            $elements[$key][FE_TG_INDEX] = 0;
            unset($elements[$key][FE_ADMIN_NOTE]);
//            $elements[$key][FE_DATA_REFERENCE] = '';

            $elements[$key] = array_merge($default, $elements[$key]);
        }


        return $elements;
    }

    /**
     * Take all rows from field $element[$keyName] and merge them with $element itself. '$element' grows in size.
     *
     * @param array $element
     * @param string $keyName Typically F_PARAMETER or FE_PARAMETER (both: 'parameter')
     *
     * @param bool $flagAllowOverwrite
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function explodeParameter(array &$element, $keyName, $flagAllowOverwrite = false) {

        // Something to explode?
        if (isset($element[$keyName]) && $element[$keyName] !== '') {
            $feName = $element[FE_NAME] ?? '';
            self::$store = Store::getInstance();
            self::$store->setVar(SYSTEM_FORM_ELEMENT_ID, $element[FE_ID], STORE_SYSTEM);
            self::$store->setVar(FORM_NAME_FORM_ELEMENT, $element[FE_ID] . ' / ' . $feName . ' / ', STORE_SYSTEM);

            // Explode
            $arr = KeyValueStringParser::parse($element[$keyName], "=", "\n");

            if (!$flagAllowOverwrite) {
                // Check if some of the exploded keys conflict with existing keys
                $checkKeys = array_keys($arr);
                foreach ($checkKeys as $checkKey) {
                    if (!empty($element[$checkKey])) {
                        self::$store = Store::getInstance();
                        self::$store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($element), STORE_SYSTEM);
                        self::$store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, $keyName, STORE_SYSTEM);
                        throw new \UserFormException("Found reserved key name '$checkKey'", ERROR_RESERVED_KEY_NAME);
                    }
                }
            }
            $element = array_merge($element, $arr);

            // For retype elements: copy the language specific value.
            if (isset($element[FE_RETYPE_SOURCE_NAME])) {

                if (!empty($element[FE_RETYPE_LABEL])) {
                    $element[FE_LABEL] = $element[FE_RETYPE_LABEL];
                }

                if (!empty($element[FE_RETYPE_NOTE])) {
                    $element[FE_NOTE] = $element[FE_RETYPE_NOTE];
                }
            }

            $element[$keyName] = ''; // to not expand it a second time
        }
    }

    /**
     * Take language specific definitions and overwrite the default values.
     *
     * @param array $formSpecFeSpecNative
     *
     * @param $parameterLanguageFieldName
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function setLanguage(array $formSpecFeSpecNative, $parameterLanguageFieldName) {

        if (is_string($parameterLanguageFieldName) && isset($formSpecFeSpecNative[$parameterLanguageFieldName])) {
            self::explodeParameter($formSpecFeSpecNative, $parameterLanguageFieldName, true);
        }

        return $formSpecFeSpecNative;
    }

    /**
     * Build the FE name: <field>-<record index>) or -$index
     *
     * @param array $formElement
     * @param string $id
     * @param $index
     * @return string
     */
    public static function buildFormElementName(array $formElement, $id, $index = null) {
        $field = ($formElement[FE_NAME] == '') ? $formElement[FE_ID] : $formElement[FE_NAME];
        $formElementName = "$field" . HTML_DELIMITER_NAME . "$id";
        if ($id === 0 && ($index != null || $index === 0)) {
            $formElementName .= HTML_DELIMITER_NAME . "$index";
        }
        return $formElementName;
    }

    /**
     * Build the internal FE name for an imageCut element (used only in SIP): <field>-imageCut
     *
     * @param array $formElement
     * @return string
     */
    public static function AppendFormElementNameImageCut(array $formElement) {
        $field = ($formElement[FE_NAME] == '') ? $formElement[FE_ID] : $formElement[FE_NAME];

        return "$field" . HTML_DELIMITER_NAME . FE_TYPE_IMAGE_CUT;
    }

    /**
     * Build the FE id: <$formId>-<$formElementId>-<$formElementCopy>
     * Attention: Radio's gets an additional index count as fourth parameter (not here).
     *
     * @param $formId
     * @param $formElementId
     * @param $recordId
     * @param $formElementCopy
     *
     * @return string
     */
    public static function buildFormElementId($formId, $formElementId, $formElementCopy, $appendix = '') {
        return "$formId" . HTML_DELIMITER_ID . "$formElementId" . HTML_DELIMITER_ID . "$formElementCopy" . $appendix;
    }


    /**
     * In an array for $feSpecNative, set FE_HTML_ID for all fe.class=FE_CONTAINER Elements.
     *
     * @param array $feSpecNative
     * @param $formId
     * @param $recordId
     * @param int $formElementCopy
     * @return array
     */
    public static function setFeContainerFormElementId(array $feSpecNative, $formId, $recordId, $formElementCopy = 0) {

        foreach ($feSpecNative as $key => $fe) {

            switch ($fe[FE_CLASS]) {
                case FE_CLASS_CONTAINER:
                    self::$store = Store::getInstance();
                    $ttContentUid = self::$store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
                    $formHtmlId = Form::getHtmlIdStatic($ttContentUid, $formId, $recordId);
                    $feSpecNative[$key][FE_HTML_ID] = self::buildFormElementId($formHtmlId, $fe[FE_ID], $formElementCopy);
                    break;
                default:
                    break;
            }

        }

        return $feSpecNative;
    }

    /**
     * Checkboxes, belonging to one element, grouped together by name: <fe>_<field>_<index>
     *
     * @param string $field
     * @param string $index
     *
     * @param bool $appendArray
     * @return string
     */
    public static function prependFormElementNameCheckBoxMulti($field, $index, $appendArray = false) {
//        return '_' . $index . '_' . $field;

        if ($appendArray) {
            return $field . '[]';
        }
        return $field;
    }

    /**
     * Check all FormElements if there are some with the attribute FE_RETYPE.
     * If yes: duplicate such elements and update FE_NAME, FE_LABEL, FE_NOTE.
     *
     * @param array $elements
     *
     * @return array
     */
    public static function duplicateRetypeElements(array $elements) {
        $arr = array();

        foreach ($elements as $fe) {

            // adjust FE_RETYPE=1
            if (isset($fe[FE_RETYPE]) && ($fe[FE_RETYPE] == '' || $fe[FE_RETYPE] == '1')) {
                $fe[FE_RETYPE] = '1';
            }

            $arr[] = $fe;

            if (isset($fe[FE_RETYPE]) && $fe[FE_RETYPE] == '1') {

                // Reference to Source FormElement
                $fe[FE_RETYPE_SOURCE_NAME] = $fe[FE_NAME];

                // Create copy of FE, adjust name, label, note
                $fe[FE_NAME] .= FE_RETYPE_NAME_EXTENSION;

                if (isset($fe[FE_RETYPE_LABEL])) {
                    $fe[FE_LABEL] = $fe[FE_RETYPE_LABEL];
                    unset($fe[FE_RETYPE_LABEL]);
                }

                if (isset($fe[FE_RETYPE_NOTE])) {
                    $fe[FE_NOTE] = $fe[FE_RETYPE_NOTE];
                    unset($fe[FE_RETYPE_NOTE]);
                }

                $fe[FE_TG_INDEX] = 1; // Not sure if this is helpful in case of dynamic update - but it will make the element unique.

                unset($fe[FE_RETYPE]);
                $arr[] = $fe;
            }
        }

        return $arr;
    }

    /**
     * Copy specific attributes defined on the form to all FormElements.
     *
     * @param array $formSpec
     * @param array $feSpecNative
     *
     * @return mixed
     */
    public static function copyAttributesToFormElements(array $formSpec, array $feSpecNative) {

        // Iterate over all FormElement
        foreach ($feSpecNative as $key => $element) {

            $feSpecNative[$key][F_FE_DATA_PATTERN_ERROR_SYSTEM] = $formSpec[F_FE_DATA_PATTERN_ERROR_SYSTEM];

            Support::setIfNotSet($feSpecNative[$key], F_FE_DATA_PATTERN_ERROR, $formSpec[F_FE_DATA_PATTERN_ERROR]);
            Support::setIfNotSet($feSpecNative[$key], F_FE_DATA_REQUIRED_ERROR, $formSpec[F_FE_DATA_REQUIRED_ERROR]);
            Support::setIfNotSet($feSpecNative[$key], F_FE_DATA_MATCH_ERROR, $formSpec[F_FE_DATA_MATCH_ERROR]);
            Support::setIfNotSet($feSpecNative[$key], F_FE_DATA_ERROR, $formSpec[F_FE_DATA_ERROR]);
            if ($feSpecNative[$key][F_FE_LABEL_ALIGN] == F_FE_LABEL_ALIGN_DEFAULT) {
                $feSpecNative[$key][F_FE_LABEL_ALIGN] = $formSpec[F_FE_LABEL_ALIGN];
            }
            Support::setIfNotSet($feSpecNative[$key], F_FE_REQUIRED_POSITION, $formSpec[F_FE_REQUIRED_POSITION]);
            Support::setIfNotSet($feSpecNative[$key], F_FE_MIN_WIDTH, $formSpec[F_FE_MIN_WIDTH]);
            Support::setIfNotSet($feSpecNative[$key], FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH, $formSpec[FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH]);
            Support::setIfNotSet($feSpecNative[$key], F_FE_FIELDSET_CLASS, $formSpec[F_FE_FIELDSET_CLASS]);
            Support::setIfNotSet($feSpecNative[$key], F_UPLOAD_PDF_PROTECTED_ACTION, $formSpec[F_UPLOAD_PDF_PROTECTED_ACTION]);
        }

        return $feSpecNative;
    }

    /**
     * Check for specific FE to clean.
     *
     * @param array $feSpecNative
     * @return array
     */
    public static function cleanFormElements(array $feSpecNative): array {

        foreach ($feSpecNative as $key => $fe) {
            foreach ([FE_TYPEAHEAD_SQL, FE_TYPEAHEAD_SQL_PREFETCH, FE_TYPEAHEAD_INITIAL_SUGGESTION] as $arg) {
                if (isset($feSpecNative[$key][$arg])) {
                    $feSpecNative[$key][$arg] = OnString::removeDoubleCurlyBraces($feSpecNative[$key][$arg]);
                }
            }
        }

        return $feSpecNative;
    }

    /**
     * Set all necessary keys - subsequent 'isset()' are not necessary anymore.
     *
     * @param array $fe
     *
     * @return array
     */
    public static function initActionFormElement(array $fe) {

        $list = [FE_TYPE, FE_SQL_VALIDATE, FE_SLAVE_ID, FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE,
            FE_SQL_AFTER, FE_EXPECT_RECORDS, FE_REQUIRED_LIST, FE_ALERT, FE_QFQ_LOG, FE_MESSAGE_FAIL, FE_SENDMAIL_TO, FE_SENDMAIL_CC,
            FE_SENDMAIL_BCC, FE_SENDMAIL_FROM, FE_SENDMAIL_SUBJECT, FE_SENDMAIL_REPLY_TO, FE_SENDMAIL_FLAG_AUTO_SUBMIT,
            FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID, FE_SENDMAIL_X_ID2, FE_SENDMAIL_X_ID3, FE_SENDMAIL_BODY_MODE,
            FE_SENDMAIL_BODY_HTML_ENTITY, FE_SENDMAIL_SUBJECT_HTML_ENTITY];

        foreach ($list as $key) {
            Support::setIfNotSet($fe, $key);
        }

        return $fe;
    }

    /**
     * Set all necessary keys - subsequent 'isset()' are not necessary anymore.
     *
     * @param array $fe
     *
     * @return array
     */
    public static function initUploadFormElement(array $fe) {

        $list = [FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE, FE_SQL_AFTER];

        foreach ($list as $key) {
            Support::setIfNotSet($fe, $key);
        }

        return $fe;
    }

    /**
     * Prepare code of 'lock', 'password', 'info' to extend a FormElement.
     * The 'info' will always be added, 'lock' and 'password' only on FE with mode=show|required
     * Depending on $showInside:
     *    * true: a button is shown inside the 'input' or 'select' element.
     *    * false: an icon is shown below the FormElement.
     *
     * 'Add' means:
     *    * $rcButton contains all HTML button code. The calling function is responsible to build the correct code.
     *    * $formElement[FE_INPUT_EXTRA_BUTTON_INFO] has been wrapped with HTML-tag and HTML-id.
     *    * $formElement[FE_MODE] - for 'lock' it will be faked to 'readonly'
     *    * $formElement[FE_TYPE] - for 'password' it will be faked to 'password'
     *
     * @param array $formElement
     * @param bool $showInline
     * @param bool $customBtnWrapper
     *
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException|\DbException
     */
    public static function prepareExtraButton(array $formElement, $showInline, $customBtnWrapper = false): array {

        self::$store = Store::getInstance();

        $infoSymbolInside = self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_INLINE, STORE_SYSTEM);
        $infoSymbolOutside = self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_BELOW, STORE_SYSTEM);

        if (SYSTEM_EXTRA_BUTTON_INFO_POSITION_BELOW == self::$store->getVar(SYSTEM_EXTRA_BUTTON_INFO_POSITION, STORE_SYSTEM)) {
            $showInline = false;
        }

        $extraButton = '';
        $id = $formElement[FE_HTML_ID];

        if (false !== strpos($formElement[FE_NAME], FE_TEMPLATE_GROUP_NAME_PATTERN)) {
            $id .= '-' . FE_TEMPLATE_GROUP_NAME_PATTERN;
        }

        $formElement[FE_TMP_EXTRA_BUTTON_HTML] = '';
        $minWidth = $formElement[FE_INPUT_EXTRA_BUTTON_INFO_MIN_WIDTH] ?? '';
        $minWidth .= 'px';

        // AI assist dropdown (parameter aiAssist): first in the row, i.e. left of extraButtonInfo
        if (($formElement[FE_TMP_AI_ASSIST_ACTIVE] ?? '') === '1') {
            // Inside a textarea the buttons sit at the bottom - open the menu upwards.
            $dropUp = $customBtnWrapper === 'textarea-button-wrapper' ? ' dropup' : '';
            $extraButton .= self::buildAiAssistDropdown($formElement, $id, $dropUp);
        }

        // INFO: $showinline =- TRUE ('input' elements)
        if (isset($formElement[FE_INPUT_EXTRA_BUTTON_INFO]) && $showInline) {
            $extraButton .= <<<EOF
            <button type="button" class="btn btn-info extraButtonInfo" onclick="$('#$id-extra-info').slideToggle('swing')">
                $infoSymbolInside
            </button>
EOF;

            $value = $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
            $formElement[FE_INPUT_EXTRA_BUTTON_INFO] = <<<EOF
            <div class="alert alert-info" id="$id-extra-info" style="display: none; min-width: $minWidth;">
                <p>$value</p>
            </div>
EOF;
        }

//        <span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true" onclick="$('#$id-extra-info').slideToggle('swing')"></span>

        $js = " onclick=\"$('#$id-extra-info').slideToggle('swing')\" ";
        $arr = explode(' ', $infoSymbolOutside, 2);
        $infoSymbolOutside = $arr[0] . $js . $arr[1];

        // INFO: $showinline == FALSE (e.g. 'textarea' elements)
        if (isset($formElement[FE_INPUT_EXTRA_BUTTON_INFO]) && !$showInline) {
            $class = $formElement[FE_INPUT_EXTRA_BUTTON_INFO_CLASS];
            $extraButton .= <<<EOF
            <span class="$class">$infoSymbolOutside</span>
EOF;

            $value = $formElement[FE_INPUT_EXTRA_BUTTON_INFO];
            $formElement[FE_INPUT_EXTRA_BUTTON_INFO] = <<<EOF
            <div class="alert alert-info" id="$id-extra-info" style="display: none; min-width: $minWidth;">
                <p>$value</p>
            </div>
EOF;
        }

        $skip = (!($formElement[FE_MODE] == FE_MODE_SHOW
            || $formElement[FE_MODE] == FE_MODE_REQUIRED
            || $formElement[FE_MODE] == FE_MODE_SHOW_REQUIRED
            || $formElement[FE_MODE] == FE_MODE_HIDDEN));

        // LOCK
        if (!$skip && HelperFormElement::booleParameter($formElement[FE_INPUT_EXTRA_BUTTON_LOCK] ?? '-')) {
            switch ($formElement[FE_TYPE]) {
                case FE_TYPE_SELECT:
                    if (isset($formElement[FE_SELECT_BS])) {
                        $extraButton .= <<<EOF
                            <button class="btn btn-info"
                                    onclick="$('#$id-i').find('.input-group .qfq-select-bs-parent').toggleClass('qfq-disabled')">
                                <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                            </button>
                            <script>$('#$id-i').find('.input-group .qfq-select-bs-parent').addClass('qfq-disabled')</script>
EOF;
                        break;
                    }
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$('#$id').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
                    <script>$('#$id').addClass('qfq-disabled')</script>
EOF;
                    break;
                case FE_TYPE_CHECKBOX:
                case FE_TYPE_RADIO:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$('[id^=$id]').parent('label').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
                    <script>$('[id^=$id]').parent('label').addClass('qfq-disabled')</script>
EOF;
                    break;
                case FE_TYPE_SUBRECORD:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$(this).closest('table').find('thead, tbody').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
EOF;
                    break;
                case FE_TYPE_IMAGE_CUT:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$(this).closest('.form-group').find('.qfq-image-adjust').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
EOF;
                    break;
                case FE_TYPE_CHAT:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$(this).closest('.qfq-chat').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
EOF;
                    break;
                case FE_TYPE_UPLOAD:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$(this).closest('.filepond--root').toggleClass('qfq-disabled')">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
EOF;
                    break;
                case FE_TYPE_EDITOR:
                case  FE_TYPE_ANNOTATE:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info extraButtonLock">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
EOF;
                    break;
                default:
                    $extraButton .= <<<EOF
                    <button class="btn btn-info"
                            onclick="$('#$id').prop('readonly',!$('#$id').prop('readonly'))">
                        <span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
                    </button>
                    <script>$('#$id').prop('readonly', true)</script>
EOF;
                    break;

            }
        }
        // PASSWORD
//      if (!$skip && isset($formElement[FE_INPUT_EXTRA_BUTTON_PASSWORD])) {
        if (!$skip && HelperFormElement::booleParameter($formElement[FE_INPUT_EXTRA_BUTTON_PASSWORD] ?? '-')) {

            $formElement[FE_TYPE] = 'password';

            $extraButton .= <<<EOF
            <button class="btn btn-info"
                    onclick="$('#$id').toggleClass('qfq-password')">
                <span class="glyphicon glyphicon-eye-open" aria-hidden="true"></span>
            </button>
EOF;
        }
        $wrapper = $customBtnWrapper === false ? 'input-group-btn' : $customBtnWrapper;
        if (($formElement[FE_TMP_AI_ASSIST_ACTIVE] ?? '') === '1') {
            $wrapper .= ' qfq-ai-row';
        }
//        $formElement[FE_TMP_EXTRA_BUTTON_HTML] = Support::wrapTag('<div class="input-group-btn" style="font-size: 1em;">', $extraButton, true);
        $formElement[FE_TMP_EXTRA_BUTTON_HTML] = Support::wrapTag("<div class='$wrapper'>", $extraButton, true);
        Support::setIfNotSet($formElement, FE_INPUT_EXTRA_BUTTON_INFO);

        return $formElement;
    }

    /**
     * Bootstrap 3 dropdown with the enabled AI actions of this FormElement.
     * Clicks are handled client side (AiAssist.js, delegated handler) - the items
     * reference the target element via data-ai-target; sip/url live as data
     * attributes on the target element itself.
     *
     * @param array $formElement
     * @param string $id
     * @param string $dropUp '' or ' dropup'
     *
     * @return string
     */
    private static function buildAiAssistDropdown(array $formElement, string $id, string $dropUp): string {

        $modeLabel = [
            AI_MODE_IMPROVE => 'Improve',
            AI_MODE_GENERATE => 'Generate',
            AI_MODE_TRANSLATE => 'Translate',
        ];

        $languages = array_values(array_filter(array_map('trim',
            explode(',', $formElement[FE_AI_TARGET_LANGUAGE] ?? ''))));

        $items = '';
        foreach (explode(',', $formElement[FE_AI_ASSIST] ?? '') as $mode) {
            $mode = trim($mode);
            if (!isset($modeLabel[$mode])) {
                continue;
            }
            if ($mode === AI_MODE_TRANSLATE && count($languages) > 1) {
                foreach ($languages as $language) {
                    $languageAttr = htmlspecialchars($language, ENT_QUOTES);
                    $label = $modeLabel[$mode] . ' &rarr; ' . htmlspecialchars(self::aiLanguageLabel($language));
                    $items .= "<li><a href='#' data-ai-mode='$mode' data-ai-language='$languageAttr' data-ai-target='$id'>$label</a></li>";
                }
            } else {
                $items .= "<li><a href='#' data-ai-mode='$mode' data-ai-target='$id'>" . $modeLabel[$mode] . "</a></li>";
            }
        }

        return <<<EOF
        <div class="btn-group$dropUp qfq-ai-dropdown">
            <button type="button" class="btn btn-info qfq-ai-toggle dropdown-toggle" data-toggle="dropdown"
                    aria-haspopup="true" aria-expanded="false" title="AI assist">AI</button>
            <ul class="dropdown-menu dropdown-menu-right">
                $items
            </ul>
        </div>
EOF;
    }

    /**
     * Short display label for a translate target ('German' -> 'DE'). Falls back to
     * the name itself. Keep in sync with AiAssist.js languageLabel().
     *
     * @param string $language
     * @return string
     */
    private static function aiLanguageLabel(string $language): string {
        $map = ['english' => 'EN', 'german' => 'DE', 'french' => 'FR', 'italian' => 'IT', 'spanish' => 'ES'];
        return $map[strtolower($language)] ?? $language;
    }

    /**
     * Returns $maxLength if greater than 0, else FE_TEMPLATE_GROUP_DEFAULT_MAX_LENGTH
     *
     * @param $maxLength
     *
     * @return int
     */
    public static function tgGetMaxLength($maxLength) {
        return (empty($maxLength)) ? FE_TEMPLATE_GROUP_DEFAULT_MAX_LENGTH : $maxLength;
    }

    /**
     * Converts a string of '00ff00' and returns a string of '{"red": 0, "green": 255, "blue": 0}'.
     *
     * @param array $formElement
     * @return string
     * @throws \UserFormException
     */
    public static function penColorToHex(array $formElement) {

        if (empty($formElement[FE_DEFAULT_PEN_COLOR])) {
            return '';
        }

        if (strlen($formElement[FE_DEFAULT_PEN_COLOR]) != LENGTH_HEX_COLOR) {
            throw new \UserFormException("Invalid Format for " . FE_DEFAULT_PEN_COLOR .
                ". Expect like '#ffdd00', got: '" . $formElement[FE_DEFAULT_PEN_COLOR] . "'", ERROR_INVALID_OR_MISSING_PARAMETER);
        }

        $rgb['red'] = hexdec($formElement[FE_DEFAULT_PEN_COLOR][0] . $formElement[FE_DEFAULT_PEN_COLOR][1]);
        $rgb['green'] = hexdec($formElement[FE_DEFAULT_PEN_COLOR][2] . $formElement[FE_DEFAULT_PEN_COLOR][3]);
        $rgb['blue'] = hexdec($formElement[FE_DEFAULT_PEN_COLOR][4] . $formElement[FE_DEFAULT_PEN_COLOR][5]);

        return json_encode($rgb);
    }

    /**
     * Depending on value in $requiredPosition the array $classArr will contain the CSS class to align the required mark.
     * @param $requiredPosition
     * @return array
     * @throws \UserFormException
     */
    public static function getRequiredPositionClass($requiredPosition) {
        $classArr[FE_LABEL] = '';
        $classArr[FE_TYPE] = '';
        $classArr[FE_NOTE] = '';

        switch ($requiredPosition) {
            case F_FE_REQUIRED_POSITION_LABEL_LEFT:
                $classArr[FE_LABEL] = CSS_REQUIRED_LEFT;
                break;
            case F_FE_REQUIRED_POSITION_LABEL_RIGHT:
                $classArr[FE_LABEL] = CSS_REQUIRED_RIGHT;
                break;
            case F_FE_REQUIRED_POSITION_INPUT_LEFT:
                $classArr[FE_INPUT] = CSS_REQUIRED_LEFT;
                break;
            case F_FE_REQUIRED_POSITION_INPUT_RIGHT:
                $classArr[FE_INPUT] = CSS_REQUIRED_RIGHT;
                break;
            case F_FE_REQUIRED_POSITION_NOTE_LEFT:
                $classArr[FE_NOTE] = CSS_REQUIRED_LEFT;
                break;
            case F_FE_REQUIRED_POSITION_NOTE_RIGHT:
                $classArr[FE_NOTE] = CSS_REQUIRED_RIGHT;
                break;
            default:
                throw new \UserFormException('Unknown value for ' . F_FE_REQUIRED_POSITION . ': ' . $requiredPosition, ERROR_INVALID_VALUE);
        }

        return $classArr;
    }

    /**
     * Function to check if a parameter is set (no value) or 0 or 1
     * Example:
     *   a) extraButtonLock
     *   b) extraButtonLock=0
     *   c) extraButtonLock=1
     *
     * a) and c) means 'on' and b) means off
     *
     * IMPORTANT: Call this function :   HelperFormElement::booleParameter( $fe[FE_INPUT_EXTRA_BUTTON_LOCK] ?? '-' )
     *
     * @param $data
     * @return bool
     */
    public static function booleParameter($data) {
        return $data == '' || $data == '1';
    }

    /**
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
     * Copies the found keys to &$itemKey and the values to &$itemValue
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
     *
     * @param array $formElement
     * @param array $itemKey
     * @param array $itemValue
     * @param array $itemGroupKey
     * @param array $itemGroupValue
     * @param array $classValue
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function getKeyValueListFromSqlEnumSpec(array $formElement, array &$itemKey, array &$itemValue, array &$itemGroupKey = array(), array &$itemGroupValue = array(), array &$classValue = array(), array &$itemCount = array()): void {
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();
        $classValue = array();

        // selectBS
        $itemGroupKey = array();
        $itemGroupValue = array();

        // Optional count column for filter display
        $itemCount = array();

        self::$store = Store::getInstance();

        // Call getItemsForEnumOrSet() only if a corresponding column really exists.
        if (self::$store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) !== false) {
            $itemValue = self::getItemsForEnumOrSet($formElement[FE_NAME], $fieldType);
        }
        if (is_array($formElement[FE_SQL1])) {
            if (count($formElement[FE_SQL1]) > 0) {
                $keys = array_keys($formElement[FE_SQL1][0]);

                $idColumn = array_intersect(['id', '_id', 'key', '_key'], $keys);
                $itemKey = array_column($formElement[FE_SQL1], reset($idColumn));
                // If there is no column 'id' and at least two columns in total
                if (count($itemKey) === 0 && count($keys) >= 2) {
                    // 1. column is used for 'id'
                    $idx = 0;
                    $itemKey = array_column($formElement[FE_SQL1], $keys[$idx]);
                }

                $valueColumn = array_intersect(['label', '_label', 'value', '_value'], $keys);
                $itemValue = array_column($formElement[FE_SQL1], reset($valueColumn));
                // If there is no column 'label' (e.g.: SHOW tables)
                if (count($itemValue) === 0) {
                    // Only one column selected: 'id' = 'label'
                    // Else: 2. column is used for 'label'
                    $idx = count($keys) == 1 ? 0 : 1;
                    $itemValue = array_column($formElement[FE_SQL1], $keys[$idx]);
                }

                // selectBS
                // At least three columns have to be selected
                if (count($keys) >= 3) {

                    $itemGroupKey = array_column($formElement[FE_SQL1], in_array('groupKey', $keys) ? 'groupKey' : '_groupKey');
                    // If there is no column 'groupKey' (e.g.: SHOW tables)
                    //array_intersect is for all other keywords that must not appear in $keys can be included.
                    if (count($itemGroupKey) === 0 && !array_intersect(['class', COLUMN_COUNT], $keys)) {
                        // 3. column is used for 'groupKey'
                        $idx = 2;
                        if (isset($keys[$idx]) ) $itemGroupKey = array_column($formElement[FE_SQL1], $keys[$idx]);
                    }

                    $itemGroupValue = array_column($formElement[FE_SQL1], in_array('groupLabel', $keys) ? 'groupLabel' : '_groupLabel');
                    // If there is no column 'groupLabel' (e.g.: SHOW tables)
                    //array_intersect is for all other keywords that must not appear in $keys can be included.
                    if (count($itemGroupValue) === 0 && !array_intersect(['class', COLUMN_COUNT], $keys)) {
                        // 4. column is used for 'groupLabel'
                        $idx = 3;
                        if (isset($keys[$idx])) $itemGroupValue = array_column($formElement[FE_SQL1], $keys[$idx]);
                    }

                    $classValue = array_column($formElement[FE_SQL1], in_array('class', $keys) ? 'class' : '_class');
                    //Fallback disable has an not existing alias
                    if (count($classValue) === 0) {
                        $idx = 4;
                        if (isset($keys[$idx])) $classValue = array_column($formElement[FE_SQL1], $keys[$idx]);
                    }
                }

                // Optional count column - only extracted if explicitly named 'count'
                $itemCount = array_column($formElement[FE_SQL1], COLUMN_COUNT);
            }
        } elseif (isset($formElement[FE_ITEM_LIST]) && strlen($formElement[FE_ITEM_LIST]) > 0) {
            $arr = KeyValueStringParser::parse($formElement[FE_ITEM_LIST], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement[FE_NAME], $fieldType);'
        } elseif (isset($formElement[FE_CHECKBOX_CHECKED]) && $formElement[FE_TYPE] == FE_TYPE_CHECKBOX) {
            // Nothing to do here.
        } elseif ((isset($formElement[FE_CHECKBOX_MODE]) && $formElement[FE_CHECKBOX_MODE] == FE_CHECKBOX_MODE_MULTI_STATE) || isset($formElement[FE_MULTI_CHECKBOX_ITEMS])) {
            // will be done later.
        } else {
            throw new \UserFormException("Missing definition - nothing found in 'sql1', 'parameter:itemList/checkBoxState', 'enum-' or 'set-definition'. Or sql1 is missing exclamation mark at the start of the query! {{! ...}}", ERROR_MISSING_ITEM_LIST);
        }


        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }

        // selectBS: sort result by itemGroupKey
        if (count($itemGroupKey) !== 0) {
            asort($itemGroupKey);
            $itemKey = array_replace($itemGroupKey, $itemKey);
            $itemValue = array_replace($itemGroupKey, $itemValue);
            $itemGroupValue = (count($itemGroupValue) === 0) ? $itemGroupValue : array_replace($itemGroupKey, $itemGroupValue);
            $classValue = (count($classValue) === 0) ? $classValue : array_replace($itemGroupKey, $classValue);
            $itemCount = (count($itemCount) === 0) ? $itemCount : array_replace($itemGroupKey, $itemCount);
            $itemKey = array_values($itemKey);
            $itemValue = array_values($itemValue);
            $itemGroupKey = array_values($itemGroupKey);
            $itemGroupValue = array_values($itemGroupValue);
            $classValue = array_values($classValue);
            $itemCount = array_values($itemCount);
        }

        // Process 'emptyHide'  before 'emptyItemAtStart' / 'emptyItemAtEnd': than 'emptyItem*' are still possible.
        if (isset($formElement['emptyHide'])) {
            $itemKey = OnArray::removeEmptyElementsFromArray($itemKey);
            $itemValue = OnArray::removeEmptyElementsFromArray($itemValue);
            $classValue = OnArray::removeEmptyElementsFromArray($classValue);
        }

        if (isset($formElement[FE_EMPTY_ITEM_AT_START])) {
            $placeholder = isset($formElement[FE_PLACEHOLDER]) ? $formElement[FE_PLACEHOLDER] : '';
            array_unshift($itemKey, '');
            array_unshift($itemValue, $placeholder);

            // selectBS
            if (count($itemGroupKey) != 0) {
                array_unshift($itemGroupKey, '');
                array_unshift($itemGroupValue, $placeholder);
            }
            if (count($classValue) != 0) {
                array_unshift($classValue, '');
            }

            // itemCount: add null at start to keep alignment
            if (count($itemCount) != 0) {
                array_unshift($itemCount, null);
            }
        }

        if (isset($formElement[FE_EMPTY_ITEM_AT_END])) {
            $itemKey[] = '';
            $itemValue[] = '';

            // itemCount: add null at end to keep alignment
            if (count($itemCount) != 0) {
                $itemCount[] = null;
            }
        }
    }

    /**
     * Get the attribute definition list of an enum or set column. For strings, get the default value.
     * Return elements as an array.
     *
     * @param string $column
     * @param string $fieldType
     *
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private static function getItemsForEnumOrSet($column, &$fieldType) {

        self::$store = Store::getInstance();

        // Get column definition
        $fieldTypeDefinition = self::$store->getVar($column, STORE_TABLE_COLUMN_TYPES);

        if ($fieldTypeDefinition === false) {
            throw new \UserFormException("Column '$column' in primary table.", ERROR_DB_UNKNOWN_COLUMN);
        }

        $length = strlen($fieldTypeDefinition);

        // enum('...   set('
        switch (substr($fieldTypeDefinition, 0, 4)) {
            case 'enum':
                $startPosition = 5;
                break;
            case 'set(':
                $startPosition = 4;
                break;
            default:
                $fieldType = 'string';

                return array();
        }

        // enum('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        // set('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        $values = substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1);
        $items = OnArray::trimArray(explode(',', $values), "'");
        $fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1);

        return $items;
    }

    /**
     * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
     *
     * @param string $feMode
     * @param bool $cssDisable
     * @param string $feType
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function getAttributeFeMode($feMode, $cssDisable = true, $feType = null) {
        $attribute = '';

        self::getFeMode($feMode, $hidden, $disabled, $required);

        switch ($feMode) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
            case FE_MODE_SHOW_REQUIRED:
                break;
            case FE_MODE_REQUIRED:
                $attribute .= Support::doAttribute('required', 'required');
                break;
            case FE_MODE_READONLY:

                // #18668: <button> inside disabled <fieldset> (or other element) does not register click events
                $attr = ($feType === FE_TYPE_FIELDSET) ? 'readonly' : 'disabled';
                $attribute .= Support::doAttribute($attr, $attr);
                break;
            default:
                throw new \UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }

        // Attributes: data-...
        $attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
        if ($cssDisable) {
            $attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
        }
        $attribute .= Support::doAttribute(DATA_REQUIRED, $required);

        return $attribute;
    }

    /**
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
     * @param string $feMode
     * @param string $hidden
     * @param string $disabled
     * @param string $required
     *
     * @throws \UserFormException
     */
    public static function getFeMode($feMode, &$hidden, &$disabled, &$required) {
        $hidden = 'no';
        $disabled = 'no';
        $required = 'no';

        switch ($feMode) {
            case FE_MODE_SHOW:
            case FE_MODE_SHOW_REQUIRED:
                break;
            case FE_MODE_REQUIRED:
                $required = 'yes';
                break;
            case FE_MODE_READONLY:
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new \UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
     *
     * E.g.: attributeList: [ 'type', 'autofocus' ]
     *       generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement[FE_AUTOFOCUS]" '
     *
     * @param array $formElement
     * @param array $attributeList
     * @param bool $flagOmitEmpty
     *
     * @return string
     * @throws \CodeException
     */
    public static function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item], $flagOmitEmpty);
        }

        return $attribute;
    }

    /**
     * Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
     *
     * @param        $htmlFormElementName
     * @param string $value
     *
     * @return string
     */
    public static function buildNativeHidden($htmlFormElementName, $value) {
        return '<input type="hidden" name="' . $htmlFormElementName . '" value="' . htmlentities($value) . '">';
    }

    /**
     * Set corresponding JSON attributes readonly/required/disabled, based on $formElement[FE_MODE].
     *
     * @param string $feMode
     *
     * @return array
     * @throws \UserFormException
     */
    public static function getJsonFeMode($feMode) {

        self::getFeMode($feMode, $dummy, $disabled, $required);

        return [API_FORM_UPDATE_DISABLED => ($disabled === 'yes'), API_FORM_UPDATE_REQUIRED => ($required === 'yes')];
    }

    /**
     * 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 AbstractFormElement $fe
     * @param string $wrap
     * @param int $optionIdx
     * @param string $optionClass
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function getFormElementForJson(AbstractFormElement $fe, $wrap = '', $optionIdx = 0, $optionClass = ''): array {

        $htmlFormElementName = $fe->htmlAttributes[HTML_ATTR_NAME];
        $addClassRequired = array();

        $json = HelperFormElement::getJsonFeMode($fe->attributes[FE_MODE]); // disabled, required

        $json[API_FORM_UPDATE_FORM_ELEMENT] = $htmlFormElementName;

        if (isset($fe->attributes[FE_FLAG_ROW_OPEN_TAG]) && isset($fe->attributes[FE_FLAG_ROW_CLOSE_TAG])) {
            $flagRowUpdate = ($fe->attributes[FE_FLAG_ROW_OPEN_TAG] && $fe->attributes[FE_FLAG_ROW_CLOSE_TAG]);
        } else {
            $flagRowUpdate = true;
        }

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

        // 'VALUE' update via 'form-update' on the full row: only if there is no other FE in that row
        if ($flagRowUpdate) {
            // Single Checkbox: Value must be FALSE if the checkbox is unchecked
            if ($fe->attributes[FE_TYPE] == FE_TYPE_CHECKBOX && $fe->attributes[FE_CHECKBOX_MODE] == FE_CHECKBOX_MODE_SINGLE) {
                $checkBoxValue = false;
                if ($fe->attributes[FE_CHECKBOX_CHECKED] === $fe->value) {
                    $checkBoxValue = $fe->value;
                }
                $json[API_FORM_UPDATE_VALUE] = $checkBoxValue;
            } // Dropdown: All options must be returned as a value in this specific way
            else if ($fe->attributes[FE_TYPE] == FE_TYPE_SELECT) {
                $firstSelect = true;
                $allOptions = array();
                for ($ii = 0; $ii < count($fe->itemKeys); $ii++) {

                    $allOptions[] = [
                        'value' => $fe->itemKeys[$ii],
                        'text' => $fe->itemValues[$ii],
                        'selected' => ($fe->itemKeys[$ii] == $fe->value && $firstSelect),
                    ];

                    if ($fe->itemKeys[$ii] == $fe->value && $firstSelect) $firstSelect = false;
                }
                $json[API_FORM_UPDATE_VALUE] = $allOptions;
            } else {
                $json[API_FORM_UPDATE_VALUE] = $fe->value;
            }
        }

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

        // Extra Button Info
        if (isset($fe->attributes[FE_INPUT_EXTRA_BUTTON_INFO])) {
            $key = $fe->attributes[FE_HTML_ID] . '-extra-info';
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = '<p>' . $fe->attributes[FE_INPUT_EXTRA_BUTTON_INFO] . '<p>';
        }

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

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

        // Input
        if (isset($fe->attributes[FE_TYPE])) {
            $key = $fe->attributes[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT;

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

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

                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['required'] = ($fe->attributes[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 ($fe->attributes[FE_TYPE] == FE_TYPE_CHECKBOX) {
                    $optionsId = $fe->attributes[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($fe->attributes[FE_TMP_CLASS_OPTION])) {
                        $optionsLabelId = HelperFormElement::getCheckboxRadioOptionId($fe->attributes[FE_HTML_ID], $optionIdx, HTML_ID_EXTENSION_LABEL);
                        $json[API_ELEMENT_UPDATE][$optionsLabelId][API_ELEMENT_ATTRIBUTE]['class'] = $fe->attributes[FE_TMP_CLASS_OPTION];
                    }
                }

                $class .= ($fe->attributes[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;
                }

            }

            // #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) {
            if ($fe->attributes[FE_TYPE] != FE_TYPE_SELECT && $fe->attributes[FE_TYPE] != FE_TYPE_UPLOAD && $fe->attributes[FE_TYPE] != FE_TYPE_CHECKBOX) {
                $json[API_ELEMENT_UPDATE][$fe->attributes[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['value'] = $fe->value;
                $json[API_ELEMENT_UPDATE][$fe->attributes[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['required'] = ($fe->attributes[FE_MODE] == 'required');
                $json[API_ELEMENT_UPDATE][$fe->attributes[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['hidden'] = $statusHidden;
                if ($pattern !== null) {
                    $json[API_ELEMENT_UPDATE][$fe->attributes[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['pattern'] = $pattern;
                }
            }
        }

        // Show / Hide the complete FormElement Row.
        if (isset($fe->attributes[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 ($fe->attributes[FE_MODE] == FE_MODE_HIDDEN) {
                $class .= ' hidden';
            }

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

            $key = $fe->attributes[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 ($fe->attributes[FE_TYPE] == FE_TYPE_UPLOAD && isset($fe->attributes[FE_FILE_DOWNLOAD_BUTTON]) && isset($_GET["submit_reason"])) {
                $json['type-file'] = true;
                $json['html-content'] = $fe->attributes[FE_FILE_DOWNLOAD_BUTTON_HTML_INTERNAL] ?? '';
            }
        }

        return $json;
    }

    /**
     * Builds a label, typically for an html-'<input>'-element.
     *
     * @param string $htmlFormElementName
     * @param string $label
     * @param string $addClass
     *
     * @return string
     * @throws \CodeException
     */
    public static function buildLabel($htmlFormElementName, $label, $addClass = '') {
        $attributes = Support::doAttribute('for', $htmlFormElementName);
        $attributes .= Support::doAttribute('class', ['control-label', $addClass]);

        $html = Support::wrapTag("<label $attributes>", $label);

        return $html;
    }

    /**
     * Construct HTML ID for checkbox/radio option elements.
     * Optional add $type.
     * Example: $base='173-21612-1-0', $index='0', $type='l' >> '173-21612-1-0-0-l'
     *
     * @param $base
     * @param $index
     * @param string $type -  ';' for label
     * @return string
     */
    public static function getCheckboxRadioOptionId($base, $index, $type = '') {
        return $base . '-' . $index . $type;
    }

    /**
     * Build HelpBlock
     *
     * @return string
     */
    public static function getHelpBlock() {
        return '<div class="help-block with-errors hidden"></div>';
    }

    /**
     * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them.
     * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS.
     * If match: everything is fine, do nothing.
     * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
     *
     * @param array $fe
     * @param Evaluate $evaluate
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     * @throws \InfoException
     */
    public static function sqlValidate(Evaluate $evaluate, array $fe) {

        // Is there something to check?
        if ($fe[FE_SQL_VALIDATE] === '') {
            return;
        }

        if ($fe[FE_EXPECT_RECORDS] === '') {
            throw new \UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS);
        }
        $expect = $evaluate->parse($fe[FE_EXPECT_RECORDS]);

        // ToDo: how to go about the change from messageFail to alert? messageFail is still supported at the moment.
        if ($fe[FE_ALERT] === '' && $fe[FE_MESSAGE_FAIL] === '') {
            throw new \UserFormException("Missing parameter '" . FE_ALERT . "'", ERROR_MISSING_ALERT);
        } else if ($fe[FE_ALERT] !== '' && $fe[FE_MESSAGE_FAIL] !== '') {
            throw new \UserFormException("Simultaneous use of parameter '" . FE_ALERT . "' and '" . FE_MESSAGE_FAIL . "'. It is recommended to use '" . FE_ALERT . "', '" . FE_MESSAGE_FAIL . "' will no longer be maintained.", ERROR_DOUBLE_USAGE_ALERT_AND_MESSAGE_FAIL);
        }

        // Replace possible dynamic parts
        $alert = $evaluate->parse($fe[FE_ALERT]);

        $arr = OnArray::explodeWithoutEscaped(':', $alert);
        $arr = array_merge($arr, ['', '', '', '', '', '']);

        $text = ($arr[FE_ALERT_INDEX_TEXT] === '') ? $fe[FE_MESSAGE_FAIL] : $arr[FE_ALERT_INDEX_TEXT];
        $level = ($arr[FE_ALERT_INDEX_LEVEL] === '') ? DEFAULT_ALERT_LEVEL : $arr[FE_ALERT_INDEX_LEVEL];
        $ok = ($arr[FE_ALERT_INDEX_BUTTON_OK] === '') ? DEFAULT_ALERT_BUTTON_OK : $arr[FE_ALERT_INDEX_BUTTON_OK];
        $force = $arr[FE_ALERT_INDEX_BUTTON_FORCE];
        $timeout = ($arr[FE_ALERT_INDEX_TIMEOUT] === '') ? DEFAULT_ALERT_TIMEOUT : $arr[FE_ALERT_INDEX_TIMEOUT] * 1000;
        $flagModalStatus = ($arr[FE_ALERT_INDEX_FLAG_MODAL] === '') ? DEFAULT_ALERT_FLAG_MODAL : $arr[FE_ALERT_INDEX_FLAG_MODAL];
        $flagModal = $flagModalStatus === '1';
        $qfqLog = $fe[FE_QFQ_LOG] !== '0';

        // Do the check
        $result = $evaluate->parse($fe[FE_SQL_VALIDATE], ROW_REGULAR);
        if (!is_array($result)) {
            throw new \UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY);
        }

        // If there is at least one record count given, who matches: return 'check succeeded'
        $countRecordsArr = explode(',', $expect);
        foreach ($countRecordsArr as $count) {
            if (count($result) == $count) {
                return; // check successfully passed
            }
        }

        $msg = $evaluate->parse($text); // Replace possible dynamic parts in case messageFail is used

        self::$store = Store::getInstance();
        self::$store->setVar(FE_ALERT_TEXT, $text, STORE_SYSTEM);
        self::$store->setVar(FE_ALERT_LEVEL, $level, STORE_SYSTEM);
        self::$store->setVar(FE_ALERT_BUTTON_OK, $ok, STORE_SYSTEM);
        self::$store->setVar(FE_ALERT_BUTTON_FORCE, $force, STORE_SYSTEM);
        self::$store->setVar(FE_ALERT_TIMEOUT, $timeout, STORE_SYSTEM);
        self::$store->setVar(FE_ALERT_FLAG_MODAL, $flagModal, STORE_SYSTEM);
        self::$store->setVar(SYSTEM_SHOW_DEBUG_INFO, SYSTEM_SHOW_DEBUG_INFO_AUTO, STORE_SYSTEM); // No debug info

        // Throw user error message
        if ($qfqLog) {

            // Error including timestamp and reference (logged)
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => $msg
                    , ERROR_MESSAGE_TO_DEVELOPER => "validate() failed.\nSQL Raw: " . $fe[FE_SQL_VALIDATE]])
                , ERROR_REPORT_FAILED_ACTION);
        } else {

            // Plain message (not logged)
            throw new \InfoException($msg);
        }
    }

    /**
     * Adds a specified number of generic, empty elements to an array of parent records.
     * These new elements are derived from a template based on the first record in the list,
     * ensuring they have the same structure but with empty values.
     *
     * @param $parentRecords
     * @param $count
     * @return void
     */
    public static function addGenericElements(&$parentRecords, $count, $idName) {
        // Use the first record as a template to ensure new elements have the same structure
        $template = $parentRecords[0];

        // Create a new element template based on the keys of the first element
        // Setting all values to an empty string
        $newElementTemplate = array_fill_keys(array_keys($template), '');

        // set id default 0
        $newElementTemplate[$idName] = 0;

        // set Default _processRow if set
        if (isset($newElementTemplate['_processRow'])) {
            $newElementTemplate['_processRow'] = 0;
        }
        // Add $count number of new elements
        for ($i = 0; $i < $count; $i++) {
            $parentRecords[] = $newElementTemplate;
        }
    }

    /**
     * Checks a form / formElement against a list of rules and throws a error if they are not met.
     * When checking parameters it also checks if they exist in the $FORM_RULES and throws and error if they are not known.
     * @param array $element
     * @return void
     * @throws \UserFormException
     * @throws \CodeException
     * @throws \UserReportException
     * @see Documentation-develop/NEW_PARAMETER.md
     */
    public static function validateFormRules(array $element): void {
        // self::$FORM_RULES defines the validation rules for each parameter.
        // Each parameter must adhere to at least one of the specified rules to be valid.
        // The rules specify whether the value must be wrapped in {{ }}, {{! }}, or have no wrapper at all.

        $initialElementState = $element;

        $generalErrorState = Store::getVar(SYSTEM_THROW_GENERAL_ERROR, STORE_SYSTEM);
        Store::setVar(SYSTEM_THROW_GENERAL_ERROR, 'no', STORE_SYSTEM);
        self::explodeParameter($element, FE_PARAMETER);
        Store::setVar(SYSTEM_THROW_GENERAL_ERROR, $generalErrorState, STORE_SYSTEM);

        // Allowed unknowns
        // editor- can have any extension that the endpoint provides
        // codemirror- can have any extension that the endpoint provides
        // btnCustom[] names can be freely given
        $allowedPatterns = [
            '/^editor-.*$/',
            '/^codemirror-.*$/',
            '/^btnCustom\[.*\]$/',
            '/^column\[.*\]$/'
        ];

        // Identify keys that exist in the processed element but are not part of the initial state or allowed rules
        $missingKeys = array_diff(array_keys($element), array_keys($initialElementState), array_keys(self::$FORM_RULES));

        $filteredMissingKeys = array_filter($missingKeys, function ($key) use ($allowedPatterns) {
            foreach ($allowedPatterns as $pattern) {
                if (preg_match($pattern, $key)) {
                    return false; // key is allowed, so don't include it in the filtered list
                }
            }
            return true; // key does not match any allowed pattern, so keep it
        });

        // If there are any remaining missing keys then they are unknown parameters.
        if (!empty($filteredMissingKeys)) {
            throw new \CodeException("The following parameters are unknown: " . implode(', ', $filteredMissingKeys) . ". Please check for spelling mistakes or contact the developer.");
        }

        foreach ($element as $paramKey => $value) {
            if (!isset(self::$FORM_RULES[$paramKey])) {
                continue;
            }

            $rules = self::$FORM_RULES[$paramKey];

            if ($value === '') {
                continue;
            }

            $value = trim($value);
            $escapedValue = OnString::trimQuote($value);
            $isValid = false;

            foreach ($rules as $rule) {
                // Rule: '' (No wrapper allowed)
                // If the rule is '', ensure the value is not wrapped by {{ }}
                // Example: 'value' is valid; '{{value}}' is invalid if NO_WRAPPER is the only rule in $rules
                if ($rule === '' && (!preg_match('/^\{\{[^{}]*}}$/s', $escapedValue) || count($rules) > 1)) {
                    $isValid = true;
                }

                // Rule: '{{' or '{{!' (Specific wrappers allowed)
                // Check if the value starts with the rule and ends with }}
                // Example for '{{': '{{value}}' is valid; '{{!value}}' is invalid
                // Example for '{{!': '{{!value}}' is valid; '{{value}}' is invalid
                elseif ($rule && preg_match('/^' . preg_quote($rule, '/') . '.*}}$/s', $escapedValue)) {
                    // Additional brace balance validation
                    $innerContent = substr($escapedValue, strlen($rule), -2); // Extract content inside the wrapper
                    if (OnString::validateBraceBalance($innerContent)) {
                        $isValid = true;
                    }
                }
            }
            if (!$isValid) {
                // turn '' into 'no wrapper' to make the exception less confusing
                $readableRules = array_map(fn($rule) => $rule === '' ? 'no wrapper' : $rule, $rules);
                throw new \UserFormException("{$paramKey} is not wrapped correctly. Allowed wrapper(s): " . implode(', ', $readableRules));
            }
        }
    }

    public static function buildToggleInput($feHTML, AbstractFormElement $fe): string {
        $disabled = '';
        if ($fe->form->formModeGlobal === F_MODE_READONLY || $fe->attributes[FE_MODE] === FE_MODE_READONLY) {
            $disabled = 'disabled="disabled"';
        }

        $dynamicUpdate = '';
        if ($fe->attributes[FE_DYNAMIC_UPDATE] === 'yes') {
            $dynamicUpdate = 'data-dynamic-Update="true"';
        }

        // Wrapper
        $toggleWrapperId = $fe->htmlAttributes[HTML_ATTR_ID] . HTML_ID_EXTENSION_TOGGLE_INPUT;

        $hidden = $fe->attributes[FE_INPUT_TOGGLE][FE_INPUT_TOGGLE_CONFIG_HIDDEN] ? 'qfq-toggle-hidden' : '';
        $checked = $fe->attributes[FE_INPUT_TOGGLE][FE_INPUT_TOGGLE_CONFIG_HIDDEN] ? '' : 'checked="true"';

        // default value for HTML
        $rawDefault = $fe->attributes[FE_INPUT_TOGGLE][FE_INPUT_TOGGLE_CONFIG_DEFAULT] ?? '';
        $defaultValue = htmlspecialchars($rawDefault, ENT_QUOTES);

        // label for HTML
        $rawLabel = $fe->attributes[FE_INPUT_TOGGLE][FE_INPUT_TOGGLE_CONFIG_LABEL] ?? '';
        $labelText = htmlspecialchars($rawLabel, ENT_QUOTES);

        // Layout wrapper with flexbox
        $html = <<<HTML
<div class="qfq-toggle-input-container">
    <div class="qfq-toggle-checkbox">
        <label class="checkbox">
            <input type="checkbox" class="toggle-input-checkbox" $dynamicUpdate $disabled $checked data-toggle-target="{$toggleWrapperId}" data-default-value="$defaultValue" />
            {$labelText}
        </label>
    </div>
    <div id="{$toggleWrapperId}" class="toggle-input-body $hidden" >
        {$feHTML}
    </div>
</div>
HTML;

        return $html;
    }
}