<?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 TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Context\PageContext;
use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
use TYPO3\CMS\Backend\Module\ModuleData;
use TYPO3\CMS\Backend\Module\ModuleProvider;
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Service\PageLinkMessageProvider;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
use TYPO3\CMS\Backend\Template\Components\Buttons\LanguageSelectorBuilder;
use TYPO3\CMS\Backend\Template\Components\Buttons\LanguageSelectorMode;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\Drawing\BackendLayoutRenderer;
use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Backend\View\PageViewMode;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\SchemaLabelResolver;
use TYPO3\CMS\Core\Schema\TcaSchema;
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\Versioning\VersionState;

/**
 * The Content > Layout module.
 *
 * @internal This class is not part of the TYPO3 API.
 */
#[AsController]
class PageLayoutController
{
    protected PageContext $pageContext;
    protected ?TcaSchema $schema = null;
    protected ?ModuleData $moduleData = null;

    public function __construct(
        protected readonly ComponentFactory $componentFactory,
        protected readonly IconFactory $iconFactory,
        protected readonly PageRenderer $pageRenderer,
        protected readonly UriBuilder $uriBuilder,
        protected readonly PageRepository $pageRepository,
        protected readonly ModuleTemplateFactory $moduleTemplateFactory,
        protected readonly EventDispatcherInterface $eventDispatcher,
        protected readonly ModuleProvider $moduleProvider,
        protected readonly BackendLayoutRenderer $backendLayoutRenderer,
        protected readonly BackendLayoutView $backendLayoutView,
        protected readonly TcaSchemaFactory $tcaSchemaFactory,
        protected readonly ConnectionPool $connectionPool,
        protected readonly LanguageSelectorBuilder $languageSelectorBuilder,
        protected readonly PageLinkMessageProvider $pageLinkMessageProvider,
        private readonly SchemaLabelResolver $schemaLabelResolver,
    ) {}

    public function mainAction(ServerRequestInterface $request): ResponseInterface
    {
        $pageContext = $request->getAttribute('pageContext');
        if (!$pageContext instanceof PageContext) {
            throw new \RuntimeException('Required PageContext not available', 1731415237);
        }
        $this->pageContext = $pageContext;

        $languageService = $this->getLanguageService();
        $view = $this->moduleTemplateFactory->create($request);
        if (!$this->pageContext->isAccessible() || $this->pageContext->pageId === 0) {
            // In case page could not be resolved or we are on pid=0, show info to select a valid page in the tree
            $view->setTitle($languageService->translate('title', 'backend.modules.layout'));
            $view->assignMultiple([
                'pageId' => $this->pageContext->pageId,
                'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '',
            ]);
            $view->getDocHeaderComponent()->disableAutomaticReloadButton();
            return $view->renderResponse('PageLayout/PageModuleNoAccess');
        }

        $this->moduleData = $request->getAttribute('moduleData');
        $this->schema = $this->tcaSchemaFactory->get('pages');

        $pageLayoutContext = $this->createPageLayoutContext($request);

        $this->updateModuleData();
        $this->createViewModeSelection($view);
        $this->addButtonsToButtonBar($view, $request);
        $this->initializeClipboard($request);
        $event = $this->eventDispatcher->dispatch(new ModifyPageLayoutContentEvent($request, $view));

        $mainLayoutHtml = $this->backendLayoutRenderer->drawContent($request, $pageLayoutContext);
        $primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
        $pageLocalizationRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId);

        $this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');

        $view->setTitle($languageService->translate('title', 'backend.modules.layout'), $this->pageContext->getPageTitle());
        $view->getDocHeaderComponent()->setPageBreadcrumb($this->pageContext->pageRecord);
        $view->assignMultiple([
            'pageId' => $this->pageContext->pageId,
            'localizedPageId' => $pageLocalizationRecord['uid'] ?? 0,
            'pageLayoutContext' => $pageLayoutContext,
            'infoBoxes' => $this->generateMessagesForCurrentPage($request),
            'isPageEditable' => $this->isPageEditable($primaryLanguageId),
            'localizedPageTitle' => $this->pageContext->getPageTitle($primaryLanguageId),
            'eventContentHtmlTop' => $event->getHeaderContent(),
            'mainContentHtml' => $mainLayoutHtml,
            'eventContentHtmlBottom' => $event->getFooterContent(),
        ]);
        return $view->renderResponse('PageLayout/PageModule');
    }

    protected function updateModuleData(): void
    {
        $backendUser = $this->getBackendUser();
        if (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) === null) {
            // Invalid function, reset to default
            $this->moduleData->set('viewMode', PageViewMode::LayoutView->value);
        }
        if ($backendUser->workspace !== 0) {
            // In draft workspaces, always show all elements (including hidden)
            $this->moduleData->set('showHidden', true);
        }

        // Store selected languages in module data for persistence
        $this->moduleData->set('languages', $this->pageContext->selectedLanguageIds);

        // Write module data
        $backendUser->pushModuleData($this->moduleData->getModuleIdentifier(), $this->moduleData->toArray());
    }

    /**
     * Creates the menu dropdown for switching between view modes.
     *
     * Available actions:
     * - LayoutView: Single-language content editing (always available)
     * - LanguageComparisonView: Multi-language comparison (only if translations exist)
     *
     * Smart Language Selection:
     * When building URLs for mode switching, this method implements smart language selection:
     * - Switching to layout mode with 1 translation selected → keep that translation
     * - Switching to layout mode with 2+ translations selected → show default
     */
    protected function createViewModeSelection(ModuleTemplate $view): void
    {
        $languageService = $this->getLanguageService();
        $modes = [
            PageViewMode::LayoutView->value => $languageService->sL(PageViewMode::LayoutView->getLabel()),
        ];

        // Only show comparison mode if page has translations
        if (!empty($this->pageContext->languageInformation->existingTranslations)) {
            $modes[PageViewMode::LanguageComparisonView->value] = $languageService->sL(PageViewMode::LanguageComparisonView->getLabel());
        }

        // Apply TSconfig blinding
        $moduleTsConfig = $this->pageContext->getModuleTsConfig('web_layout');
        $blindActions = $moduleTsConfig['menu']['functions'] ?? [];
        foreach ($blindActions as $key => $value) {
            if (!$value && array_key_exists($key, $modes)) {
                unset($modes[$key]);
            }
        }

        // Only create menu if there are multiple actions to choose from
        if (count($modes) <= 1) {
            if (count($modes) === 1) {
                $this->moduleData->set('viewMode', array_key_first($modes));
            }
            return;
        }

        $selectedMode = (int)$this->moduleData->get('viewMode');
        if (!array_key_exists($selectedMode, $modes)) {
            // Current function is not in available modes - reset to first available mode
            $this->moduleData->set('viewMode', array_key_first($modes));
            $selectedMode = (int)array_key_first($modes);
        }

        $actionMenu = $this->componentFactory->createMenu();
        $actionMenu->setIdentifier('actionMenu');
        $actionMenu->setLabel(
            $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:pagelayout.moduleMenu.dropdown.label')
        );

        foreach ($modes as $modeValue => $label) {
            $urlParams = $this->buildViewModeSwitchParams($modeValue);
            $menuItem = $this->componentFactory->createMenuItem()
                ->setTitle($label)
                ->setHref((string)$this->uriBuilder->buildUriFromRoute('web_layout', $urlParams));

            if ($selectedMode === $modeValue) {
                $menuItem->setActive(true);
            }
            $actionMenu->addMenuItem($menuItem);
        }
        $view->getDocHeaderComponent()->getMenuRegistry()->addMenu($actionMenu);
    }

    /**
     * Build URL parameters for switching to a specific view mode.
     *
     * Implements smart language selection when switching modes:
     *
     * FROM comparison TO layout:
     * - 1 translation selected → keep that translation (user focused on it)
     * - 2+ translations selected → show default language
     *
     * FROM layout TO comparison:
     * - Keep the current language selection to preserve context
     *
     * @return array{id: int, viewMode: int, languages?: int[]}
     */
    private function buildViewModeSwitchParams(int $targetModeValue): array
    {
        $params = ['id' => $this->pageContext->pageId, 'viewMode' => $targetModeValue];
        $targetMode = PageViewMode::tryFrom($targetModeValue);

        // Smart language selection when switching to layout mode FROM comparison mode
        if ($targetMode === PageViewMode::LayoutView && $this->pageContext->hasMultipleLanguagesSelected()) {
            $nonDefaultLanguages = array_filter($this->pageContext->selectedLanguageIds, static fn($id) => $id > 0);
            $params['languages'] = [
                count($nonDefaultLanguages) === 1
                    ? reset($nonDefaultLanguages) // Single translation: keep it
                    : 0, // Multiple: show default
            ];
        }

        // When switching to comparison mode, preserve current language selection
        // The comparison mode will auto-add default language if needed
        if ($targetMode === PageViewMode::LanguageComparisonView) {
            $params['languages'] = $this->pageContext->selectedLanguageIds;
        }

        return $params;
    }

    protected function createPageLayoutContext(
        ServerRequestInterface $request
    ): PageLayoutContext {
        $backendLayout = $this->backendLayoutView->getBackendLayoutForPage($this->pageContext->pageId);
        $viewMode = count($this->pageContext->languageInformation->availableLanguages) > 1
            ? PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView
            : PageViewMode::LayoutView;
        $configuration = DrawingConfiguration::create($backendLayout, $this->pageContext->pageTsConfig, $viewMode);
        $configuration->setShowHidden((bool)$this->moduleData->get('showHidden'));

        // Build language columns map from available languages
        $languageColumns = [];
        foreach ($this->pageContext->languageInformation->availableLanguages as $language) {
            $languageColumns[$language->getLanguageId()] = $language->getTitle();
        }
        // @todo Check if this is still used at all.
        $configuration->setLanguageColumns($languageColumns);

        $configuration->setSelectedLanguageIds($this->pageContext->selectedLanguageIds);
        return GeneralUtility::makeInstance(PageLayoutContext::class, $this->pageContext, $backendLayout, $configuration, $request);
    }

    /**
     * Return an array of various messages for the current page record,
     * such as if the page has a special doktype, that can be rendered as info boxes.
     */
    protected function generateMessagesForCurrentPage(ServerRequestInterface $request): array
    {
        $languageService = $this->getLanguageService();
        $backendUser = $this->getBackendUser();

        $infoBoxes = [];
        $currentDocumentType = (int)($this->pageContext->pageRecord['doktype'] ?? 0);
        if ($currentDocumentType === PageRepository::DOKTYPE_SYSFOLDER && $this->moduleProvider->accessGranted('records', $backendUser)) {
            $infoBoxes[] = [
                'title' => $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToRecordsModule'),
                'message' => '<p>' . $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToListModuleMessage') . '</p>'
                    . '<button type="button" class="btn btn-primary" data-dispatch-action="TYPO3.ModuleMenu.showModule" data-dispatch-args-list="records">'
                        . $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:goToRecordsModule')
                    . '</button>',
                'state' => ContextualFeedbackSeverity::INFO,
            ];
        }
        if ($currentDocumentType === PageRepository::DOKTYPE_SHORTCUT) {
            $shortcutMode = (int)($this->pageContext->pageRecord['shortcut_mode'] ?? 0);
            $targetPage = [];
            $state = ContextualFeedbackSeverity::ERROR;
            if ($shortcutMode || ($this->pageContext->pageRecord['shortcut'] ?? false)) {
                switch ($shortcutMode) {
                    case PageRepository::SHORTCUT_MODE_NONE:
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['shortcut'] ?? 0), true));
                        $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredOrNotAccessibleInternalLinkMessage') : '';
                        break;
                    case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
                        $menuOfPages = $this->pageRepository->getMenu((int)($this->pageContext->pageRecord['uid'] ?? 0), '*', 'sorting', 'AND hidden = 0', true, true);
                        $targetPage = reset($menuOfPages) ?: [];
                        $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredFirstSubpageMessage') : '';
                        break;
                    case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
                        $targetPage = $this->getTargetPageIfVisible($this->pageRepository->getPage((int)($this->pageContext->pageRecord['pid'] ?? 0), true));
                        $message = $targetPage === [] ? $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredParentPageMessage') : '';
                        break;
                    default:
                        $message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage'));
                        break;
                }
                $message = htmlspecialchars($message);
                if ($targetPage !== []) {
                    $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $targetPage['uid']]);
                    $path = BackendUtility::getRecordPath($targetPage['uid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW), 1000);
                    $linkedPath = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($path) . '</a>';
                    $message .= sprintf(htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsInternalLinkMessage')), $linkedPath);
                    $message .= ' (' . htmlspecialchars($languageService->sL($this->schemaLabelResolver->getLabelForFieldValue('pages', 'shortcut_mode', (string)$shortcutMode, $this->pageContext->pageRecord))) . ')';
                    $state = ContextualFeedbackSeverity::INFO;
                }
            } else {
                $message = htmlspecialchars($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:pageIsMisconfiguredInternalLinkMessage'));
            }
            $infoBoxes[] = [
                'message' => $message,
                'state' => $state,
            ];
        }
        if ($currentDocumentType === PageRepository::DOKTYPE_LINK) {
            $primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
            $pageRecord = ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->existingTranslations[$primaryLanguageId] ?? []) !== [])
                ? $overlayRecord
                : $this->pageContext->pageRecord;
            $infoBoxes[] = $this->pageLinkMessageProvider->generateMessagesForPageTypeLink($pageRecord, $request);
        }
        if ($this->pageContext->pageRecord['content_from_pid'] ?? false) {
            // If content from different pid is displayed
            $contentPage = BackendUtility::getRecord('pages', (int)$this->pageContext->pageRecord['content_from_pid']);
            if ($contentPage === null) {
                $infoBoxes[] = [
                    'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_invalid_title'), $this->pageContext->pageRecord['content_from_pid']),
                    'state' => ContextualFeedbackSeverity::ERROR,
                ];
            } else {
                $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $this->pageContext->pageRecord['content_from_pid']]);
                $title = BackendUtility::getRecordTitle('pages', $contentPage);
                $title = BackendUtility::cropToTitleLength($title);
                $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$this->pageContext->pageRecord['content_from_pid'] . ')</a>';
                $infoBoxes[] = [
                    'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_from_pid_title'), $link),
                    'state' => ContextualFeedbackSeverity::INFO,
                ];
            }
        } elseif ($this->pageContext->pageId > 0) {
            $links = $this->getPageLinksWhereContentIsAlsoShownOn($this->pageContext->pageId);
            if (!empty($links)) {
                $infoBoxes[] = [
                    'message' => sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:content_on_pid_title'), $links),
                    'state' => ContextualFeedbackSeverity::INFO,
                ];
            }
        }
        return $infoBoxes;
    }

    /**
     * Get all pages with links where the content of a page $pageId is also shown on.
     */
    protected function getPageLinksWhereContentIsAlsoShownOn(int $pageId): string
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        $queryBuilder->getRestrictions()->removeAll();
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
        $queryBuilder->select('*')
            ->from('pages')
            ->where($queryBuilder->expr()->eq('content_from_pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)));
        $links = [];
        $rows = $queryBuilder->executeQuery()->fetchAllAssociative();
        if (!empty($rows)) {
            foreach ($rows as $row) {
                $linkToPid = $this->uriBuilder->buildUriFromRoute('web_layout', ['id' =>  $row['uid']]);
                $title = BackendUtility::getRecordTitle('pages', $row);
                $title = BackendUtility::cropToTitleLength($title);
                $link = '<a href="' . htmlspecialchars((string)$linkToPid) . '">' . htmlspecialchars($title) . ' (PID ' . (int)$row['uid'] . ')</a>';
                $links[] = $link;
            }
        }
        return implode(', ', $links);
    }

    protected function addButtonsToButtonBar(ModuleTemplate $view, ServerRequestInterface $request): void
    {
        $languageService = $this->getLanguageService();

        // Close button (show only if returnUrl is set)
        $returnUrl = GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request);
        if ($returnUrl) {
            // use button group -1 so that close button is to the left of other buttons
            $view->addButtonToButtonBar($this->componentFactory->createCloseButton($returnUrl), ButtonBar::BUTTON_POSITION_LEFT, -1);
        }

        // Language selector
        $this->createLanguageSelector($view);

        // View
        if ($viewButton = $this->makeViewButton()) {
            $view->addButtonToButtonBar($viewButton);
        }

        // Edit
        if ($editButton = $this->makeEditButton($request)) {
            $view->addButtonToButtonBar($editButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
        }

        // Cache
        $clearCacheButton = $this->componentFactory->createGenericButton()
            ->setTag('button')
            ->setLabel($languageService->sL('core.cache:page.label'))
            ->setClasses('t3js-clear-page-cache')
            ->setAttributes([
                'type' => 'button',
                'data-id' => (string)($this->pageContext->pageRecord['uid'] ?? 0),
            ])
            ->setIcon($this->iconFactory->getIcon('actions-system-cache-clear', IconSize::SMALL));
        $view->addButtonToButtonBar($clearCacheButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);

        // View settings
        if ($this->getBackendUser()->check('tables_select', 'tt_content')) {
            $viewSettingsButton = $this->componentFactory->createDropDownButton()
                ->setLabel($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view'))
                ->setIcon($this->iconFactory->getIcon('actions-cog'))
                ->setShowLabelText(true);
            $this->pageRenderer->loadJavaScriptModule('@typo3/backend/layout-module/toggle-hidden-element.js');
            $toggleHidden = $this->componentFactory->createDropDownGeneric();
            $toggleHidden->setTag('typo3-backend-page-layout-toggle-hidden');
            $toggleHiddenAttributes = ['count' => (string)$this->getNumberOfHiddenElements()];
            if ((bool)$this->moduleData->get('showHidden')) {
                $toggleHiddenAttributes['active'] = 'active';
            }
            $toggleHidden->setAttributes($toggleHiddenAttributes);
            $viewSettingsButton->addItem($toggleHidden);
            $view->addButtonToButtonBar($viewSettingsButton, ButtonBar::BUTTON_POSITION_RIGHT, 0);
        }

        // Shortcut
        $view->getDocHeaderComponent()->setShortcutContext(
            'web_layout',
            sprintf(
                '%s: %s [%d]',
                $this->getLanguageService()->translate('short_description', 'backend.modules.layout'),
                $this->pageContext->getPageTitle(),
                $this->pageContext->pageId
            ),
            [
                'id' => $this->pageContext->pageId,
                'showHidden' => (bool)$this->moduleData->get('showHidden'),
                'viewMode' => (int)$this->moduleData->get('viewMode'),
                'languages' => $this->pageContext->selectedLanguageIds,
            ]
        );
    }

    protected function initializeClipboard(ServerRequestInterface $request): void
    {
        $clipboard = GeneralUtility::makeInstance(Clipboard::class);
        $clipboard->initializeClipboard($request);
        $clipboard->lockToNormal();
        $clipboard->cleanCurrent();
        $clipboard->endClipboard();
        $elFromTable = $clipboard->elFromTable('tt_content');
        if (!empty($elFromTable) && $this->isContentEditable($this->pageContext->getPrimaryLanguageId())) {
            $pasteItem = (int)substr((string)key($elFromTable), 11);
            $pasteRecord = BackendUtility::getRecordWSOL('tt_content', $pasteItem);
            $pasteTitle = BackendUtility::getRecordTitle('tt_content', $pasteRecord);
            $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
                JavaScriptModuleInstruction::create('@typo3/backend/layout-module/paste.js')
                    ->instance([
                        'itemOnClipboardUid' => $pasteItem,
                        'itemOnClipboardTitle' => $pasteTitle,
                        'copyMode' => $clipboard->clipData['normal']['mode'] ?? '',
                    ])
            );
        }
    }

    /**
     * View Button
     */
    protected function makeViewButton(): ?ButtonInterface
    {
        // Do not create a "View webpage" button if
        // * Multiple languages are selected
        // * record is a placeholder
        // * not in "Columns" view,
        if (
            $this->pageContext->hasMultipleLanguagesSelected()
            || VersionState::tryFrom($this->pageContext->pageRecord['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER
            || PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView
        ) {
            return null;
        }

        $previewUriBuilder = PreviewUriBuilder::create($this->pageContext->pageRecord);
        if (!$previewUriBuilder->isPreviewable()) {
            return null;
        }

        return $this->componentFactory->createViewButton($previewUriBuilder
            ->withRootLine($this->pageContext->rootLine)
            ->withLanguage($this->pageContext->getPrimaryLanguageId())
            ->buildDispatcherDataAttributes() ?? []);
    }

    /**
     * Edit Button
     */
    protected function makeEditButton(ServerRequestInterface $request): ?ButtonInterface
    {
        $primaryLanguageId = $this->pageContext->getPrimaryLanguageId();
        if (!$this->isPageEditable($primaryLanguageId)
            || PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) !== PageViewMode::LayoutView
        ) {
            return null;
        }

        $pageUid = $this->pageContext->pageId;
        if ($primaryLanguageId > 0 && ($overlayRecord = $this->pageContext->languageInformation->getTranslationRecord($primaryLanguageId)) !== null) {
            $pageUid = $overlayRecord['uid'];
        }

        $editParams = [
            'edit' => ['pages' => [$pageUid => 'edit']],
            'module' => 'web_layout',
            'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
        ];
        return $this->componentFactory->createGenericButton()
            ->setTag('typo3-backend-contextual-record-edit-trigger')
            ->setAttributes([
                'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit_contextual', $editParams),
                'edit-url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', $editParams),
            ])
            ->setLabel($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:editPageProperties'))
            ->setShowLabelText(true)
            ->setIcon($this->iconFactory->getIcon('actions-page-open', IconSize::SMALL));
    }

    /**
     * Creates the language selector dropdown in the module toolbar.
     */
    protected function createLanguageSelector(ModuleTemplate $view): void
    {
        if (count($this->pageContext->languageInformation->availableLanguages) <= 1) {
            return;
        }

        $viewMode = PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView;
        $isComparisonMode = $viewMode === PageViewMode::LanguageComparisonView;
        $mode = $isComparisonMode ? LanguageSelectorMode::MULTI_SELECT : LanguageSelectorMode::SINGLE_SELECT;

        $languageSelector = $this->languageSelectorBuilder->build(
            $this->pageContext,
            $mode,
            fn(array $languageIds): string => (string)$this->uriBuilder->buildUriFromRoute('web_layout', [
                'id' => $this->pageContext->pageId,
                'viewMode' => $viewMode->value,
                'languages' => $languageIds,
            ]),
            $isComparisonMode && !empty($this->pageContext->languageInformation->existingTranslations)
        );

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

    /**
     * Returns the number of hidden elements (including those hidden by start/end times)
     * on the current page (for the current site language)
     */
    protected function getNumberOfHiddenElements(): int
    {
        $isComparisonView = (PageViewMode::tryFrom((int)$this->moduleData->get('viewMode')) ?? PageViewMode::LayoutView) === PageViewMode::LanguageComparisonView;
        $andWhere = [];
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
        $queryBuilder
            ->count('uid')
            ->from('tt_content')
            ->where(
                $queryBuilder->expr()->eq(
                    'pid',
                    $queryBuilder->createNamedParameter($this->pageContext->pageId, Connection::PARAM_INT)
                )
            );

        $languageField = $this->schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();

        // Build list of language IDs to include: always include -1 (all languages) and all selected languages
        $languageIds = [-1];
        foreach ($this->pageContext->selectedLanguageIds as $languageId) {
            $languageIds[] = $languageId;
            // In comparison mode, also include default language (0) if not already selected
            if ($isComparisonView && $languageId > 0 && !$this->pageContext->isDefaultLanguageSelected()) {
                $languageIds[] = 0;
            }
        }
        $languageIds = array_unique($languageIds);

        $queryBuilder->andWhere(
            $queryBuilder->expr()->in(
                $languageField,
                $queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY)
            )
        );

        if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)) {
            $andWhere[] = $queryBuilder->expr()->neq(
                $this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName(),
                $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
            );
        }
        if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
            $starttimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName();
            $andWhere[] = $queryBuilder->expr()->and(
                $queryBuilder->expr()->neq(
                    $starttimeField,
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                ),
                $queryBuilder->expr()->gt(
                    $starttimeField,
                    $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT)
                )
            );
        }
        if ($this->schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
            $endtimeField = $this->schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName();
            $andWhere[] = $queryBuilder->expr()->and(
                $queryBuilder->expr()->neq(
                    $endtimeField,
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                ),
                $queryBuilder->expr()->lte(
                    $endtimeField,
                    $queryBuilder->createNamedParameter($GLOBALS['SIM_ACCESS_TIME'], Connection::PARAM_INT)
                )
            );
        }
        if ($andWhere !== []) {
            $queryBuilder->andWhere(
                $queryBuilder->expr()->or(...$andWhere)
            );
        }
        $count = $queryBuilder
            ->executeQuery()
            ->fetchOne();
        return (int)$count;
    }

    /**
     * Check if page can be edited by current user.
     */
    protected function isPageEditable(int $languageId): bool
    {
        if (empty($this->pageContext->pageRecord)) {
            return false;
        }
        if ($this->schema->hasCapability(TcaSchemaCapability::AccessReadOnly)) {
            return false;
        }
        $backendUser = $this->getBackendUser();
        if ($backendUser->isAdmin()) {
            return true;
        }
        if ($this->schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
            return false;
        }
        $isEditLocked = false;
        if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) {
            $isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false;
        }
        if ($isEditLocked) {
            return false;
        }
        return $backendUser->doesUserHaveAccess($this->pageContext->pageRecord, Permission::PAGE_EDIT)
            && $backendUser->checkLanguageAccess($languageId)
            && $backendUser->check('tables_modify', 'pages');
    }

    /**
     * Check if content can be edited by current user
     */
    protected function isContentEditable(int $languageId): bool
    {
        if ($this->getBackendUser()->isAdmin()) {
            return true;
        }
        $isEditLocked = false;
        if ($this->schema->hasCapability(TcaSchemaCapability::EditLock)) {
            $isEditLocked = $this->pageContext->pageRecord[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false;
        }
        if ($isEditLocked) {
            return false;
        }
        return $this->getBackendUser()->doesUserHaveAccess($this->pageContext->pageRecord, Permission::CONTENT_EDIT)
            && $this->getBackendUser()->check('tables_modify', 'tt_content')
            && $this->getBackendUser()->checkLanguageAccess($languageId);
    }

    /**
     * Returns the target page if visible
     */
    protected function getTargetPageIfVisible(array $targetPage): array
    {
        $fieldName = $this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
        return !($targetPage[$fieldName] ?? false) ? $targetPage : [];
    }

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

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