<?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\Template\Components;

use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbContext;
use TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory;
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
use TYPO3\CMS\Backend\Template\Components\Buttons\Action\ShortcutButton;
use TYPO3\CMS\Backend\Template\Components\Buttons\ButtonInterface;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Document header component for backend modules.
 *
 * This component manages the header area of backend module views, providing:
 * - Breadcrumb navigation (via BreadcrumbContext)
 * - Button bar for action buttons (save, close, delete, etc.)
 * - Drop-down menus for module-specific actions
 *
 * The component can be enabled or disabled to control visibility of the entire
 * document header. It integrates with the ModuleTemplate to provide a consistent
 * header across all backend modules.
 *
 * Usage in a controller:
 *
 * ```
 * public function __construct(
 *     protected readonly ComponentFactory $componentFactory,
 * ) {}
 *
 * public function myAction(): ResponseInterface
 * {
 *     $view = $this->moduleTemplateFactory->create($request);
 *     $docHeader = $view->getDocHeaderComponent();
 *
 *     // Set breadcrumb for a page
 *     $docHeader->setPageBreadcrumb($pageInfo);
 *
 *     // Add action buttons using ComponentFactory
 *     $buttonBar = $docHeader->getButtonBar();
 *     $saveButton = $this->componentFactory->createSaveButton('editform');
 *     $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
 * }
 * ```
 */
#[Autoconfigure(public: true)]
class DocHeaderComponent
{
    /**
     * Button bar component for managing action buttons.
     */
    protected ButtonBar $buttonBar;

    /**
     * Breadcrumb component for rendering navigation trails.
     */
    protected Breadcrumb $breadcrumb;

    /**
     * Context information for breadcrumb rendering.
     *
     * Contains the main context (page, record, or resource) and optional suffix nodes
     * for additional navigation elements.
     */
    protected ?BreadcrumbContext $breadcrumbContext = null;

    /**
     * Whether the document header is enabled and should be rendered.
     */
    protected bool $enabled = true;

    /**
     * Language selector component.
     */
    protected ?ComponentInterface $languageSelector = null;

    /**
     * The automatic shortcut button instance, if configured.
     */
    protected ?ShortcutButton $automaticShortcutButton = null;

    /**
     * Whether the automatic reload button should be added.
     */
    protected bool $automaticReloadButton = true;

    public function __construct(
        protected readonly MenuRegistry $menuRegistry,
        protected readonly BreadcrumbFactory $breadcrumbFactory,
        protected readonly ComponentFactory $componentFactory,
    ) {
        $this->buttonBar = GeneralUtility::makeInstance(ButtonBar::class);
        $this->breadcrumb = GeneralUtility::makeInstance(Breadcrumb::class);
    }

    /**
     * Sets the breadcrumb context for rendering.
     *
     * This is the main API for providing breadcrumb information.
     *
     * For common scenarios, use the convenience methods instead:
     * - setPageBreadcrumb() for page records
     * - setRecordBreadcrumb() for any record
     * - setResourceBreadcrumb() for files or folders
     *
     * @param BreadcrumbContext|null $breadcrumbContext The breadcrumb context
     */
    public function setBreadcrumbContext(?BreadcrumbContext $breadcrumbContext): void
    {
        $this->breadcrumbContext = $breadcrumbContext;
    }

    /**
     * Sets breadcrumb from a page record array.
     *
     * Example:
     *     $view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);
     *
     * @param array $pageRecord The page record array (must contain 'uid')
     */
    public function setPageBreadcrumb(array $pageRecord): void
    {
        $this->breadcrumbContext = $this->breadcrumbFactory->forPageArray($pageRecord);
    }

    /**
     * Sets breadcrumb for editing a record.
     *
     * Example:
     *     $view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);
     *
     * @param string $table The table name
     * @param int $uid The record UID
     */
    public function setRecordBreadcrumb(string $table, int $uid): void
    {
        $this->breadcrumbContext = $this->breadcrumbFactory->forEditAction($table, $uid);
    }

    /**
     * Sets breadcrumb for any resource (file or folder).
     *
     * Example:
     *     $view->getDocHeaderComponent()->setResourceBreadcrumb($file);
     *     $view->getDocHeaderComponent()->setResourceBreadcrumb($folder);
     *
     * @param ResourceInterface $resource The resource (file or folder)
     */
    public function setResourceBreadcrumb(ResourceInterface $resource): void
    {
        $this->breadcrumbContext = $this->breadcrumbFactory->forResource($resource);
    }

    /**
     * Adds a suffix node to the current breadcrumb context.
     *
     * Suffix nodes are appended after the main breadcrumb trail and are useful for:
     * - Indicating "Create New" actions
     * - Showing "Edit Multiple" states
     * - Adding custom contextual information
     *
     * Example:
     *
     *     $docHeader->setPageBreadcrumb($pageInfo);
     *     $docHeader->addBreadcrumbSuffixNode(
     *         new BreadcrumbNode(
     *             identifier: 'new',
     *             label: 'Create New Content Element',
     *             icon: 'actions-add'
     *         )
     *     );
     *
     * Note: This creates or modifies the breadcrumb context. If you need to build
     * a complete context, use BreadcrumbFactory instead.
     *
     * @param BreadcrumbNode $node The node to append
     */
    public function addBreadcrumbSuffixNode(BreadcrumbNode $node): void
    {
        if ($this->breadcrumbContext === null) {
            $this->breadcrumbContext = new BreadcrumbContext(null, [$node]);
        } else {
            // Create new context with added suffix node
            $existingSuffixNodes = $this->breadcrumbContext->suffixNodes;
            $existingSuffixNodes[] = $node;
            $this->breadcrumbContext = new BreadcrumbContext(
                $this->breadcrumbContext->mainContext,
                $existingSuffixNodes
            );
        }
    }

    /**
     * Returns the menu registry for adding drop-down menus to the document header.
     */
    public function getMenuRegistry(): MenuRegistry
    {
        return $this->menuRegistry;
    }

    /**
     * Returns the button bar for adding action buttons to the document header.
     *
     * The button bar supports multiple button positions (left, right) and groups
     * to organize buttons logically.
     */
    public function getButtonBar(): ButtonBar
    {
        return $this->buttonBar;
    }

    /**
     * Determines whether this component is enabled and should be rendered.
     *
     * When disabled, the entire document header (including breadcrumbs, buttons,
     * and menus) will not be displayed in the backend module.
     */
    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    /**
     * Enables this component for rendering.
     */
    public function enable(): void
    {
        $this->enabled = true;
    }

    /**
     * Disables this component to prevent rendering.
     */
    public function disable(): void
    {
        $this->enabled = false;
    }

    public function setLanguageSelector(?ComponentInterface $component): void
    {
        $this->languageSelector = $component;
    }

    public function getLanguageSelector(): ?ComponentInterface
    {
        return $this->languageSelector;
    }

    /**
     * Sets the context for the automatic shortcut button.
     *
     * Controllers can use this method to provide shortcut information without
     * manually creating and adding the shortcut button. The button will be
     * automatically added to the button bar in the correct position.
     *
     * Example:
     *
     *     $docHeader->setShortcutContext('site_configuration.edit', sprintf('Edit site: %s', $siteIdentifier), ['site' => $siteIdentifier]);
     *
     * @param string $routeIdentifier The route identifier for the shortcut
     * @param string $displayName The display name shown in the bookmark list
     * @param array $arguments Optional arguments to include in the shortcut URL
     */
    public function setShortcutContext(string $routeIdentifier, string $displayName, array $arguments = []): void
    {
        $this->automaticShortcutButton = $this->componentFactory->createShortcutButton()
            ->setRouteIdentifier($routeIdentifier)
            ->setDisplayName($displayName)
            ->setArguments($arguments);
    }

    /**
     * Disables the automatic reload button for this module.
     *
     * Use this if your module needs custom reload behavior or should not
     * have a reload button at all.
     */
    public function disableAutomaticReloadButton(): void
    {
        $this->automaticReloadButton = false;
    }

    /**
     * Disables the automatic shortcut button for this module.
     *
     * Use this if your module should not have a shortcut button.
     */
    public function disableAutomaticShortcutButton(): void
    {
        $this->automaticShortcutButton = null;
    }

    /**
     * Returns the complete document header content as an array for rendering.
     *
     * This method aggregates all components (buttons, breadcrumbs) into
     * a structured array that can be consumed by the Fluid template rendering
     * the backend module layout.
     *
     * The returned array structure:
     * - 'enabled': Whether the document header should be rendered
     * - 'buttons': Array of button configurations from the button bar
     * - 'breadcrumb': Breadcrumb trail data from the breadcrumb context
     * - 'languageSelector': Language Selector
     */
    public function docHeaderContent(?ServerRequestInterface $request): array
    {
        // Process MenuRegistry and add any menus as dropdown buttons to the button bar
        $moduleMenuButton = $this->processMenuRegistry();
        if ($moduleMenuButton !== null) {
            $this->buttonBar->addButton($moduleMenuButton, ButtonBar::BUTTON_POSITION_LEFT, 0);
        }

        // Add automatic buttons (reload, shortcut)
        $this->addAutomaticButtons($request);

        return [
            'enabled' => $this->isEnabled(),
            'buttons' => $this->buttonBar->getButtons($request),
            'breadcrumb' => $this->breadcrumb->getBreadcrumb($request, $this->breadcrumbContext),
            'languageSelector' => $this->getLanguageSelector(),
        ];
    }

    /**
     * Adds automatic reload and shortcut buttons to the button bar.
     *
     * This method is called automatically by docHeaderContent() and handles:
     * - Adding automatic reload button (if enabled)
     * - Adding automatic shortcut button (if configured)
     *
     * The buttons are added to groups 90 and 91 on the right side, which are conventionally
     * used for these system buttons. This ensures they appear at the end of the button bar
     * while still allowing PSR-14 event listeners to modify or remove them via ModifyButtonBarEvent.
     */
    private function addAutomaticButtons(?ServerRequestInterface $request): void
    {
        if ($request === null) {
            return;
        }

        // Add automatic reload button if enabled
        if ($this->automaticReloadButton) {
            $reloadButton = $this->componentFactory->createReloadButton(
                $request->getAttribute('normalizedParams')->getRequestUri()
            );
            // Add to group 90 on the right (conventionally second-to-last position)
            $this->buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 90);
        }

        // Add automatic shortcut button if configured
        if ($this->automaticShortcutButton !== null) {
            // Add to group 91 on the right (conventionally last position)
            $this->buttonBar->addButton($this->automaticShortcutButton);
        }
    }

    /**
     * Processes registered menus from the MenuRegistry into a dropdown button component.
     *
     * Takes the first registered menu from the MenuRegistry and creates a dropdown button
     * component that can be added to the button bar.
     *
     * @return ButtonInterface|null The dropdown button, or null if no menus registered
     */
    private function processMenuRegistry(): ?ButtonInterface
    {
        $menus = $this->menuRegistry->getMenus();

        if ($menus === []) {
            return null;
        }

        if (count($menus) > 1) {
            throw new \RuntimeException('The menuRegistry should only contain one menu. '
            . 'Multiple DocHeaderComponents can not be displayed - prefer to add distinct dropdown '
            . 'buttons to add more view possibilities, or create actual submodules instead of secondary menus.', 1783447740);
        }

        // Use the first menu (most controllers only register one menu)
        $menu = reset($menus);

        // Hide menu if it's either empty or offers only one item
        if (count($menu->getMenuItems()) < 2) {
            return null;
        }

        $label = $menu->getLabel();
        $dropdownButton = $this->componentFactory->createDropDownButton()
            ->setShowActiveLabelText(true)
            ->setShowLabelText(true);

        foreach ($menu->getMenuItems() as $menuItem) {
            if ($label === '') {
                // Previously, the menu was rendered as a <select>, which meant the first or
                // currently selected <option> acted as the visible label. The menu itself had
                // no separate label. As a fallback, we now use the first menu item title as the
                // button label, ensuring the DropDownButton is valid. The button will still
                // always display the active item, because setShowActiveLabelText(true) is set.
                $label = $menuItem->getTitle();
            }
            $dropdownItem = $this->componentFactory->createDropDownRadio()
                ->setHref($menuItem->getHref())
                ->setLabel($menuItem->getTitle())
                ->setActive($menuItem->isActive());

            $dropdownButton->addItem($dropdownItem);
        }

        $dropdownButton->setLabel($label);

        return $dropdownButton;
    }
}
