<?php

/*
 * This file is part of the Behat Testwork.
 * (c) Konstantin Kudryashov <ever.zet@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Behat\Testwork\ServiceContainer;

use Behat\Testwork\ServiceContainer\Configuration\ConfigurationTree;
use Behat\Testwork\ServiceContainer\Exception\ExtensionException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;

/**
 * Loads Symfony DI container with testwork extension services.
 *
 * @author Konstantin Kudryashov <ever.zet@gmail.com>
 */
final class ContainerLoader
{
    /**
     * @var ExtensionManager
     */
    private $extensionManager;
    /**
     * @var ConfigurationTree
     */
    private $configuration;
    /**
     * @var Processor
     */
    private $processor;

    /**
     * Initialize extension.
     *
     * @param ExtensionManager       $extensionManager
     * @param null|ConfigurationTree $configuration
     * @param null|Processor         $processor
     */
    public function __construct(
        ExtensionManager $extensionManager,
        ?ConfigurationTree $configuration = null,
        ?Processor $processor = null
    ) {
        $this->extensionManager = $extensionManager;
        $this->configuration = $configuration ? : new ConfigurationTree();
        $this->processor = $processor ? : new Processor();
    }

    /**
     * Loads container extension.
     *
     * @param ContainerBuilder $container
     * @param array            $configs
     */
    public function load(ContainerBuilder $container, array $configs)
    {
        $configs = $this->initializeExtensions($container, $configs);
        $config = $this->processConfig($configs);

        $this->loadExtensions($container, $config);
    }

    /**
     * Processes config against extensions.
     *
     * @param array $configs
     *
     * @return array
     */
    private function processConfig(array $configs)
    {
        $tree = $this->configuration->getConfigTree($this->extensionManager->getExtensions());

        return $this->processor->process($tree, $configs);
    }

    /**
     * Initializes extensions using provided config.
     *
     * @param ContainerBuilder $container
     * @param array            $configs
     *
     * @return array
     */
    private function initializeExtensions(ContainerBuilder $container, array $configs)
    {
        $extensions = [];
        foreach ($configs as $i => $config) {
            $extensions[$i] = [];
            if (array_key_exists('extensions', $config)) {
                if (null === $config['extensions']) {
                    $extensions = []; // Disable all extensions
                    break;
                }
                foreach ($config['extensions'] as $extensionLocator => $extensionConfig) {
                    $extensions[$i][$extensionLocator] = $extensionConfig;
                }
            }
        }

        foreach ($configs as $i => $config) {
            unset($configs[$i]['extensions']);
        }

        foreach ($extensions as $i => $extensionConfigs) {
            foreach ($extensionConfigs as $extensionLocator => $extensionConfig) {
                $extension = $this->extensionManager->activateExtension($extensionLocator);
                $configs[$i][$extension->getConfigKey()] = $extensionConfig;
            }
        }

        $this->extensionManager->initializeExtensions();

        $container->setParameter('extensions', $this->extensionManager->getExtensionClasses());

        return $configs;
    }

    /**
     * Loads all extensions into container using provided config.
     *
     * @param ContainerBuilder $container
     * @param array            $config
     *
     * @throws ExtensionException
     */
    private function loadExtensions(ContainerBuilder $container, array $config)
    {
        // Load default extensions first
        foreach ($this->extensionManager->getExtensions() as $extension) {
            $extensionConfig = array();
            if (isset($config[$extension->getConfigKey()])) {
                $extensionConfig = $config[$extension->getConfigKey()];
                unset($config[$extension->getConfigKey()]);
            }

            $this->loadExtension($container, $extension, $extensionConfig);
        }

        // Load activated extensions
        foreach ($config as $extensionConfigKey => $extensionConfig) {
            if (null === $extension = $this->extensionManager->getExtension($extensionConfigKey)) {
                throw new ExtensionException(
                    sprintf('None of the activated extensions use `%s` config section.', $extensionConfigKey),
                    $extensionConfigKey
                );
            }

            $this->loadExtension($container, $extension, $extensionConfig);
        }
    }

    /**
     * Loads extension configuration.
     *
     * @param ContainerBuilder $container
     * @param Extension        $extension
     * @param array            $config
     */
    private function loadExtension(ContainerBuilder $container, Extension $extension, array $config)
    {
        $tempContainer = new ContainerBuilder(new ParameterBag(array(
            'paths.base' => $container->getParameter('paths.base'),
            'extensions' => $container->getParameter('extensions'),
        )));
        $tempContainer->addObjectResource($extension);
        $extension->load($container, $config);
        $container->merge($tempContainer);
        $container->addCompilerPass($extension);
    }
}
