<?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\DependencyInjection;

use Psr\Container\ContainerInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Package\PackageManager;

/**
 * @internal
 */
class ContainerBuilder
{
    /**
     * @var array
     */
    protected $cacheIdentifiers;

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

    /**
     * @var string
     */
    protected $serviceProviderRegistryServiceName = 'service_provider_registry';

    public function __construct(array $earlyInstances)
    {
        $this->defaultServices = $earlyInstances + [ self::class => $this ];
    }

    /**
     * @internal
     */
    public function warmupCache(PackageManager $packageManager, FrontendInterface $cache): void
    {
        $registry = new ServiceProviderRegistry($packageManager);
        $containerBuilder = $this->buildContainer($packageManager, $registry);
        $cacheIdentifier = $this->getCacheIdentifier($packageManager);
        $this->dumpContainer($containerBuilder, $cache, $cacheIdentifier);
    }

    public function createDependencyInjectionContainer(PackageManager $packageManager, FrontendInterface $cache, bool $failsafe = false): ContainerInterface
    {
        if (!$cache instanceof PhpFrontend) {
            throw new \RuntimeException('Cache must be instance of PhpFrontend', 1582022226);
        }
        $serviceProviderRegistry = new ServiceProviderRegistry($packageManager, $failsafe);

        if ($failsafe) {
            return new FailsafeContainer($serviceProviderRegistry, $this->defaultServices);
        }

        $cacheIdentifier = $this->getCacheIdentifier($packageManager);
        $containerClassName = $cacheIdentifier;

        $hasCache = $cache->requireOnce($cacheIdentifier) !== false;
        if (!$hasCache) {
            $containerBuilder = $this->buildContainer($packageManager, $serviceProviderRegistry);
            $this->dumpContainer($containerBuilder, $cache, $cacheIdentifier);
            $cache->requireOnce($cacheIdentifier);
        }
        $container = new $containerClassName();

        foreach ($this->defaultServices as $id => $service) {
            $container->set('_early.' . $id, $service);
        }

        $container->set($this->serviceProviderRegistryServiceName, $serviceProviderRegistry);

        return $container;
    }

    protected function buildContainer(PackageManager $packageManager, ServiceProviderRegistry $registry): SymfonyContainerBuilder
    {
        $containerBuilder = new SymfonyContainerBuilder();

        $containerBuilder->addCompilerPass(new ResolveClassPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1000);
        $containerBuilder->addCompilerPass(new ServiceProviderCompilationPass($registry, $this->serviceProviderRegistryServiceName));

        $globalConfigDir = Environment::getConfigPath();
        // If the config folder is outside of the document root, we allow further services per-project
        // This is usually the case in composer-based installations
        if (Environment::getPublicPath() !== Environment::getProjectPath()) {
            if (file_exists($globalConfigDir . '/system/services.php')) {
                $phpFileLoader = new PhpFileLoader($containerBuilder, new FileLocator($globalConfigDir . '/system'));
                $phpFileLoader->load('services.php');
            }
            if (file_exists($globalConfigDir . '/system/services.yaml')) {
                $yamlFileLoader = new YamlFileLoader($containerBuilder, new FileLocator($globalConfigDir . '/system'));
                $yamlFileLoader->load('services.yaml');
            }
        }

        $packages = $packageManager->getActivePackages();
        foreach ($packages as $package) {
            $diConfigDir = $package->getPackagePath() . 'Configuration/';
            if (file_exists($diConfigDir . 'Services.php')) {
                $phpFileLoader = new PhpFileLoader($containerBuilder, new FileLocator($diConfigDir));
                $phpFileLoader->load('Services.php');
            }
            if (file_exists($diConfigDir . 'Services.yaml')) {
                $yamlFileLoader = new YamlFileLoader($containerBuilder, new FileLocator($diConfigDir));
                $yamlFileLoader->load('Services.yaml');
            }
        }
        // Store defaults entries in the DIC container
        // We need to use a workaround using aliases for synthetic services
        // But that's common in symfony (same technique is used to provide the
        // Symfony container interface as well).
        foreach (array_keys($this->defaultServices) as $id) {
            $syntheticId = '_early.' . $id;
            $containerBuilder->register($syntheticId)->setSynthetic(true)->setPublic(true);
            $containerBuilder->setAlias($id, $syntheticId)->setPublic(true);
        }

        // Optional service, set by BootService as back reference to the original bootService
        $containerBuilder->register('_early.boot-service')->setSynthetic(true)->setPublic(true);

        $containerBuilder->compile();

        return $containerBuilder;
    }

    protected function dumpContainer(SymfonyContainerBuilder $containerBuilder, FrontendInterface $cache, string $cacheIdentifier): string
    {
        $containerClassName = $cacheIdentifier;

        $phpDumper = new PhpDumper($containerBuilder);
        $code = $phpDumper->dump(['class' => $containerClassName]);
        $code = str_replace('<?php', '', $code);
        // We need to patch the generated source code to use GeneralUtility::makeInstanceForDi() instead of `new`.
        // This is ugly, but has to stay, as long as we support SingletonInstances to be created/retrieved
        // through GeneralUtility::makeInstance.
        $code = preg_replace('/new ([^\(\s]+)\(/', '\\TYPO3\\CMS\\Core\\Utility\\GeneralUtility::makeInstanceForDi(\\1::class, ', $code);
        if ($code === null) {
            throw new \RuntimeException('Could not generate container code', 1599767133);
        }
        $code = str_replace(', )', ')', $code);

        $cache->set($cacheIdentifier, $code);

        return $code;
    }

    /**
     * @internal may only be used in this class or in functional tests
     */
    public function getCacheIdentifier(PackageManager $packageManager): string
    {
        $packageManagerCacheIdentifier = $packageManager->getCacheIdentifier() ?? '';
        return $this->cacheIdentifiers[$packageManagerCacheIdentifier] ?? $this->createCacheIdentifier($packageManager, $packageManagerCacheIdentifier);
    }

    protected function createCacheIdentifier(PackageManager $packageManager, string $additionalIdentifier): string
    {
        return $this->cacheIdentifiers[$additionalIdentifier] = (new PackageDependentCacheIdentifier($packageManager))
            ->withPrefix('DependencyInjectionContainer')
            ->withAdditionalHashedIdentifier('PHP-' . PHP_MAJOR_VERSION . '-' . PHP_MINOR_VERSION)
            ->toString();
    }
}
