<?php
/**
 * Yaml MenuItem loader
 *
 * @author Elze Kool <info@visualmedia.nl>
 */

namespace VisualM\ManagementBundle\Menu;

use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;

/**
 * Yaml MenuItem loader
 *
 * @internal The Service Container is included for fetching parameters, no services should be extracted from it!
 *
 * @author Elze Kool <info@visualmedia.nl>
 */
class YamlLoader implements MenuItemLoaderInterface
{

    /**
     * Translator
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * Router
     * @var RouterInterface
     */
    private $router;

    /**
     * Container
     * @var ContainerInterface
     */
    private $container;

    /**
     * Items
     * @var MenuItem
     */
    private $rootItem;

    /**
     * Options Resolver
     * @var OptionsResolver
     */
    private $optionsResolver;

    /**
     * Constructor
     *
     * @param TranslatorInterface $translator Translator
     * @param RouterInterface     $router     Router
     * @param ContainerInterface  $container  Container
     */
    public function __construct(TranslatorInterface $translator, RouterInterface $router, ContainerInterface $container)
    {
        $this->translator = $translator;
        $this->router = $router;
        $this->container = $container;
        $this->rootItem = new MenuItem();

        $this->optionsResolver = new OptionsResolver();
        $this->configureOptionsResolver($this->optionsResolver);
    }

    /**
     * Load YAML file
     *
     * @param string    $filename Yaml Filename
     * @param MenuItem  $item     If provided add items to this item instead of global list
     */
    public function load($filename, MenuItem $item = null)
    {
        if (!file_exists($filename)) {
            throw new RuntimeException(sprintf(
                'The YAML file %s could not be found for %s',
                $filename,
                get_class($this)
            ));
        }

        try {
            $yaml = new Parser();
            $data = $yaml->parse(file_get_contents($filename));
        } catch(ParseException $ex) {
            throw new RuntimeException(sprintf(
                'The YAML file %s could not parsed (%s) for %s',
                $filename,
                $ex->getMessage(),
                get_class($this)
            ));
        }

        if (!is_array($data)) {
            throw new RuntimeException(sprintf(
                'The YAML file %s should result in an array for %s',
                $filename,
                get_class($this)
            ));
        }

        foreach($data as $raw_item) {
            $this->processMenuItemData(
                $raw_item,
                ($item === null)
                    ? $this->rootItem
                    : $item
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getItems()
    {
        return $this->rootItem->getChildren();
    }

    /**
     * Configure OptionsResolver for MenuItem
     *
     * @param OptionsResolver $resolver Options Resolver
     */
    protected function configureOptionsResolver(OptionsResolver $resolver)
    {
        $resolver->setRequired(['title']);
        $resolver->setDefaults([
            'link' => null,
            'icon' => null,
            'active_regex' => null,
            'permission' => null,
            'children' => null,
            'priority' => 0,
            'open' => false,
            'js' => null,
        ]);

        $resolver->setAllowedTypes('title', [ 'string', 'array' ]);
        $resolver->setAllowedTypes('link', [ 'string', 'array', 'null' ]);
        $resolver->setAllowedTypes('icon', [ 'string', 'null' ]);
        $resolver->setAllowedTypes('active_regex', [ 'string', 'null' ]);
        $resolver->setAllowedTypes('permission', [ 'string', 'null' ]);
        $resolver->setAllowedTypes('priority', [ 'int' ]);
        $resolver->setAllowedTypes('children', [ 'string', 'array', 'null' ]);
        $resolver->setAllowedTypes('open', [ 'boolean', 'false' ]);
        $resolver->setAllowedTypes('js', [ 'string', 'null' ]);

        $resolver->setNormalizer('title', function(Options $options, $value) {
            return is_array($value)
                ? call_user_func_array(array($this->translator, 'trans'), $value)
                : $value;
        });

        $resolver->setNormalizer('link', function(Options $options, $value) {
            return is_array($value)
                ? call_user_func_array(array($this->router, 'generate'), $value)
                : $value;
        });

        $resolver->setNormalizer('children', function(Options $options, $value) {
            return is_string($value)
                ? [ $value ]
                : $value;
        });

    }

    /**
     * Convert Raw item to MenuItem
     *
     * @param array $config Raw Item
     */
    private function processMenuItemData($config, MenuItem $target)
    {
        if (!is_string($config) AND !is_array($config)) {
            throw new RuntimeException(sprintf(
                'Invalid MenuItem configuration, must be a string or array, %s given',
                (is_object($config) ? get_class($config) : gettype($config))
            ));
        }

        if (is_string($config)) {
            $this->load(
                $this->resolveBundlePath($this->resolveParameters($config)),
                $target
            );
            return;
        }

        $resolved = $this->optionsResolver->resolve($config);

        $item = new MenuItem();
        $item->setTitle($resolved['title']);
        $item->setLink($resolved['link']);
        $item->setPriority($resolved['priority']);
        if (isset($resolved['icon'])) { $item->setIconClass($resolved['icon']); }
        if (isset($resolved['active_regex'])) { $item->setActiveRouteRegex($resolved['active_regex']); }
        if (isset($resolved['permission'])) { $item->setRequiredPermission($resolved['permission']); }
        if (isset($resolved['open'])) { $item->setOpen($resolved['open']); }
        if (isset($resolved['js'])) { $item->setJs($resolved['js']); }

        if (isset($resolved['children'])) {
            foreach($config['children'] as $raw_child) {
                $this->processMenuItemData($raw_child, $item);
            }
        }

        $target->addChild($item);
    }

    /**
     * Resolve path to bundle
     *
     * @param string $str String to resolve bundle in
     *
     * @return string String with resolved bundle
     */
    private function resolveBundlePath($str)
    {
        if (preg_match('/^@(?<bundle>[a-zA-Z_\x7f-\xff]+)/', $str, $m) === 0) {
            return $str;
        }

        $kernel = $this->container->get('kernel');
        $bundle = $kernel->getBundle($m['bundle'], true);
        return str_replace($m[0], $bundle->getPath(), $str);
    }

    /**
     * Resolve parameters in string
     *
     * @internal $resolving Resolving is used to prevent circulair reference
     *
     * @param string $str       Input string
     * @param array  $resolving Internal use
     *
     * @return string String with resolved parameters
     */
    private function resolveParameters($str, $resolving = [])
    {
        return preg_replace_callback('/%%|%([^%\s]+)%/', function($match) use ($str, $resolving) {

            list(,$parameter) = array_merge($match, [ '', '' ]);

            if (empty($parameter)) {
                return '%%';
            }

            if (in_array($parameter, $resolving)) {
                throw new RuntimeException(sprintf(
                    'Circulair reference detected for parameter "%s" in string "%s" for %s', $parameter, $str, get_class($this)
                ));
            }

            if (!$this->container->hasParameter($parameter)) {
                throw new RuntimeException(sprintf(
                    'Parameters "%s" not found in string "%s" for %s', $parameter, $str, get_class($this)
                ));
            }

            $resolving[] = $parameter;
            $resolved = $this->container->getParameter($parameter);

            if (!is_string($resolved) && !is_numeric($resolved)) {
                throw new RuntimeException(sprintf(
                    'A string value must be composed of strings and/or numbers, but found parameter "%s" of type %s inside string value "%s". for %s',
                    $parameter,
                    gettype($resolved),
                    $str,
                    get_class($this)
                ));
            }

            $resolved = (string) $resolved;
            if (strpos($resolved, '%') !== false) {
                $resolved = $this->resolveParameters($resolved, $resolving);
            }

            return $resolved;
        }, $str);
    }

}
