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

use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\LinkHandling\Event\AfterLinkResolvedByStringRepresentationEvent;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

/**
 * Class responsible to find what kind of resource (type) is used
 * to link to (email, external url, file, page etc)
 * with the possibility to get a system-wide understandable "urn" to identify
 * what type it actually is, based on the scheme or prefix.
 */
class LinkService implements SingletonInterface
{
    public const TYPE_PAGE = 'page';
    public const TYPE_INPAGE = 'inpage';
    public const TYPE_URL = 'url';
    public const TYPE_EMAIL = 'email';
    public const TYPE_TELEPHONE = 'telephone';
    public const TYPE_FILE = 'file';
    public const TYPE_FOLDER = 'folder';
    public const TYPE_RECORD = 'record';
    public const TYPE_UNKNOWN = 'unknown';

    /**
     * All registered LinkHandlers
     *
     * @var LinkHandlingInterface[]
     */
    protected $handlers;

    /**
     * LinkService constructor initializes the registered handlers.
     */
    public function __construct(
        protected readonly EventDispatcherInterface $eventDispatcher,
    ) {
        $registeredLinkHandlers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['linkHandler'] ?? [];
        $registeredLinkHandlers = is_array($registeredLinkHandlers) ? $registeredLinkHandlers : [];
        /** @var array<string,class-string> $registeredLinkHandlers */
        if ($registeredLinkHandlers !== []) {
            foreach ($registeredLinkHandlers as $type => $handlerClassName) {
                if (!isset($this->handlers[$type]) || !is_object($this->handlers[$type])) {
                    $handler = GeneralUtility::makeInstance($handlerClassName);
                    if ($handler instanceof LinkHandlingInterface) {
                        $this->handlers[$type] = $handler;
                    }
                }
            }
        }
    }

    /**
     * Part of the typolink construction functionality, called by typoLink()
     * Used to resolve "legacy"-based typolinks and URNs.
     *
     * Tries to get the type of the link from the link parameter
     * could be
     *  - "mailto" an email address
     *  - "url" external URL
     *  - "file" a local file (checked AFTER getPublicUrl() is called)
     *  - "page" a page (integer)
     *
     * Does NOT check if the page exists or the file exists.
     *
     * @param string $linkParameter could be "fileadmin/myfile.jpg", "info@typo3.org", "13" or "http://www.typo3.org"
     */
    public function resolve(string $linkParameter): array
    {
        try {
            // Check if the new syntax with "t3://" is used
            return $this->resolveByStringRepresentation($linkParameter);
        } catch (UnknownUrnException $e) {
            $legacyLinkNotationConverter = GeneralUtility::makeInstance(LegacyLinkNotationConverter::class);
            return $legacyLinkNotationConverter->resolve($linkParameter);
        }
    }

    /**
     * Returns an array with data interpretation of the link target, something like t3://page?uid=23.
     *
     * @throws Exception\UnknownLinkHandlerException
     * @throws Exception\UnknownUrnException
     */
    public function resolveByStringRepresentation(string $urn): array
    {
        $result = [];
        $resolveException = null;
        try {
            // linking to any t3:// syntax
            if (stripos($urn, 't3://') === 0) {
                // lets parse the urn
                $urnParsed = parse_url($urn);
                $type = $urnParsed['host'];
                if (isset($urnParsed['query'])) {
                    parse_str(htmlspecialchars_decode($urnParsed['query']), $data);
                } else {
                    $data = [];
                }
                $fragment = $urnParsed['fragment'] ?? null;

                if (isset($this->handlers[$type])) {
                    $result = $this->handlers[$type]->resolveHandlerData($data);
                    $result['type'] = $type;
                } else {
                    $resolveException = new UnknownLinkHandlerException('LinkHandler for ' . $type . ' was not registered', 1460581769);
                }
                // this was historically named "section"
                if ($fragment) {
                    $result['fragment'] = $fragment;
                }
            } elseif (($this->handlers[self::TYPE_URL] ?? false) && PathUtility::hasProtocolAndScheme($urn)) {
                $result = $this->handlers[self::TYPE_URL]->resolveHandlerData(['url' => $urn]);
                $result['type'] = self::TYPE_URL;
            } elseif (($this->handlers[self::TYPE_EMAIL] ?? false) && str_starts_with(strtolower($urn), 'mailto:')) {
                $result = $this->handlers[self::TYPE_EMAIL]->resolveHandlerData(['email' => $urn]);
                $result['type'] = self::TYPE_EMAIL;
            } elseif (($this->handlers[self::TYPE_TELEPHONE] ?? false) && str_starts_with(strtolower($urn), 'tel:')) {
                $result = $this->handlers[self::TYPE_TELEPHONE]->resolveHandlerData(['telephone' => $urn]);
                $result['type'] = self::TYPE_TELEPHONE;
            }
        } finally {
            $result = $this->eventDispatcher->dispatch(
                new AfterLinkResolvedByStringRepresentationEvent(
                    result: $result,
                    urn: $urn,
                    resolveException: $resolveException
                )
            )->getResult();

            if (empty($result['type'])) {
                // In case no link type could be resolved and UnknownLinkHandlerException
                // has been added before, throw the exception now to inform calling components.
                if ($resolveException === null) {
                    // Use the general UnknownUrnException in case neither a defined
                    // handler nor an event listener could resolve the given URN.
                    $resolveException = new UnknownUrnException('No valid URN to resolve found', 1457177667);
                }

                throw $resolveException;
            }
        }

        // @todo If resolved result (linkDetails) are later used to build an uri using LinkBuilder->build(), it's needed
        //       to have the original $linkParameter in the result array. Otherwise, places may break like e.g. the
        //       DatabaseRecordLinkBuilder. Can we safely set this here directly and avoiding calls before build like
        //       "$linkDetails['typoLinkParameter'] = $redirectTarget;" - e.g. like in the ext:redirects
        //       TYPO3\CMS\Redirects\Service\RedirectService::resolveLinkDetailsFromLinkTarget() and other places.

        return $result;
    }

    /**
     * Returns a string interpretation of the link target, something like
     *
     *  - t3://page?uid=23&my=value#cool
     *  - https://www.typo3.org/
     *  - t3://file?uid=13
     *  - t3://folder?storage=2&identifier=/my/folder/
     *  - mailto:mac@safe.com
     *
     * @param array $parameters
     * @throws Exception\UnknownLinkHandlerException
     */
    public function asString(array $parameters): string
    {
        $linkHandler = $this->handlers[$parameters['type']] ?? null;
        if ($linkHandler !== null) {
            return $this->handlers[$parameters['type']]->asString($parameters);
        }
        if (isset($parameters['url']) && !empty($parameters['url'])) {
            // This usually happens for tel: or other types where a URL is available and the
            // legacy link service could resolve at least something
            return $parameters['url'];
        }
        throw new UnknownLinkHandlerException('No valid handlers found for type: ' . $parameters['type'], 1460629247);
    }
}
