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

namespace VisualM\ManagementBundle\Menu;

use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerInterface;
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 array
     */
    private $items = [];

    /**
     * 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;
    }

    /**
     * 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) {
            if ($item === null) {
                $this->items[] = $this->convertItem($raw_item);
            } else {
                $item->addChild($this->convertItem($raw_item));
            }
        }   
    }

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


    /**
     * Convert Raw item to MenuItem
     *
     * @param array $raw_item Raw Item
     *
     * @return MenuItem MenuItem
     */
    private function convertItem($raw_item)
    {
        foreach([ 'title', 'link' ] as $requiredField) {
            if (!isset($raw_item[$requiredField])) {
                throw new RuntimeException(sprintf(
                    'Missing required field in item configuration for %s',
                    $requiredField,
                    get_class($this)
                ));
            }
        }
        
        if (is_string($raw_item['title'])) {
            $title = $raw_item['title'];
        } elseif (is_array($raw_item['title'])) {
            $title = call_user_func_array(array($this->translator, 'trans'), $raw_item['title']);
        } else {
            throw new RuntimeException(sprintf(
                'Invalid title field, should be an string or an array that can be passed to TranslatorInterface::trans() for %s', get_class($this)
            ));
        }

        if (is_string($raw_item['link'])) {
            $link = $raw_item['link'];
        } elseif (is_array($raw_item['link'])) {
            $link = call_user_func_array(array($this->router, 'generate'), $raw_item['link']);
        } else {
            throw new RuntimeException(sprintf(
                'Invalid link field, should be an string or an array that can be passed to RouterInterface::generate() for %s', get_class($this)
            ));
        }

        $item = new MenuItem();
        $item->setTitle($title);
        $item->setLink($link);
        if (!empty($raw_item['icon'])) { $item->setIconClass($raw_item['icon']); }
        if (!empty($raw_item['active_regex'])) { $item->setActiveRouteRegex($raw_item['active_regex']); }
        if (!empty($raw_item['permission'])) { $item->setRequiredPermission($raw_item['permission']); }

        if (isset($raw_item['children'])) {
            if (is_string($raw_item['children'])) {
                $this->load($this->resolveParameters($raw_item['children']), $item);
            } elseif (is_array($raw_item['children'])) {
                foreach($raw_item['children'] as $raw_child) {
                    $item->addChild($this->convertItem($raw_child));
                }
            } else {
                throw new RuntimeException(sprintf(
                    'Invalid children field, should be an string with an YAML filename or an array with children elements for %s', get_class($this)
                ));
            }            
        }

        return $item;
    }

    /**
     * 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);
    }

}