<?php

namespace IMATHUZH\Qfq\Core\Form;

use IMATHUZH\Qfq\Core\Database\DatabaseManager;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\Token;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler;

/**
 * This class represents a QFQ Form with or without a loaded record.
 * - Containing the form's definition in $this->specRaw. Sanitized definition (after constructor) in $this->specFinal
 * - Containing the loaded record (with id= $this->recordId) in $this->data
 */
class Form {
    public int $recordId = 0;

    public ?array $specRaw = null;
    public ?array $specFinal = null;
    public ?array $data = null;

    /**
     * @var array|null
     * An array of AbstractFormElement.
     * In case of a multiform, this is an assoviative array instead, which uses "recordId-index" as a key that points to another array of AbstractFormElements
     * (The inner array represents all formElements of one row)
     */
    public ?array $formElements = array();

    public ?array $formTagAttributes = null;

    public ?string $formModeGlobal = null;
    public ?string $parameterLanguageFieldName = null;

    public bool $showDebugInfoFlag = false;
    private ?Store $store = null;
    public ?Evaluate $evaluate = null;

    private ?DatabaseManager $databaseManager = null;

    public function __construct($specRaw) {
        $this->specRaw = $specRaw;
        $this->specFinal = $specRaw;

        $this->store = Store::getInstance();
        $this->databaseManager = DatabaseManager::getInstance();
        $this->evaluate = new Evaluate($this->store, $this->databaseManager->getDbByIndex($this->specFinal[F_DB_INDEX]));

        $this->showDebugInfoFlag = Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM));
        $tmpRecordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $int_value = is_int($tmpRecordId) || (is_string($tmpRecordId) && ctype_digit($tmpRecordId)) ? intval($tmpRecordId) : null;
        $this->recordId = $int_value === null ? 0 : $tmpRecordId;

        $this->sanitizeBsColumns();
        $this->sanitizeMaxVisiblePill();

        $this->fillData();

        // No records selected
        if (!empty($this->specFinal[F_BTN_PREVIOUS_NEXT_SQL])) {
            $this->specFinal[F_BTN_PREVIOUS_NEXT_SQL] = $this->getPreviousNextButtons();
        }

        // Create form elements for a multi form or a normal form
        if (($this->specFinal[F_MULTI_SQL] ?? '') != "") {
            $this->createFormElementsMulti();
        } else {
            $this->createFormElements();
        }

        $this->checkAutoFocus();

        $this->formModeGlobal = $this->getFormModeGlobal($this->specFinal[F_MODE_GLOBAL] ?? '');
        // Form Tag needs a lot of attributes (Class and Style tags are handled exclusively by Renderer though)
        $this->formTagAttributes = $this->getFormTagAttributes();
        $this->parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

    }

    /**
     * Get the Html id of this form
     */
    public function getHtmlId(): string {
        $uid = $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3);
        return self::getHtmlIdStatic($uid, $this->specFinal[F_ID], $this->recordId);
    }

    /**
     * Get a valid htmlId for a form with a given ttContentUid, formId and recordId.
     * @param string $ttContentUid
     * @param string $formId
     * @param string $recordId
     * @return string
     */
    public static function getHtmlIdStatic(string $ttContentUid, string $formId, string $recordId) {
        return 'qfq-form-' . $ttContentUid . '-' . $formId . '-' . $recordId;
    }

    /**
     * Creates a FormElement object for each element in $this->specFinal[FORM_ELEMENTS_NATIVE]
     * Passes a reference of itself to each $fe->form, so that FE's can access form-wide information.
     * This is only for non-multi forms.
     *
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function createFormElements(): void {
        $isFirstPill = true;
        for ($i = 0; $i < count($this->specFinal[FORM_ELEMENTS_NATIVE]); $i++) {
            $fe = FeFactory::createFormElement($this->specFinal[FORM_ELEMENTS_NATIVE][$i], $this);

            if ($isFirstPill && $fe->attributes[FE_TYPE] == FE_TYPE_PILL) {
                $isFirstPill = false;
                $fe->isFirstPill = true;
            }

            array_push($this->formElements, $fe);
        }
    }

    /**
     * Creates all form elements needed for a MultiForm.
     * Creates an array in $this->formElements for each unique "recordId-index". These arrays represent the data for one row.
     * Then fills these arrays with all FormElements of the respective row.
     */
    private function createFormElementsMulti(): void {
        $allRecords = $this->evaluate->parse($this->specFinal[F_MULTI_SQL], ROW_REGULAR);
        $this->specFinal[F_MULTI_SQL_RESULT] = $allRecords;

        // No rows: nothing to do.
        if (empty($allRecords)) return;

        // Check for 'id' or '_id' as column name
        $idName = isset($allRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID;
        $this->specFinal[F_MULTI_SQL_ID_NAME] = $idName;
        if (!isset($allRecords[0][$idName])) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Missing column "_' . F_MULTI_COL_ID . '"', ERROR_MESSAGE_TO_DEVELOPER => $this->specFinal[F_MULTI_SQL]]),
                ERROR_INVALID_OR_MISSING_PARAMETER);
        }

        // Create array per record
        $sameRecordCount = array();
        for ($i = 0; $i < count($allRecords); $i++) {
            // Set STORE_PARENT
            $this->store->setStore($allRecords[$i], STORE_PARENT_RECORD, true);
            $this->store->setVar(F_MULTI_COL_ID, $allRecords[$i][$idName], STORE_PARENT_RECORD); // In case '_id' is used, both '_id' and 'id' should be accessible.

            // Set STORE_RECORD first, so that STORE_VAR can use R-Variables (#19042)
            $record = $this->databaseManager->getDataDb()->sql('SELECT * FROM `' . $this->specFinal[F_TABLE_NAME] . '` WHERE `id`= ?',
                ROW_EXPECT_0_1, [$allRecords[$i][$idName]]);
            $this->store->setStore($record, STORE_RECORD, true);

            // Set STORE_VAR
            $storeVarBase = $this->evaluate->parse($this->specFinal[FE_FILL_STORE_VAR]);
            if (!is_array($storeVarBase)) $storeVarBase = array();
            if (count($storeVarBase) == 1 && is_array($storeVarBase[0])) $storeVarBase = $storeVarBase[0];
            $this->store->setStore($storeVarBase, STORE_VAR, true);


            // Get unique identifier "recordId_index"
            $recordCount = 0;
            if (isset($sameRecordCount[$allRecords[$i][$idName]])) {
                $recordCount = $sameRecordCount[$allRecords[$i][$idName]];
            }
            $recordId = $allRecords[$i][$idName];
            $this->recordId = $recordId;
            $recordKey = $recordId . '_' . $recordCount;
            $sameRecordCount[$allRecords[$i][$idName]] = $recordCount + 1;
            $this->formElements[$recordKey] = array();

            // Fill row with formElements
            for ($j = 0; $j < count($this->specFinal[FORM_ELEMENTS_NATIVE]); $j++) {
                // Set the record id of the form to record id of the multi form row. (so that FEs can initialize properly)
                $fe = FeFactory::createFormElement($this->specFinal[FORM_ELEMENTS_NATIVE][$j], $this);
                array_push($this->formElements[$recordKey], $fe);
            }
        }

        $this->store::unsetStore(STORE_RECORD); // Clean record store for next row
        $this->recordId = 0; // Reset recordId of the form, incase the tmp record id breaks something
    }

    /**
     * Sanitize the bs column fields to make sure they are all valid BS3 classes
     * @return void
     */
    private function sanitizeBsColumns(): void {
        // Default 3-6-3
        if (!isset($this->specFinal[FE_BS_LABEL_COLUMNS])) $this->specFinal[FE_BS_LABEL_COLUMNS] = 3;
        if (!isset($this->specFinal[FE_BS_INPUT_COLUMNS])) $this->specFinal[FE_BS_INPUT_COLUMNS] = 6;
        if (!isset($this->specFinal[FE_BS_NOTE_COLUMNS])) $this->specFinal[FE_BS_NOTE_COLUMNS] = 3;

        if (is_numeric($this->specFinal[FE_BS_LABEL_COLUMNS])) $this->specFinal[FE_BS_LABEL_COLUMNS] = "col-md-" . $this->specFinal[FE_BS_LABEL_COLUMNS];
        if (is_numeric($this->specFinal[FE_BS_INPUT_COLUMNS])) $this->specFinal[FE_BS_INPUT_COLUMNS] = "col-md-" . $this->specFinal[FE_BS_INPUT_COLUMNS];
        if (is_numeric($this->specFinal[FE_BS_NOTE_COLUMNS])) $this->specFinal[FE_BS_NOTE_COLUMNS] = "col-md-" . $this->specFinal[FE_BS_NOTE_COLUMNS];
    }

    /**
     * Sanitize $this->form->specFinal['maxVisiblePill'], which will control how many individual pills should later be rendered before the rest of them are grouped in a dropdown.
     */
    private function sanitizeMaxVisiblePill() {
        $this->specRaw[F_MAX_VISIBLE_PILL] = isset($this->specRaw[F_MAX_VISIBLE_PILL]) && $this->specRaw[F_MAX_VISIBLE_PILL] !== '' ? $this->specRaw[F_MAX_VISIBLE_PILL] : 1000;
    }

    /**
     * SELECT the current record from the database and store each field in $data[]
     */
    private function fillData() {
        $primaryKey = $this->specFinal[F_PRIMARY_KEY];

        if ($this->recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
            $tableName = $this->specFinal[F_TABLE_NAME];
            $row = $this->databaseManager->getDataDb()->sql("SELECT * FROM `$tableName` WHERE `$primaryKey` = ?", ROW_EXPECT_1,
                array($this->recordId), "Form '" . $this->specFinal[F_NAME] . "' failed to load record '$primaryKey'='$this->recordId' from table '" .
                $this->specFinal[F_TABLE_NAME] . "'.");
            $this->data = $row;
            $this->store->setStore($row, STORE_RECORD); // FORM overwrites whole R-Store on load
        }
    }

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

        if ($found) {
            return;
        }

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

                    return;
                }

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

    private function getFormTagAttributes(): array {
        $attribute['id'] = 'qfq-form'
            . '-' . $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3)
            . '-' . $this->specFinal[F_ID] . '-' . $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_SIP . STORE_RECORD . STORE_ZERO);

        $attribute['method'] = 'post';
        $attribute['action'] = Path::urlApi(API_SAVE_PHP);;
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
        // TODO $attribute['enctype'] = $this->getEncType();

        $attribute['data-disable-return-key-submit'] = isset($this->specFinal[F_ENTER_AS_SUBMIT]) && $this->specFinal[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
        $attribute['data-activate-first-required-tab'] = $this->specFinal[F_ACTIVATE_FIRST_REQUIRED_TAB] == '1' ? "true" : "false"; // attribute meaning is inverted
        $attribute['data-toggle'] = 'validator';
        if ($this->formModeGlobal == F_MODE_REQUIRED_OFF_BUT_MARK || $this->formModeGlobal == F_MODE_REQUIRED_OFF) {
            $attribute[DATA_REQUIRED_OFF_BUT_MARK] = 'true';
        }
        if (isset($this->specFinal[F_SAVE_BUTTON_ACTIVE]) && $this->specFinal[F_SAVE_BUTTON_ACTIVE] != '0') {
            $attribute[DATA_ENABLE_SAVE_BUTTON] = 'true';
        }

        # Replacing the attributes set via tail / javascript
        $attribute["data-form-id"] = $attribute["id"];
        $attribute["data-tabs-id"] = 'qfqTabs'
            . '-' . $this->store->getVar(TYPO3_TT_CONTENT_UID, STORE_TYPO3)
            . '-' . $this->specFinal[F_ID]
            . '-' . $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_SIP . STORE_RECORD . STORE_ZERO);
        if (0 < $this->recordId) {
            $attribute["data-delete-url"] = $this->getDeleteUrl();
        }
        if (isset($this->formSpec[F_DIRTY_MODE]) && $this->formSpec[F_DIRTY_MODE] === DIRTY_MODE_NONE) {
            $attribute["data-dirty-url"] = Path::urlApi(API_DIRTY_PHP);
        }
        $attribute["data-dirty-url"] = Path::urlApi(API_DIRTY_PHP);
        $attribute["data-submit-to"] = Path::urlApi(API_SAVE_PHP);
        $attribute["data-type-ahead-url"] = Path::urlApi(API_TYPEAHEAD_PHP);
        $attribute["data-refresh-url"] = Path::urlApi(API_LOAD_PHP);

        $attribute["data-file-upload-to"] = Path::urlApi(API_FILE_PHP) . '?' . FILE_ACTION . '=' . FILE_ACTION_UPLOAD;
        $attribute["data-file-delete-url"] = Path::urlApi(API_FILE_PHP) . '?' . FILE_ACTION . '=' . FILE_ACTION_DELETE;
        $attribute["data-log-level"] = 0; // Not sure if raos did implement this fully, but it was hardcoded 0 before.
        $attribute["data-api-delete-url"] = Path::urlApi(API_DELETE_PHP);

        return $attribute;
    }

    public function getDeleteUrl($mode = RETURN_URL): string {

        $urlParam = $this->store->getNonSystemSipParam(); // especially 'periodId' from calling URL should be passed to delete.php to evaluate it later.
        Support::appendTypo3ParameterToArray($urlParam);
        $urlParam[SIP_RECORD_ID] = $this->recordId;
        $urlParam[SIP_FORM] = $this->specFinal[F_NAME];
        $urlParam[SIP_MODE_ANSWER] = MODE_JSON;
        $queryString = Support::arrayToQueryString($urlParam);
        $sip = $this->store->getSipInstance();
        return $sip->queryStringToSip($queryString, $mode, Path::urlApi(API_DELETE_PHP));
    }

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

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

        Support::appendTypo3ParameterToArray($urlParam);

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

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

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

        return $url;
    }

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

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

        $queryStringArray = [
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
            'form' => FORM_NAME_FORM,
            'r' => $this->specFinal[F_ID],
            PARAM_DB_INDEX_DATA => $this->databaseManager->indexQfq,
        ];

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

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

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

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

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

        return $sip->queryStringToSip($queryString);
    }


    /**
     * Get formModeGlobal from STORE_USER, STORE_SIP or directly from $mode.
     *
     * @param $mode
     * @return array|string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function getFormModeGlobal($mode): string {

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

        $formModeGlobal = $this->store->getVar(F_MODE_GLOBAL, STORE_USER);

        if ($formModeGlobal === '' || $formModeGlobal === false) {
            $formModeGlobal = $this->store->getVar(F_MODE_GLOBAL, STORE_SIP);
        }

        if ($formModeGlobal === '' || $formModeGlobal === false) {
            $formModeGlobal = $mode;
        }

        return $formModeGlobal;
    }

    /**
     * Generates a Record Hash using table name, primary key and record id.
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    public function getRecordHashMd5(): string {
        $record = array();
        if ($this->recordId != 0) {
            $tableName = $this->specFinal[F_TABLE_NAME];
            $primaryKey = $this->specFinal[F_PRIMARY_KEY];
            $record = $this->databaseManager->getDbByIndex($this->specFinal[F_DB_INDEX])->sql("SELECT * FROM `$tableName` WHERE `$primaryKey`=?", ROW_EXPECT_1, [$this->recordId], "Record to load not found. "
                . (FEATURE_FORM_FILE_SYNC ? FormAsFile::errorHintFormImport($this->specFinal[F_TABLE_NAME]) : ''));
        }

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

        return OnArray::getMd5($record);
    }


    /**
     * Builds two (optionally three) buttons as provided by the developer.
     * Can be used to jump to previous/next record within the form.
     *
     * @param $btnPreviousNextSqlArr
     * @param $currentRecordId
     * @param $btnPreviousNextLoopFlag
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function getPreviousNextButtons(): array {
        $btnPreviousNextSqlArr = $this->specFinal[F_BTN_PREVIOUS_NEXT_SQL];
        $currentRecordId = $this->recordId;
        $btnPreviousNextLoopFlag = $this->specFinal[F_BTN_PREVIOUS_NEXT_LOOP] ?? false;
        $btnPreviousNextLoopFlag = $btnPreviousNextLoopFlag === '' || $btnPreviousNextLoopFlag === '1';

        $recordIdArr = array_column($btnPreviousNextSqlArr, F_BTN_PREVIOUS_NEXT_SQL_ID);
        $currentRecordIndex = array_search($currentRecordId, $recordIdArr);

        $btnPreviousGiven = isset($btnPreviousNextSqlArr[$currentRecordIndex][F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS]);
        $btnNextGiven = isset($btnPreviousNextSqlArr[$currentRecordIndex][F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT]);
        $firstRecordFlag = ($currentRecordIndex === array_key_first($recordIdArr));
        $lastRecordFlag = ($currentRecordIndex === array_key_last($recordIdArr));
        // Attribute needed to handle click via JS so that a qfq modal can pop up if form changes were made but not saved
        $previousIdLinkAttribute = '|A:id="' . ID_PREVIOUS_FORM . '"';
        $nextIdLinkAttribute = '|A:id="' . ID_NEXT_FORM . '"';
        // r=0 disables both buttons
        if (!$currentRecordId) {
            $firstRecordFlag = true;
            $lastRecordFlag = true;
            $btnPreviousNextLoopFlag = false;
        }

        $btnCurrent = (!$currentRecordId) ? null : $btnPreviousNextSqlArr[$currentRecordIndex][F_BTN_PREVIOUS_NEXT_SQL_BTN_CURRENT] ?? '';

        // Keyword btnPrevious is used
        if ($btnPreviousGiven) {
            $btnPrevious = $btnPreviousNextSqlArr[$currentRecordIndex - 1][F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS] ?? null;
            $btnPrevious = $btnPrevious ?? $btnPreviousNextSqlArr[array_key_last($btnPreviousNextSqlArr)][F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS];
            $btnPrevious .= $previousIdLinkAttribute;
            // btnPreviousNextLoop is not enabled and first record is currently selected
            // Button should be disabled
            if (!$btnPreviousNextLoopFlag && $firstRecordFlag) {
                $btnPreviousArr = Token::explodeTokenString($btnPreviousNextSqlArr[array_key_last($btnPreviousNextSqlArr)][F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS]);
                $btnPreviousArr['r'] = '3';
                $btnPreviousArr['t'] = 'First record';
                $btnPreviousArr['o'] = F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS_TOOLTIP;
                $btnPrevious = KeyValueStringParser::unparse($btnPreviousArr, PARAM_KEY_VALUE_DELIMITER, PARAM_DELIMITER);
            }

            // Keyword btnPrevious is not used
            // Button is built from scratch
        } else {
            $pageSlug = $this->store->getVar(TYPO3_PAGE_SLUG, STORE_TYPO3);
            $form = $this->specFinal[F_NAME];
            $id = $recordIdArr[$currentRecordIndex - 1] ?? $recordIdArr[array_key_last($recordIdArr)];

            // btnPreviousNextLoop is not enabled and first record is currently selected
            // Button should be disabled
            $renderModeTooltip = !$btnPreviousNextLoopFlag && $firstRecordFlag ? '|r:3|o:' . F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS_TOOLTIP : $previousIdLinkAttribute;

            $btnPrevious = 'p:' . $pageSlug . '?form=' . $form . '&r=' . $id . '|s|b|G:' . F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS_GLYPHICON . $renderModeTooltip;
        }

        // Keyword btnNext is used
        if ($btnNextGiven) {
            $btnNext = $btnPreviousNextSqlArr[$currentRecordIndex + 1][F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT] ?? null;
            $btnNext = $btnNext ?? $btnPreviousNextSqlArr[array_key_first($btnPreviousNextSqlArr)][F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT];
            $btnNext .= $nextIdLinkAttribute;
            // btnPreviousNextLoop is not enabled and last record is currently selected
            // Button should be disabled
            if (!$btnPreviousNextLoopFlag && $lastRecordFlag) {
                $btnPreviousArr = Token::explodeTokenString($btnPreviousNextSqlArr[array_key_first($recordIdArr)][F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT]);
                $btnPreviousArr['r'] = '3';
                $btnPreviousArr['t'] = 'Last record';
                $btnPreviousArr['o'] = F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT_TOOLTIP;
                $btnNext = KeyValueStringParser::unparse($btnPreviousArr, PARAM_KEY_VALUE_DELIMITER, PARAM_DELIMITER);
            }

            // Keyword btnNext is not used
            // Button is built from scratch
        } else {
            $pageSlug = $this->store->getVar(TYPO3_PAGE_SLUG, STORE_TYPO3);
            $form = $this->specFinal[F_NAME];
            $id = $recordIdArr[$currentRecordIndex + 1] ?? $recordIdArr[array_key_first($recordIdArr)];

            // btnPreviousNextLoop is not enabled and last record is currently selected
            // Button should be disabled
            $renderModeTooltip = !$btnPreviousNextLoopFlag && $lastRecordFlag ? '|r:3|o:' . F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT_TOOLTIP : $nextIdLinkAttribute;

            $btnNext = 'p:' . $pageSlug . '?form=' . $form . '&r=' . $id . '|s|b|G:' . F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT_GLYPHICON . $renderModeTooltip;
        }

        return [F_BTN_PREVIOUS_NEXT_SQL_BTN_PREVIOUS => $btnPrevious, F_BTN_PREVIOUS_NEXT_SQL_BTN_CURRENT => $btnCurrent, F_BTN_PREVIOUS_NEXT_SQL_BTN_NEXT => $btnNext];
    }


}