<?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\Fluid\Core\Component;

use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Event\ModifyComponentDefinitionEvent;
use TYPO3\CMS\Fluid\Event\ProvideStaticVariablesToComponentEvent;
use TYPO3\CMS\Fluid\View\TemplatePaths;
use TYPO3Fluid\Fluid\Core\Component\ComponentAdapter;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinition;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinitionProviderInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentListProviderInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentRendererInterface;
use TYPO3Fluid\Fluid\Core\Component\ComponentTemplateResolverInterface;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
use TYPO3Fluid\Fluid\Core\ViewHelper\TemplateStructureViewHelperResolver;
use TYPO3Fluid\Fluid\Core\ViewHelper\UnresolvableViewHelperException;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolverDelegateInterface;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;

/**
 * @internal
 */
#[Autoconfigure(autowire: false)]
final readonly class DeclarativeComponentCollection implements ViewHelperResolverDelegateInterface, ComponentDefinitionProviderInterface, ComponentTemplateResolverInterface, ComponentListProviderInterface
{
    private string $templateNamePattern;

    public function __construct(
        private FrontendInterface $cache,
        private EventDispatcherInterface $eventDispatcher,
        private string $namespace,
        private array $templatePaths,
        string $templateNamePattern = '{path}/{name}/{name}',
        private bool $additionalArgumentsAllowed = false,
    ) {
        $this->templateNamePattern = trim($templateNamePattern, '/');
    }

    public function withTemplateNamePattern(string $templateNamePattern): static
    {
        return new static($this->cache, $this->eventDispatcher, $this->namespace, $this->templatePaths, $templateNamePattern, $this->additionalArgumentsAllowed);
    }

    public function withAdditionalArgumentsAllowed(bool $additionalArgumentsAllowed): static
    {
        return new static($this->cache, $this->eventDispatcher, $this->namespace, $this->templatePaths, $this->templateNamePattern, $additionalArgumentsAllowed);
    }

    public function resolveTemplateName(string $viewHelperName): string
    {
        $fragments = array_map(ucfirst(...), explode('.', $viewHelperName));
        $name = array_pop($fragments);
        $path = implode('/', $fragments);
        return ltrim(str_replace(['{path}', '{name}'], [$path, $name], $this->templateNamePattern), '/');
    }

    public function getAvailableComponents(): array
    {
        $availableTemplates = $this->getTemplatePaths()->resolveAvailableTemplateFiles(null, null, true);
        $templateNamePattern = self::convertTemplatePatternToRegularExpression($this->templateNamePattern);
        $availableComponents = [];
        foreach ($availableTemplates as $templatePath) {
            // Remove template root path
            foreach ($this->getTemplatePaths()->getTemplateRootPaths() as $rootPath) {
                if (str_starts_with($templatePath, $rootPath)) {
                    $templatePath = substr($templatePath, strlen($rootPath));
                    break;
                }
            }
            // Convert template name into ViewHelper name and validate directory structure
            // (resolveTemplateName() in reverse)
            if (!preg_match($templateNamePattern, $templatePath, $matches)) {
                continue;
            }
            $fragments = $matches['path'] ? GeneralUtility::trimExplode('/', $matches['path'], true) : [];
            $fragments[] = $matches['name'];
            $availableComponents[] = implode('.', array_map(lcfirst(...), $fragments));
        }
        return array_values(array_unique($availableComponents));
    }

    public function getTemplatePaths(): TemplatePaths
    {
        $templatePaths = new TemplatePaths();
        $templatePaths->setTemplateRootPaths($this->templatePaths);
        return $templatePaths;
    }

    public function getAdditionalVariables(string $viewHelperName): array
    {
        // Allow to provide additional variables to the component template.
        // Note that this deliberately cannot depend on runtime characteristics,
        // such as the request, as this should be done in the renderer.
        $event = $this->eventDispatcher->dispatch(
            new ProvideStaticVariablesToComponentEvent($this, $viewHelperName)
        );
        return $event->getStaticVariables();
    }

    public function getComponentDefinition(string $viewHelperName): ComponentDefinition
    {
        $cacheIdentifier = hash('xxh3', $this->namespace . '--' . $viewHelperName);
        $componentDefinition = $this->cache->get($cacheIdentifier);
        if ($componentDefinition instanceof ComponentDefinition) {
            return $componentDefinition;
        }
        $templateName = $this->resolveTemplateName($viewHelperName);
        /**
         * Extract component definition from component template
         * This part is an ugly workaround because of shortcomings in the Fluid parser.
         * Once this has been resolved on the Fluid side, there will most likely be a better API.
         * @see \TYPO3Fluid\Fluid\Core\Component\AbstractComponentCollection
         */
        $renderingContext = new RenderingContext();
        $renderingContext->setViewHelperResolver(new TemplateStructureViewHelperResolver());
        $parsedTemplate = $renderingContext->getTemplateParser()->parse(
            $this->getTemplatePaths()->getTemplateSource('Default', $templateName),
            $this->getTemplatePaths()->getTemplateIdentifier('Default', $templateName),
            $this->getTemplatePaths()->resolveTemplateFileForControllerAndActionAndFormat('Default', $templateName),
        );
        $componentDefinition = new ComponentDefinition(
            $viewHelperName,
            $parsedTemplate->getArgumentDefinitions(),
            $this->additionalArgumentsAllowed,
            $parsedTemplate->getAvailableSlots(),
        );
        // Allow modification of component definition before it's written to cache.
        // Note that this deliberately cannot depend on runtime characteristics,
        // such as the request, as this should be done during rendering (e. g. by allowing
        // arbitrary arguments)
        $event = $this->eventDispatcher->dispatch(
            new ModifyComponentDefinitionEvent($this->namespace, $componentDefinition)
        );
        $componentDefinition = $event->getComponentDefinition();
        $this->cache->set($cacheIdentifier, $componentDefinition);
        return $componentDefinition;
    }

    public function getComponentRenderer(): ComponentRendererInterface
    {
        return new EventBasedComponentRenderer($this->eventDispatcher, $this);
    }

    public function resolveViewHelperClassName(string $name): string
    {
        $expectedTemplateName = $this->resolveTemplateName($name);
        try {
            $this->getTemplatePaths()->resolveTemplateFileForControllerAndActionAndFormat('Default', $expectedTemplateName, null, true);
        } catch (InvalidTemplateResourceException $e) {
            throw new UnresolvableViewHelperException(sprintf(
                'The component template "%s" in format ".%s" could not be found in the configured template paths. %s',
                $expectedTemplateName,
                $this->getTemplatePaths()->getFormat(),
                $e->evaluatedTemplatePaths !== [] ? 'The following file paths were evaluated: "' . implode('", "', $e->evaluatedTemplatePaths) . '"' : 'No paths configured.',
            ), 1765711586);
        }
        return ComponentAdapter::class;
    }

    public function getNamespace(): string
    {
        return $this->namespace;
    }

    private static function convertTemplatePatternToRegularExpression(string $templateNamePattern): string
    {
        $delimiter = '~';
        $pathMarker = preg_quote('{path}/', $delimiter);
        $nameMarker = preg_quote('{name}', $delimiter);
        $templateNamePattern = preg_quote($templateNamePattern, $delimiter);
        if (str_contains($templateNamePattern, $pathMarker)) {
            [$beforePath, $afterPath] = explode($pathMarker, $templateNamePattern, 2);
            $templateNamePattern = $beforePath . '(?<path>(?:.+?/)?)' . str_replace($pathMarker, '(?P=path)', $afterPath);
        }
        if (str_contains($templateNamePattern, $nameMarker)) {
            [$beforeName, $afterName] = explode($nameMarker, $templateNamePattern, 2);
            $templateNamePattern = $beforeName . '(?<name>[^/]+?)' . str_replace($nameMarker, '(?P=name)', $afterName);
        }
        return $delimiter . '^' . $templateNamePattern . '$' . $delimiter;
    }
}
