<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\DependencyInjection\Widget;

use Chill\MainBundle\DependencyInjection\Widget\AbstractWidgetsCompilerPass as WidgetsCompilerPass;
use Generator;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * This trait allow to add automatic configuration for widget inside your config.
 *
 * Usage
 * ======
 *
 * 1. Register widget factories
 * ----------------------------
 *
 * Add widget factories, using `setWidgetFactories`
 *
 * Example :
 *
 * ```
 * use Symfony\Component\DependencyInjection\ContainerBuilder;
 * use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait;
 *
 * class MyConfig
 * {
 *
 *    use addWidgetConfigurationTrait;
 *
 *    public function __construct(array $widgetFactories = array(), ContainerBuilder $container)
 *        {
 *            $this->setWidgetFactories($widgetFactories);
 *            // will be used on next step
 *            $this->container = $container;
 *        }
 *
 * }
 * ```
 *
 *
 *
 * 2. add widget config to your config
 * -----------------------------------
 *
 * ```
 * use Symfony\Component\DependencyInjection\ContainerBuilder;
 * use Chill\MainBundle\DependencyInjection\Widget\AddWidgetConfigurationTrait;
 * use Symfony\Component\Config\Definition\Builder\TreeBuilder;
 *
 * class MyConfig
 * {
 *
 *    use addWidgetConfigurationTrait;
 *
 *    private $container;
 *
 *    public function __construct(array $widgetFactories = array(), ContainerBuilder $container)
 *    {
 *        $this->setWidgetFactories($widgetFactories);
 *        $this->container;
 *    }
 *
 *    public function getConfigTreeBuilder()
 *    {
 *        $treeBuilder = new TreeBuilder();
 *        $root = $treeBuilder->root('my_root');
 *
 *        $root->children()
 *            ->arrayNode('widgets')
 *            ->canBeDisabled()
 *            ->children()
 *                ->append($this->addWidgetsConfiguration('homepage', $this->container))
 *            ->end()
 *        ->end()
 *        ;
 *
 *        return $treeBuilder;
 *    }
 *
 * }
 * ```
 *
 * the above code will add to the config :
 *
 * ```
 *     widgets:
 *      enabled:              true
 *
 *      # register widgets on place "homepage"
 *      homepage:
 *          order:                ~ # Required
 *          widget_alias:         ~ # One of "person_list"; "add_person", Required
 *          person_list:
 *              # options for the person_list
 * ```
 */
trait AddWidgetConfigurationTrait
{
    /**
     * @param WidgetFactoryInterface[]
     */
    private $widgetFactories;

    /**
     * @return WidgetFactoryInterface[]
     */
    public function getWidgetFactories()
    {
        return $this->widgetFactories;
    }

    /**
     * @param WidgetFactoryInterface[] $widgetFactories
     */
    public function setWidgetFactories(array $widgetFactories)
    {
        $this->widgetFactories = $widgetFactories;
    }

    /**
     * add configuration nodes for the widget at the given place.
     */
    protected function addWidgetsConfiguration(string $place, ContainerBuilder $containerBuilder)
    {
        $treeBuilder = new TreeBuilder($place);
        $root = $treeBuilder->getRootNode()
            ->canBeUnset()
            ->info('register widgets on place "'.$place.'"');

        // if no childen, return the root
        if (0 === \count(iterator_to_array($this->filterWidgetByPlace($place)))) {
            return $root;
        }

        $prototypeChildren = $root->prototype('array')->children();

        $prototypeChildren
            ->floatNode(WidgetsCompilerPass::WIDGET_CONFIG_ORDER)
            ->isRequired()
            ->info('the ordering of the widget. May be a number with decimal')
            ->example('10.58')
            ->end()
            ->scalarNode(WidgetsCompilerPass::WIDGET_CONFIG_ALIAS)
                // this node is scalar: when the configuration is build, the
                // tagged services are not available. But when the config reference
                // is build, the services are avaialble => we add the possible aliases
                // in the info.
            ->info('the widget alias (see your installed bundles config). '
                        .'Possible values are (maybe incomplete) : '.
                        \implode(', ', $this->getWidgetAliasesbyPlace($place, $containerBuilder)))
            ->isRequired()
            ->end();

        // adding the possible config on each widget under the widget_alias
        foreach ($this->filterWidgetByPlace($place) as $factory) {
            $builder = new TreeBuilder($factory->getWidgetAlias());
            $widgetOptionsRoot = $builder->getRootNode();
            $widgetOptionsRoot->canBeUnset()
                ->info(sprintf(
                    'the configuration for the widget "%s" (only required if this widget is set in widget_alias)',
                    $factory->getWidgetAlias()
                ));
            $factory->configureOptions($place, $widgetOptionsRoot->children());
            $prototypeChildren->append($widgetOptionsRoot);
        }

        return $root;
    }

    /**
     * get all widget  factories for the given place.
     *
     * @param string $place
     *
     * @return \Generator a generator containing a widget factory
     */
    protected function filterWidgetByPlace($place)
    {
        foreach ($this->widgetFactories as $factory) {
            if (\in_array($place, $factory->getAllowedPlaces(), true)) {
                yield $factory;
            }
        }
    }

    /**
     * get the all possible aliases for the given place. This method
     * search within service tags and widget factories.
     *
     * **Note** that services are not available when the config is build: the whole
     * aliases will be checked in compiler pass, or when the command
     * `config:dump-reference` is runned.
     *
     * @param type $place
     *
     * @return array
     *
     * @throws InvalidConfigurationException if a service's tag does not have the "alias" key
     */
    protected function getWidgetAliasesbyPlace($place, ContainerBuilder $containerBuilder)
    {
        $result = [];

        foreach ($this->filterWidgetByPlace($place) as $factory) {
            $result[] = $factory->getWidgetAlias();
        }

        // append the aliases added without factory
        foreach ($containerBuilder
            ->findTaggedServiceIds(WidgetsCompilerPass::WIDGET_SERVICE_TAG_NAME) as $serviceId => $tags) {
            foreach ($tags as $tag) {
                // throw an error if no alias in definition
                if (!\array_key_exists(WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS, $tag)) {
                    throw new InvalidConfigurationException(sprintf('The service with id %s does not have any %d key', $serviceId, WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS));
                }
                // add the key to the possible results
                $result[] = $tag[WidgetsCompilerPass::WIDGET_SERVICE_TAG_ALIAS];
            }
        }

        return $result;
    }
}
