<?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 Symfony\Component\Form;

use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TypeDefinitionException;
use Symfony\Component\Form\Exception\CreationException;

class FormFactory implements FormFactoryInterface
{
    private static $requiredOptions = array(
        'data',
        'required',
        'max_length',
    );

    /**
     * Extensions
     * @var array An array of FormExtensionInterface
     */
    private $extensions = array();

    /**
     * All known types (cache)
     * @var array An array of FormTypeInterface
     */
    private $types = array();

    /**
     * The guesser chain
     * @var FormTypeGuesserChain
     */
    private $guesser;

    /**
     * Constructor.
     *
     * @param array $extensions An array of FormExtensionInterface
     *
     * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface
     */
    public function __construct(array $extensions)
    {
        foreach ($extensions as $extension) {
            if (!$extension instanceof FormExtensionInterface) {
                throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormExtensionInterface');
            }
        }

        $this->extensions = $extensions;
    }

    /**
     * Returns whether the given type is supported.
     *
     * @param string $name The name of the type
     *
     * @return Boolean Whether the type is supported
     */
    public function hasType($name)
    {
        if (isset($this->types[$name])) {
            return true;
        }

        try {
            $this->loadType($name);
        } catch (FormException $e) {
            return false;
        }

        return true;
    }

    /**
     * Add a type.
     *
     * @param FormTypeInterface $type The type
     */
    public function addType(FormTypeInterface $type)
    {
        $this->loadTypeExtensions($type);

        $this->validateFormTypeName($type);

        $this->types[$type->getName()] = $type;
    }

    /**
     * Returns a type by name.
     *
     * This methods registers the type extensions from the form extensions.
     *
     * @param string|FormTypeInterface $name The name of the type or a type instance
     *
     * @return FormTypeInterface The type
     *
     * @throws FormException if the type can not be retrieved from any extension
     */
    public function getType($name)
    {
        if (!is_string($name)) {
            throw new UnexpectedTypeException($name, 'string');
        }

        if (!isset($this->types[$name])) {
            $this->loadType($name);
        }

        return $this->types[$name];
    }

    /**
     * Returns a form.
     *
     * @see createBuilder()
     *
     * @param string|FormTypeInterface  $type       The type of the form
     * @param mixed                     $data       The initial data
     * @param array                     $options    The options
     *
     * @return Form The form named after the type
     *
     * @throws FormException if any given option is not applicable to the given type
     */
    public function create($type, $data = null, array $options = array())
    {
        return $this->createBuilder($type, $data, $options)->getForm();
    }

    /**
     * Returns a form.
     *
     * @see createNamedBuilder()
     *
     * @param string|FormTypeInterface  $type       The type of the form
     * @param string                    $name       The name of the form
     * @param mixed                     $data       The initial data
     * @param array                     $options    The options
     *
     * @return Form The form
     *
     * @throws FormException if any given option is not applicable to the given type
     */
    public function createNamed($type, $name, $data = null, array $options = array())
    {
        return $this->createNamedBuilder($type, $name, $data, $options)->getForm();
    }

    /**
     * Returns a form for a property of a class.
     *
     * @see createBuilderForProperty()
     *
     * @param string $class     The fully qualified class name
     * @param string $property  The name of the property to guess for
     * @param mixed  $data      The initial data
     * @param array  $options   The options for the builder
     *
     * @return Form The form named after the property
     *
     * @throws FormException if any given option is not applicable to the form type
     */
    public function createForProperty($class, $property, $data = null, array $options = array())
    {
        return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
    }

    /**
     * Returns a form builder
     *
     * @param string|FormTypeInterface  $type       The type of the form
     * @param mixed                     $data       The initial data
     * @param array                     $options    The options
     *
     * @return FormBuilder The form builder
     *
     * @throws FormException if any given option is not applicable to the given type
     */
    public function createBuilder($type, $data = null, array $options = array())
    {
        $name = is_object($type) ? $type->getName() : $type;

        return $this->createNamedBuilder($type, $name, $data, $options);
    }

    /**
     * Returns a form builder.
     *
     * @param string|FormTypeInterface  $type       The type of the form
     * @param string                    $name       The name of the form
     * @param mixed                     $data       The initial data
     * @param array                     $options    The options
     *
     * @return FormBuilder The form builder
     *
     * @throws FormException if any given option is not applicable to the given type
     */
    public function createNamedBuilder($type, $name, $data = null, array $options = array())
    {
        $builder = null;
        $types = array();
        $knownOptions = array();
        $passedOptions = array_keys($options);
        $optionValues = array();

        if (!array_key_exists('data', $options)) {
            $options['data'] = $data;
        }

        while (null !== $type) {
            if ($type instanceof FormTypeInterface) {
                if ($type->getName() == $type->getParent($options)) {
                    throw new FormException(sprintf('The form type name "%s" for class "%s" cannot be the same as the parent type.', $type->getName(), get_class($type)));
                }

                $this->addType($type);
            } elseif (is_string($type)) {
                $type = $this->getType($type);
            } else {
                throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
            }

            $defaultOptions = $type->getDefaultOptions($options);
            $optionValues = array_merge_recursive($optionValues, $type->getAllowedOptionValues($options));

            foreach ($type->getExtensions() as $typeExtension) {
                $defaultOptions = array_replace($defaultOptions, $typeExtension->getDefaultOptions($options));
                $optionValues = array_merge_recursive($optionValues, $typeExtension->getAllowedOptionValues($options));
            }

            $options = array_replace($defaultOptions, $options);
            $knownOptions = array_merge($knownOptions, array_keys($defaultOptions));
            array_unshift($types, $type);
            $type = $type->getParent($options);
        }

        $type = end($types);
        $diff = array_diff(self::$requiredOptions, $knownOptions);

        if (count($diff) > 0) {
            throw new TypeDefinitionException(sprintf('Type "%s" should support the option(s) "%s"', $type->getName(), implode('", "', $diff)));
        }

        $diff = array_diff($passedOptions, $knownOptions);

        if (count($diff) > 1) {
            throw new CreationException(sprintf('The options "%s" do not exist', implode('", "', $diff)));
        }

        if (count($diff) > 0) {
            throw new CreationException(sprintf('The option "%s" does not exist', current($diff)));
        }

        foreach ($optionValues as $option => $allowedValues) {
            if (!in_array($options[$option], $allowedValues, true)) {
                throw new CreationException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
            }
        }

        for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) {
            $builder = $types[$i]->createBuilder($name, $this, $options);
        }

        if (!$builder) {
            throw new TypeDefinitionException(sprintf('Type "%s" or any of its parents should return a FormBuilder instance from createBuilder()', $type->getName()));
        }

        $builder->setTypes($types);
        $builder->setCurrentLoadingType($type->getName());

        foreach ($types as $type) {
            $type->buildForm($builder, $options);

            foreach ($type->getExtensions() as $typeExtension) {
                $typeExtension->buildForm($builder, $options);
            }
        }
        $builder->setCurrentLoadingType(null);

        return $builder;
    }

    /**
     * Returns a form builder for a property of a class.
     *
     * If any of the 'max_length', 'required' and type options can be guessed,
     * and are not provided in the options argument, the guessed value is used.
     *
     * @param string $class     The fully qualified class name
     * @param string $property  The name of the property to guess for
     * @param mixed  $data      The initial data
     * @param array  $options   The options for the builder
     *
     * @return FormBuilder The form builder named after the property
     *
     * @throws FormException if any given option is not applicable to the form type
     */
    public function createBuilderForProperty($class, $property, $data = null, array $options = array())
    {
        if (!$this->guesser) {
            $this->loadGuesser();
        }

        $typeGuess = $this->guesser->guessType($class, $property);
        $maxLengthGuess = $this->guesser->guessMaxLength($class, $property);
        $minLengthGuess = $this->guesser->guessMinLength($class, $property);
        $requiredGuess = $this->guesser->guessRequired($class, $property);

        $type = $typeGuess ? $typeGuess->getType() : 'text';

        if ($maxLengthGuess) {
            $options = array_merge(array('max_length' => $maxLengthGuess->getValue()), $options);
        }

        if ($minLengthGuess) {
            if ($maxLengthGuess) {
                $options = array_merge(array('pattern' => '.{'.$minLengthGuess->getValue().','.$maxLengthGuess->getValue().'}'), $options);
            } else {
                $options = array_merge(array('pattern' => '.{'.$minLengthGuess->getValue().',}'), $options);
            }
        }

        if ($requiredGuess) {
            $options = array_merge(array('required' => $requiredGuess->getValue()), $options);
        }

        // user options may override guessed options
        if ($typeGuess) {
            $options = array_merge($typeGuess->getOptions(), $options);
        }

        return $this->createNamedBuilder($type, $property, $data, $options);
    }

    /**
     * Initializes the guesser chain.
     */
    private function loadGuesser()
    {
        $guessers = array();

        foreach ($this->extensions as $extension) {
            $guesser = $extension->getTypeGuesser();

            if ($guesser) {
                $guessers[] = $guesser;
            }
        }

        $this->guesser = new FormTypeGuesserChain($guessers);
    }

    /**
     * Loads a type.
     *
     * @param string $name The type name
     *
     * @throws FormException if the type is not provided by any registered extension
     */
    private function loadType($name)
    {
        $type = null;

        foreach ($this->extensions as $extension) {
            if ($extension->hasType($name)) {
                $type = $extension->getType($name);
                break;
            }
        }

        if (!$type) {
            throw new FormException(sprintf('Could not load type "%s"', $name));
        }

        $this->loadTypeExtensions($type);

        $this->validateFormTypeName($type);

        $this->types[$name] = $type;
    }

    /**
     * Loads the extensions for a given type.
     *
     * @param FormTypeInterface $type The type
     */
    private function loadTypeExtensions(FormTypeInterface $type)
    {
        $typeExtensions = array();

        foreach ($this->extensions as $extension) {
            $typeExtensions = array_merge(
                $typeExtensions,
                $extension->getTypeExtensions($type->getName())
            );
        }

        $type->setExtensions($typeExtensions);
    }

    private function validateFormTypeName(FormTypeInterface $type)
    {
        if (!preg_match('/^[a-z0-9_]+$/i', $type->getName())) {
            throw new FormException(sprintf('The "%s" form type name ("%s") is not valid. Names must only contain letters, numbers, and "_".', get_class($type), $type->getName()));
        }
    }
}
