<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

/**
 * Managers converters.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
 */
class ParamConverterManager
{
    /**
     * @var array
     */
    private $converters = array();

    /**
     * @var array
     */
    private $namedConverters = array();

    /**
     * Applies all converters to the passed configurations and stops when a
     * converter is applied it will move on to the next configuration and so on.
     *
     * @param array|object $configurations
     */
    public function apply(Request $request, $configurations)
    {
        if (is_object($configurations)) {
            $configurations = array($configurations);
        }

        foreach ($configurations as $configuration) {
            $this->applyConverter($request, $configuration);
        }
    }

    /**
     * Applies converter on request based on the given configuration.
     */
    private function applyConverter(Request $request, ParamConverter $configuration)
    {
        $value = $request->attributes->get($configuration->getName());
        $className = $configuration->getClass();

        // If the value is already an instance of the class we are trying to convert it into
        // we should continue as no conversion is required
        if (is_object($value) && $value instanceof $className) {
            return;
        }

        if ($converterName = $configuration->getConverter()) {
            if (!isset($this->namedConverters[$converterName])) {
                throw new \RuntimeException(sprintf(
                    "No converter named '%s' found for conversion of parameter '%s'.",
                    $converterName, $configuration->getName()
                ));
            }

            $converter = $this->namedConverters[$converterName];

            if (!$converter->supports($configuration)) {
                throw new \RuntimeException(sprintf(
                    "Converter '%s' does not support conversion of parameter '%s'.",
                    $converterName, $configuration->getName()
                ));
            }

            $converter->apply($request, $configuration);

            return;
        }

        foreach ($this->all() as $converter) {
            if ($converter->supports($configuration)) {
                if ($converter->apply($request, $configuration)) {
                    return;
                }
            }
        }
    }

    /**
     * Adds a parameter converter.
     *
     * Converters match either explicitly via $name or by iteration over all
     * converters with a $priority. If you pass a $priority = null then the
     * added converter will not be part of the iteration chain and can only
     * be invoked explicitly.
     *
     * @param int    $priority the priority (between -10 and 10)
     * @param string $name     name of the converter
     */
    public function add(ParamConverterInterface $converter, $priority = 0, $name = null)
    {
        if ($priority !== null) {
            if (!isset($this->converters[$priority])) {
                $this->converters[$priority] = array();
            }

            $this->converters[$priority][] = $converter;
        }

        if (null !== $name) {
            $this->namedConverters[$name] = $converter;
        }
    }

    /**
     * Returns all registered param converters.
     *
     * @return array An array of param converters
     */
    public function all()
    {
        krsort($this->converters);

        $converters = array();
        foreach ($this->converters as $all) {
            $converters = array_merge($converters, $all);
        }

        return $converters;
    }
}
