<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\Controller;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Controller\Event\AfterFormEnginePageInitializedEvent;
use TYPO3\CMS\Backend\Controller\Event\AfterRecordOpenedEvent;
use TYPO3\CMS\Backend\Controller\Event\BeforeFormEnginePageInitializedEvent;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Dto\FormElementData;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordWorkspaceDeletePlaceholderException;
use TYPO3\CMS\Backend\Form\Exception\NoFieldsToRenderException;
use TYPO3\CMS\Backend\Form\FormAction;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormResult;
use TYPO3\CMS\Backend\Form\FormResultCollection;
use TYPO3\CMS\Backend\Form\FormResultFactory;
use TYPO3\CMS\Backend\Form\FormResultHandler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Module\ModuleInterface;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Enum\ModuleLayout;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\SchemaLabelResolver;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;

/**
 * Main backend controller almost always used if some database record is edited in the backend.
 *
 * Main job of this controller is to evaluate and sanitize $request parameters,
 * call the DataHandler if records should be created or updated and
 * execute FormEngine for record rendering.
 */
#[AsController]
class EditDocumentController
{
    /**
     * An array looking approx like [tablename][list-of-ids]=command, eg. "&edit[pages][123]=edit".
     *
     * @var array<string,mixed>
     */
    protected array $editconf = [];

    /**
     * Array of tables with a lists of field names to edit for those tables. If specified, only those fields
     * will be rendered. Otherwise, all (available) fields in the record are shown according to the TCA type.
     */
    protected array $columnsOnly = [];

    /**
     * Default values for fields
     *
     * @var array|null [table][field]
     */
    protected $defVals;

    /**
     * Array of values to force being set as hidden fields in FormEngine
     *
     * @var array|null [table][field]
     */
    protected $overrideVals;

    /**
     * If set, this value will be set in $this->retUrl as "returnUrl", if not,
     * $this->retUrl will link to dummy action
     *
     * @var string|null
     */
    protected $returnUrl;

    /**
     * Prepared return URL. Contains the URL that we should return to from FormEngine if
     * close button is clicked. Usually passed along as 'returnUrl', but falls back to
     * "dummy" action.
     *
     * @var string
     */
    protected $retUrl;

    /**
     * Boolean: If set, then the GET var "&id=" will be added to the
     * retUrl string so that the NEW id of something is returned to the script calling the form.
     */
    protected bool $returnNewPageId = false;

    /**
     * The preview page id.
     * ID for displaying the page in the frontend, "save and view"
     * Is set to the pid value of the last shown record from "viewId" - thus indicating which page to
     * show when clicking the SAVE/VIEW button and transferred via GET/POST parameter "popViewId"
     */
    protected int $popViewId = 0;

    /**
     * If true, $this->editconf array is added a redirect response, used by Wizard/AddController
     */
    protected bool $returnEditConf = false;

    /**
     * @var array
     */
    protected $pageinfo;

    /**
     * Array of the elements to create edit forms for.
     *
     * @var FormElementData[]
     */
    protected array $elementsData = [];

    /**
     * Pointer to the first element in $elementsData
     */
    protected ?FormElementData $firstEl = null;

    /**
     * Counter, used to count the number of errors (when users do not have edit permissions)
     */
    protected int $numberOfErrors = 0;

    protected ?ModuleInterface $module = null;

    public function __construct(
        private readonly ComponentFactory $componentFactory,
        protected readonly EventDispatcherInterface $eventDispatcher,
        protected readonly IconFactory $iconFactory,
        protected readonly RecordFactory $recordFactory,
        protected readonly BreadcrumbFactory $breadcrumbFactory,
        protected readonly PageRenderer $pageRenderer,
        protected readonly UriBuilder $uriBuilder,
        protected readonly ModuleTemplateFactory $moduleTemplateFactory,
        protected readonly BackendEntryPointResolver $backendEntryPointResolver,
        protected readonly ModuleProvider $moduleProvider,
        private readonly FormDataCompiler $formDataCompiler,
        private readonly NodeFactory $nodeFactory,
        private readonly FormResultFactory $formResultFactory,
        private readonly FormResultHandler $formResultHandler,
        protected TcaSchemaFactory $tcaSchemaFactory,
        protected readonly LocalizationRepository $localizationRepository,
        private readonly SchemaLabelResolver $schemaLabelResolver,
        protected readonly ResourceFactory $resourceFactory,
        protected readonly FlashMessageService $flashMessageService,
    ) {}

    /**
     * Main dispatcher entry method registered as "record_edit" end point.
     */
    public function mainAction(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->moduleTemplateFactory->create($request);
        $view->setLayout(ModuleLayout::NORMAL);
        $view->setUiBlock(true);
        $body = '';

        // Unlock all locked records
        BackendUtility::lockRecords();
        $queryParams = $request->getQueryParams();
        $parsedBody = $request->getParsedBody();
        $this->module = $this->moduleProvider->getModule((string)($queryParams['module'] ?? ''), $this->getBackendUser());

        $this->editconf = $this->sanitizeEditConf($parsedBody['edit'] ?? $queryParams['edit'] ?? []);
        $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
        $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
        $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request);
        $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
        $this->columnsOnly = $this->prepareColumnsOnlyConfigurationFromRequest($request);
        $this->popViewId = (int)($parsedBody['popViewId'] ?? $queryParams['popViewId'] ?? 0);

        // Set overrideVals as default values if defVals does not exist.
        // @todo: Why?
        if (!is_array($this->defVals) && is_array($this->overrideVals)) {
            $this->defVals = $this->overrideVals;
        }

        // Set final return URL
        $this->retUrl = $this->returnUrl ?: $this->resolveDefaultReturnUrl();

        // Close document if a request for closing the document has been sent
        $requestAction = FormAction::createFromRequest($request);
        if ($requestAction->shouldHandleDocumentClosing()) {
            if ($response = $this->closeAndPossiblyRedirectAction($requestAction)) {
                return $response;
            }
        }

        $event = new BeforeFormEnginePageInitializedEvent($this, $request);
        $this->eventDispatcher->dispatch($event);

        // Process incoming data via DataHandler?
        if ($requestAction->shouldProcessData()) {
            $this->processData($view, $request);
            // Redirect if element should be closed after save
            if ($requestAction->shouldCloseAfterSave()) {
                return $this->closeAndPossiblyRedirectAction($requestAction);
            }
        }

        // Prepare current request url parameters (which might have been changed already, especially "editconf")
        // Contains $request query parameters. This array is the foundation for creating
        // the $currentEditingUrl var which becomes the url to which forms are submitted.
        $queryParamsForGeneratingCurrentUrl = $queryParams;
        $queryParamsForGeneratingCurrentUrl['edit'] = $this->editconf;
        $queryParamsForGeneratingCurrentUrl['returnUrl'] = $this->retUrl;
        if ($requestAction->shouldProcessData()) {
            // Unset default values since we don't need them anymore. But only if all
            // records have been persisted. If a DataHandler hook rejected a new record,
            // editconf still contains the NEW* key and defVals must be preserved so
            // the form reloads with the correct default values, such as the record type.
            $hasUnresolvedNewRecords = false;
            foreach ($this->editconf as $tableCmds) {
                foreach (array_keys($tableCmds) as $uid) {
                    if (str_contains((string)$uid, 'NEW')) {
                        $hasUnresolvedNewRecords = true;
                        break 2;
                    }
                }
            }
            if (!$hasUnresolvedNewRecords) {
                unset($queryParamsForGeneratingCurrentUrl['defVals']);
            }
        }

        // Preview code is implicit only generated for GET requests, having the query
        // parameters "popViewId" (the preview page id) and "showPreview" set.
        if ($this->popViewId && ($queryParams['showPreview'] ?? false)) {
            // Generate the preview code (markup), which is added to the module body later
            $body = $this->getPreviewUriBuilderForRecordPreview($this->popViewId)->buildImmediateActionElement([PreviewUriBuilder::OPTION_SWITCH_FOCUS => null]);
            // After generating the preview code, those params should no longer be applied to the form
            // action, as this would otherwise always refresh the preview window on saving the record.
            unset($queryParamsForGeneratingCurrentUrl['showPreview'], $queryParamsForGeneratingCurrentUrl['popViewId']);
        }

        $event = new AfterFormEnginePageInitializedEvent($this, $request);
        $this->eventDispatcher->dispatch($event);

        if ($requestAction->isPostRequest) {
            // In case save&view is requested, we have to add this information to the redirect
            // URL, since the ImmediateAction will be added to the module body afterward.
            if ($requestAction->savedokview()) {
                $queryParamsForGeneratingCurrentUrl['showPreview'] = true;
                $queryParamsForGeneratingCurrentUrl['popViewId'] = $this->popViewId;
            }
            $url = $this->uriBuilder->buildUriFromRoute('record_edit', $queryParamsForGeneratingCurrentUrl);
            return new RedirectResponse($url, 302);
        }

        // Begin to show the edit form
        $this->setModuleContext($view);
        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
        $this->pageRenderer->addInlineSetting('ShowItem', 'moduleUrl', (string)$this->uriBuilder->buildUriFromRoute('show_item'));

        // Generate the URL to the current request with modified GET parameters
        // This is used in various places to "return to" in the form and for buttons etc.
        $currentEditingUrl = $this->uriBuilder->buildUriFromRoute('record_edit', $queryParamsForGeneratingCurrentUrl);

        // Creating the editing form, wrap it with buttons, document selector etc.
        $formResults = $this->makeEditForm($request, $view, $currentEditingUrl);
        if (count($formResults) > 0) {
            $this->firstEl = $this->elementsData !== [] ? reset($this->elementsData) : null;
            $lastEl = $this->elementsData !== [] ? end($this->elementsData) : null;
            // Dispatch event for extensions to track open documents
            $this->openCurrentDocuments();
            $this->formResultHandler->addAssets($formResults);
            // Put together the various elements (buttons, selectors, form) into a table
            $body .= '
            <form
                action="' . htmlspecialchars((string)$currentEditingUrl) . '"
                method="post"
                enctype="multipart/form-data"
                name="editform"
                id="EditDocumentController"
            >
            ' . $formResults->getHtml() . '
            <input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
            <input type="hidden" name="popViewId" value="' . htmlspecialchars((string)$lastEl?->viewId) . '" />
            <input type="hidden" name="closeDoc" value="0" />
            <input type="hidden" name="returnNewPageId" value="' . ($this->returnNewPageId ? 1 : 0) . '" />';
            $body .= '</form>';
        }

        if ($this->firstEl === null) {
            // In case firstEl is null, no edit form could be created. Therefore, add an
            // info box and remove the spinner, since it will never be resolved by FormEngine.
            $view->setUiBlock(false);
            $body .= $this->getInfobox(
                $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm.message'),
                $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noEditForm'),
            );
        }

        // Access check...
        // The page will show only if there is a valid page and if this page may be viewed by the user
        $perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
        $this->pageinfo = BackendUtility::readPageAccess($this->firstEl?->viewId, $perms_clause) ?: [];

        $documentTitle = $this->resolveDocumentTitle($this->getLanguageService());

        // Setting up the buttons, markers for doc header and navigation component state
        $this->createBreadcrumb($view);
        $this->getButtons($view, $request, $this->firstEl, $currentEditingUrl, $documentTitle);

        // Create language switch options if the record is already persisted, and it is a single record to edit
        if ($this->isSingleRecordView() && $this->firstEl?->isSavedRecord()) {
            $this->languageSwitch($view, $this->firstEl);
        }

        $view->setTitle($documentTitle);
        $view->setLayout(ModuleLayout::NORMAL);
        $view->assign('bodyHtml', $body);

        return $view->renderResponse('Form/EditDocument');
    }

    protected function sanitizeEditConf(array $editConf): array
    {
        $newConfiguration = [];
        $beUser = $this->getBackendUser();
        // Traverse the GPvar edit array tables
        foreach ($editConf as $table => $conf) {
            if (!is_array($conf) || !$this->tcaSchemaFactory->has($table)) {
                // Skip for invalid config or in case no TCA exists
                continue;
            }
            if (!$beUser->check('tables_modify', $table)) {
                // Skip in case the user has insufficient permissions and increment the error counter
                $this->numberOfErrors++;
                continue;
            }
            // Traverse the keys/comments of each table (keys can be a comma list of uids)
            foreach ($conf as $cKey => $command) {
                if ($command !== 'edit' && $command !== 'new') {
                    // Skip if invalid command
                    continue;
                }
                $ids = GeneralUtility::trimExplode(',', (string)$cKey, true);
                foreach ($ids as $id) {
                    $newConfiguration[$table][$id] = $command;
                }
            }
        }
        // Change $this->editconf if versioning applies to any of the records
        return $this->fixWSversioningInEditConf($newConfiguration);
    }

    protected function setModuleContext(ModuleTemplate $view): void
    {
        $view->assign('moduleContext', '');
        $view->assign('moduleContextId', '');
        if ($this->module === null) {
            return;
        }

        $view->setModuleName($this->module->getIdentifier());

        if ($this->module->isStandalone()) {
            $parent = $this->module;
        } else {
            $parent = $this->module->getParentModule();
            while ($parent?->getParentModule() !== null) {
                $parent = $parent->getParentModule();
            }
        }
        if ($parent === null) {
            return;
        }
        $moduleContext = $parent->getIdentifier();

        if ($moduleContext === 'file') {
            // Workaround for filelist using 'media' as ModuleStorage module context… :\
            $moduleContext = 'media';
        }

        $view->assign('moduleContext', $moduleContext);
    }

    protected function prepareColumnsOnlyConfigurationFromRequest(ServerRequestInterface $request): array
    {
        $columnsOnly = $request->getParsedBody()['columnsOnly'] ?? $request->getQueryParams()['columnsOnly'] ?? null;
        $usedTables = array_keys($request->getQueryParams()['edit'] ?? []);
        $finalColumnsOnly = [];
        if (is_array($columnsOnly) && $columnsOnly !== []) {
            $finalColumnsOnly = array_map(function ($fields) {
                return is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true);
            }, $columnsOnly);
            $finalColumnsOnly = $this->addSlugFieldsToColumnsOnly($finalColumnsOnly, $usedTables);
        }
        return $finalColumnsOnly;
    }

    /**
     * Always add required fields of slug field
     */
    protected function addSlugFieldsToColumnsOnly(array $finalColumnsOnly, array $tables): array
    {
        foreach ($tables as $table) {
            if (!empty($finalColumnsOnly[$table]) && $this->tcaSchemaFactory->has($table)) {
                $schema = $this->tcaSchemaFactory->get($table);
                foreach ($finalColumnsOnly[$table] as $field) {
                    if (!$schema->hasField($field)) {
                        continue;
                    }
                    $field = $schema->getField($field);
                    $postModifiers = $field->getConfiguration()['generatorOptions']['postModifiers'] ?? [];
                    if ($field->isType(TableColumnType::SLUG)
                        && (!is_array($postModifiers) || $postModifiers === [])
                    ) {
                        $fieldGroups = $field->getConfiguration()['generatorOptions']['fields'] ?? [];
                        if (is_string($fieldGroups)) {
                            $fieldGroups = [$fieldGroups];
                        }
                        foreach ($fieldGroups as $fields) {
                            $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_merge(
                                $finalColumnsOnly['__hiddenGeneratorFields'][$table] ?? [],
                                (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true))
                            );
                        }
                    }
                }
                if (!empty($finalColumnsOnly['__hiddenGeneratorFields'][$table])) {
                    $finalColumnsOnly['__hiddenGeneratorFields'][$table] = array_diff(
                        array_unique($finalColumnsOnly['__hiddenGeneratorFields'][$table]),
                        $finalColumnsOnly[$table]
                    );
                }
            }
        }
        return $finalColumnsOnly;
    }

    /**
     * Do processing of data, submitting it to DataHandler.
     *
     * Also handles the "duplication" of a record.
     */
    protected function processData(ModuleTemplate $view, ServerRequestInterface $request): void
    {
        $requestAction = FormAction::createFromRequest($request);
        $parsedBody = $request->getParsedBody();

        $beUser = $this->getBackendUser();

        $dataMap = $parsedBody['data'] ?? [];
        $dataHandlerIncomingCommandMap = $parsedBody['cmd'] ?? [];
        $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? false);

        /** @var DataHandler $dataHandler */
        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        // Only options related to $dataMap submission are included here
        $dataHandler->setControl($parsedBody['control'] ?? []);

        // Set default values fetched previously from GET / POST vars
        if (is_array($dataMap)) {
            foreach ($dataMap as $tableName => $records) {
                if (is_array($this->defVals[$tableName] ?? null)) {
                    foreach ($records as $uid => $_) {
                        if (str_contains((string)$uid, 'NEW')) {
                            $dataMap[$tableName][$uid] = array_merge($this->defVals[$tableName], $dataMap[$tableName][$uid]);
                        }
                    }
                }
            }
        }

        // Load DataHandler with data
        $dataHandler->start($dataMap, $dataHandlerIncomingCommandMap);
        if (is_array($parsedBody['mirror'] ?? null)) {
            $dataHandler->setMirror($parsedBody['mirror']);
        }

        // Perform the saving operation with DataHandler:
        if ($requestAction->shouldProcessData()) {
            $dataHandler->process_datamap();
            $dataHandler->process_cmdmap();

            // Update the module menu for the current backend user, as they updated their UI language
            $currentUserId = $beUser->getUserId();
            if ($currentUserId
                && (string)($dataMap['be_users'][$currentUserId]['lang'] ?? '') !== ''
                && $dataMap['be_users'][$currentUserId]['lang'] !== $beUser->user['lang']
            ) {
                $newLanguageKey = $dataMap['be_users'][$currentUserId]['lang'];
                // Update the current backend user language as well
                $beUser->user['lang'] = $newLanguageKey;
                // Re-create LANG to have the current request updated the translated page as well
                $this->getLanguageService()->init($newLanguageKey);
                BackendUtility::setUpdateSignal('updateBackendLanguage', [ 'language' => $newLanguageKey ]);
            }

            // If pages are being edited, we set an instruction about updating the page tree after this operation.
            if ($dataHandler->pagetreeNeedsRefresh
                && (isset($dataMap['pages']) || $beUser->workspace !== 0 && !empty($dataMap))
            ) {
                BackendUtility::setUpdateSignal('updatePageTree');
            }

            // If there was saved any new items, load them:
            if (!empty($dataHandler->substNEWwithIDs)) {
                // Save the expanded/collapsed states for new inline records, if any
                $this->updateInlineView($request->getParsedBody()['uc'] ?? $request->getQueryParams()['uc'] ?? null, $dataHandler);
                $newEditConf = [];
                // Traverse all new records and forge the content of $this->editconf so we can continue to edit these records!
                // $this->editconf is now updated to replace NEW-Ids with the actual persisted IDs.
                foreach ($this->editconf as $tableName => $tableCmds) {
                    $keys = array_keys($dataHandler->substNEWwithIDs_table, $tableName);
                    if ($keys !== []) {
                        foreach ($keys as $key) {
                            $editId = $dataHandler->substNEWwithIDs[$key];
                            // Check if the $editId isn't a child record of an IRRE action
                            if (!(is_array($dataHandler->newRelatedIDs[$tableName] ?? null)
                                && in_array($editId, $dataHandler->newRelatedIDs[$tableName]))
                            ) {
                                // Translate new id to the workspace version
                                if ($versionRec = BackendUtility::getWorkspaceVersionOfRecord(
                                    $beUser->workspace,
                                    $tableName,
                                    $editId,
                                    'uid'
                                )) {
                                    $editId = $versionRec['uid'];
                                }
                                $newEditConf[$tableName][$editId] = 'edit';
                            }
                            if ($tableName === 'pages'
                                && !$this->shouldRedirectToEmptyPage()
                                && $this->retUrl !== $this->getCloseUrl($request)
                                && $this->returnNewPageId
                            ) {
                                $this->retUrl .= '&id=' . $dataHandler->substNEWwithIDs[$key];
                            }
                        }
                    } else {
                        $newEditConf[$tableName] = $tableCmds;
                    }
                }
                if ($newEditConf !== []) {
                    $this->editconf = $newEditConf;
                }
            }
            // See if any records was auto-created as new versions?
            if (!empty($dataHandler->autoVersionIdMap)) {
                $this->editconf = $this->fixWSversioningInEditConf($this->editconf, $dataHandler->autoVersionIdMap);
            }
        }

        // If a document is saved and a new one is created right after.
        if ($requestAction->savedoknew()) {
            // Find the current table
            reset($this->editconf);
            $nTable = (string)key($this->editconf);
            // Determine insertion mode: 'top' is self-explaining,
            // otherwise new elements are inserted after one using a negative uid
            $insertRecordOnTop = ($this->getTsConfigOption($nTable, 'saveDocNew') === 'top');
            $ids = array_keys($this->editconf[$nTable]);
            // Depending on $insertRecordOnTop, retrieve either the first or last id to get the records' pid+uid
            if ($insertRecordOnTop) {
                $nUid = (int)reset($ids);
            } else {
                $nUid = (int)end($ids);
            }
            $nRec = BackendUtility::getRecord($nTable, $nUid);
            if ($insertRecordOnTop) {
                $relatedPageId = $nRec['pid'];
            } else {
                if ((int)($nRec['t3ver_oid'] ?? 0) === 0) {
                    $relatedPageId = -$nRec['uid'];
                } else {
                    // Use uid of live version of workspace version
                    $relatedPageId = -$nRec['t3ver_oid'];
                }
            }
            // Setting a blank editconf array for a new record:
            $this->editconf = [];
            $this->editconf[$nTable][$relatedPageId] = 'new';
        }

        // Explicitly require a save operation
        if ($requestAction->shouldProcessData()) {
            $erroneousRecords = $dataHandler->printLogErrorMessages();
            $messages = [];
            $table = (string)key($this->editconf);
            $uidList = array_keys($this->editconf[$table]);

            foreach ($uidList as $uid) {
                $uid = (int)abs($uid);
                if (!in_array($table . '.' . $uid, $erroneousRecords, true)) {
                    $realUidInPayload = ($tceSubstId = array_search($uid, $dataHandler->substNEWwithIDs, true)) !== false ? $tceSubstId : $uid;
                    $row = $dataMap[$table][$uid] ?? $dataMap[$table][$realUidInPayload] ?? null;
                    if ($row === null) {
                        continue;
                    }
                    // Ensure, uid is always available to make labels with foreign table lookups possible
                    $row['uid'] ??= $realUidInPayload;
                    // If the label column of the record is not available, fetch it from database.
                    // This is the case when EditDocumentController is booted in single field mode (e.g.
                    // Template module > 'info/modify' > edit 'setup' field) or in case the field is
                    // not in "showitem" or is set to readonly (e.g. "file" in sys_file_metadata).
                    $labelCapability = $this->tcaSchemaFactory->get($table)->getCapability(TcaSchemaCapability::Label);
                    $labelFields = $labelCapability->getAllLabelFieldNames();
                    foreach ($labelFields as $labelField) {
                        if (!isset($row[$labelField])) {
                            $tmpRecord = BackendUtility::getRecord($table, $uid, $labelFields);
                            if ($tmpRecord !== null) {
                                $row = array_merge($row, $tmpRecord);
                            }
                            break;
                        }
                    }
                    $recordTitle = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($table, $row));
                    $messages[] = sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.message'), $recordTitle);
                }
            }

            // Add messages to the flash message container only if the request is a save action (excludes "duplicate")
            if ($messages !== []) {
                $label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.title.plural');
                if (count($messages) === 1) {
                    $label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.record_saved.title.singular');
                }
                if (count($messages) > 10) {
                    $messages = [sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:notification.mass_saving.message'), count($messages))];
                }
                $defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE);
                $flashMessage = new FlashMessage(
                    implode(LF, $messages),
                    $label,
                    ContextualFeedbackSeverity::OK,
                    true
                );
                $defaultFlashMessageQueue->enqueue($flashMessage);
            }
        }

        // If a document should be duplicated.
        if ($requestAction->duplicatedoc()) {
            // Find current table
            reset($this->editconf);
            $nTable = (string)key($this->editconf);
            // Find the first id, getting the records pid+uid
            $nUid = array_keys($this->editconf[$nTable]);
            $nUid = reset($nUid);
            if (!MathUtility::canBeInterpretedAsInteger($nUid)) {
                $nUid = $dataHandler->substNEWwithIDs[$nUid];
            }

            $nRec = BackendUtility::getRecord($nTable, $nUid);

            // Setting a blank editconf array for a new record
            $this->editconf = [];

            if ((int)($nRec['t3ver_oid'] ?? 0) > 0) {
                $relatedPageId = -$nRec['t3ver_oid'];
            } else {
                $relatedPageId = -$nRec['uid'];
            }

            /** @var DataHandler $duplicateTce */
            $duplicateTce = GeneralUtility::makeInstance(DataHandler::class);
            $duplicateCmd = [
                $nTable => [
                    $nUid => [
                        'copy' => $relatedPageId,
                    ],
                ],
            ];

            $duplicateTce->start([], $duplicateCmd);
            $duplicateTce->process_cmdmap();
            $duplicateUid = $duplicateTce->copyMappingArray[$nTable][$nUid] ?? null;
            if ($duplicateUid !== null) {
                if ($nTable === 'pages') {
                    BackendUtility::setUpdateSignal('updatePageTree');
                }

                $this->editconf[$nTable][$duplicateUid] = 'edit';

                // Inform the user of the duplication
                $view->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicated'));
            } else {
                $this->numberOfErrors++;
                // Inform the user about the failed duplication
                $view->addFlashMessage(
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.recordDuplicationFailed'),
                    '',
                    ContextualFeedbackSeverity::ERROR
                );
            }
        }
    }

    protected function getPreviewUriBuilderForRecordPreview($pageId): PreviewUriBuilder
    {
        $array_keys = array_keys($this->editconf);
        $table = reset($array_keys);
        $recordId = 0;
        if ($table) {
            $uids = array_keys($this->editconf[$table]);
            $recordId = (int)((reset($uids) ?: null) ?? '');
        }
        return PreviewUriBuilder::createForRecordPreview($table, $recordId, $pageId);
    }

    protected function createBreadcrumb(ModuleTemplate $view): void
    {
        // Handle file metadata records
        $file = null;
        if ($this->firstEl !== null && $this->firstEl->table === 'sys_file_metadata' && $this->firstEl->uid > 0) {
            // Happens if it is a select/group
            $fileUid = $this->firstEl->record['file'] ?? 0;
            if (is_array($fileUid)) {
                $fileUid = reset($fileUid);
            }
            try {
                $file = $this->resourceFactory->getFileObject((int)$fileUid);
            } catch (FileDoesNotExistException|InsufficientUserPermissionsException $e) {
                // do nothing when file is not accessible
            }
        }

        if ($file instanceof FileInterface) {
            $view->assign('moduleContextId', $file->getParentFolder()->getCombinedIdentifier());
            $view->getDocHeaderComponent()->setResourceBreadcrumb($file);
        } elseif ($this->pageinfo !== [] && $this->firstEl !== null) {
            $l10nParent = (int)($this->pageinfo['l10n_parent'] ?? 0);
            $pageUid =  $this->pageinfo['uid'] ?? '';
            $view->assign('moduleContextId', $l10nParent !== 0 ? $l10nParent : $pageUid);

            // Determine breadcrumb based on action (edit existing vs. create new)
            if ($this->firstEl->isSavedRecord()) {
                if ($this->isSingleRecordView()) {
                    // Edit single existing record
                    $breadcrumbContext = $this->breadcrumbFactory->forEditAction(
                        $this->firstEl->table,
                        (int)$this->firstEl->uid
                    );
                } else {
                    // Edit multiple records
                    $breadcrumbContext = $this->breadcrumbFactory->forEditMultipleAction(
                        $this->firstEl->table,
                        (int)($this->pageinfo['uid'] ?? 0)
                    );
                }
            } else {
                // Create new record
                $breadcrumbContext = $this->breadcrumbFactory->forNewAction(
                    $this->firstEl->table,
                    (int)($this->pageinfo['uid'] ?? 0),
                    $this->defVals[$this->firstEl->table] ?? []
                );
            }
            $view->getDocHeaderComponent()->setBreadcrumbContext($breadcrumbContext);
        }
    }

    /**
     * Creates the editing form with FormEngine, based on the input from GPvars.
     *
     * @return FormResultCollection Form result objects
     */
    protected function makeEditForm(ServerRequestInterface $request, ModuleTemplate $view, UriInterface $currentRequestUrl): FormResultCollection
    {
        // Initialize variables
        $formResults = new FormResultCollection();
        // Traverse the GPvar edit array tables
        foreach ($this->editconf as $table => $conf) {
            // Traverse the keys/comments of each table (keys can be a comma list of uids)
            foreach ($conf as $theUid => $command) {
                try {
                    $formDataCompilerInput = [
                        'request' => $request,
                        'tableName' => $table,
                        'vanillaUid' => (int)$theUid,
                        'command' => $command,
                        'returnUrl' => (string)$currentRequestUrl,
                    ];
                    if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
                        $formDataCompilerInput['overrideValues'] = $this->overrideVals[$table];
                    }
                    if (is_array($this->defVals) && $this->defVals !== []) {
                        $formDataCompilerInput['defaultValues'] = $this->defVals;
                    }

                    $formData = $this->formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class));

                    $viewId = 0;
                    if ($table === 'pages') {
                        // Only set viewId in case it's not a new page - as this can not be viewed before being saved
                        if ($command !== 'new' && MathUtility::canBeInterpretedAsInteger($formData['databaseRow']['uid'])) {
                            $viewId = (int)$formData['databaseRow']['uid'];
                        }
                    } elseif (!empty($formData['parentPageRow']['uid'])) {
                        $viewId = $formData['parentPageRow']['uid'];
                    }

                    // Display "is-locked" message
                    if ($command === 'edit') {
                        $lockInfo = BackendUtility::isRecordLocked($table, $formData['databaseRow']['uid']);
                        if ($lockInfo) {
                            $view->addFlashMessage($lockInfo['msg'], '', ContextualFeedbackSeverity::WARNING);
                        }
                    }

                    $el = new FormElementData(
                        title: $formData['recordTitle'],
                        table: $table,
                        uid: $formData['databaseRow']['uid'],
                        pid: ($formData['databaseRow']['pid'] ?? $viewId),
                        record: $formData['databaseRow'],
                        viewId: (int)$viewId,
                        command: $command,
                        userPermissionOnPage: $formData['userPermissionOnPage'],
                    );

                    $this->elementsData[] = $el;

                    if ($command !== 'new') {
                        BackendUtility::lockRecords($table, $el->uid, $table === 'tt_content' ? $el->pid : 0);
                    }

                    // Set list if only specific fields should be rendered. This will trigger
                    // ListOfFieldsContainer instead of FullRecordContainer in FormWrapContainer
                    if (!empty($this->columnsOnly[$table])) {
                        $formData['fieldListToRender'] = implode(',', $this->columnsOnly[$table]);
                        if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) {
                            $formData['hiddenFieldListToRender'] = implode(',', $this->columnsOnly['__hiddenGeneratorFields'][$table]);
                        }
                    }

                    $formData['renderType'] = 'formWrapContainer';
                    $formResult = $this->nodeFactory->create($formData)->render();

                    if ($command === 'new') {
                        $tableTitle = htmlspecialchars($this->resolveTypeLabel($table, $formData['databaseRow']));
                        $formHeading = $this->getLanguageService()->sL('core.core:labels.createNew') . ' ' . $tableTitle;
                        $formResult['html'] = '<h1>' . $formHeading . '</h1>' . $formResult['html'];
                    } else {
                        $recordTitle = trim($formData['recordTitle']) !== ''
                            ? $formData['recordTitle']
                            : '[' . $this->getLanguageService()->sL('core.core:labels.no_title') . ']';
                        $recordTitle = BackendUtility::cropToTitleLength($recordTitle);
                        $recordIdentity = $this->getRecordIdentityHtml($table, $formData['databaseRow']);
                        $formResult['html'] = '<h1>' . htmlspecialchars($recordTitle) . '</h1>' . $recordIdentity . $formResult['html'];
                    }

                    // Seems the pid is set as hidden field (again) at end?!
                    if ($command === 'new') {
                        $formResult['html'] .= '<input type="hidden"'
                            . ' name="data[' . htmlspecialchars($table) . '][' . htmlspecialchars($el->uid) . '][pid]"'
                            . ' value="' . $el->pid . '" />';
                    }

                    $formResult = $this->formResultFactory->create($formResult);
                    $formResults->add($formResult);
                } catch (NoFieldsToRenderException $e) {
                    $this->numberOfErrors++;
                    $formResults->add(new FormResult(
                        $this->getInfobox(
                            $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm.message'),
                            $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:noFieldsEditForm'),
                        )
                    ));
                } catch (AccessDeniedException $e) {
                    $this->numberOfErrors++;

                    $message = $e->getMessage();
                    $title = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noEditPermission');
                    $formResults->add(new FormResult($this->getInfobox($message, $title)));
                } catch (DatabaseRecordException|DatabaseRecordWorkspaceDeletePlaceholderException $e) {
                    $formResults->add(new FormResult($this->getInfobox($e->getMessage())));
                }
            }
        }
        return $formResults;
    }

    /**
     * Helper function for rendering an Infobox
     */
    protected function getInfobox(string $message, ?string $title = null): string
    {
        return '<div class="callout callout-danger">'
                . '<div class="callout-icon">'
                    . '<span class="icon-emphasized">'
                        . $this->iconFactory->getIcon('actions-close', IconSize::SMALL)->render()
                    . '</span>'
                . '</div>'
                . '<div class="callout-content">'
                    . ($title ? '<div class="callout-title">' . htmlspecialchars($title) . '</div>' : '')
                    . '<div class="callout-body">' . htmlspecialchars($message) . '</div>'
                . '</div>'
            . '</div>';
    }

    /**
     * Create the panel of buttons for submitting the form or otherwise perform operations.
     */
    protected function getButtons(ModuleTemplate $view, ServerRequestInterface $request, ?FormElementData $mainFormElement, UriInterface $currentEditingUrl, string $documentTitle): void
    {
        if ($mainFormElement !== null) {
            $record = $mainFormElement->record;
            $schema = $this->tcaSchemaFactory->get($mainFormElement->table);
            $this->registerCloseButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 1);

            // Show buttons (duplicate, new, view save) when table is not read-only
            if (!$this->numberOfErrors && !$schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
                $view->addButtonToButtonBar($this->componentFactory->createSaveButton('EditDocumentController')->setDisabled(true), ButtonBar::BUTTON_POSITION_LEFT, 2);
                $this->registerViewButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 3);

                if ($mainFormElement->command !== 'new') {
                    $languageCapability = $schema->isLanguageAware() ? $schema->getCapability(TcaSchemaCapability::Language) : null;
                    $languageId = 0;
                    if (
                        $mainFormElement->isSavedRecord()
                        && $schema->isLanguageAware()
                        && isset($record[($languageField = $languageCapability->getLanguageField()->getName())])
                    ) {
                        $languageId = (int)$record[$languageField];
                    } elseif (isset($this->defVals[$mainFormElement->table]['language_tag'])) {
                        $languageId = (int)$this->defVals[$mainFormElement->table]['language_tag'];
                    }
                    $l10nParent = 0;
                    $translationOriginPointerField = $languageCapability ? $languageCapability->getTranslationOriginPointerField()->getName() : null;
                    if ($translationOriginPointerField && isset($record[$translationOriginPointerField])) {
                        $value = $record[$translationOriginPointerField];
                        if (is_array($value)) {
                            $value = reset($value);
                        }
                        // Happens on group
                        if (is_array($value) && isset($value['uid'])) {
                            $value = $value['uid'];
                        }
                        $l10nParent = (int)$value;
                    }

                    if ($mainFormElement->table === 'tt_content') {
                        $canCreateNewOrDuplicate = $this->isInconsistentLanguageHandlingAllowed() || $this->isPageContentFreeTranslationMode($mainFormElement, $languageId);
                    } else {
                        $canCreateNewOrDuplicate = $languageId === 0 || $l10nParent === 0;
                    }
                    if ($canCreateNewOrDuplicate) {
                        $this->registerNewButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 4);
                        $this->registerDuplicationButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 5);
                    }
                }
                if ($mainFormElement->isSavedRecord()) {
                    $this->registerHistoryButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 1, $currentEditingUrl);
                    $this->registerDeleteButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 6, $request);
                }
                $this->registerColumnsOnlyButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_LEFT, 7, $currentEditingUrl);
            }
        }

        $this->registerInfoButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 2);
        $this->registerOpenInNewWindowButtonToButtonBar($view, ButtonBar::BUTTON_POSITION_RIGHT, 3, $request);
        $this->registerShortcutButtonToButtonBar($view, $request, $documentTitle);
    }

    /**
     * Return true if inconsistent language handling is allowed
     */
    protected function isInconsistentLanguageHandlingAllowed(): bool
    {
        $allowInconsistentLanguageHandling = BackendUtility::getPagesTSconfig(
            $this->pageinfo['uid'] ?? 0
        )['mod']['web_layout']['allowInconsistentLanguageHandling'] ?? ['value' => '0'];

        return $allowInconsistentLanguageHandling['value'] === '1';
    }

    /**
     * Checks if the page is in free translation mode for tt_content
     */
    protected function isPageContentFreeTranslationMode(FormElementData $formElementData, int $languageId): bool
    {
        if ($formElementData->table !== 'tt_content') {
            return false;
        }
        if (!$formElementData->isSavedRecord()) {
            return $this->getFreeTranslationMode(
                (int)($this->pageinfo['uid'] ?? 0),
                (int)($this->defVals[$formElementData->table]['colPos'] ?? 0),
                $languageId
            );
        }
        return $this->getFreeTranslationMode(
            (int)($this->pageinfo['uid'] ?? 0),
            (int)($formElementData->record['colPos'] ?? 0),
            $languageId
        );
    }

    /**
     * True if the page is in free translation mode.
     */
    protected function getFreeTranslationMode(int $page, int $column, int $language): bool
    {
        $freeTranslationMode = false;
        if ($this->getConnectedContentElementTranslationsCount($page, $column, $language) === 0
            && $this->getStandAloneContentElementTranslationsCount($page, $column, $language) >= 0
        ) {
            $freeTranslationMode = true;
        }
        return $freeTranslationMode;
    }

    /**
     * Register the close button to the button bar
     */
    protected function registerCloseButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void
    {
        $closeButton = $this->componentFactory->createLinkButton()
            ->setHref('#')
            ->setClasses('t3js-editform-close')
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
            ->setShowLabelText(true)
            ->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL))
            ->setDisabled(true);
        $view->addButtonToButtonBar($closeButton, $position, $group);
    }

    /**
     * Register the view button to the button bar
     */
    protected function registerViewButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void
    {
        if ($this->firstEl === null) {
            return;
        }
        // Pid to show the record
        if (!$this->firstEl->viewId) {
            return;
        }
        if ($this->firstEl->table === '') {
            return;
        }
        // @TODO: TsConfig option should change to viewDoc
        if (!$this->getTsConfigOption($this->firstEl->table, 'saveDocView')) {
            return;
        }

        $previewUriBuilderForCurrentPage = PreviewUriBuilder::create($this->pageinfo)->isPreviewable();
        $pageId = $this->popViewId ?: $this->firstEl->viewId;
        $previewUriBuilder = PreviewUriBuilder::createForRecordPreview($this->firstEl->table, $this->firstEl->record, $pageId);
        if ($previewUriBuilderForCurrentPage || $previewUriBuilder->isPreviewable()) {
            $previewUrl = $previewUriBuilder->buildUri();
            if ($previewUrl) {
                $viewButton = $this->componentFactory->createLinkButton()
                    ->setHref((string)$previewUrl)
                    ->setIcon($this->iconFactory->getIcon('actions-view', IconSize::SMALL))
                    ->setShowLabelText(true)
                    ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.viewDoc'))
                    ->setClasses('t3js-editform-view')
                    ->setDisabled(true);
                if (!$this->firstEl->isSavedRecord() && $this->firstEl->table === 'pages') {
                    $viewButton->setDataAttributes(['is-new' => '']);
                }
                $view->addButtonToButtonBar($viewButton, $position, $group);
            }
        }
    }

    /**
     * Register the new button to the button bar
     */
    protected function registerNewButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void
    {
        if ($this->firstEl === null) {
            return;
        }
        if ($this->firstEl->table === '') {
            return;
        }
        if ($this->firstEl->table === 'sys_file_metadata') {
            return;
        }
        if (!$this->getTsConfigOption($this->firstEl->table, 'saveDocNew')) {
            return;
        }
        $newButton = $this->componentFactory->createLinkButton()
            ->setHref('#')
            ->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL))
            ->setShowLabelText(true)
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.newDoc'))
            ->setClasses('t3js-editform-new')
            ->setDisabled(true);
        if (!$this->firstEl->isSavedRecord()) {
            $newButton->setDataAttributes(['is-new' => '']);
        }
        $view->addButtonToButtonBar($newButton, $position, $group);
    }

    /**
     * Register the duplication button to the button bar
     */
    protected function registerDuplicationButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void
    {
        if (!$this->isSingleRecordView()) {
            return;
        }
        if ($this->firstEl->table === '') {
            return;
        }
        if ($this->firstEl->table === 'sys_file_metadata') {
            return;
        }
        if (!$this->getTsConfigOption($this->firstEl->table, 'showDuplicate')) {
            return;
        }
        $duplicateButton = $this->componentFactory->createLinkButton()
            ->setHref('#')
            ->setShowLabelText(true)
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.duplicateDoc'))
            ->setIcon($this->iconFactory->getIcon('actions-document-duplicates-select', IconSize::SMALL))
            ->setClasses('t3js-editform-duplicate')
            ->setDisabled(true);
        if (!$this->firstEl->isSavedRecord()) {
            $duplicateButton->setDataAttributes(['is-new' => '']);
        }
        $view->addButtonToButtonBar($duplicateButton, $position, $group);
    }

    /**
     * Register the delete button to the button bar
     */
    protected function registerDeleteButtonToButtonBar(ModuleTemplate $view, string $position, int $group, ServerRequestInterface $request): void
    {
        if (!$this->isSingleRecordView()) {
            return;
        }
        if (!$this->firstEl?->isSavedRecord()) {
            return;
        }
        if ($this->getDisableDelete()) {
            return;
        }
        if ($this->isRecordCurrentBackendUser()) {
            return;
        }
        if (!$this->firstEl->hasDeleteAccess()) {
            return;
        }
        $returnUrl = $this->retUrl;
        if ($this->firstEl->table === 'pages') {
            // The below is a hack to replace the return url with an url to the current module on id=0. Otherwise,
            // this might lead to empty views, since the current id is the page, which is about to be deleted.
            $parsedUrl = parse_url($returnUrl);
            // @todo consider using $this->module here
            $routePath = str_replace($this->backendEntryPointResolver->getPathFromRequest($request), '', $parsedUrl['path'] ?? '');
            parse_str($parsedUrl['query'] ?? '', $queryParams);
            if ($routePath
                && isset($queryParams['id'])
                && (string)$this->firstEl->uid === (string)$queryParams['id']
            ) {
                try {
                    // TODO: Use the page's pid instead of 0, this requires a clean API to manipulate the page
                    // tree from the outside to be able to mark the pid as active
                    $returnUrl = (string)$this->uriBuilder->buildUriFromRoutePath($routePath, ['id' => 0]);
                } catch (ResourceNotFoundException $e) {
                    // Resolved path can not be matched to a configured route
                }
            }
        }

        $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
        $numberOfReferences = $referenceIndex->getNumberOfReferencedRecords(
            $this->firstEl->table,
            (int)$this->firstEl->uid
        );
        $referenceCountMessage = BackendUtility::referenceCount(
            $this->firstEl->table,
            (int)$this->firstEl->uid,
            $this->getLanguageService()->sL(
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord'
            ),
            (string)$numberOfReferences
        );

        $translations = $this->localizationRepository->getRecordTranslations(
            $this->firstEl->table,
            (int)$this->firstEl->uid,
        );
        $count = count($translations);
        $translationCountMessage = '';
        if ($count > 0) {
            $translationCountMessage = sprintf($this->getLanguageService()->sL(
                'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord'
            ), $count);
        }

        $deleteUrl = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
            'cmd' => [
                $this->firstEl->table => [
                    $this->firstEl->uid => [
                        'delete' => '1',
                    ],
                ],
            ],
            'redirect' => $returnUrl,
        ]);

        $recordInfo = $this->firstEl->title;
        if ($this->getBackendUser()->shallDisplayDebugInformation()) {
            $recordInfo .= ' [' . $this->firstEl->table . ':' . $this->firstEl->uid . ']';
        }

        $deleteButton = $this->componentFactory->createLinkButton()
            ->setClasses('t3js-editform-delete-record')
            ->setDataAttributes([
                'uid' => $this->firstEl->uid,
                'table' => $this->firstEl->table,
                'record-info' => trim($recordInfo),
                'reference-count-message' => $referenceCountMessage,
                'translation-count-message' => $translationCountMessage,
            ])
            ->setHref($deleteUrl)
            ->setIcon($this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL))
            ->setShowLabelText(true)
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:deleteItem'))
            ->setDisabled(true);
        $view->addButtonToButtonBar($deleteButton, $position, $group);
    }

    /**
     * Register the info button to the button bar
     */
    protected function registerInfoButtonToButtonBar(ModuleTemplate $view, string $position, int $group): void
    {
        if (!$this->isSingleRecordView()) {
            return;
        }
        if (!$this->firstEl?->isSavedRecord()) {
            return;
        }
        $button = $this->componentFactory->createGenericButton();
        $button->setLabel($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo'));
        $button->setAttributes([
            'type' => 'button',
            'data-dispatch-action' => 'TYPO3.InfoWindow.showItem',
            'data-dispatch-args-list' => $this->firstEl->table . ',' . $this->firstEl->uid,
            'disabled' => 'disabled',
        ]);
        $button->setIcon($this->iconFactory->getIcon('actions-document-info', IconSize::SMALL));
        $view->addButtonToButtonBar($button, $position, $group);
    }

    /**
     * Register the history button to the button bar
     */
    protected function registerHistoryButtonToButtonBar(ModuleTemplate $view, string $position, int $group, UriInterface $currentEditingUrl): void
    {
        if (!$this->isSingleRecordView()) {
            return;
        }
        if ($this->firstEl === null) {
            return;
        }
        if ($this->firstEl->table === '') {
            return;
        }
        if (!$this->getTsConfigOption($this->firstEl->table, 'showHistory', '1')) {
            return;
        }
        $historyUrl = (string)$this->uriBuilder->buildUriFromRoute('record_history', [
            'element' => $this->firstEl->table . ':' . $this->firstEl->uid,
            'returnUrl' => (string)$currentEditingUrl,
        ]);
        $historyButton = $this->componentFactory->createLinkButton()
            ->setHref($historyUrl)
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:recordHistory'))
            ->setIcon($this->iconFactory->getIcon('actions-document-history-open', IconSize::SMALL))
            ->setDisabled(true);
        $view->addButtonToButtonBar($historyButton, $position, $group);
    }

    /**
     * Register "Edit whole record" button to the button bar
     */
    protected function registerColumnsOnlyButtonToButtonBar(ModuleTemplate $view, string $position, int $group, UriInterface $currentEditingUrl): void
    {
        if (!$this->isSingleRecordView()) {
            return;
        }
        if ($this->columnsOnly === []) {
            return;
        }
        $query = $currentEditingUrl->getQuery();
        $query .= '&columnsOnly=';
        $url = $currentEditingUrl->withQuery($query);
        $columnsOnlyButton = $this->componentFactory->createLinkButton()
            ->setHref((string)$url)
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:editWholeRecord'))
            ->setShowLabelText(true)
            ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL))
            ->setDisabled(true);

        $view->addButtonToButtonBar($columnsOnlyButton, $position, $group);
    }

    /**
     * Register the open in new window button to the button bar
     */
    protected function registerOpenInNewWindowButtonToButtonBar(ModuleTemplate $view, string $position, int $group, ServerRequestInterface $request): void
    {
        $closeUrl = $this->getCloseUrl($request);
        if ($this->returnUrl === $closeUrl) {
            return;
        }
        // Generate a URL to the current edit form
        $arguments = $this->getUrlQueryParamsForCurrentRequest($request);
        $arguments['returnUrl'] = $closeUrl;
        $requestUri = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $arguments);
        $openInNewWindowButton = $this->componentFactory
            ->createLinkButton()
            ->setHref('#')
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openInNewWindow'))
            ->setIcon($this->iconFactory->getIcon('actions-window-open', IconSize::SMALL))
            ->setDataAttributes([
                'dispatch-action' => 'TYPO3.WindowManager.localOpen',
                'dispatch-args' => GeneralUtility::jsonEncodeForHtmlAttribute([
                    $requestUri,
                    true, // switchFocus
                    md5($requestUri), // windowName,
                    'width=670,height=500,status=0,menubar=0,scrollbars=1,resizable=1', // windowFeatures
                ]),
            ])
            ->setDisabled(true);
        $view->addButtonToButtonBar($openInNewWindowButton, $position, $group);
    }

    /**
     * Register the shortcut button to the button bar
     */
    protected function registerShortcutButtonToButtonBar(ModuleTemplate $view, ServerRequestInterface $request, string $documentTitle): void
    {
        if ($this->returnUrl === $this->getCloseUrl($request)) {
            return;
        }
        $arguments = $this->getUrlQueryParamsForCurrentRequest($request);
        $view->getDocHeaderComponent()->setShortcutContext(
            'record_edit',
            $documentTitle,
            $arguments
        );
    }

    protected function getUrlQueryParamsForCurrentRequest(ServerRequestInterface $request): array
    {
        $queryParams = $request->getQueryParams();
        $potentialArguments = [
            'edit',
            'defVals',
            'overrideVals',
            'columnsOnly',
            'returnNewPageId',
            'module',
        ];
        $arguments = [];
        foreach ($potentialArguments as $argument) {
            if (!empty($queryParams[$argument])) {
                $arguments[$argument] = $queryParams[$argument];
            }
        }
        return $arguments;
    }

    /**
     * Get the count of connected translated content elements
     */
    protected function getConnectedContentElementTranslationsCount(int $page, int $column, int $language): int
    {
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
        return (int)$queryBuilder
            ->andWhere(
                $queryBuilder->expr()->gt(
                    $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(),
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                )
            )
            ->executeQuery()
            ->fetchOne();
    }

    /**
     * Get the count of standalone translated content elements
     */
    protected function getStandAloneContentElementTranslationsCount(int $page, int $column, int $language): int
    {
        $queryBuilder = $this->getQueryBuilderForTranslationMode($page, $column, $language);
        return (int)$queryBuilder
            ->andWhere(
                $queryBuilder->expr()->eq(
                    $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(),
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                )
            )
            ->executeQuery()
            ->fetchOne();
    }

    /**
     * Get the query builder for the translation mode
     */
    protected function getQueryBuilderForTranslationMode(int $page, int $column, int $language): QueryBuilder
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
        return $queryBuilder
            ->count('uid')
            ->from('tt_content')
            ->where(
                $queryBuilder->expr()->eq(
                    'pid',
                    $queryBuilder->createNamedParameter($page, Connection::PARAM_INT)
                ),
                $queryBuilder->expr()->eq(
                    $this->tcaSchemaFactory->get('tt_content')->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName(),
                    $queryBuilder->createNamedParameter($language, Connection::PARAM_INT)
                ),
                $queryBuilder->expr()->eq(
                    'colPos',
                    $queryBuilder->createNamedParameter($column, Connection::PARAM_INT)
                )
            );
    }

    /**
     * Update expanded/collapsed states on new inline records if any within backendUser->uc.
     *
     * @param array|null $uc The uc array to be processed and saved - uc[inlineView][...]
     * @param DataHandler $dataHandler Instance of DataHandler that saved data before
     */
    protected function updateInlineView(?array $uc, DataHandler $dataHandler): void
    {
        if (!is_array($uc['inlineView'] ?? null)) {
            return;
        }
        $backendUser = $this->getBackendUser();
        $inlineView = (array)json_decode(is_string($backendUser->uc['inlineView'] ?? false) ? $backendUser->uc['inlineView'] : '', true);
        foreach ($uc['inlineView'] as $topTable => $topRecords) {
            foreach ($topRecords as $topUid => $childElements) {
                foreach ($childElements as $childTable => $childRecords) {
                    $uids = array_keys($dataHandler->substNEWwithIDs_table, $childTable);
                    if (!empty($uids)) {
                        $newExpandedChildren = [];
                        foreach ($childRecords as $childUid => $state) {
                            if ($state && in_array($childUid, $uids)) {
                                $newChildUid = $dataHandler->substNEWwithIDs[$childUid];
                                $newExpandedChildren[] = $newChildUid;
                            }
                        }
                        // Add new expanded child records to UC (if any):
                        if (!empty($newExpandedChildren)) {
                            $inlineViewCurrent = &$inlineView[$topTable][$topUid][$childTable];
                            if (is_array($inlineViewCurrent)) {
                                $inlineViewCurrent = array_unique(array_merge($inlineViewCurrent, $newExpandedChildren));
                            } else {
                                $inlineViewCurrent = $newExpandedChildren;
                            }
                        }
                    }
                }
            }
        }
        $backendUser->uc['inlineView'] = json_encode($inlineView);
        $backendUser->writeUC();
    }

    /**
     * Returns if delete for the current table is disabled by configuration.
     * For sys_file_metadata in default language delete is always disabled.
     */
    protected function getDisableDelete(): bool
    {
        $disableDelete = false;
        if ($this->firstEl?->table === 'sys_file_metadata') {
            $row = $this->firstEl->record;
            if ((int)($row['language_tag'] ?? 0) === 0) {
                // Always disable for default language
                $disableDelete = true;
            }
        } else {
            $disableDelete = (bool)$this->getTsConfigOption($this->firstEl->table, 'disableDelete');
        }
        return $disableDelete;
    }

    /**
     * Return true in case the current record is the current backend user
     */
    protected function isRecordCurrentBackendUser(): bool
    {
        $backendUser = $this->getBackendUser();
        return $this->firstEl?->table === 'be_users' && (int)($this->firstEl->uid) === $backendUser->getUserId();
    }

    /**
     * Returns the URL (usually for the "returnUrl") which closes the current window.
     * Used when editing a record in a popup.
     */
    protected function getCloseUrl(ServerRequestInterface $request): string
    {
        return (string)PathUtility::getSystemResourceUri('EXT:backend/Resources/Public/Html/Close.html', $request);
    }

    /**
     * Make selector box for creating new translation for a record or switching to edit the record
     * in an existing language. Displays only languages which are available for the current page.
     */
    protected function languageSwitch(ModuleTemplate $view, FormElementData $formElement): void
    {
        $backendUser = $this->getBackendUser();
        if (!$this->tcaSchemaFactory->has($formElement->table)) {
            return;
        }
        $schema = $this->tcaSchemaFactory->get($formElement->table);
        if (!$schema->isLanguageAware()) {
            return;
        }
        $languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
        $languageField = $languageCapability->getLanguageField()->getName();
        $transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName();

        $table = $formElement->table;
        if (!$backendUser->check('tables_modify', $table)) {
            return;
        }
        $uid = $formElement->uid;

        // Get all available languages for the page
        // If editing a page, the translations of the current UID need to be fetched
        if ($table === 'pages') {
            if (is_array($formElement->record[$transOrigPointerField] ?? null)) {
                $l10nParent = $formElement->record[$transOrigPointerField];
                $l10nParent = reset($l10nParent);
            } else {
                $l10nParent = $formElement->record[$transOrigPointerField] ?? 0;
            }
            // Ensure the check is always done against the default language page
            $availableLanguages = $this->getLanguages(
                (int)($l10nParent ?: $uid),
                $table
            );
        } else {
            $availableLanguages = $this->getLanguages($formElement->pid, $table);
        }
        // Remove default language, if user does not have access. This is necessary, since
        // the default language is always added when fetching the system languages (#88504).
        if (isset($availableLanguages[0]) && !$this->getBackendUser()->checkLanguageAccess(0)) {
            unset($availableLanguages[0]);
        }
        // Page available in other languages than default language?
        if (count($availableLanguages) > 1) {
            $rowsByLang = [];
            $fetchFields = ['uid', $languageField, $transOrigPointerField];
            // Get record in current language
            $rowCurrent = BackendUtility::getLiveVersionOfRecord($table, $uid, $fetchFields);
            if (!is_array($rowCurrent)) {
                $rowCurrent = BackendUtility::getRecord($table, $uid, $fetchFields);
            }
            $currentLanguage = (int)$rowCurrent[$languageField];
            // Disabled for records with [all] language!
            if ($currentLanguage > -1) {
                // Get record in default language if needed
                if ($currentLanguage && $rowCurrent[$transOrigPointerField]) {
                    $rowsByLang[0] = BackendUtility::getLiveVersionOfRecord(
                        $table,
                        $rowCurrent[$transOrigPointerField],
                        $fetchFields
                    );
                    if (!is_array($rowsByLang[0])) {
                        $rowsByLang[0] = BackendUtility::getRecord(
                            $table,
                            $rowCurrent[$transOrigPointerField],
                            $fetchFields
                        );
                    }
                } else {
                    $rowsByLang[$rowCurrent[$languageField]] = $rowCurrent;
                }
                // List of language id's that should not be added to the selector
                $noAddOption = [];
                if ($rowCurrent[$transOrigPointerField] || $currentLanguage === 0) {
                    // Get record in other languages to see what's already available
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
                    $queryBuilder->getRestrictions()
                        ->removeAll()
                        ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
                        ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $backendUser->workspace));
                    $result = $queryBuilder->select(...$fetchFields)
                        ->from($table)
                        ->where(
                            $queryBuilder->expr()->eq(
                                'pid',
                                $queryBuilder->createNamedParameter($formElement->pid, Connection::PARAM_INT)
                            ),
                            $queryBuilder->expr()->gt(
                                $languageField,
                                $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                            ),
                            $queryBuilder->expr()->eq(
                                $transOrigPointerField,
                                $queryBuilder->createNamedParameter($rowsByLang[0]['uid'], Connection::PARAM_INT)
                            )
                        )
                        ->executeQuery();
                    while ($row = $result->fetchAssociative()) {
                        if ($backendUser->workspace !== 0 && $schema->isWorkspaceAware()) {
                            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $table, $row['uid'], 'uid,t3ver_state');
                            if (!empty($workspaceVersion)) {
                                $versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0);
                                if ($versionState === VersionState::DELETE_PLACEHOLDER) {
                                    // If a workspace delete placeholder exists for this translation: Mark
                                    // this language as "don't add to selector" and continue with next row,
                                    // otherwise an edit link to a delete placeholder would be created, which
                                    // does not make sense.
                                    $noAddOption[] = (int)$row[$languageField];
                                    continue;
                                }
                            }
                        }
                        $rowsByLang[$row[$languageField]] = $row;
                    }
                }
                $languageDropDownButton = $this->componentFactory->createDropDownButton()
                    ->setLabel($this->getLanguageService()->sL('core.core:labels.language'))
                    ->setShowActiveLabelText(true)
                    ->setShowLabelText(true);

                $existingLanguageItems = [];
                $newLanguageItems = [];

                foreach ($availableLanguages as $languageId => $language) {
                    $selectorOptionLabel = $language['title'];
                    // Create url for creating a localized record
                    $addOption = true;
                    $createNewLanguageLink = '';

                    if (!isset($rowsByLang[$languageId])) {
                        // Translation in this language does not exist
                        if ($this->columnsOnly[$table] ?? false) {
                            // Don't add option since we are in a view with just a subset of fields, those views
                            // are specific editing fields only views and are not meant for translation handling.
                            $addOption = false;
                        } elseif (!isset($rowsByLang[0]['uid'])) {
                            // Don't add option since no default row to localize from exists
                            // TODO: Actually tt_content is able to localize from another l10n_source then L=0.
                            //       This however is currently only possible via the translation wizard.
                            $addOption = false;
                        }
                    } else {
                        $params = [
                            'edit[' . $table . '][' . $rowsByLang[$languageId]['uid'] . ']' => 'edit',
                            'module' => $this->module?->getIdentifier() ?? '',
                            'returnUrl' => $this->retUrl,
                        ];
                        if ($this->columnsOnly[$table] ?? false) {
                            $params['columnsOnly'] = [$table => $this->columnsOnly[$table]];
                        }
                        if ($table === 'pages') {
                            // Disallow manual adjustment of the language field for pages
                            $params['overrideVals'] = [
                                'pages' => [
                                    'sys_language_uid' => $languageId,
                                ],
                            ];
                        }
                        $createNewLanguageLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $params);
                    }
                    if ($addOption && !in_array($languageId, $noAddOption, true)) {
                        if (!$createNewLanguageLink) {
                            $languageItem = $this->componentFactory->createDropDownItem()
                                ->setTag('typo3-backend-localization-button')
                                ->setAttribute('record-type', $table)
                                ->setAttribute('record-uid', (string)$rowsByLang[0]['uid'])
                                ->setAttribute('target-language', (string)$languageId)
                                ->setLabel($selectorOptionLabel);
                            if (!empty($language['flagIcon'])) {
                                $languageItem->setIcon($this->iconFactory->getIcon($language['flagIcon']));
                            }
                            $newLanguageItems[] = $languageItem;
                        } else {
                            $isActive = $languageId === $currentLanguage;
                            $languageItem = $this->componentFactory->createDropDownRadio()
                                ->setLabel($selectorOptionLabel)
                                ->setHref($createNewLanguageLink)
                                ->setActive($isActive);
                            if (!empty($language['flagIcon'])) {
                                $languageItem->setIcon($this->iconFactory->getIcon($language['flagIcon']));
                            }
                            $existingLanguageItems[] = $languageItem;
                        }
                    }
                }

                // Add existing languages first
                foreach ($existingLanguageItems as $item) {
                    $languageDropDownButton->addItem($item);
                }

                // Add separator and new languages if any
                if (!empty($newLanguageItems)) {
                    $languageDropDownButton->addItem($this->componentFactory->createDropDownDivider());
                    $languageDropDownButton->addItem(
                        $this->componentFactory->createDropDownHeader()
                            ->setLabel($this->getLanguageService()->sL('core.core:labels.new_page_translation'))
                    );
                    foreach ($newLanguageItems as $item) {
                        $languageDropDownButton->addItem($item);
                    }
                }

                $view->getDocHeaderComponent()->setLanguageSelector($languageDropDownButton);
            }
        }
    }

    /**
     * Returns languages available for record translations on given page.
     *
     * @param int $id Page id: If zero, all available system languages will be returned. If set to
     *                another value, only languages, a page translation exists for, will be returned.
     * @param string $table For pages we want all languages, for other records the languages of the page translations
     * @return array Array with languages (uid, title, ISOcode, flagIcon)
     */
    protected function getLanguages(int $id, string $table): array
    {
        // This usually happens when a non-pages record is added after another, so we are fetching the proper page ID
        if ($id < 0 && $table !== 'pages') {
            $pageId = $this->pageinfo['uid'] ?? null;
            if ($pageId !== null) {
                $pageId = (int)$pageId;
            } else {
                $fullRecord = BackendUtility::getRecord($table, abs($id));
                $pageId = (int)$fullRecord['pid'];
            }
        } else {
            if ($table === 'pages' && $id > 0) {
                $fullRecord = BackendUtility::getRecordWSOL('pages', $id);
                $id = (int)($fullRecord['t3ver_oid'] ?: $fullRecord['uid']);
            }
            $pageId = $id;
        }
        // Fetch the current translations of this page, to only show the ones where there is a page translation
        $allLanguages = array_filter(
            GeneralUtility::makeInstance(TranslationConfigurationProvider::class)->getSystemLanguages($pageId),
            static fn(array $language): bool => (int)$language['uid'] !== -1
        );
        if ($table !== 'pages' && $id > 0) {
            $translatedPages = $this->localizationRepository->getPageTranslations($pageId, [], $this->getBackendUser()->workspace);
            $availableLanguages = [];
            if ($allLanguages[0] ?? false) {
                $availableLanguages = [
                    0 => $allLanguages[0],
                ];
            }
            foreach ($translatedPages as $translatedPage) {
                $languageId = $translatedPage->get('sys_language_uid');
                if (isset($allLanguages[$languageId])) {
                    $availableLanguages[$languageId] = $allLanguages[$languageId];
                }
            }
            return $availableLanguages;
        }
        return $allLanguages;
    }

    /**
     * Fix $this->editconf if versioning applies to any of the records
     *
     * @param array|null $mapArray Mapping between old and new ids if auto-versioning has been performed.
     */
    protected function fixWSversioningInEditConf(array $editConf, ?array $mapArray = null): array
    {
        $finalConfiguration = [];
        foreach ($editConf as $table => $conf) {
            // Traverse the keys/comments of each table (keys can be a comma list of uids)
            $newConf = [];
            foreach ($conf as $theUid => $cmd) {
                if ($cmd === 'edit') {
                    if (is_array($mapArray)) {
                        if ($mapArray[$table][$theUid] ?? false) {
                            $theUid = $mapArray[$table][$theUid];
                        }
                    } else {
                        // Default, look for versions in workspace for record:
                        $calcPRec = $this->getRecordForEdit($table, (int)$theUid);
                        if (is_array($calcPRec)) {
                            // Setting UID again if it had changed, due to workspace versioning.
                            $theUid = (int)$calcPRec['uid'];
                        }
                    }
                    // Add the possibly manipulated IDs to the new-build newConf array:
                    $newConf[$theUid] = $cmd;
                } else {
                    $newConf[$theUid] = $cmd;
                }
            }
            $finalConfiguration[$table] = $newConf;
        }
        return $finalConfiguration;
    }

    /**
     * Get record for editing.
     *
     * @return array|false Returns record to edit, false if none
     */
    protected function getRecordForEdit(string $table, int $recordId): array|bool
    {
        $schema = $this->tcaSchemaFactory->get($table);
        // Fetch requested record:
        $reqRecord = BackendUtility::getRecord($table, $recordId, 'uid,pid' . ($schema->isWorkspaceAware() ? ',t3ver_oid' : ''));
        if (is_array($reqRecord)) {
            // If workspace is OFFLINE:
            if ($this->getBackendUser()->workspace !== 0) {
                // Check for versioning support of the table:
                if ($schema->isWorkspaceAware()) {
                    // If the record is already a version of "something" pass it by.
                    if ($reqRecord['t3ver_oid'] > 0 || VersionState::tryFrom($reqRecord['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER) {
                        // (If it turns out not to be a version of the current workspace there will be trouble, but
                        // that is handled inside DataHandler then and in the interface it would clearly be an error of
                        // links if the user accesses such a scenario)
                        return $reqRecord;
                    }
                    // The input record was online and an offline version must be found or made:
                    // Look for version of this workspace:
                    $versionRec = BackendUtility::getWorkspaceVersionOfRecord(
                        $this->getBackendUser()->workspace,
                        $table,
                        $reqRecord['uid'],
                        'uid,pid,t3ver_oid'
                    );
                    return is_array($versionRec) ? $versionRec : $reqRecord;
                }
                // This means that editing cannot occur on this record because it was not supporting versioning
                // which is required inside an offline workspace.
                return false;
            }
            // In ONLINE workspace, just return the originally requested record:
            return $reqRecord;
        }
        // Return FALSE because the table/uid was not found anyway.
        return false;
    }

    /**
     * The return value is used for the variable $this->storeArray to prepare 'open documents' urls
     */
    protected function compileStoreData(ServerRequestInterface $request, array $overriddenValues): array
    {
        $queryParams = $request->getQueryParams();
        $parsedBody = $request->getParsedBody();

        $storeArray = [];
        foreach (['edit', 'defVals', 'overrideVals' , 'columnsOnly'] as $key) {
            $value = $overriddenValues[$key] ?? $parsedBody[$key] ?? $queryParams[$key] ?? null;
            if ($value !== null) {
                $storeArray[$key] = $value;
            }
        }
        return $storeArray;
    }

    /**
     * Get a TSConfig 'option.' array, possibly for a specific table.
     */
    protected function getTsConfigOption(string $table, string $key, string $defaultValue = ''): string
    {
        return trim((string)(
            $this->getBackendUser()->getTSConfig()['options.'][$key . '.'][$table]
            ?? $this->getBackendUser()->getTSConfig()['options.'][$key]
            ?? $defaultValue
        ));
    }

    /**
     * Called when someone is done finishing editing - either by just hitting "close" or "save + close",
     * but also for New/AddController when using returnEditConf.
     *
     * At this time, "closing" open documents in the session and unlocking should be done already.
     *
     * @return ResponseInterface|null Redirect response if needed
     */
    protected function closeAndPossiblyRedirectAction(FormAction $requestAction): ?ResponseInterface
    {
        if ($requestAction->shouldCloseWithARedirect()) {
            // If ->returnEditConf is set, then add the current content of editconf to the ->retUrl variable: used by
            // other scripts, like wizard_add, to know which records was created or so...
            if ($this->returnEditConf && !$this->shouldRedirectToEmptyPage()) {
                $this->retUrl .= '&returnEditConf=' . rawurlencode((string)json_encode($this->editconf));
            }
            return new RedirectResponse($this->retUrl, 303);
        }
        if ($this->retUrl === '') {
            return null;
        }
        return new RedirectResponse((string)$this->returnUrl, 303);
    }

    protected function shouldRedirectToEmptyPage(): bool
    {
        return $this->retUrl === (string)$this->uriBuilder->buildUriFromRoute('dummy');
    }

    /**
     * Close the currently open document(s) by dispatching the appropriate event.
     * This is called when the user explicitly closes, or when transitioning to a new/duplicated record.
     */
    /**
     * Notify extensions that document(s) have been opened for editing.
     * Dispatches one event per record being opened.
     */
    protected function openCurrentDocuments(): void
    {
        // Dispatch one event per record
        foreach ($this->elementsData as $element) {
            $this->eventDispatcher->dispatch(
                new AfterRecordOpenedEvent(
                    table: $element->table,
                    uid: $element->uid,
                    record: $element->record,
                )
            );
        }
    }

    /**
     * Resolves the document title used for the browser tab and shortcut.
     *
     * Examples:
     *   - No form:            "Edit form could not be loaded"
     *   - New record:          "Create new {table}"
     *   - Single record:       "{record title} · {table} · #{uid}"
     *   - Record with subtype: "{record title} · {subtype} · #{uid}"
     *   - Multiple records:    "Edit multiple · {table} · N records"
     */
    protected function resolveDocumentTitle(LanguageService $languageService): string
    {
        $firstEl = $this->elementsData[0] ?? null;
        if ($firstEl === null) {
            return $languageService->sL('backend.alt_doc:noEditForm');
        }

        $typeLabel = htmlspecialchars($this->resolveTypeLabel($firstEl->table, $firstEl->record));

        if ($firstEl->command === 'new') {
            return $languageService->sL('core.core:labels.createNew') . ' ' . $typeLabel;
        }

        $recordCount = count($this->elementsData);
        if ($recordCount > 1) {
            return implode(' · ', [
                $languageService->sL('core.core:labels.editMultiple'),
                $typeLabel,
                $languageService->translate('labels.records', 'core.core', ['count' => $recordCount]),
            ]);
        }

        $recordTitle = trim($firstEl->title) !== ''
            ? $firstEl->title
            : '[' . $languageService->sL('core.core:labels.no_title') . ']';
        $recordTitle = BackendUtility::cropToTitleLength($recordTitle);
        $recordTitle = htmlspecialchars($recordTitle);

        return implode(' · ', array_filter([$recordTitle, $typeLabel, '#' . $firstEl->uid]));
    }

    /**
     * Returns an HTML snippet showing the record type icon, table title and uid.
     */
    protected function getRecordIdentityHtml(string $table, array $row): string
    {
        $icon = $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL)->render();
        $tableTitle = $this->resolveTypeLabel($table, $row);
        $uid = (string)($row['uid'] ?? '');

        $recordType = '<span class="recordidentity-type">' . $icon . htmlspecialchars($tableTitle) . '</span>';

        $debugInfo = $this->getBackendUser()->shallDisplayDebugInformation() ? $table . ':' : '';
        $recordIdentity = sprintf(
            '<small class="recordidentity-id" title="%s">[%s%s]</small>',
            htmlspecialchars($this->getLanguageService()->sL('core.core:labels.uid') . ' ' . $uid),
            htmlspecialchars($debugInfo),
            htmlspecialchars($uid),
        );

        return '<div class="recordidentity mb-3">' . $recordType . $recordIdentity . '</div>';
    }

    /**
     * Resolves a human-readable type label for a given table and record.
     */
    protected function resolveTypeLabel(string $table, array $record): string
    {
        $languageService = $this->getLanguageService();
        $schema = $this->tcaSchemaFactory->has($table)
            ? $this->tcaSchemaFactory->get($table)
            : null;
        $typeLabel = $schema !== null
            ? $schema->getTitle($languageService->sL(...))
            : $table;

        if ($schema !== null && $schema->supportsSubSchema()) {
            $fieldName = $schema->getSubSchemaTypeInformation()->getFieldName();
            $rawTypeValue = $record[$fieldName] ?? '';
            $typeValue = is_array($rawTypeValue) ? (string)($rawTypeValue[0] ?? '') : (string)$rawTypeValue;
            if ($typeValue !== '') {
                $label = $languageService->sL($this->schemaLabelResolver->getLabelForFieldValue($table, $fieldName, $typeValue, $record));
                if ($label === '' && $schema->hasSubSchema($typeValue)) {
                    $label = $schema->getSubSchema($typeValue)->getTitle($languageService->sL(...));
                }
                if ($label !== '') {
                    $typeLabel = $label;
                }
            }
        }

        return $typeLabel;
    }

    protected function resolveDefaultReturnUrl(): string
    {
        $module = $this->moduleProvider->getFirstAccessibleModule($this->getBackendUser());
        $routeName = $module ? $module->getIdentifier() : 'dummy';
        return (string)$this->uriBuilder->buildUriFromRoute($routeName);
    }

    /**
     * Whether a single record view is requested. This
     * means, only one element exists in $elementsData.
     */
    protected function isSingleRecordView(): bool
    {
        return count($this->elementsData) === 1;
    }

    protected function getBackendUser(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }
}
