<?php
/***************************************************************
 *  Copyright notice
 *
 *  (c) 2010 Glowbase GmbH
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

namespace IMATHUZH\Qfq\Core\Report;

use IMATHUZH\Qfq\Core\FormBuilder;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Exception\RedirectResponse;
use IMATHUZH\Qfq\Core\Form\FeFactory;
use IMATHUZH\Qfq\Core\Form\Form;
use IMATHUZH\Qfq\Core\Form\FormElement\AbstractFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
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\Helper\Token;
use IMATHUZH\Qfq\Core\QuickFormQuery;
use IMATHUZH\Qfq\Core\Renderer\BaseRenderer;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Imap\Imap;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/*
 * a:AltText
 * A:Attribute
 * b:bootstrap [0|1|<button>] #  btn-info btn-small
 * B:bullet
 * c:class  [n|i|e|<class>]
 * C:checkbox    [name]
 * d:download
 * D:delete
 * e:encryption 0|1
 * E:edit
 * f:input (Render: <Form>.<FormElement>) | f:<email> ('m:' mailto) | f:<file> ('d:' for downloads, deprecated)
 * F:File
 * g:target
 * G:Glyph
 * h:http # redirection status code or enum: perm=308, temp=307, get=303
 * H:Help
 * i:icon (Font Awesome, t)
 * I:information
 * j:
 * J:IMAP actions # 'j' is meant as the 'i' from IMAP
 * k:
 * K:
 * l:
 * L:Monitor
 * m:mailto
 * M:Mode
 * n:GET/POST Rest Call
 * N:new
 * o:ToolTip
 * O:Sticky ToolTip
 * p:page
 * P:picture       [file]
 * q:question  <text>
 * Q:
 * r:render
 * R:right
 * s:sip
 * S:Show
 * t:text
 * T:Thumbnail
 * u:url
 * U:URL Param
 * v:Text before link
 * V:Text after link
 * w:websocket
 * W:Dimension
 * x:Delete
 * X:
 * y:Copy to clipboard
 * Y:   (Geplant in #11892 fuer 'order via tablesorter')
 * z:DropDown Menu
 * Z:
 *
 */

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

    const SUBMODE_PLAIN = 0;
    const SUBMODE_ENC_EMAIL = 100;
    const SUBMODE_AJAX = 200;
    const SUBMODE_INLINE_FILTER = 300;
    const SUBMODE_INLINE_EDIT = 400;

    /**
     * @var Sip
     */
    protected $sip = null;

    private $sipId = '';

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

    /**
     * @var Database
     */
    private $dbArray = null;

    /**
     * @var Thumbnail
     */
    private $thumbnail = null;

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

    private $phpUnit;
    private $renderControl = array();
//    private $linkClassSelector = array(TOKEN_CLASS_INTERNAL => "internal ", TOKEN_CLASS_EXTERNAL => "external ");
//    private $cssLinkClassInternal = '';
//    private $cssLinkClassExternal = '';
    private $ttContentUid = '';

    private $formSpec = array();

    private $defaultButtons = array();
    private $tooltipDebug = '';

    private $imap = array();

    private array $filterTargetRegistry = [];

    private $tableVarName = [
        TOKEN_URL => NAME_URL,
        TOKEN_MAIL => NAME_MAIL,
        TOKEN_PAGE => NAME_PAGE,
        TOKEN_UID => NAME_UID,
        TOKEN_SOURCE => NAME_SOURCE,
        TOKEN_DROPDOWN => NAME_DROPDOWN,
        TOKEN_DOWNLOAD => NAME_DOWNLOAD,
        TOKEN_DOWNLOAD_MODE => NAME_DOWNLOAD_MODE,
        TOKEN_TEXT => NAME_TEXT,
        TOKEN_ALT_TEXT => NAME_ALT_TEXT,
        TOKEN_BOOTSTRAP_BUTTON => NAME_BOOTSTRAP_BUTTON,
        TOKEN_TOOL_TIP => NAME_TOOL_TIP,
        TOKEN_PICTURE => NAME_IMAGE,
        TOKEN_BULLET => NAME_IMAGE,
        TOKEN_CHECK => NAME_IMAGE,
        TOKEN_DELETE => NAME_IMAGE,
        TOKEN_EDIT => NAME_IMAGE,
        TOKEN_HELP => NAME_IMAGE,
        TOKEN_INFO => NAME_IMAGE,
        TOKEN_NEW => NAME_IMAGE,
        TOKEN_SHOW => NAME_IMAGE,
        TOKEN_GLYPH => NAME_IMAGE,
        TOKEN_RENDER => NAME_RENDER,
        TOKEN_TARGET => NAME_TARGET,
        TOKEN_CLASS => NAME_LINK_CLASS,
        TOKEN_QUESTION => NAME_QUESTION,
        TOKEN_ENCRYPTION => NAME_ENCRYPTION,
        TOKEN_SIP => NAME_SIP,
        TOKEN_URL_PARAM => NAME_URL_PARAM,
        TOKEN_RIGHT => NAME_RIGHT,
        TOKEN_ACTION_DELETE => NAME_ACTION_DELETE,
        TOKEN_FILE => NAME_FILE,
        TOKEN_FILE_DEPRECATED => NAME_FILE,
        TOKEN_THUMBNAIL => NAME_THUMBNAIL,
        TOKEN_THUMBNAIL_DIMENSION => NAME_THUMBNAIL_DIMENSION,
        TOKEN_COPY_TO_CLIPBOARD => NAME_COPY_TO_CLIPBOARD,
        TOKEN_ATTRIBUTE => NAME_ATTRIBUTE,
        TOKEN_ORDER_TEXT => NAME_ORDER_TEXT,
        TOKEN_REDIRECT_HTTP_CODE => NAME_REDIRECT_HTTP_CODE,
        TOKEN_STICKY_TOOL_TIP => NAME_STICKY_TOOL_TIP,

        TOKEN_MONITOR => NAME_MONITOR,
        TOKEN_BEFORE_LINK => NAME_BEFORE_LINK,
        TOKEN_AFTER_LINK => NAME_AFTER_LINK,

        // The following don't need a renaming: already 'long'
        TOKEN_L_FILE => TOKEN_L_FILE,
        TOKEN_L_TAIL => TOKEN_L_TAIL,
        TOKEN_L_APPEND => TOKEN_L_APPEND,
        TOKEN_L_INTERVAL => TOKEN_L_INTERVAL,
        TOKEN_L_HTML_ID => TOKEN_L_HTML_ID,
        TOKEN_CACHE => NAME_CACHE,
        NAME_MAILTO_SELECT => NAME_MAILTO_SELECT,

        TOKEN_EXPORT => TOKEN_EXPORT,
        TOKEN_IMPORT => TOKEN_IMPORT,

        // Inline filter/edit tokens
        TOKEN_INPUT_UID => NAME_INLINE_UID,
        TOKEN_INPUT_TARGET_CLASS => NAME_INLINE_TARGET_CLASS,
    ];

    // Used to find double definitions.
    private $tokenMapping = [
        TOKEN_URL => LINK_ANCHOR,
        TOKEN_MAIL => LINK_ANCHOR,
        TOKEN_PAGE => LINK_ANCHOR,
        TOKEN_UID => LINK_ANCHOR,
        TOKEN_DOWNLOAD => LINK_ANCHOR,
        TOKEN_FILE => NAME_FILE,
        TOKEN_COPY_TO_CLIPBOARD => LINK_ANCHOR,

        TOKEN_PICTURE => LINK_PICTURE,
        TOKEN_BULLET => LINK_PICTURE,
        TOKEN_CHECK => LINK_PICTURE,
        TOKEN_DELETE => LINK_PICTURE,
        TOKEN_EDIT => LINK_PICTURE,
        TOKEN_HELP => LINK_PICTURE,
        TOKEN_INFO => LINK_PICTURE,
        TOKEN_NEW => LINK_PICTURE,
        TOKEN_SHOW => LINK_PICTURE,
        TOKEN_GLYPH => LINK_PICTURE,
    ];

    /**
     * __construct
     *
     * @param Sip $sip
     * @param string $dbIndexData
     * @param bool $phpUnit
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct(Sip $sip, $dbIndexData = DB_INDEX_DEFAULT, $phpUnit = false, $formSpec = array()) {

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
        $this->phpUnit = $phpUnit;

        if ($phpUnit) {
            $_SERVER['REQUEST_URI'] = 'localhost';
        }

        $this->formSpec = $formSpec;

        $this->sip = $sip;
        $this->store = Store::getInstance('', $phpUnit);
        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);

        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);
        if ($this->dbIndexData != $this->dbIndexQfq) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
        } else {
            $this->dbArray[$this->dbIndexQfq] = $this->dbArray[$this->dbIndexData];
        }

        $this->ttContentUid = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
        $this->dbIndexData = $dbIndexData;
        /*
         * mode:
         * 0: no output
         * 1: <span title='...'>text</span>   (no href)
         * 2: <span title='...'>url</span>    (no href)
         * 3: <a href=url>url</a>
         * 4: <a href=url>Text</a>
         * 5: text
         * 6: url
         * 8: SIP only - 's=badcaffee1234'
         * 9: http redirection with the link as the location header
         *
         *  r=render mode, u=url, t:text and/or image.
         *
         *                  [r][u][t] = mode
         */

        $this->renderControl[0][0][0] = 0;
        $this->renderControl[0][0][1] = 0;
        $this->renderControl[0][1][0] = 3;
        $this->renderControl[0][1][1] = 4;

        $this->renderControl[1][0][0] = 0;
        $this->renderControl[1][0][1] = 1;
        $this->renderControl[1][1][0] = 3;
        $this->renderControl[1][1][1] = 4;

        $this->renderControl[2][0][0] = 0;
        $this->renderControl[2][0][1] = 0;
        $this->renderControl[2][1][0] = 0;
        $this->renderControl[2][1][1] = 4;

        $this->renderControl[3][0][0] = 0;
        $this->renderControl[3][0][1] = 1;
        $this->renderControl[3][1][0] = 2;
        $this->renderControl[3][1][1] = 1;

        $this->renderControl[4][0][0] = 0;
        $this->renderControl[4][0][1] = 1;
        $this->renderControl[4][1][0] = 2;
        $this->renderControl[4][1][1] = 2;

        $this->renderControl[5][0][0] = 0;
        $this->renderControl[5][0][1] = 0;
        $this->renderControl[5][1][0] = 0;
        $this->renderControl[5][1][1] = 0;

        $this->renderControl[6][0][0] = 0;
        $this->renderControl[6][0][1] = 5;
        $this->renderControl[6][1][0] = 0;
        $this->renderControl[6][1][1] = 5;

        $this->renderControl[7][0][0] = 0;
        $this->renderControl[7][0][1] = 0;
        $this->renderControl[7][1][0] = 6;
        $this->renderControl[7][1][1] = 6;

        $this->renderControl[8][0][0] = 0;
        $this->renderControl[8][0][1] = 0;
        $this->renderControl[8][1][0] = 8;
        $this->renderControl[8][1][1] = 8;

        $this->renderControl[9][0][0] = 0;
        $this->renderControl[9][0][1] = 0;
        $this->renderControl[9][1][0] = 9;
        $this->renderControl[9][1][1] = 9;

        $this->defaultButtons = [
            FORM_BUTTON_SAVE => [
                FINAL_IS_LINK => false,
                'buttonHtmlId' => 'save-button'
            ],
            FORM_BUTTON_CLOSE => [
                FINAL_IS_LINK => false,
                'buttonHtmlId' => 'close-button'
            ],
            FORM_BUTTON_DELETE => [
                FINAL_IS_LINK => false,
                'buttonHtmlId' => 'delete-button'
            ],
            FORM_BUTTON_NEW => [
                FINAL_IS_LINK => true,
                'buttonHtmlId' => 'form-new-button'
            ],
            FORM_BUTTON_FORM_EDIT => [
                FINAL_IS_LINK => true,
                'buttonHtmlId' => 'form-edit-button'
            ],
            FORM_BUTTON_FORM_VIEW => [
                FINAL_IS_LINK => true,
                'buttonHtmlId' => 'form-view-' . ($this->formSpec[F_ID] ?? 0)
            ],
            FORM_BUTTON_HISTORY => [
                FINAL_IS_LINK => false,
                'buttonHtmlId' => 'form-history'
            ]
        ];
    }

    /**
     * In render mode 3,4,5 there is no '<a href ...>'. Nevertheless, tooltip and BS Button should be displayed.
     * Do this by applying a '<span>' attribute around the text.
     *
     * @param array $vars
     * @param       $keyName
     *
     * @return mixed|string
     * @throws \CodeException
     */
    private function wrapLinkTextOnly(array $vars, $keyName) {
        $disabledClass = 'disabled';
        $text = $vars[$keyName];
        if ($vars[NAME_BOOTSTRAP_BUTTON] == '' && $vars[FINAL_TOOL_TIP] == '' && $vars[NAME_ATTRIBUTE] == '') {
            return $text;
        }

        $attributes = Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
        if ($vars[NAME_ATTRIBUTE] != '') {
            $attributes .= $vars[NAME_ATTRIBUTE] . ' ';
        }

        if (isset($vars[NAME_STICKY_TOOL_TIP])) {
            $disabledClass .= ' ' . CLASS_TRIGGER_STICKY_TOOLTIP;
        }

        if ($vars[NAME_BOOTSTRAP_BUTTON] != '') {
            $attributes .= Support::doAttribute('class', [$vars[NAME_BOOTSTRAP_BUTTON], $disabledClass]);
        }

        if (isset($vars[FLAG_COPY_TO_CLIPBOARD])) {
            return $text;
        }


        return Support::wrapTag("<span $attributes>", $text);
    }

    /**
     * Renders a (BS)-Dropdown menu.
     *
     * @param string $str
     *      'z|t:menu|b|o:click me'  - the menu button to click on. A text 'menu', a BS button, a tooltip 'click me'.
     *      '||p:detail&pId=1&s|t:Person 1'     - Page id=detail with pId=1 will be opened in the browser.
     *      '||d:file.pdf|p:detail&pId=1&_sip=1||t:Person as PDF' - Page id=detail with pId=1 will downloaded as a PDF.
     *
     * @param array $rcMenuEntryStrArr
     * @return string
     */
    private function processDropdown($str, array &$rcMenuEntryStrArr) {

        $rcMenuEntryStrArr = array();
        $tokenCollect = array();

        // Split 'z|t:menu|b|o:click me||p:detail&pId=1&s|t:Person 1||...'. Add '||' to take care that the last element is flushed.
        $paramArr = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str . '||');

        // Iterate over token. Find delimiter to separate dropdown definition and all individual menu entries.
        foreach ($paramArr as $tokenStr) {
            $tokenArr = explode(PARAM_TOKEN_DELIMITER, $tokenStr, 2);
            switch ($tokenArr[0] ?? '') {
                // Indicator to start menu entry: force a flush of existing token and start a new round.
                case '':
                    // New menu entry.
                    if (!empty($tokenCollect)) {
                        $rcMenuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);
                    }
                    $tokenCollect = array();
                    break;

                default:
                    $tokenCollect[] = $tokenStr;
            }
        }

        // First entry is the dropdown button, all others are the menu entries
        $dropdownButtonStr = array_shift($rcMenuEntryStrArr);

        return $dropdownButtonStr;
    }

    /**
     * Renders a inline edit report button.
     *
     * @param string $str
     *      'i:uid=12.
     *
     * @param array $rcMenuEntryStrArr
     * @return string
     */
    private function processInlineEdit($str) {
        // Get uid
        $param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
        $uidArr = KeyValueStringParser::explodeEscape('=', $param[TOKEN_INLINE_EDIT]);
        if ($uidArr[0] !== COLUMN_UID) {
            // Exception missing uid parameter
            throw new \UserReportException("Missing or invalid uid parameter name: $uidArr[0]", ERROR_MISSING_VALUE);
        }

        $uid = $uidArr[1];
        if ($uid === '' || !is_numeric($uid)) {
            // Exception empty or non-numeric uid value
            throw new \UserReportException("Missing or invalid uid value: $uid ", ERROR_MISSING_VALUE);
        }

        # Define inline editor theme
        $systemInlineReportDarkTheme = $this->store->getVar(SYSTEM_EDIT_INLINE_REPORT_DARK_THEME, STORE_SYSTEM, SANITIZE_ALLOW_ALLBUT);
        $editorTheme = $systemInlineReportDarkTheme ? 'qfqdark' : 'default';

        // Build inline report editing
        return QuickFormQuery::buildInlineReport($uid, null, $this->dbArray[$this->dbIndexQfq], null, $param[TOKEN_BOOTSTRAP_BUTTON] ?? '', $param[TOKEN_TEXT] ?? '', $param[TOKEN_TOOL_TIP] ?? '', $editorTheme);
    }

    /**
     * https://getbootstrap.com/docs/3.4/components/#dropdowns
     *
     * Start
     * <span class="dropdown">
     *   <span class="glyphicon glyphicon-option-vertical dropdown-toggle" id="dropdownMenu11" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
     *   </span>
     *   <ul class="dropdown-menu" aria-labelledby="dropdownMenu11">
     *    <li><a href="#">Action</a></li>
     *    <li><a href="#">Another action</a></li>
     *    <li><a href="#">Something else here</a></li>
     *    <li role="separator" class="divider"></li>
     *    <li><a href="#">Separated link</a></li>
     *   </ul>
     * </span>
     *
     * <div class="btn-group">
     *   <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
     *     <span class="glyphicon glyphicon-option-vertical"></span>Button text
     *   </button>
     *   <ul class="dropdown-menu" aria-labelledby="dropdownMenu11">
     *    <li><a href="#">Action</a></li>
     *    <li><a href="#">Another action</a></li>
     *    <li><a href="#">Something else here</a></li>
     *    <li role="separator" class="divider"></li>
     *    <li><a href="#">Separated link</a></li>
     *   </ul>
     * </div>
     * End
     *
     * @param array $menuEntryStrArr
     * @param $htmlId
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function renderDropdownUl(array $menuEntryStrArr, $htmlId) {
        $li = '';

        foreach ($menuEntryStrArr as $str) {
            $attribute = '';

            $link = $this->renderLink($str);

            switch (substr($link, 0, 3)) {
                case '---':
                    $link = substr($link, 3);
                    if ($link == '') {
                        # Separator
                        $attribute = ' role="separator" class="divider"';
                    } else {
                        # Disabled
                        $attribute = ' class="disabled"';
                        if (false === strstr($link, '<a ')) {
                            # If there is no '<a>'-tag, the 'disabled' class is broken - set a fake one.
                            $link = Support::wrapTag('<a href="#">', $link);
                        }
                    }
                    break;

                case '===':
                    // Header
                    $link = substr($link, 3);
                    $attribute = ' class="dropdown-header"';
                    break;

                default:
                    break;
            }
            // Menu entries
            $li .= '<li' . $attribute . '>' . $link . '</li>';
        }

        // Wrapped Menu entries
        return Support::wrapTag('<ul style="max-height: 70vh; overflow-y: auto" class="dropdown-menu" aria-labelledby="' . $htmlId . '">', $li);
    }

    /**
     * @param $str
     * @return string
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function processWebSocket($str) {

        $websocket = new WebSocket();

        $answer = '';

        // str="w:wss://antmedia.math.uzh.ch:6334/test|t:<payload>|timeout:..."
        $param = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
        if (empty($param[TOKEN_WEBSOCKET]) || empty($param[TOKEN_TEXT])) {
            throw new \UserReportException("Missing Websocket target or text to send", ERROR_MISSING_VALUE);
        }

        $urlParts = parse_url($param[TOKEN_WEBSOCKET]);
        $urlParts = array_merge(['scheme' => 'ws', 'host' => '', 'port' => 80, 'path' => ''], $urlParts);
        if (empty($urlParts['host'])) {
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Target URL incomplete',
                    ERROR_MESSAGE_TO_DEVELOPER =>
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
                , ERROR_MISSING_VALUE);
        }

        // Check for wss >> ssl
        if ($urlParts['scheme'] == 'wss') {
            $urlParts['host'] = 'ssl://' . $urlParts['host'];
            if ($urlParts['port'] == 0) {
                $urlParts['port'] = 443;
            }
        }

        // Open Socket
        $errorMsg = '';
        if (false === $websocket->connect($urlParts['host'], $urlParts['port'], $urlParts['path'], '', $errorMsg)) {
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Failed connect websocket: ' . $errorMsg,
                    ERROR_MESSAGE_TO_DEVELOPER =>
                        'host: ' . $urlParts['host'] . ', ' .
                        'port: ' . $urlParts['port'] . ', ' .
                        'path: ' . $urlParts['path']])
                , ERROR_MISSING_VALUE);
        }

        $answer = $websocket->sendData($param[TOKEN_TEXT]);

        return $answer;
    }

    /**
     * @param $imapAccount
     * @return int|mixed|string|null
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function getFirstImapAccount($imapAccount = '') {

        if (empty($imapAccount)) {

            $emailSync = $this->store->getVar(SYSTEM_EMAIL_SYNC, STORE_SYSTEM);
            # $emailSync: account1:Inbox,Sent|account2:done
            if (!empty($emailSync)) {
                $args = KeyValueStringParser::parse($emailSync, PARAM_VAR_DELIMITER, PARAM_DELIMITER);
                $imapAccount = array_key_first($args);
                if (empty($imapAccount)) {
                    $imapAccount = '';
                }
            }
        }
        return $imapAccount;
    }

    /**
     * @param string $str - 'J:<action>:[<arg>]:[<srcFolder>]:[<srcId>]:[<account>]' AS _link`
     * @return string
     */

    private function processImap(string $str): string {

        $args = KeyValueStringParser::explodeEscape(PARAM_VAR_DELIMITER, $str);
        $args = array_merge($args, ['', '', '', '', '', '']);

        $imapAccount = $this->getFirstImapAccount($args[INDEX_IMAP_ACCOUNT] ?? '');
        if (!isset($this->imap[$imapAccount])) {
            $this->imap[$imapAccount] = new Imap($imapAccount, $this->dbArray, $this->dbIndexQfq, $args[INDEX_IMAP_ACTION] === 'checkAccount');
        }

        return $this->imap[$imapAccount]->process($args);
    }

    /**
     * Build the whole link.
     *
     * @param string $str Qualifier with params. 'report'-syntax. F.e.:  u:www.example.com|P:home.gif|t:Home"
     * @param string $strDefault Same as $str, but might give some defaults if corresponding values in $str are missing.
     * @param array $rcExplicit Return some explicit data like TOOLTIP_DEBUG
     * @return string The complete HTML encoded Link like or with r>=6 some pure data (value, sip, redirect)
     *           <a href='http://example.com' class='...'><img src='icon.gif' title='help text'>Description</a>
     * @throws RedirectResponse
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function renderLink($str, $strDefault = '', &$rcExplicit = array()) {

        $tokenGiven = array();
        $link = "";
        $ddHtmlId = '';
        $rcMenuEntryStrArr = array();
        $vars = $this->initVars();

        if (empty($str) && empty($strDefault)) {
            return '';
        }

        // New tokens can be long. This gives more possibilities.
        // Default token is the first char
        $token = $str[0] ?? '';
        // Handle longer tokens
        if (str_starts_with($str, 'import') || str_starts_with($str, 'export')) {
            $token = substr($str, 0, 6);
        }

        // Special cases
        switch ($token) {
            case TOKEN_DROPDOWN:
                $ddHtmlId = Support::uniqIdQfq('dd_');
                // Parse and split $str to  '$rcMenuEntryStrArr' and 'remaining button'
                $str = $this->processDropdown($str, $rcMenuEntryStrArr);
                break;

            case TOKEN_WEBSOCKET:
                return $this->processWebSocket($str);

            case TOKEN_IMPORT:
            case TOKEN_EXPORT:
                $str = $this->processSyncByRule($vars, $str);
                break;

            case TOKEN_REST_CLIENT:
                $restClient = new RestClient();
                return $restClient->process($str);

            case TOKEN_INLINE_EDIT:
                return $this->processInlineEdit($str);

            case TOKEN_INPUT_FORM_ELEMENT:
                return $this->processInlineFormElement($str);

            case TOKEN_IMAP:
                return $this->processImap($str);
            default:
                break;
        }

        // General processing
        $vars = $this->fillParameter($vars, $str, $tokenGiven, $strDefault);
        $vars = $this->processParameter($vars, $tokenGiven, $str);

        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $tokenGiven[TOKEN_DOWNLOAD] === true) {
            $this->store->setVar(SYSTEM_DOWNLOAD_POPUP, DOWNLOAD_POPUP_REQUEST, STORE_SYSTEM);
        }

        if (isset($tokenGiven[TOKEN_DELETE]) && $tokenGiven[TOKEN_DELETE] === true) {
            $vars[NAME_BOOTSTRAP_BUTTON] = "btn btn-default";
        }

        if (($vars[NAME_DROPDOWN] ?? '') == '1') {
            $ul = '';
            // Render menu items only if the menu is active.
            if ($vars[NAME_RENDER] == RENDER_MODE_0_LINK) {
                $ul = $this->renderDropdownUl($rcMenuEntryStrArr, $ddHtmlId);
            }

            // Tooltip and attributes
            $attributes = Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
            if ($vars[NAME_ATTRIBUTE] != '') {
                $attributes .= $vars[NAME_ATTRIBUTE] . ' ';
            }

            // Bootstrap or plain
            if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
                $tag = '<span class="dropdown" ' . $attributes . '>';
            } else {
                $tag = '<div class="btn-group" ' . $attributes . '>';
            }

            return Support::wrapTag($tag, $vars[FINAL_CONTENT] . $ul);
        }

        // Compute the rendering mode:
        //   0-8     plain URL or email
        //     9     http redirection
        //   100-104 encrypted email
        //   200-204 deleted // ajax
        list($mode, $submode) = $this->getModeRender($vars, $tokenGiven);

        if ($submode !== self::SUBMODE_PLAIN && $mode > 4) {
            throw new \UserReportException ("Mode not implemented. Internal render mode=$mode, submode=$submode", ERROR_UNKNOWN_MODE);
        } elseif ($submode === self::SUBMODE_AJAX && $mode > 0) {
            //TODO: Alter Code, umstellen auf JS Client von RO. Vorlage koennte 'Delete' in Subrecord sein.
            $link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',sip:'',forward:'" .
                $vars[NAME_PAGE] . "'});\" " . $vars[NAME_LINK_CLASS] . ">" . $vars[NAME_TEXT] . "</a>";
        } else switch ($mode) {
            // 0: No Output
            case 0:
                break;

            // 1: 'text'
            case 1:
                $link = $this->wrapLinkTextOnly($vars, FINAL_CONTENT);
                break;

            // 2: 'url'
            case 2:
                $link = $this->wrapLinkTextOnly($vars, FINAL_HREF);
                break;

            // 3: <a href=url ...>url</a>
            case 3:
                // In caes of mailto link (mode=3 and no 'text'): take care that there is no 'mailto:...' at the start
                $href = ($vars[NAME_MAIL] == '') ? $vars[FINAL_HREF] : $vars[NAME_MAIL];
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $href);
                break;

            // 4: <a href=url ...>Text</a>
            case 4:
                $link = Support::wrapTag($vars[FINAL_ANCHOR], $vars[FINAL_CONTENT]);
                break;

            // 5: plain text, no <span> around
            case 5:
                $link = $vars[NAME_TEXT];
                break;

            // 6: plain url, no <span> around
            case 6:
                $link = $vars[FINAL_HREF];
                break;

            case 8:
                $link = $this->sipId;
                break;

            // 9: trigger a redirection
            case 9:
                throw new RedirectResponse($vars[FINAL_HREF], $vars[FINAL_HTTP_CODE]);

            default:
                throw new \UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
        }

        $rcExplicit[TOOLTIP_DEBUG] = $this->tooltipDebug;

        // Email Select
        if (!empty($vars[NAME_MAILTO_SELECT])) {
            $emailList = explode(',', $vars[NAME_MAILTO_SELECT]);
            $emailJson = htmlspecialchars(json_encode($emailList), ENT_QUOTES, 'UTF-8');

            if (isset($tokenGiven[TOKEN_BOOTSTRAP_BUTTON])) {
                // Bootstrap Button Group
                $link .= '<a href="#" class="btn btn-default email-popup-trigger" title="Select recipients" data-emails="' . $emailJson . '"><i class="fa fa-check-square"></i></a>';
                $link = Support::wrapTag('<div class="btn-group" role="group">', $link);
            } else {
                // Normal Link
                $link .= '<a href="#" class="email-popup-trigger" style="margin: 5px;" title="Select recipients" data-emails="' . $emailJson . '"><i class="far fa-check-square"></i></a>';
            }
        }

        // Merge span to link if something exists from qualifier 'Y'
        return $vars[NAME_BEFORE_LINK] . ($vars[NAME_ORDER_TEXT_WRAP] ?? '') . $link . $vars[NAME_AFTER_LINK];
    }

    /**
     * $str and $strDefault are standard QFQ link format strings like 'p:{{pageAlias:T}}|t:linktext|b|s|...'
     * Parameter missing in $str and given in $strDefault will be used.
     *
     * Split Parameter string in num Array (assoc is not possible cause for 'download', multiple sources with same key are possible).
     * Reorder param to bring prio token (currently only 'd') to top.
     *
     * @param $str
     * @param $strDefault
     * @return array
     */
    private function paramPreparation($str, $strDefault = '') {
        $prio = array();
        $regular = array();
        $toolTipParamGiven = false;
        $toolTipParamGivenKey = false;

        // str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no"
        // Return a numbered array with strings like [ 0 => 'p:..', 1 => 't:...' , ...]
        $param = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);

        // Return an assoc array like [ 'd' => 'file.pdf', 'p' => 'content', ... ]
        $assocDefault = KeyValueStringParser::explodeKvpSimple($strDefault, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);

        foreach ($param as $value) {

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

            $arr = explode(PARAM_TOKEN_DELIMITER, $value, 2);
            $key = $arr[0];
            $value = $arr[1] ?? '';

            // Allow multiple '|o:' params but concat values to a single ToolTip.
            if ($key == TOKEN_TOOL_TIP) {
                // First ToolTip
                if (!$toolTipParamGiven) {
                    $toolTipParamGiven = true;
                    $toolTipParamGivenKey = count($regular);
                } else {
                    // All other ToolTips
                    $regular[$toolTipParamGivenKey][TOKEN_TOOL_TIP] .= $value;
                    continue;
                }
            }

            if ($key == TOKEN_DOWNLOAD) {
                $prio[] = [$key => $value ?? ''];
            } else {
                $regular[] = [$key => $value ?? ''];
            }

            // Explicit given arg: remove from default
            if (isset($assocDefault[$key])) {
                unset ($assocDefault[$key]);
            }
        }

        // Apply defaults, if not already given

        // First check if there is a prio item - currently only TOKEN_DOWNLOAD is of this type.
        if (isset($assocDefault[TOKEN_DOWNLOAD])) {
            $prio[] = [TOKEN_DOWNLOAD => $assocDefault[TOKEN_DOWNLOAD]];
            unset ($assocDefault[TOKEN_DOWNLOAD]);
        }
        // Append all remaining defaults to regular
        foreach ($assocDefault as $key => $value) {
            $regular[] = [$key => $value];
        }

        return array_merge($prio, $regular);
    }

    /**
     * Iterate over all given token. Check for double definition.
     *
     * @param $vars
     * @param string $str
     * @param array $rcTokenGiven - return an array with found token.
     *
     * @param string $strDefault
     * @return array
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function fillParameter($vars, $str, array &$rcTokenGiven, $strDefault = '') {

        $rcTokenGiven = array();
        if ($vars == array()) {
            // Define all possible vars: no more isset().
            $vars = $this->initVars();
        }

        $flagArray = array();

        $items = $this->paramPreparation($str, $strDefault);
        $skipQualifierCheck = false;

        // Parse all parameter, fill variables.
        foreach ($items as $item) {

            $value = reset($item);
            $key = key($item);

            // Skip empty entries
            if (empty($key)) {
                continue;
            }

            // Bookkeeping defined parameter.
            if (isset($rcTokenGiven[$key])) {
                throw new \UserReportException ("Multiple definitions for key '$key'", ERROR_MULTIPLE_DEFINITION);
            }

            $rcTokenGiven[$key] = true;

            if (!isset($this->tableVarName[$key]) && $skipQualifierCheck === false) {
                $msg[ERROR_MESSAGE_TO_USER] = "Unknown link qualifier: '$key' - did you forget the one character qualifier?";
                $msg[ERROR_MESSAGE_TO_DEVELOPER] = $str;
                throw new \UserReportException (json_encode($msg), ERROR_UNKNOWN_LINK_QUALIFIER);
            }

            $keyName = $this->tableVarName[$key]; // convert token to name

            // if ($key == TOKEN_PAGE && ($value == '' || $value[0]='&')) {
            if ($key == TOKEN_PAGE && $value == '') {
                // If no pageid|pagealias|pageSlug is defined, take current page (added onstring to remove first /)
                $value = $this->store->getVar((T3Handler::useSlugsInsteadOfPageAlias() ? TYPO3_PAGE_SLUG : TYPO3_PAGE_ID), STORE_TYPO3);
            }
            $value = Token::checkForEmptyValue($key, $value);

            $value = $this->checkValue($key, $value);

            // Store value
            if ((isset($rcTokenGiven[TOKEN_DOWNLOAD]) || isset($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD])) &&
                ($key == TOKEN_PAGE || $key == TOKEN_URL || $key == TOKEN_URL_PARAM || $key == TOKEN_UID
                    || $key == TOKEN_SOURCE || $key == TOKEN_FILE || $key == TOKEN_FILE_DEPRECATED )) {

                $vars[NAME_COLLECT_ELEMENTS][] = $key . ':' . $value;

                unset($rcTokenGiven[$key]); // Skip Bookkeeping for TOKEN_URL_PARAM | TOKEN_FILE | TOKEN_URL.
                continue;
            } else {
                // TOKEN_GLYPH should not be treated as a regular image. Same applies to the other Glyph symbols,
                // but those don't have a value, and therefore do not fill $vars['image'].
                if ($key != TOKEN_GLYPH) {
                    $vars[$keyName] = $value;
                }
            }

            // Check for double anchor or picture definition.
            if (isset($this->tokenMapping[$key])) {
                $type = $this->tokenMapping[$key];

                if (isset($flagArray[$type])) {
                    throw new \UserReportException ("Multiple definitions of url/mail/page/download or picture", ERROR_MULTIPLE_DEFINITION);
                }
                $flagArray[$type] = true;

//                if ($type === LINK_PICTURE) {
//                    $build = 'build' . strtoupper($keyName[0]) . substr($keyName, 1);
//                    $this->$build($vars, $value);
//                }
            }

            switch ($key) {
                case TOKEN_RENDER:
                    // If not a number, then the default render mode (0) is used
                    $vars[$keyName] = intval($value);
                    break;
                case TOKEN_URL:
                    $vars = $this->buildUrl($vars, $value);
                    break;
                case TOKEN_MAIL:
                    $vars = $this->buildMail($vars, $value);
                    break;
                case TOKEN_PAGE:
                    if (array_key_exists($value, $this->defaultButtons) && $this->defaultButtons[$value][FINAL_IS_LINK] === false) {
                        $vars = $this->buildDefaultButtonId($vars, $value);
                        break;
                    }
                    if (array_key_exists($value, $this->defaultButtons) && $this->defaultButtons[$value][FINAL_IS_LINK] === true) {
                        $vars = $this->buildDefaultButtonLink($vars, $value);
                        break;
                    }
                    $vars = $this->buildPage($vars, $value, $rcTokenGiven);
                    break;
                case TOKEN_COPY_TO_CLIPBOARD:
                    $vars = $this->buildCopyToClipboard($vars, $value);
                    break;
                case TOKEN_DOWNLOAD:
                    $vars = $this->buildDownload($vars, $value);
                    break;
                case TOKEN_DROPDOWN:
                    $vars = $this->buildDropdown($vars, $value);
                    break;
                case TOKEN_TOOL_TIP:
                    $vars = $this->buildToolTip($vars, $value);
                    break;
                case TOKEN_PICTURE:
                    $vars = $this->buildPicture($vars, $value);
                    break;
                case TOKEN_BULLET:
                    $vars = $this->buildBullet($vars, $value);
                    break;
                case TOKEN_CHECK:
                    $vars = $this->buildCheck($vars, $value);
                    break;
                case TOKEN_DELETE:
                    $vars = $this->buildDeleteIcon($vars, $value);
                    break;
                case TOKEN_ACTION_DELETE:
                    $vars = $this->buildActionDelete($vars, $value);
                    break;
                case TOKEN_EDIT:
                    $vars = $this->buildEdit($vars, $value);
                    break;
                case TOKEN_HELP:
                    $vars = $this->buildHelp($vars, $value);
                    break;
                case TOKEN_INFO:
                    $vars = $this->buildInfo($vars, $value);
                    break;
                case TOKEN_NEW:
                    $vars = $this->buildNew($vars, $value);
                    break;
                case TOKEN_SHOW:
                    $vars = $this->buildShow($vars, $value);
                    break;
                case TOKEN_FILE:
                case TOKEN_FILE_DEPRECATED:
                    $vars = $this->buildFile($vars, $value);
                    break;
                case TOKEN_GLYPH:
                    $vars = $this->buildGlyph($vars, $value);
                    break;
                case TOKEN_BOOTSTRAP_BUTTON:
                    $vars = $this->buildBootstrapButton($vars, $value);
                    break;
                case TOKEN_ORDER_TEXT:
                    $vars = $this->buildOrderText($vars, $value);
                    break;
                case TOKEN_BEFORE_LINK:
                    $vars = $this->buildTextBeforeLink($vars, $value);
                    break;
                case TOKEN_AFTER_LINK:
                    $vars = $this->buildTextAfterLink($vars, $value);
                    break;
                case TOKEN_CACHE:
                    $vars = $this->buildCacheDate($vars, $value);
                    break;
                case TOKEN_REDIRECT_HTTP_CODE:
                    $vars = $this->buildRedirectHttpCode($vars, $value);
                    break;
                case NAME_MAILTO_SELECT:
                    $vars[NAME_MAILTO_SELECT] = 1;
                    break;
                default:
                    break;
            }

            if ($key == TOKEN_IMPORT || $key == TOKEN_EXPORT) {
                // After import/export no further processing.
                $skipQualifierCheck = true;
            }
        }

        // Download Link needs some extra work
        if ($rcTokenGiven[TOKEN_DOWNLOAD] ?? false) {
            $vars = $this->buildDownloadLate($vars);
        }

        // CopyToClipboard (Download) Link needs some extra work
        if ($rcTokenGiven[TOKEN_COPY_TO_CLIPBOARD] ?? false) {
            $vars = $this->buildCopyToClipboardLate($vars);
        }

        // Dropdown Link needs some extra work
        if ($rcTokenGiven[TOKEN_DROPDOWN] ?? false) {
            $vars = $this->buildDropdownLate($vars);
        }

        if ($rcTokenGiven[TOKEN_IMPORT] ?? $rcTokenGiven[TOKEN_EXPORT] ?? false) {
            $vars = $this->processSyncByRule($vars, '', false);
        }

        // Check for special default setting.
        if ($vars[NAME_SIP] === false) {
            $vars[NAME_SIP] = "0";
        }

        // Final Checks
        $this->checkParam($rcTokenGiven, $vars);

        return $vars;
    }

    /**
     * Cleans and make existing the standard vars used every time to render a link.
     *
     * @return array
     */
    protected function initVars() {

        return [
            NAME_MAIL => '',
            NAME_URL => '',
            NAME_PAGE => '',

            NAME_TEXT => '',
            NAME_ALT_TEXT => '',
            NAME_BOOTSTRAP_BUTTON => '',
            NAME_IMAGE => '',
            NAME_IMAGE_TITLE => '',
            NAME_GLYPH => '',
            NAME_GLYPH_TITLE => '',
            NAME_QUESTION => '',
            NAME_TARGET => '',
            NAME_TOOL_TIP => '',
            NAME_TOOL_TIP_JS => '',
            NAME_URL_PARAM => '',
            NAME_EXTRA_CONTENT_WRAP => '',
            NAME_DOWNLOAD_MODE => '',
            NAME_COLLECT_ELEMENTS => array(),
            NAME_COPY_TO_CLIPBOARD => '',
            NAME_ATTRIBUTE => '',
            NAME_ATTRIBUTE2 => '',
            NAME_ORDER_TEXT => '',

            NAME_RENDER => RENDER_MODE_0_LINK,
            NAME_RIGHT => 'l',
            NAME_SIP => false,
            NAME_ENCRYPTION => '0',
            NAME_DELETE => '',

            NAME_MONITOR => '0',

            NAME_LINK_CLASS => '', // class name
            NAME_LINK_CLASS_DEFAULT => '', // Depending of 'as page' or 'as url'. Only used if class is not explicit set.

            NAME_ACTION_DELETE => '',

            NAME_BEFORE_LINK => '',
            NAME_AFTER_LINK => '',

            // Inline filter/edit vars
            NAME_INLINE_FORM_ELEMENT => '',
            NAME_INLINE_UID => '',
            NAME_INLINE_TARGET_CLASS => '',
            NAME_INLINE_TYPE => '',

            FINAL_HREF => '',
            FINAL_RENDER_HREF => true,
            FINAL_CONTENT => '',
            FINAL_SYMBOL => '',
            FINAL_TOOL_TIP => '',
            FINAL_CLASS => '',
            FINAL_QUESTION => '',
            FINAL_THUMBNAIL => '',
            FINAL_BUTTON_ID => '',
            FINAL_JS => '',
            FINAL_IS_LINK => true,
            FINAL_NO_REDIRECT => false,
            FINAL_HTTP_CODE => HTTP_REDIRECT_DEFAULT
        ];
    }

    /**
     * Validate value for token
     *
     * @param $key
     * @param $value
     *
     * @return mixed
     * @throws \UserReportException
     */
    private function checkValue($key, $value) {

        switch ($key) {
            case TOKEN_ENCRYPTION:
            case TOKEN_SIP:
                if ($value !== '0' && $value !== '1') {
                    throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
                }
                break;
            case TOKEN_ACTION_DELETE:
                switch ($value) {
                    case TOKEN_ACTION_DELETE_AJAX:
                    case TOKEN_ACTION_DELETE_REPORT:
                    case TOKEN_ACTION_DELETE_CLOSE:
                        break;
                    default:
                        throw new \UserReportException ("Invalid value for token '$key': '$value''", ERROR_INVALID_VALUE);
                }
                break;
            case TOKEN_BOOTSTRAP_BUTTON:
                if ($value == '1') {
                    $value = 'btn-default';
                }
                break;
            default:
        }

        return $value;
    }

    /**
     * Check for double definition.
     *
     * @param array $tokenGiven
     *
     * @param array $vars
     * @throws \UserReportException
     */
    private function checkParam(array $tokenGiven, array &$vars) {
        $countLinkAnchor = 0;
        $countLinkPicture = 0;
        $countSources = 0;

        foreach ($tokenGiven as $token => $value) {
            if (isset($this->tokenMapping[$token])) {
                switch ($this->tokenMapping[$token]) {
                    case LINK_ANCHOR:
                        $countLinkAnchor++;
                        break;
                    case LINK_PICTURE:
                        $countLinkPicture++;
                        break;
                    default:
                        break;
                }
            }
        }

        if ($countLinkAnchor > 1) {
            throw new \UserReportException ("Multiple URL / PAGE / MAILTO or COPY_TO_CLIPBOARD definition", ERROR_MULTIPLE_URL_PAGE_MAILTO_DEFINITION);
        }

        if ($countLinkPicture > 1) {
            throw new \UserReportException ("Multiple definitions for token picture/bullet/check/edit...delete'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION);
        }

        if (isset($tokenGiven[TOKEN_MAIL]) && isset($tokenGiven[TOKEN_TARGET])) {
            throw new \UserReportException ("Token Mail and Target at the same time not possible'" . TOKEN_PAGE . "'", ERROR_MULTIPLE_DEFINITION);
        }

        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $vars[NAME_SIP] == "1" && count($vars[NAME_COLLECT_ELEMENTS]) == 0) {
            throw new \UserReportException ("Missing element sources for download", ERROR_MISSING_REQUIRED_PARAMETER);
        }

        if (isset($tokenGiven[TOKEN_DOWNLOAD]) && $vars[NAME_SIP] == "0" && count($vars[NAME_COLLECT_ELEMENTS]) > 0) {
            throw new \UserReportException ("For persistent downloads, sources are not necessary/allowed in the link definition. Instead, please define the sources in QFQ extension setup > file.", ERROR_MISSING_REQUIRED_PARAMETER);
        }

        if (isset($tokenGiven[TOKEN_ACTION_DELETE])) {
            $this->checkDeleteParam($vars);
        }
    }

    /**
     * Compute final link parameter.
     *
     * @param array $vars
     * @param array $tokenGiven
     * @param string $devTooltipAppend
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function processParameter(array $vars, array $tokenGiven, string $devTooltipAppend): array {

        if (!isset($this->defaultButtons[$vars[NAME_PAGE]]) || !$this->defaultButtons[$vars[NAME_PAGE]][FINAL_IS_LINK]) {
            $vars[FINAL_HREF] = $this->doHref($vars, $tokenGiven, $devTooltipAppend); // must be called before doToolTip()
        }
        $vars[FINAL_TOOL_TIP] = $this->doToolTip($vars);
        $vars[FINAL_CLASS] = $this->doCssClass($vars);
        $vars[FINAL_SYMBOL] = $this->doImage($vars) . $this->doGlyph($vars);
        $vars[FINAL_THUMBNAIL] = $this->doThumbnail($vars);
        $vars[FINAL_CONTENT] = $this->doContent($vars, $vars[FINAL_CONTENT_PURE]); // must be called after doSymbol()
        $vars[FINAL_QUESTION] = $this->doQuestion($vars);
        $vars[FINAL_ANCHOR] = $this->doAnchor($vars);

        return $vars;
    }

    /**
     * Determine DownloadMode: explicit given or detected by given download sources.
     * Do some basic checks if parameter are correct.
     *
     * @param array $vars
     *
     * @return string - DOWNLOAD_MODE_PDF | DOWNLOAD_MODE_ZIP | DOWNLOAD_MODE_FILE | DOWNLOAD_MODE_EXCEL
     * @throws \UserFormException
     */
    private function getDownloadModeNCheck(array $vars) {

        $cnt = count($vars[NAME_COLLECT_ELEMENTS]);
        $mode = $vars[NAME_DOWNLOAD_MODE];

        // In case mode is missing - determine one.
        if ($mode == '') {
            if ($cnt == 1) {
                $token = substr($vars[NAME_COLLECT_ELEMENTS][0], 0, 1);
                $mode = ( $token == TOKEN_FILE || $token == TOKEN_FILE_DEPRECATED) ? DOWNLOAD_MODE_FILE : DOWNLOAD_MODE_PDF;
            } else {
                $mode = DOWNLOAD_MODE_PDF;
            }
        }

        // Do some checks.
        switch ($mode) {
            case DOWNLOAD_MODE_PDF:
            case DOWNLOAD_MODE_QFQPDF:
            case DOWNLOAD_MODE_SAVE_PDF:
            case DOWNLOAD_MODE_ZIP:
            case DOWNLOAD_MODE_SAVE_ZIP:
            case DOWNLOAD_MODE_EXCEL:
            case DOWNLOAD_MODE_TEXT:
                break;
            case DOWNLOAD_MODE_FILE:
                if ($cnt > 1) {
                    throw new \UserFormException("With 'downloadMode' = 'file' only one element source is allowed.", ERROR_DOWNLOAD_UNEXPECTED_NUMBER_OF_SOURCES);
                }
                break;
            default:
                throw new \UserFormException("Unknown mode: $mode", ERROR_UNKNOWN_MODE);
        }

        return $mode;
    }

    /**
     * Concat final HREF string. Might be used directly (load new page) or as an AJAX call (e.g. Download).
     *
     * @param array $vars
     * @param array $tokenGiven
     * @param string $devTooltipAppend
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    protected function doHref(array &$vars, array $tokenGiven, string $devTooltipAppend = '') {
        $urlNParam = '';

        // Download
        if (isset($tokenGiven[TOKEN_DOWNLOAD])) {
            // Message in download popup.
            $altText = ($vars[NAME_ALT_TEXT] == '') ? 'Please wait' : addslashes($vars[NAME_ALT_TEXT]);
            $vars[NAME_EXTRA_CONTENT_WRAP] = str_replace(DOWNLOAD_POPUP_REPLACE_TEXT, $altText, $vars[NAME_EXTRA_CONTENT_WRAP]);
            $vars[NAME_EXTRA_CONTENT_WRAP] = str_replace(DOWNLOAD_POPUP_REPLACE_TITLE, addslashes($vars[NAME_DOWNLOAD]), $vars[NAME_EXTRA_CONTENT_WRAP]);

            if ($vars[NAME_SIP] == '1' || $vars[NAME_SIP] == '') {
                // Special encoding only necessary for SIP based downloads.
                $tmpUrlParam = array();
                $tmpUrlParam[DOWNLOAD_MODE] = $this->getDownloadModeNCheck($vars);
                $vars[NAME_EXTRA_CONTENT_WRAP] = str_replace(DOWNLOAD_POPUP_REPLACE_MODE, $tmpUrlParam[DOWNLOAD_MODE], $vars[NAME_EXTRA_CONTENT_WRAP]);

                $tmpUrlParam[DOWNLOAD_EXPORT_FILENAME] = $vars[NAME_DOWNLOAD];
                if (isset($vars[NAME_CACHE])) {
                    $tmpUrlParam[DOWNLOAD_PARAM_CACHE] = $vars[NAME_CACHE];
                }
                $tmpUrlParam[SIP_DOWNLOAD_PARAMETER] = base64_encode(implode(PARAM_DELIMITER, $vars[NAME_COLLECT_ELEMENTS]));
                $vars[NAME_URL_PARAM] = KeyValueStringParser::unparse($tmpUrlParam, '=', '&');
            }
        }

        // CopyToClipboard
        if (isset($tokenGiven[TOKEN_COPY_TO_CLIPBOARD]) && $vars[NAME_COPY_TO_CLIPBOARD] === '') {

//            $tmpUrlParam = array();
//            $tmpUrlParam[DOWNLOAD_MODE] = DOWNLOAD_MODE_FILE;
//            $tmpUrlParam[DOWNLOAD_OUTPUT_FORMAT] = DOWNLOAD_OUTPUT_FORMAT_JSON;
//            $tmpUrlParam[SIP_DOWNLOAD_PARAMETER] = base64_encode(implode(PARAM_DELIMITER, $vars[NAME_COLLECT_ELEMENTS]));
//            $vars[NAME_URL_PARAM] = KeyValueStringParser::unparse($tmpUrlParam, '=', '&');
            return '';
        }

        // Delete
        if ($vars[NAME_ACTION_DELETE] !== '') {
            $vars[NAME_URL_PARAM] = $this->adjustDeleteParameter($vars[NAME_ACTION_DELETE], $vars[NAME_URL_PARAM]);

            if ($vars[NAME_ACTION_DELETE] == TOKEN_ACTION_DELETE_AJAX) {
                $s = $this->sip->queryStringToSip($vars[NAME_URL_PARAM] . '&' . SIP_MODE_ANSWER . '=' . MODE_JSON, RETURN_SIP);
                $vars[NAME_ATTRIBUTE] = DATA_SIP . '="' . $s . '"';
                // No href for TOKEN_ACTION_DELETE_AJAX
                return '';
            }
        }

        // Regular Link
        if ($vars[NAME_MAIL] === '') {

            if ($vars[NAME_SIP] === "1") {
                // Only for SIP encoded Links: append the current $dbIndexData to make record locking work even for records in dbIndexQfq
                if ($vars[NAME_URL_PARAM] != '') {
                    $vars[NAME_URL_PARAM] .= '&';
                }

                $vars[NAME_URL_PARAM] .= PARAM_DB_INDEX_DATA . '=' . $this->dbIndexData;
            }

            // Normalize '?pageAlias'
            if (substr($vars[NAME_URL], 0, 1) === '?') {
                if (T3Handler::useSlugsInsteadOfPageAlias()) {
                    $url = substr($vars[NAME_URL], 1);
                    $url = OnString::strReplaceFirst('&', '?', $url);
                    $vars[NAME_URL] = Path::join($this->store->getVar(SYSTEM_BASE_URL_LANG, STORE_SYSTEM), $url);
                } else {
                    $vars[NAME_URL] = INDEX_PHP . $vars[NAME_URL];
                }
            }

            // Either NAME_URL is empty or NAME_PAGE is empty
            $urlNParam = Support::concatUrlParam($vars[NAME_URL] . $vars[NAME_PAGE], $vars[NAME_URL_PARAM]);

            if ($vars[NAME_SIP] === "1") {

                $paramArray = $this->sip->queryStringToSip($urlNParam, RETURN_ARRAY);
                $urlNParam = $paramArray[SIP_SIP_URL];
                $this->sipId = $paramArray[CLIENT_SIP];

                if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
                    $this->tooltipDebug = PHP_EOL . PHP_EOL . $this->sip->debugSip($paramArray);

                    if ($devTooltipAppend != '') {
                        $this->tooltipDebug .= PHP_EOL . 'Def = ' . $devTooltipAppend;
                    }
                    $vars[NAME_TOOL_TIP] .= $this->tooltipDebug;
                }
            }

        } else {
            // Link: MAILTO

            // If there is no encryption: handle the mailto as an ordinary URL
            if ($vars[NAME_ENCRYPTION] === '1') {
                throw new \UserReportException ("Oops, sorry: encrypted mail not implemented ...", ERROR_NOT_IMPLEMENTED);
            } else {
                $urlNParam = "mailto:" . $vars[NAME_MAIL];
                if (!empty($vars[NAME_MAILTO_SELECT])) {
                    $vars[NAME_MAILTO_SELECT] = $vars[NAME_MAIL];
                }
            }
        }

        return $urlNParam;
    }


    /**
     * @param $tokenActionDelete
     * @param $nameUrlParam
     *
     * @return string
     * @throws \UserReportException
     */
    private function adjustDeleteParameter($tokenActionDelete, $nameUrlParam) {

        // Split in: [p => 'r=100&table=note&..', 'D' => ''... ],
//        $param = KeyValueStringParser::parse($nameUrlParam, ':', '|');

//        Support::setIfNotSet($param, TOKEN_URL_PARAM);

        switch ($tokenActionDelete) {
            case TOKEN_ACTION_DELETE_AJAX:
                break;
            case TOKEN_ACTION_DELETE_REPORT:
                $nameUrlParam .= '&' . SIP_MODE_ANSWER . '=' . MODE_HTML;
                // Encode '&' in url to preserve parameters (#4545) - need to decode on use.
                $nameUrlParam .= '&' . SIP_TARGET_URL . '=' . str_replace('&', '--amp--', $_SERVER['REQUEST_URI'] ?? '');
                break;
            case TOKEN_ACTION_DELETE_CLOSE:
                // TODO: Implement for Form (primary Record wird geloescht)
                throw new \UserReportException ("Not implemented!", ERROR_NOT_IMPLEMENTED);
                break;
            default:
                throw new \UserReportException ("Invalid value for token '" . TOKEN_ACTION_DELETE . "': '$tokenActionDelete''", ERROR_INVALID_VALUE);
        }

        return $nameUrlParam;
    }

    /**
     * Return $vars[NAME_TOOL_TIP]. If $vars[NAME_TOOL_TIP] is empty, set $vars[NAME_GLYPH_TITLE] as tooltip.
     *
     * @param array $vars
     *
     * @return mixed
     */
    private function doToolTip(array $vars) {

        // Set default tooltip
        if ($vars[NAME_TOOL_TIP] == '') {
            $vars[NAME_TOOL_TIP] = $vars[NAME_GLYPH_TITLE];
        }
        if (isset($vars[NAME_STICKY_TOOL_TIP])) {
            $vars[NAME_TOOL_TIP] = $vars[NAME_STICKY_TOOL_TIP] . $vars[NAME_TOOL_TIP];
        }
        // Replace < and > so it does not bug out wrapTag() and add random floating <, >, </> in the html
        $vars[NAME_TOOL_TIP] = str_replace('>', '&gt;', $vars[NAME_TOOL_TIP]);
        $vars[NAME_TOOL_TIP] = str_replace('<', '&lt;', $vars[NAME_TOOL_TIP]);

        return $vars[NAME_TOOL_TIP];
    }

    /**
     * Parse CSS Class Settings
     *
     * @param array $vars
     *
     * @return    string
     */
    private function doCssClass(array $vars) {

//        $class = ($vars[NAME_LINK_CLASS] === '') ? $vars[NAME_LINK_CLASS_DEFAULT] : $vars[NAME_LINK_CLASS];
        $class = $vars[NAME_LINK_CLASS];

        switch ($class) {
            case TOKEN_CLASS_NONE:
                $class = '';
                break;
// #5302
//            case TOKEN_CLASS_INTERNAL:
//            case TOKEN_CLASS_EXTERNAL:
//                $class = $this->linkClassSelector[$vars[NAME_LINK_CLASS]] . ' ';
//                break;
            default:
                break;
        }

        if ($class === NO_CLASS) {
            $class = '';
        }

        //TODO: Old way to detect if BS Button should be rendered - should be replaced by 'b:'
        if ($vars[NAME_GLYPH] !== '' && $vars[NAME_EXTRA_CONTENT_WRAP] == '' && $vars[NAME_BOOTSTRAP_BUTTON] == '') {
            $class = 'btn btn-default ' . $class;
        }

        if (isset($vars[NAME_STICKY_TOOL_TIP])) {
            $class .= ' ' . CLASS_TRIGGER_STICKY_TOOLTIP;
        }

        return implode(' ', [$vars[NAME_BOOTSTRAP_BUTTON], $class]);
    }

    /**
     * Create Image HTML Tag
     *
     * @param array $vars
     *
     * @return string
     * @throws \CodeException
     */
    private function doImage(array $vars) {
        if ($vars[NAME_IMAGE] === '') {
            return '';
        }

        // Build Image
        $attrs = implode('', [
            Support::doAttribute('alt', $vars[NAME_ALT_TEXT]),
            Support::doAttribute('src', $vars[NAME_IMAGE]),
            Support::doAttribute('title', $vars[FINAL_TOOL_TIP] === '' ? $vars[NAME_IMAGE_TITLE] : $vars[FINAL_TOOL_TIP])
        ]);
        return "<img $attrs>";
    }

    /**
     * Create HTML i tag to display an icon
     *
     * @param array $vars
     *
     * @return string
     * @throws \CodeException
     *
     * @todo decouple from glyphicon
     */
    private function doGlyph(array $vars) {
        if ($vars[NAME_GLYPH] === '' || $vars[NAME_GLYPH] === '0') {
            return '';
        }
        // Build the tag
        $attrs = Support::doAttribute('class', 'glyphicon ' . $vars[NAME_GLYPH]);
        return "<span $attrs></span>";
    }

    /**
     * Concat Text, Image, Glyph and/or Thumbnail.
     * Depending of $vars[NAME_RIGHT_PICTURE_POSITION], swap the position,
     *
     * @param array $vars
     *
     * @param $rcContentPure
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function doContent(array $vars, &$rcContentPure) {
        $arr = array();

        if ($vars[NAME_MONITOR] == '1') {
            $monitor = new Monitor();
            return $monitor->process($vars);
        }

        $order = ($vars[NAME_RIGHT] === "l") ? [FINAL_SYMBOL, FINAL_THUMBNAIL, NAME_TEXT] : [NAME_TEXT, FINAL_THUMBNAIL, FINAL_SYMBOL];
        foreach ($order as $key) {
            if (isset($vars[$key]) && $vars[$key] != '') { // empty() is wrong here, cause '0' would be recognized as empty.
                $arr[] = $vars[$key];
            }
        }

        $rcContentPure = implode(' ', $arr);
        return Support::wrapTag($vars[NAME_EXTRA_CONTENT_WRAP], $rcContentPure);
    }

    /**
     * Build 'alert' JS. $vars['question'] = '<text>:<color>:<button ok>:<button fail>'
     * Return JS Code to place in '<a>' tag. Be carefull: function creates a uniqe 'id' tag.
     *
     * @param array $vars
     *
     * @return string
     * @throws \CodeException
     * @throws \UserReportException
     */
    private function doQuestion(array $vars) {

        if ($vars[NAME_QUESTION] === '') {
            return '';
        }

        $arr = OnArray::explodeWithoutEscaped(':', $vars[NAME_QUESTION]);
        $arr = array_merge($arr, ['', '', '', '', '', '']);
        $target = $vars[NAME_TARGET];
        $noRedirect = $vars[FINAL_NO_REDIRECT] ? 'true' : 'false';
        $id = Support::uniqIdQfq('a_');
        $content = Support::doAttribute('id', $id);

        $text = $arr[QUESTION_INDEX_TEXT] === '' ? DEFAULT_QUESTION_TEXT : $arr[QUESTION_INDEX_TEXT];
        $level = ($arr[QUESTION_INDEX_LEVEL] === '') ? DEFAULT_QUESTION_LEVEL : $arr[QUESTION_INDEX_LEVEL];
        $ok = ($arr[QUESTION_INDEX_BUTTON_OK] === '') ? 'Ok' : $arr[QUESTION_INDEX_BUTTON_OK];
        $cancel = ($arr[QUESTION_INDEX_BUTTON_FALSE] === '') ? 'Cancel' : $arr[QUESTION_INDEX_BUTTON_FALSE];
        $cancel = ($cancel == '-') ? '' : ", { label: '$cancel',eventName: 'cancel'}";

        $timeout = ($arr[QUESTION_INDEX_TIMEOUT] === '') ? '0' : $arr[QUESTION_INDEX_TIMEOUT] * 1000;
        $flagModalStatus = ($arr[QUESTION_INDEX_FLAG_MODAL] === '') ? '1' : $arr[QUESTION_INDEX_FLAG_MODAL];
        $flagModal = ($flagModalStatus === "1") ? 'true' : 'false';
        // Check if affected button has class 'record-delete' and prevent it being triggered with enter press if its not focused
        $preventScript = ($vars[NAME_LINK_CLASS] === 'record-delete') ? 'if (event.detail === 0 && event.target !== document.activeElement) { event.preventDefault(); return false; }' : '';

        $js = <<<EOF
$preventScript var alert = new QfqNS.Alert({ message: '$text', type: '$level', modal: $flagModal, timeout: $timeout, buttons: [
    { label: '$ok', eventName: 'ok' }
    $cancel
] } );
alert.on('alert.ok', function() {
  if ($noRedirect) { return; }
  var href = '$target';
  if (href.startsWith('http')) {
    window.location = href;
  } else if (href !== '') {
    window.open($('#$id').attr('href'), href);
  } else {
    window.location = $('#$id').attr('href');
  }
});

alert.show();
return false;
EOF;

        $content .= Support::doAttribute('onClick', $js);

        return $content;
    }

    /**
     * Create the HTML anchor.
     * - <a href="mailto:info@example.com" title=".." class="..">
     * - <a href="http://example.com" title=".." class="..">
     *
     * @param array $vars
     *
     * @return string
     * @throws \CodeException
     */
    private function doAnchor(array $vars) {

        $attributes = '';

        if ($vars[FINAL_IS_LINK] === true && $vars[FINAL_RENDER_HREF]) {
            if ($vars[FINAL_BUTTON_ID] != '' || empty($vars[FINAL_BUTTON_ID])) {
                $attributes .= Support::doAttribute('href', $vars[FINAL_HREF]);
            }
        }

        if ($vars[FINAL_BUTTON_ID] != '') {
            $attributes .= Support::doAttribute('id', $vars[FINAL_BUTTON_ID]);
        }
        $attributes .= Support::doAttribute('class', $vars[FINAL_CLASS]);
        $attributes .= Support::doAttribute('target', $vars[NAME_TARGET]);
        $attributes .= Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
        if ($vars[NAME_ATTRIBUTE] != '') $attributes .= $vars[NAME_ATTRIBUTE] . ' ';
        if ($vars[NAME_ATTRIBUTE2] != '') $attributes .= $vars[NAME_ATTRIBUTE2] . ' ';

        $attributes .= $vars[FINAL_QUESTION];

        $useButtonTag = $vars[FINAL_IS_LINK] === false || $vars[NAME_ACTION_DELETE] == TOKEN_ACTION_DELETE_AJAX;
        $tag = $useButtonTag ? 'button' : 'a';
        $anchor = '<' . $tag . ' ' . $attributes . '>';

        return $anchor;
    }

    /**
     *
     *
     * @param array $vars
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function doThumbnail(array $vars) {

        // Check if there is a thumbnail and if it should be rendered
        if (empty($vars[NAME_THUMBNAIL]) || $vars[NAME_RENDER] == RENDER_MODE_7_ONLY_URL) {
            return '';
        }

        $arr = array();
        $arr[] = TOKEN_THUMBNAIL . ':' . $vars[NAME_THUMBNAIL];
        $arr[] = TOKEN_THUMBNAIL_DIMENSION . ':' . $vars[NAME_THUMBNAIL_DIMENSION];
        $arr[] = TOKEN_SIP . ':' . $vars[NAME_SIP];

        $str = implode('|', $arr);

        if ($this->thumbnail === null) {
            $this->thumbnail = new Thumbnail();
        }

        return $this->thumbnail->process($str);
    }

    /**
     * Get mode from table $this->renderControl, depending on $modeRender, $modeUrl, $modeText
     *
     * @param array $vars
     * @param array $tokenGiven
     * @return int[]
     * @throws \UserReportException
     */
    protected function getModeRender(array $vars, array $tokenGiven) {

        if (isset($tokenGiven[TOKEN_COPY_TO_CLIPBOARD]) && $tokenGiven[TOKEN_COPY_TO_CLIPBOARD]) {
            if ($vars[NAME_RENDER] == RENDER_MODE_0_LINK) {
                $vars[NAME_RENDER] = RENDER_MODE_1_LINK_OR_TEXT;
            }
        }

        if ($vars[NAME_MONITOR] == '1') {
            return [1, self::SUBMODE_PLAIN];
        }

        $modeRender = $vars[NAME_RENDER];
        $modeUrl = ($vars[FINAL_HREF] === '') ? 0 : 1;
        $modeText = ($vars[FINAL_CONTENT] === '') ? 0 : 1;

        if ($vars[NAME_ACTION_DELETE] == TOKEN_ACTION_DELETE_AJAX) {
            $modeUrl = 1;
        }
        // Create 'fake' modes for encrypted 'mailto'
        $submode = self::SUBMODE_PLAIN;
        if ($vars[NAME_MAIL] != '') {
            $submode = self::SUBMODE_ENC_EMAIL;
//            $vars[NAME_URL] = "dummy";
        }

        // Create 'fake' mode for ajax/html delete
        if ($vars[NAME_DELETE]) {
            $submode = self::SUBMODE_AJAX;
        }

        $mode = $this->renderControl[$modeRender][$modeUrl][$modeText] ?? null;
        if ($mode === null) {
            throw new \UserReportException ("Undefined combination of 'rendering mode' / 'url' / 'text': " .
                $modeRender . ' / ' . $modeUrl . ' / ' . $modeText, ERROR_UNDEFINED_RENDER_CONTROL_COMBINATION);
        }

        return [$mode, $submode];
    }

    /**
     *  Encrypt the mailto address via JS.
     *  Email address protected against email crawler (as long as they don't interpret JS).
     *
     *    <script language=javascript><!--
     *    var mm1 = name
     *    var mm2 = @domain.
     *    var ... = tld
     *    document.write("<a href=" + "mail" + "to:" + mm1 + mm2 + ... + ">" + name + "</a>")
     *    document.write("<a href=" + "mail" + "to:" + mm1 + mm2 + ... + ">" + @domain. + "</a>")
     *    document.write("<a href=" + "mail" + "to:" + mm1 + mm2 + ... + ">" + tld + "</a>")
     *    //--></script>';
     *
     * @param array $vars
     * @param bool|TRUE $href TRUE: create a '<a>',   false: just encrypt or show the email, no link.
     *
     * @return string
     */
    private function encryptMailtoJS(array $vars, $href = true) {

        // Split $mailto
        $tmp = $this->splitAndAddDelimter($vars[NAME_MAIL], "@");
        $arr = array_merge($this->splitAndAddDelimter($tmp[0], "."), $this->splitAndAddDelimter($tmp[1], "."));

        $tt = "<script language=javascript><!--" . chr(10);
        $ii = 0;
        if ($href) {
            $dw = 'document.write("<a href=" + "mail" + "to:"';
            foreach ($arr as $value) {
                // Setup JS Variables
                $tt .= 'var mm' . $ii . '= "' . $value . '"' . chr(10);
                // Compose $dw (documentwrite statement)
                $dw .= ' + mm' . $ii++;
            }
            $dw .= ' + "' . $vars[NAME_LINK_CLASS] . str_replace('"', '\\"', $vars[NAME_TOOL_TIP_JS][0]) . '>"';
            $closeDw = '"</a>")';
        } else {
            $dw = 'document.write(';
            $closeDw = ')';
        }

        // Wrap mailto around text
        if ($vars[NAME_MAIL] == $vars[NAME_TEXT]) {
            // Text is an email: wrap every single part of mailto.
            $ii = 0;
            foreach ($arr as $value) {
                $tt .= $dw . " + mm" . $ii++ . ' + ' . $closeDw . chr(10);
            }
        } else {
            // Single wrap text
            $tt .= $dw . ' + "' . $vars[NAME_TEXT] . '" + ' . $closeDw . chr(10);
        }

        $tt .= '//--></script>';
        if ($href) {
            $tt .= $vars[NAME_TOOL_TIP_JS][1];
        }

        return ($tt);
    }

    /**
     * Split a string around the $delimiter.
     *
     * Append the delimiter to each part except the last one.
     *
     * @param $mailto
     * @param $delimiter
     *
     * @return array
     */
    private function splitAndAddDelimter($mailto, $delimiter) {
        //TODO: Ich verstehe die Funktion nicht - funktioniert das hier wirklich?
        $value = '';

        $arr = explode($delimiter, $mailto);            // split string

        foreach ($arr as $key => $value) {
            $arr[$key] = $value . $delimiter;
        }

        if (isset($key)) {
            $arr[$key] = $value;
        }

        return ($arr);
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildUrl($vars, $value) {

        $vars[NAME_LINK_CLASS_DEFAULT] = '';

        if ($value === '-') {
            $vars[FINAL_IS_LINK] = false;
            $vars[FINAL_NO_REDIRECT] = true;
            return $vars;
        }

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildMail($vars, $value) {

        $vars[NAME_LINK_CLASS_DEFAULT] = '';

        return $vars;
    }


    /**
     * Builds a default button link.
     * there are 'new' , 'formEdit', 'formView'
     *
     * @param $vars
     * @param $value
     * @return mixed
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildDefaultButtonLink($vars, $value) {

        switch ($value) {
            // build new button
            case FORM_BUTTON_NEW:
                $urlParam = $this->store->getNonSystemSipParam();
                $urlParam[SIP_RECORD_ID] = 0;
                $urlParam[SIP_FORM] = $this->formSpec[F_NAME];
                Support::appendTypo3ParameterToArray($urlParam);
                $sip = $this->store->getSipInstance();
                $vars[FINAL_HREF] = $sip->queryStringToSip(OnArray::toString($urlParam));
                break;
            // build form edit button
            case FORM_BUTTON_FORM_EDIT:
                $queryStringArray = [
                    'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
                    'form' => FORM_NAME_FORM,
                    'r' => $this->formSpec[F_ID],
                    PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
                ];

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

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

                if (T3Handler::useSlugsInsteadOfPageAlias()) {
                    $queryString = Path::urlApp($this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM)) . '?' . $queryString;
                }
                $sip = $this->store->getSipInstance();
                $vars[FINAL_HREF] = $sip->queryStringToSip($queryString);
                break;
            // build formview button
            case FORM_BUTTON_FORM_VIEW:
                switch ($this->formSpec[F_NAME]) {
                    case 'form':
                        $form = $this->store->getVar(F_NAME, STORE_RECORD);
                        break;
                    case 'formElement':
                        if (false !== ($formId = $this->store->getVar(FE_FORM_ID, STORE_SIP . STORE_RECORD))) {
                            $row = $this->dbArray[$this->dbIndexQfq]->sql("SELECT `f`.`name` FROM `Form` AS f WHERE `id`=" . $formId, ROW_EXPECT_1);
                            $form = current($row);
                        }
                        break;
                    default:
                        return $vars;
                }
                $queryStringArray = [
                    'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
                    'form' => $form,
                    'r' => 0,
                ];
                $queryString = Support::arrayToQueryString($queryStringArray);

                $vars[FINAL_HREF] = $this->sip->queryStringToSip($queryString);
                break;
        }


        $vars[FINAL_BUTTON_ID] = $this->defaultButtons[$value]['buttonHtmlId'];
        $vars[FINAL_IS_LINK] = $this->defaultButtons[$value][FINAL_IS_LINK];

        return $vars;
    }

    /**
     * Build Default button which do need a id
     * save, delete and close
     *
     * @param $vars
     * @param $value
     * @return mixed
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildDefaultButtonId($vars, $value) {
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $formName = $this->store->getVar(FORM_NAME_FORM, STORE_SIP);
        $formId = $this->dbArray[$this->dbIndexQfq]->sql("SELECT f.id AS formId FROM Form AS f WHERE f.name = '" . $formName . "'");
        $formId = Form::getHtmlIdStatic($this->ttContentUid, $formId[0]['formId'], $recordId);
        $vars[FINAL_BUTTON_ID] = $this->defaultButtons[$value]['buttonHtmlId'] . '-' . $formId;
        $vars[FINAL_IS_LINK] = $this->defaultButtons[$value][FINAL_IS_LINK];
        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildPage($vars, $value, array $tokenGiven) {

        $vars[NAME_LINK_CLASS_DEFAULT] = ''; // No idea why this is here... (Marc)

        if ($value === '-') {
            $vars[FINAL_IS_LINK] = false;
            $vars[FINAL_NO_REDIRECT] = true;
            return $vars;
        }

        if (T3Handler::useSlugsInsteadOfPageAlias()) {

            // if page is given by a numeric id then prepend 'id=' and return
            if (preg_match('/^((?:id=)?)([0-9]+)((?:&.*)?)$/s', $value, $matches)) {
                $vars[NAME_PAGE] = Support::concatUrlParam('', 'id=' . $matches[2] . $matches[3]);
                return $vars;
            }

            // cut id= from beginning
            if (substr($value, 0, 3) === 'id=') {
                $value = substr($value, 3);
            }

            // replace first '&' with '?' if there is no '?'. (for convenience in case someone writes something like 'p:{{pageSlug:T}}&foo=bar&...' )
            if (!OnString::strContains($value, '?') && OnString::strContains($value, '&')) {
                $value = OnString::strReplaceFirst('&', '?', $value);
            }

            if ($vars[NAME_PAGE] == NAME_BACK_BUTTON) {
                $vars[FINAL_RENDER_HREF] = false;
                if (!($tokenGiven[TOKEN_TEXT] ?? false)) {
                    $vars[NAME_TEXT] = 'Back';
                }
                $vars[NAME_ATTRIBUTE2] .= 'onClick="window.history.back()"';
            }

            $value = Support::appendTypo3ParameterToUrl($value);
            $vars[NAME_PAGE] = Path::join($this->store->getVar(SYSTEM_BASE_URL_LANG, STORE_SYSTEM), $value);

        } else {
            if (substr($value, 0, 3) !== 'id=') {
                $value = 'id=' . $value;
            }
            $vars[NAME_PAGE] = Support::concatUrlParam('', $value);
//          $vars[NAME_LINK_CLASS_DEFAULT] = $this->cssLinkClassInternal;

        }
        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildDownload($vars, $value) {

        // By default, qfq saves everything HTML encoded. E.g. in form '&#39;' - decode them back to regual UTF-8 text.
        $filename = html_entity_decode($vars[DOWNLOAD_EXPORT_FILENAME], ENT_QUOTES | ENT_XML1, 'UTF-8');

        // Remove unsafe characters. For '... AS _savePdf' slashes have to be allowed.
        $vars[DOWNLOAD_EXPORT_FILENAME] = Sanitize::safeFilename($filename, false, true);

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildCopyToClipboard($vars, $value) {

        return $vars;
    }

    /**
     * @param $vars
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildDownloadLate($vars) {
        $baseUrl = Store::getVar(SYSTEM_BASE_URL, STORE_SYSTEM);

        if ($vars[NAME_BOOTSTRAP_BUTTON] == '') {
            $vars = $this->buildBootstrapButton($vars, '');
        }

        $bootstrapButton = $vars[NAME_BOOTSTRAP_BUTTON];
        if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
            $bootstrapButton = '';
            $vars[NAME_GLYPH] = '';
            $vars[NAME_EXTRA_CONTENT_WRAP] = '';
        } else {
            switch ($vars[NAME_GLYPH]) {
                case '0':
                    $vars[NAME_GLYPH] = '';
                    $vars[NAME_GLYPH_TITLE] = '';
                    break;
                case '':
                case '1':
                    $vars[NAME_GLYPH] = GLYPH_ICON_FILE;
                    $vars[NAME_GLYPH_TITLE] = "Download";
                    break;
                default:
                    break;
            }
        }

        // Download links should be by default SIP encoded.
        if (($vars[NAME_SIP]) === false) {
            $vars[NAME_SIP] = "1";
        }

        // No download link! Only text/button
        if ($vars[NAME_RENDER] == RENDER_MODE_3_TEXT_OR_URL) {
            return $vars;
        }

        if ($vars[NAME_LINK_CLASS] !== '') { // custom class
            $bootstrapButton = $vars[NAME_LINK_CLASS];
            $vars[NAME_LINK_CLASS] = ''; // prevent this class from also being used in the wrapping element
        }

        $attributes = Support::doAttribute('class', $bootstrapButton, true);
        $onClick = '';
        // Only add download progress attributes if there are more than 1 element to be downloaded.
        if (count($vars[NAME_COLLECT_ELEMENTS]) > 1) {
            $text = DOWNLOAD_POPUP_REPLACE_TEXT;
            $title = DOWNLOAD_POPUP_REPLACE_TITLE;
            $downloadMode = DOWNLOAD_POPUP_REPLACE_MODE;
            $attributes .= ' data-title="' . $title .
                '" data-text="' . $text . '" data-download-mode="' . $downloadMode . '" ';
            // Add CSS class so the JS event listener can identify this as a download modal trigger
            $vars[NAME_LINK_CLASS] .= ' ' . DOWNLOAD_MODAL_TRIGGER_CLASS . ' ';
        }

        // Check for SIP encoded download link or persistent download link
        if ($vars[NAME_SIP] == '1') {
            // SIP encoded.
            $vars[NAME_URL] = Path::urlApi(API_DOWNLOAD_PHP);
        } else {
            // Persistent Download Link
            if ($vars[NAME_DOWNLOAD] == '') {
                throw new \UserReportException("Persistent download link (s:0): please specify target key like 'd:d/tokenName/ID'", ERROR_MISSING_VALUE);
            }
            // d:d/tokenName/ID → d.php/tokenName/ID
            if (str_starts_with($vars[NAME_DOWNLOAD], DIRECT_DOWNLOAD_TOKEN_PREFIX)) {
                $pathAfterPrefix = substr($vars[NAME_DOWNLOAD], strlen(DIRECT_DOWNLOAD_TOKEN_PREFIX));
                $vars[NAME_URL] = Path::urlApp(API_DIRECT_DOWNLOAD_PHP, $pathAfterPrefix);
            } else {
                // Fallback: d:ID → download.php/ID (SIP-less simple mode)
                $vars[NAME_URL] = Path::urlApi(API_DOWNLOAD_PHP, $vars[NAME_DOWNLOAD]);
            }
        }
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        $vars[NAME_EXTRA_CONTENT_WRAP] = '<span ' . $attributes . $onClick . '>';
        $vars[NAME_BOOTSTRAP_BUTTON] = '0';

        return $vars;
    }

    /**
     * @param $vars
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildCopyToClipboardLate($vars) {

        $jsAction = '';
        $source = '';
        $attributes = '';
        if ($vars[NAME_BOOTSTRAP_BUTTON] == '') {
            $vars = $this->buildBootstrapButton($vars, '');
        }

        $bootstrapButton = $vars[NAME_BOOTSTRAP_BUTTON];
        $vars[FLAG_COPY_TO_CLIPBOARD] = NAME_COPY_TO_CLIPBOARD;
        if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
            $bootstrapButton = '';
            $vars[NAME_GLYPH] = '';
            $vars[NAME_EXTRA_CONTENT_WRAP] = '';
        } else {
            $vars[NAME_GLYPH] = GLYPH_ICON_COPY;
            $attributes .= Support::doAttribute(HTML_ATTR_TITLE, "Copy to clipboard", true);
        }

        $attributes .= Support::doAttribute(HTML_ATTR_CLASS, $bootstrapButton, true);

        // Clipboard Source
        if ($vars[NAME_COPY_TO_CLIPBOARD] !== '') {
            $jsAction = 'onClick';
            // Take care that ' and " are properly escaped
            $source = json_encode(["text" => $vars[NAME_COPY_TO_CLIPBOARD]], JSON_HEX_QUOT | JSON_HEX_APOS);
        } elseif (isset($vars[NAME_COLLECT_ELEMENTS][0])) {

            $jsAction = 'onmousedown';

            $tmpUrlParam = array();
            $tmpUrlParam[DOWNLOAD_MODE] = DOWNLOAD_MODE_FILE;
            $tmpUrlParam[DOWNLOAD_OUTPUT_FORMAT] = DOWNLOAD_OUTPUT_FORMAT_JSON;
            $tmpUrlParam[SIP_DOWNLOAD_PARAMETER] = base64_encode(implode(PARAM_DELIMITER, $vars[NAME_COLLECT_ELEMENTS]));
            $urlNParam = KeyValueStringParser::unparse($tmpUrlParam, '=', '&');
            $paramArray = $this->sip->queryStringToSip($urlNParam, RETURN_ARRAY);

            if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
                $vars[NAME_TOOL_TIP] .= PHP_EOL . PHP_EOL . $this->sip->debugSip($paramArray);
            }

            $source = json_encode(['uri' => Path::urlApi(API_DOWNLOAD_PHP) . '?s=' . $paramArray[SIP_SIP]]);
        } else {
            throw new \UserReportException("Missing content for 'copy to clipboard'", ERROR_MISSING_CONTENT);
        }

        // Depending on 'text' or 'file' different JS class are necessary.
        //   onClick="new QfqNS.Clipboard({text: 'test clipboard'});">
        //   onmousedown="new QfqNS.Clipboard({uri: 'mockData/downloadResponse.json'});">
        $copyFeedback = '';

        // Needed JS code for changing copyToClipboard button after clicking
        if ($jsAction === 'onClick') {
            $copyFeedback = 'let btn = $(this); let btnIcon = $(this).find("span"); btnIcon.fadeOut(400); btnIcon.removeClass("glyphicon-copy").addClass("glyphicon-ok"); this.style.borderColor = "green"; btnIcon.fadeIn(400);  var delayInMilliseconds = 3000; setTimeout(function() { btnIcon.removeClass("glyphicon-ok").addClass("glyphicon-copy"); btn.css("border-color", "");},delayInMilliseconds);';
        }

        // Add JS code $copyFeedback to onClick event
        $onClick = "$jsAction='new QfqNS.Clipboard($source);$copyFeedback'";

        $vars[NAME_EXTRA_CONTENT_WRAP] = '<button ' . $attributes . $onClick . '>';
        $vars[NAME_BOOTSTRAP_BUTTON] = '';
        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildPicture($vars, $value) {
        if ($vars[NAME_ALT_TEXT] == '') {
            $vars[NAME_ALT_TEXT] = $value;
        }

        $vars[NAME_IMAGE_TITLE] = $value;
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildBullet($vars, $value) {
        if ($value == '') {
            $value = DEFAULT_BULLET_COLOR;
        }

        if ($vars[NAME_ALT_TEXT] == '') {
            $vars[NAME_ALT_TEXT] = "Bullet " . $value;
        }

        $vars[NAME_IMAGE] = Path::urlExt(Path::EXT_TO_PATH_ICONS) . "/bullet-" . $value . '.gif';
        $vars[NAME_IMAGE_TITLE] = $value;
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildCheck($vars, $value) {

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

        if ($vars[NAME_ALT_TEXT] == '') {
            $vars[NAME_ALT_TEXT] = "Checked " . $value;
        }

        $vars[NAME_IMAGE] = Path::urlExt(Path::EXT_TO_PATH_ICONS) . "/checked-" . $value . '.gif';
        $vars[NAME_IMAGE_TITLE] = $value;
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     *
     * @return array
     */
    private function buildDeleteIcon($vars) {

        $vars[NAME_GLYPH] = GLYPH_ICON_DELETE;
        $vars[NAME_GLYPH_TITLE] = "Delete";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildActionDelete($vars, $value) {

        if ($vars[NAME_ACTION_DELETE] == TOKEN_ACTION_DELETE_AJAX) {
            $vars[NAME_LINK_CLASS] = 'record-delete';
            if (empty($vars[NAME_QUESTION])) {
                $vars[NAME_QUESTION] = QUESTION_DELETE . ':warning';
            }
        } else {
            if ($vars[NAME_URL] == '') {
                $vars[NAME_URL] = Path::urlApi(API_DELETE_PHP);
            }
        }

        if (!isset($vars[NAME_LINK_CLASS])) {
            // no_class: By default a button will be rendered.
            $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;
        }

        $vars[NAME_SIP] = "1";

        return $vars;
    }

    /**
     * Check that at least SIP_RECORD_ID is given and SIP_TABLE or SIP_FORM.
     * This check is only processed for COLUMN_PAGED & COLUMN_PAGED. Not for COLUMN_LINK, cause it's not known there.
     * In case of missing parameter, throw an exception.
     *
     * @param array $vars
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function checkDeleteParam(&$vars) {

        $urlParam = $vars[NAME_URL_PARAM];

        // Fill array 'found' with every given token
        $found = KeyValueStringParser::parse($urlParam, '=', '&');

        $flagRecordId = isset($found[SIP_RECORD_ID]) && $found[SIP_RECORD_ID] != '' && $found[SIP_RECORD_ID] != 0;
        $flagTable = isset($found[SIP_TABLE]) && $found[SIP_TABLE] != '';
        $flagForm = isset($found[SIP_FORM]) && $found[SIP_FORM] != '';

        if (!$flagRecordId) {
            // If there is no 'r' or 'r'=0: no delete button will be rendered
            $vars[NAME_RENDER] = RENDER_MODE_5_NONE;
        } else {
            if (!$flagTable && !$flagForm) {
                throw new \UserReportException ("Missing some qualifier/value for column " . COLUMN_PAGED . '/' . COLUMN_PPAGED . ": " .
                    SIP_RECORD_ID . ", " . SIP_FORM . " or " . SIP_TABLE, ERROR_MISSING_REQUIRED_DELETE_QUALIFIER);
            }
        }
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildEdit($vars, $value) {

        $vars[NAME_GLYPH] = GLYPH_ICON_EDIT;
        $vars[NAME_GLYPH_TITLE] = "Edit";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildHelp($vars, $value) {
        $vars[NAME_GLYPH] = GLYPH_ICON_HELP;
        $vars[NAME_GLYPH_TITLE] = "Help";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildInfo($vars, $value) {

        $vars[NAME_GLYPH] = GLYPH_ICON_INFO;
        $vars[NAME_GLYPH_TITLE] = "Information";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildNew($vars, $value) {

        $vars[NAME_GLYPH] = GLYPH_ICON_NEW;
        $vars[NAME_GLYPH_TITLE] = "New";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildShow($vars, $value) {

        $vars[NAME_GLYPH] = GLYPH_ICON_SHOW;
        $vars[NAME_GLYPH_TITLE] = "Details";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildGlyph($vars, $value) {

        $vars[NAME_GLYPH] = $value;
        $vars[NAME_GLYPH_TITLE] = "Details";
        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Called by $this->callTable
     * Parameter: b:?
     *
     * 0 : no button, do nothing, return $vars unchanged
     * '': default
     * 'success', 'btn-success': 'btn btn-success'
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildBootstrapButton($vars, $value) {

        $value = trim($value);

        if ($value === '0') {
            return $vars;
        }

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

        // Just in case the user forgot 'btn-' in front of btn-default, btn-primary, btn-su...
        if (substr($value, 0, 4) != 'btn-') {
            $value = 'btn-' . $value;
        }

        $vars[NAME_BOOTSTRAP_BUTTON] = 'btn ' . $value;

        return $vars;
    }

    /**
     * @param $vars
     * @param $value
     * @return mixed
     */
    private function buildTextBeforeLink($vars, $value) {
        $vars[NAME_BEFORE_LINK] = $value;
        return $vars;
    }

    /**
     * @param $vars
     * @param $value
     * @return mixed
     */
    private function buildTextAfterLink($vars, $value) {
        $vars[NAME_AFTER_LINK] = $value;
        return $vars;
    }

    /**
     * Called by $this->callTable
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildFile($vars, $value) {

        // Don't know what to do for file - seems to missing.
//        $vars[NAME_GLYPH] = 'glyphicon ' . $value;
//        $vars[NAME_GLYPH_TITLE] = "Details";
//        $vars[NAME_LINK_CLASS_DEFAULT] = NO_CLASS;

        return $vars;
    }

    /**
     * Create a ToolTip: $toolTip[0] and $toolTip[1] have to inserted in HTML code accordingly.
     * $vars[NAME_TOOL_TIP_JS][0]: JS to show '$toolTip[1]'.
     * $vars[NAME_TOOL_TIP_JS][1]: '<span>...</span>' with the tooltip text.
     *
     * @param $vars
     * @param $value
     *
     * @return array
     */
    private function buildToolTip($vars, $value) {
        static $count = 0;

//        $toolTipIndex = 'tooltip.' . $GLOBALS["TSFE"]->currentRecord . '.' . ++$count;
        $toolTipIndex = 'fake';

        $vars[NAME_TOOL_TIP_JS] = array();

        // Expample: <img src="fileadmin/icons/bullet-gray.gif" onmouseover="document.getElementById('gm167979').style.
        // display='block';" onmouseout="document.getElementById('gm167979').style.display='none';" />
        $vars[NAME_TOOL_TIP_JS][0] = " onmouseover=\"document.getElementById('" . $toolTipIndex .
            "').style.display='block';\" onmouseout=\"document.getElementById('" . $toolTipIndex . "').style.display='none';\"";

        // Example: <span id="gm167979" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0;
        // padding:3px;">My pesonal tooltip</span>
        $vars[NAME_TOOL_TIP_JS][1] = '<span id="' . $toolTipIndex .
            '" style="display:none; position:absolute; border:solid 1px black; background-color:#F9F3D0; padding:3px;">' .
            $value . '</span>';

        return $vars;
    }

    /**
     * @param array $vars
     * @param $value
     * @return array
     */
    private function buildDropdown(array $vars, $value) {
        return $vars;
    }

    /**
     * $vars[NAME_EXTRA_CONTENT_WRAP]:
     *   Bootstrap:           <button type="button" class="dropdown-toggle btn btn-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
     *   Bootstrap disabled:  <button type="button" class="dropdown-toggle disabled btn btn-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
     *   Plain:               <span class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
     *   Plain disabled:      <span class="dropdown-toggle disabled" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
     *
     * @param array $vars
     * @param $value
     * @return array
     */
    private function buildDropdownLate(array $vars) {
        if ($vars[NAME_GLYPH] == '') {
            $vars[NAME_GLYPH] = GLYPH_ICON_OPTION_VERTICAL;
        }

        $class = "dropdown-toggle";
        if ($vars[NAME_RENDER] == RENDER_MODE_3_TEXT_OR_URL) {
            $class .= ' disabled';
        }

        if ($vars[NAME_BOOTSTRAP_BUTTON] !== '0' && $vars[NAME_BOOTSTRAP_BUTTON] !== '') {
            $class .= ' ' . $vars[NAME_BOOTSTRAP_BUTTON];
        } else if ($vars[NAME_BOOTSTRAP_BUTTON] !== '0') {
            $class .= ' btn btn-default';
        }

        // Add additional classes from "c" token
        if (isset($vars[NAME_LINK_CLASS]) && !empty($vars[NAME_LINK_CLASS])) {
            $class .= ' ' . $vars[NAME_LINK_CLASS];
        }

        $attributes = 'data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"';

        if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
            $vars[NAME_EXTRA_CONTENT_WRAP] = '<span class="' . $class . '" ' . $attributes . '>';
        } else {
            $vars[NAME_EXTRA_CONTENT_WRAP] = '<button type="button" class="' . $class . '" ' . $attributes . '>';
        }

        return $vars;
    }

    /**
     *
     *
     * @throws \UserFormException
     * @throws \CodeException
     */
    private function processSyncByRule(array &$vars, string $value = '', bool $split = true) {
        // Split export button notation from payload
        if ($split) {
            $paramArr = KeyValueStringParser::explodeEscape('||', $value);

            $sbrBtnDefinition = $paramArr[0] ?? SYNC_BY_RULE_EXPORT;
            $definition = $paramArr[1] ?? '';

            $mode = KeyValueStringParser::explodeEscape('|', $sbrBtnDefinition)[0];
            $s = $this->sip->queryStringToSip('mode=' . $mode . '&value=' . $definition, RETURN_ARRAY);
            $vars[$sbrBtnDefinition] = $definition;
            $vars[NAME_SIP] = $s;

            if ($vars[NAME_RENDER] === RENDER_MODE_0_LINK) {
                $vars[NAME_RENDER] = RENDER_MODE_1_LINK_OR_TEXT;
            }

            return $sbrBtnDefinition;
        }

        // Build button mode
        $defaultGlyph = GLYPH_ICON_PASTE;
        $syncByRuleType = SYNC_BY_RULE_EXPORT;

        if (isset($vars[TOKEN_IMPORT])) {
            $defaultGlyph = GLYPH_ICON_COPY;
            $syncByRuleType = SYNC_BY_RULE_IMPORT;
        }

        if ($vars[NAME_GLYPH] == '') {
            $vars[NAME_GLYPH] = $defaultGlyph;
        }

        $class = SYNC_BY_RULE_CLASS_QFQ_SYNCBYRULE;
        if ($vars[NAME_RENDER] == RENDER_MODE_3_TEXT_OR_URL) {
            $class .= ' disabled';
        }

        if ($vars[NAME_BOOTSTRAP_BUTTON] !== '0' && $vars[NAME_BOOTSTRAP_BUTTON] !== '') {
            $class .= ' ' . $vars[NAME_BOOTSTRAP_BUTTON];
        } else if ($vars[NAME_BOOTSTRAP_BUTTON] !== '0') {
            $class .= ' btn btn-default';
        }

        // Add additional classes from "c" token
        if (isset($vars[NAME_LINK_CLASS]) && !empty($vars[NAME_LINK_CLASS])) {
            $class .= ' ' . $vars[NAME_LINK_CLASS];
        }

        // If tooltip is not removed from $vars a <span> will be rendered because of render mode = 1
        $tooltip = $vars[NAME_TOOL_TIP];
        $vars[NAME_TOOL_TIP] = '';

        // Debug tooltip
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
            $this->tooltipDebug = PHP_EOL . PHP_EOL . $this->sip->debugSip($vars[NAME_SIP]);
            $tooltip .= PHP_EOL . PHP_EOL . $this->tooltipDebug;
        }
        $vars[NAME_SIP] = $vars[NAME_SIP][TOKEN_SIP];

        // Remove sip if render mode 3 is active
        if ($vars[NAME_RENDER] == RENDER_MODE_3_TEXT_OR_URL) {
            $vars[NAME_SIP] = '';
        }

        $attributes = Support::doAttribute(HTML_ATTR_DATA_SYNC_BY_RULE_TYPE, $syncByRuleType);
        $attributes .= Support::doAttribute(HTML_ATTR_DATA_SIP, $vars[NAME_SIP]);
        $attributes .= Support::doAttribute(HTML_ATTR_DATA_BASE_URL, $this->store->getVar(SYSTEM_BASE_URL_LANG, STORE_SYSTEM));
        $attributes .= Support::doAttribute(HTML_ATTR_TITLE , $tooltip);
        $attributes .= Support::doAttribute(HTML_ATTR_CLASS, $class);

        if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
            $vars[NAME_EXTRA_CONTENT_WRAP] = "<span {$attributes}></span>";
        } else {
            $attributes .= Support::doAttribute(HTML_ATTR_TYPE, BTN_TYPE_BUTTON);
            $vars[NAME_EXTRA_CONTENT_WRAP] = "<button {$attributes}>";
        }

        return $vars;
    }

    /**
     * Builds hidden span tag for column link, used as search criteria in table sorter.
     *
     * @param array $vars
     * @return array
     */
    private function buildOrderText(array $vars) {
        $vars[NAME_ORDER_TEXT_WRAP] = Support::wrapTag('<span style="display: none;">', $vars[NAME_ORDER_TEXT]);
        return $vars;
    }

    /**
     * @param array $vars
     * @param string $value
     * @return array
     *
     */
    private function buildCacheDate(array $vars, string $value): array {

        return $vars;
    }

    /**
     * @param array $vars
     * @param string $value
     * @return array
     * @throws \UserReportException
     */
    private function buildRedirectHttpCode(array $vars, string $value): array {
        static $stringValues = [
            // all allowed redirection HTTP status codes
            '301' => 301, '302' => 302, '303' => 303, '307' => 307, '308' => 308,
            // aliases
            '' => HTTP_REDIRECT_DEFAULT,
            'get' => HTTP_REDIRECT_GET,
            'temp' => HTTP_REDIRECT_TEMP,
            'perm' => HTTP_REDIRECT_PERM
        ];
        $intValue = $stringValues[trim($value)] ?? 0;
        if ($intValue === 0) {
            throw new \UserReportException(
                "Invalid redirection http code $value",
                ERROR_INVALID_VALUE
            );
        }

        $vars[FINAL_HTTP_CODE] = $intValue;
        return $vars;
    }

    /**
     * Entry point for inline FormElement processing.
     * Determines whether to render as filter or edit and dispatches accordingly.
     *
     * @param string $str The link parameter string
     * @return string Rendered HTML
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function processInlineFormElement(string $str): string {
        // Parse tokens
        $tokens = KeyValueStringParser::parse($str, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);

        // Token 'f' (form element) is always required
        if (empty($tokens[TOKEN_INPUT_FORM_ELEMENT])) {
            throw new \UserReportException(
                "Invalid inline format. Missing required token 'f:'. " .
                "Expected: f:formName.feName|uid:functionName|targetClass:className, got: $str",
                ERROR_MISSING_VALUE
            );
        }

        $formFeValue = $tokens[TOKEN_INPUT_FORM_ELEMENT];

        // Load the FormElement spec once (parse formName.feName; ignore any ?params for the
        // lookup) so the type can pick the variant and renderInlineFilter() need not load it again.
        list($formName, $feName) = KeyValueStringParser::explodeEscape('.', explode('?', $formFeValue, 2)[0], 2);
        $feSpec = empty($feName) ? null : $this->loadInlineFormElementSpec($formName, $feName);

        // An aiChat FormElement renders as the chat widget (no filter / edit, regardless of '?').
        if ($feSpec !== null && ($feSpec[FE_TYPE] ?? '') === FE_TYPE_AI_CHAT) {
            return $this->renderInlineAiChat($feSpec, $tokens);
        }

        // Check if Filter-Variant (no ?) or Data-Variant (has ?)
        $isFilter = (strpos($formFeValue, '?') === false);

        if ($isFilter) {
            return $this->renderInlineFilter($tokens, $str, $feSpec);
        } else {
            return $this->renderInlineEdit($tokens, $str);
        }
    }

    /**
     * Validate required tokens for Filter Input variant
     *
     * @param array $tokens Parsed tokens
     * @param string $originalValue Original value for error messages
     * @throws \UserReportException If required tokens are missing or unknown tokens are present
     */
    private function validateInlineFilterTokens(array $tokens, string $originalValue): void {
        $requiredTokens = [
            TOKEN_INPUT_FORM_ELEMENT => 'f',
            TOKEN_INPUT_UID => 'uid',
            TOKEN_INPUT_TARGET_CLASS => 'targetClass',
        ];

        $missing = [];
        foreach ($requiredTokens as $constant => $tokenName) {
            if (empty($tokens[$constant])) {
                $missing[] = $tokenName;
            }
        }

        if (!empty($missing)) {
            throw new \UserReportException(
                "Invalid inline (filter) format. Missing required token(s): " . implode(', ', $missing) . ". " .
                "Expected: f:formName.feName|uid:functionName|targetClass:className, got: $originalValue",
                ERROR_MISSING_VALUE
            );
        }

        // Validate recognized tokens (warn about unknown tokens)
        $validTokens = [
            TOKEN_INPUT_FORM_ELEMENT,
            TOKEN_INPUT_UID,
            TOKEN_INPUT_TARGET_CLASS,
            TOKEN_BEFORE_LINK,
            TOKEN_AFTER_LINK,
            TOKEN_TOOL_TIP,
            TOKEN_RENDER,
        ];
        foreach ($tokens as $key => $value) {
            if (!in_array($key, $validTokens)) {
                throw new \UserReportException(
                    "Token '$key' not recognized for inline column. Valid tokens: f, uid, targetClass, v, V, o, r",
                    ERROR_UNKNOWN_TOKEN
                );
            }
        }
    }

    /**
     * Render Filter Input Element
     *
     * @param array $tokens Parsed tokens from KeyValueStringParser::parse()
     * @param string $originalValue Original column value for error messages
     * @return string Rendered HTML
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function renderInlineFilter(array $tokens, string $originalValue, ?array $feSpec = null): string {
        // Validate required tokens for Filter variant
        $this->validateInlineFilterTokens($tokens, $originalValue);

        $formFe = $tokens[TOKEN_INPUT_FORM_ELEMENT];       // formName.feName
        $uidValue = $tokens[TOKEN_INPUT_UID];              // qfqFunctionName[?param1=value1&param2=value2]
        $targetClass = $tokens[TOKEN_INPUT_TARGET_CLASS];  // targetClass

        // Parse uid value: extract qfqFunctionName and optional parameters
        $uidParsed = OnString::parseUidToken($uidValue);
        $qfqFunctionName = $uidParsed['name'];
        $uidParams = $uidParsed['params'];

        // Ensure targetClass has leading dot for CSS selector (user can omit it)
        if (!empty($targetClass) && $targetClass[0] !== '.') {
            $targetClass = '.' . $targetClass;
        }

        // Parse formName.feName
        list($formName, $feName) = KeyValueStringParser::explodeEscape('.', $formFe, 2);
        if (empty($feName)) {
            throw new \UserReportException(
                "Invalid inline format. Token 'f:' must contain formName.feName, got: $formFe",
                ERROR_MISSING_VALUE
            );
        }

        // Load FormElement from DB (unless the caller already loaded it)
        $feSpec = $feSpec ?? $this->loadInlineFormElementSpec($formName, $feName);

        // Apply token values to feSpec (before createFormElement)
        $feSpec = $this->applyInlineTokensToFeSpec($feSpec, $tokens, FE_INLINE_FILTER);

        // Create FormElement object using FeFactory (form=null for standalone/inline elements)
        $formElement = FeFactory::createFormElement($feSpec, null);

        // Apply token values and inline settings to FormElement (after createFormElement)
        $this->applyInlineTokensToFormElement($formElement, $tokens, FE_INLINE_FILTER);

        // Get renderer and render input only
        $renderer = BaseRenderer::getInstance();
        $html = $renderer->renderFormElementHtml($formElement, true);

        // Generate SIP for count refresh (only if FormElement has sql1 with counts)
        $countSip = '';
        if (!empty($feSpec[FE_SQL1])) {
            $countSip = $this->generateCountSip($feSpec[FE_SQL1]);
        }

        // Collect allowed values for server-side validation
        $allowedValues = $this->collectAllowedValues($formElement);

        // Register target info for combined SIP generation at the end
        $this->registerFilterTarget($targetClass, $qfqFunctionName, $uidParams, $feName, $allowedValues);

        // Add filter-specific attributes (no SIP yet - JS will get it from registry)
        $label = $feSpec[FE_LABEL] ?? '';
        $html = $renderer->feAddFilterAttributes($html, '', $targetClass, $feName, $countSip, $label);

        // Wrap with before/after text using Link.php if v: or V: tokens are present
        if (!empty($tokens[TOKEN_BEFORE_LINK]) || !empty($tokens[TOKEN_AFTER_LINK])) {
            $html = $this->wrapInlineWithLinkTokens($html, $tokens);
        }

        return $html;
    }

    /**
     * Render Data Input Element (for Inline-Editing) - stub for future implementation
     *
     * @param array $tokens Parsed tokens from KeyValueStringParser::parse()
     * @param string $originalValue Original column value for error messages
     * @return string Rendered HTML
     */
    private function renderInlineEdit(array $tokens, string $originalValue): string {
        // For Data Input, only 'f' token is required (uid and targetClass are NOT required)
        // The 'f' token value will contain: formName.feName?r=23

        // TODO: Implement data input variant
        return "<!-- inline edit variant not yet implemented: $originalValue -->";
    }

    /**
     * Render an aiChat FormElement standalone in a Report (special column name _input). The
     * chatbot widget is rendered as-is - the AiChatFormElement constructor packs its SIP and data
     * attributes, the renderer outputs the chat container. No filter behaviour (no uid /
     * targetClass, no count SIP, no filter target registry); the mode comes from the FormElement.
     *
     * @param array $feSpec The aiChat FormElement spec (already loaded from DB)
     * @param array $tokens Parsed tokens from the inline column
     * @return string Rendered chat widget HTML
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function renderInlineAiChat(array $feSpec, array $tokens): string {
        // Allow parameter overwrite (avoids "reserved key" errors for inline elements).
        $feSpec[FE_ALLOW_PARAMETER_OVERWRITE] = true;

        // form=null: standalone element. The SIP instance comes from the store, so the
        // AiChatFormElement constructor (prepareChat) works without a Form.
        $formElement = FeFactory::createFormElement($feSpec, null);
        $html = BaseRenderer::getInstance()->renderFormElementHtml($formElement, true);

        // Optional before/after text via v: / V: tokens (same as the filter variant).
        if (!empty($tokens[TOKEN_BEFORE_LINK]) || !empty($tokens[TOKEN_AFTER_LINK])) {
            $html = $this->wrapInlineWithLinkTokens($html, $tokens);
        }

        return $html;
    }

    /**
     * Load FormElement specification from database
     *
     * @param string $formName Form name
     * @param string $feName FormElement name
     * @return array FormElement specification
     * @throws \DbException
     * @throws \UserReportException
     */
    private function loadInlineFormElementSpec(string $formName, string $feName): array {
        $db = $this->dbArray[$this->dbIndexQfq];

        // Query FormElement including Form reference
        $sql = "SELECT fe.* FROM FormElement fe
                JOIN Form f ON fe.formId = f.id
                WHERE f.name = ? AND fe.name = ? AND fe.deleted = 'no'
                LIMIT 1";
        $result = $db->sql($sql, ROW_REGULAR, [$formName, $feName]);

        if (empty($result)) {
            throw new \UserReportException(
                "FormElement not found: form=$formName, fe=$feName",
                ERROR_RECORD_NOT_FOUND
            );
        }

        return $result[0];
    }

    /**
     * Apply token values to feSpec before createFormElement.
     *
     * These are values that must be set on the feSpec array before the FormElement
     * is created, as they affect how the FormElement is initialized.
     *
     * @param array $feSpec The FormElement specification array
     * @param array $tokens Parsed tokens from inline column
     * @param string $inlineType The inline type constant (FE_INLINE_FILTER or FE_INLINE_EDIT)
     * @return array Modified feSpec
     */
    private function applyInlineTokensToFeSpec(array $feSpec, array $tokens, string $inlineType = FE_INLINE_FILTER): array {
        // Allow parameter overwrite for inline elements (avoids "reserved key" error)
        $feSpec[FE_ALLOW_PARAMETER_OVERWRITE] = true;

        // Set inline type flag (needed before createFormElement for setFilterCount to work)
        $feSpec[$inlineType] = true;

        // Set FE mode based on r: token
        // r:0 = show, r:3 = readonly, r:5 = hidden, r:1 = required (for Input Data only)
        // If no r: token, use the mode from the FormElement in DB
        if (isset($tokens[TOKEN_RENDER])) {
            switch ($tokens[TOKEN_RENDER]) {
                case RENDER_MODE_0_LINK:
                    $feSpec[FE_MODE] = FE_MODE_SHOW;
                    break;
                case RENDER_MODE_3_TEXT_OR_URL:
                    $feSpec[FE_MODE] = FE_MODE_READONLY;
                    break;
                case RENDER_MODE_5_NONE:
                    $feSpec[FE_MODE] = FE_MODE_HIDDEN;
                    break;
                // RENDER_MODE_1 = required will be handled in Input Data variant
            }
        }

        return $feSpec;
    }

    /**
     * Apply token values and inline settings to FormElement after createFormElement.
     *
     * These are values that must be set after the FormElement is created, either because
     * they need highest priority (overriding DB parameter values) or because they are
     * FormElement-specific settings not part of the feSpec.
     *
     * @param AbstractFormElement $formElement The created FormElement
     * @param array $tokens Parsed tokens from inline column
     * @param string $inlineType The inline type constant (FE_INLINE_FILTER or FE_INLINE_EDIT)
     */
    private function applyInlineTokensToFormElement(AbstractFormElement $formElement, array $tokens, string $inlineType): void {
        // Set inline type flag
        $formElement->attributes[$inlineType] = true;

        // Add CSS class to individual elements (except multi-checkbox and radio where it goes on container)
        $isMultiCheckbox = ($formElement->attributes[FE_CHECKBOX_MODE] ?? '') === FE_CHECKBOX_MODE_MULTI;
        $isRadio = $formElement instanceof \IMATHUZH\Qfq\Core\Form\FormElement\RadioButtonFormElement;
        if (!$isMultiCheckbox && !$isRadio) {
            $formElement->cssClasses[] = ($inlineType === FE_INLINE_FILTER) ? 'qfq-inline-filter' : 'qfq-inline-edit';
        }

        // Set tooltip from o: token (highest priority - overrides DB parameter value)
        if (!empty($tokens[TOKEN_TOOL_TIP])) {
            $formElement->attributes[FE_TOOLTIP] = $tokens[TOKEN_TOOL_TIP];
            $formElement->htmlAttributes[HTML_ATTR_TITLE] = $tokens[TOKEN_TOOL_TIP];
        }

        // For hidden mode, add 'hidden' class directly to input element
        // (standard FE hidden mode only hides the parent wrapper, not the input itself)
        if (($formElement->attributes[FE_MODE] ?? '') === FE_MODE_HIDDEN) {
            $formElement->cssClasses[] = 'hidden';
        }
    }

    /**
     * Collect allowed values from FormElement for server-side validation
     *
     * @param AbstractFormElement $formElement The created FormElement
     * @return array|null Array of allowed values or null for text fields
     */
    private function collectAllowedValues(AbstractFormElement $formElement): ?array {
        // Single checkbox: use checked/unchecked values
        if (($formElement->attributes[FE_CHECKBOX_MODE] ?? '') === FE_CHECKBOX_MODE_SINGLE) {
            return [
                $formElement->attributes[FE_CHECKBOX_CHECKED] ?? '',
                $formElement->attributes[FE_CHECKBOX_UNCHECKED] ?? '',
            ];
        }

        // Multi-select, radio, select: use itemKeys
        if (property_exists($formElement, 'itemKeys') && !empty($formElement->itemKeys)) {
            return $formElement->itemKeys;
        }

        // Text fields: no value restriction
        return null;
    }

    /**
     * Generate SIP for filter count refresh
     *
     * Creates a SIP that stores the sql1 directly to avoid DB lookup on each count refresh.
     *
     * @param string $sql1 The SQL query for counting
     * @return string SIP token
     */
    private function generateCountSip(string $sql1): string {
        $params = http_build_query([
            API_MODE => INPUT_FILTER_COUNT_MODE,
            FE_SQL1 => $sql1,
        ]);
        return $this->sip->queryStringToSip($params, RETURN_SIP);
    }

    /**
     * Register a filter target for combined SIP generation
     *
     * @param string $targetClass CSS target class
     * @param string $qfqFunctionName QFQ function name
     * @param array $uidParams Additional parameters from uid token
     * @param string $feName FormElement name
     * @param array|null $allowedValues Allowed values for validation
     */
    private function registerFilterTarget(string $targetClass, string $qfqFunctionName, array $uidParams, string $feName, ?array $allowedValues): void {
        if (!isset($this->filterTargetRegistry[$targetClass])) {
            $this->filterTargetRegistry[$targetClass] = [
                INPUT_FILTER_KEY_QFQ_FUNCTION_NAME => $qfqFunctionName,
                INPUT_FILTER_KEY_UID_PARAMS => $uidParams,
                INPUT_FILTER_KEY_PARAMS => [],
            ];
        }
        $this->filterTargetRegistry[$targetClass][INPUT_FILTER_KEY_PARAMS][$feName] = $allowedValues;
    }

    /**
     * Get the filter target registry for combined SIP generation in Report.php
     *
     * @return array Filter target registry
     */
    public function getFilterTargetRegistry(): array {
        return $this->filterTargetRegistry;
    }

    /**
     * Clear the filter target registry (useful for testing)
     */
    public function clearFilterTargetRegistry(): void {
        $this->filterTargetRegistry = [];
    }

    /**
     * Wrap HTML content with before/after text using Link.php
     *
     * Uses render mode 6 (RENDER_MODE_6_ONLY_TEXT) to output only the content
     * without wrapping it in an <a> tag.
     *
     * @param string $html The HTML content to wrap
     * @param array $tokens Parsed tokens containing v: and/or V: values
     * @return string Wrapped HTML
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function wrapInlineWithLinkTokens(string $html, array $tokens): string {
        // Build link parameter string for Link.php
        // Use render mode 6 to output only text without <a> tag
        $linkParts = [];
        $linkParts[] = TOKEN_TEXT . PARAM_TOKEN_DELIMITER . $html;
        $linkParts[] = TOKEN_RENDER . PARAM_TOKEN_DELIMITER . RENDER_MODE_6_ONLY_TEXT;

        if (!empty($tokens[TOKEN_BEFORE_LINK])) {
            $linkParts[] = TOKEN_BEFORE_LINK . PARAM_TOKEN_DELIMITER . $tokens[TOKEN_BEFORE_LINK];
        }
        if (!empty($tokens[TOKEN_AFTER_LINK])) {
            $linkParts[] = TOKEN_AFTER_LINK . PARAM_TOKEN_DELIMITER . $tokens[TOKEN_AFTER_LINK];
        }

        $linkString = implode(PARAM_DELIMITER, $linkParts);

        return $this->renderLink($linkString);
    }
}