<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Form;

use 
ArrayAccess;
use 
Traversable;
use 
Zend\InputFilter\Factory as InputFilterFactory;
use 
Zend\InputFilter\InputFilterInterface;
use 
Zend\Stdlib\ArrayUtils;
use 
Zend\Stdlib\Hydrator;

class 
Factory
{
    
/**
     * @var InputFilterFactory
     */
    
protected $inputFilterFactory;

    
/**
     * @var FormElementManager
     */
    
protected $formElementManager;

    
/**
     * @param FormElementManager $formElementManager
     */
    
public function __construct(FormElementManager $formElementManager null)
    {
        if (
$formElementManager) {
            
$this->setFormElementManager($formElementManager);
        }
    }

    
/**
     * Set input filter factory to use when creating forms
     *
     * @param  InputFilterFactory $inputFilterFactory
     * @return Factory
     */
    
public function setInputFilterFactory(InputFilterFactory $inputFilterFactory)
    {
        
$this->inputFilterFactory $inputFilterFactory;
        return 
$this;
    }

    
/**
     * Get current input filter factory
     *
     * If none provided, uses an unconfigured instance.
     *
     * @return InputFilterFactory
     */
    
public function getInputFilterFactory()
    {
        if (
null === $this->inputFilterFactory) {
            
$this->setInputFilterFactory(new InputFilterFactory());
        }
        return 
$this->inputFilterFactory;
    }

    
/**
     * Set the form element manager
     *
     * @param  FormElementManager $formElementManager
     * @return Factory
     */
    
public function setFormElementManager(FormElementManager $formElementManager)
    {
        
$this->formElementManager $formElementManager;
        return 
$this;
    }

    
/**
     * Get form element manager
     *
     * @return FormElementManager
     */
    
public function getFormElementManager()
    {
        if (
$this->formElementManager === null) {
            
$this->setFormElementManager(new FormElementManager());
        }

        return 
$this->formElementManager;
    }

    
/**
     * Create an element, fieldset, or form
     *
     * Introspects the 'type' key of the provided $spec, and determines what
     * type is being requested; if none is provided, assumes the spec
     * represents simply an element.
     *
     * @param  array|Traversable $spec
     * @return ElementInterface
     * @throws Exception\DomainException
     */
    
public function create($spec)
    {
        
$spec $this->validateSpecification($spec__METHOD__);
        
$type = isset($spec['type']) ? $spec['type'] : 'Zend\Form\Element';

        
$element $this->getFormElementManager()->get($type);

        if (
$element instanceof FormInterface) {
            return 
$this->configureForm($element$spec);
        }

        if (
$element instanceof FieldsetInterface) {
            return 
$this->configureFieldset($element$spec);
        }

        if (
$element instanceof ElementInterface) {
            return 
$this->configureElement($element$spec);
        }

        throw new 
Exception\DomainException(sprintf(
            
'%s expects the $spec["type"] to implement one of %s, %s, or %s; received %s',
            
__METHOD__,
            
'Zend\Form\ElementInterface',
            
'Zend\Form\FieldsetInterface',
            
'Zend\Form\FormInterface',
            
$type
        
));
    }

    
/**
     * Create an element
     *
     * @param  array $spec
     * @return ElementInterface
     */
    
public function createElement($spec)
    {
        if (!isset(
$spec['type'])) {
            
$spec['type'] = 'Zend\Form\Element';
        }

        return 
$this->create($spec);
    }

    
/**
     * Create a fieldset
     *
     * @param  array $spec
     * @return ElementInterface
     */
    
public function createFieldset($spec)
    {
        if (!isset(
$spec['type'])) {
            
$spec['type'] = 'Zend\Form\Fieldset';
        }

        return 
$this->create($spec);
    }

    
/**
     * Create a form
     *
     * @param  array $spec
     * @return ElementInterface
     */
    
public function createForm($spec)
    {
        if (!isset(
$spec['type'])) {
            
$spec['type'] = 'Zend\Form\Form';
        }

        return 
$this->create($spec);
    }

    
/**
     * Configure an element based on the provided specification
     *
     * Specification can contain any of the following:
     * - type: the Element class to use; defaults to \Zend\Form\Element
     * - name: what name to provide the element, if any
     * - options: an array, Traversable, or ArrayAccess object of element options
     * - attributes: an array, Traversable, or ArrayAccess object of element
     *   attributes to assign
     *
     * @param  ElementInterface              $element
     * @param  array|Traversable|ArrayAccess $spec
     * @throws Exception\DomainException
     * @return ElementInterface
     */
    
public function configureElement(ElementInterface $element$spec)
    {
        
$spec $this->validateSpecification($spec__METHOD__);

        
$name       = isset($spec['name'])       ? $spec['name']       : null;
        
$options    = isset($spec['options'])    ? $spec['options']    : null;
        
$attributes = isset($spec['attributes']) ? $spec['attributes'] : null;

        if (
$name !== null && $name !== '') {
            
$element->setName($name);
        }

        if (
is_array($options) || $options instanceof Traversable || $options instanceof ArrayAccess) {
            
$element->setOptions($options);
        }

        if (
is_array($attributes) || $attributes instanceof Traversable || $attributes instanceof ArrayAccess) {
            
$element->setAttributes($attributes);
        }

        return 
$element;
    }

    
/**
     * Configure a fieldset based on the provided specification
     *
     * Specification can contain any of the following:
     * - type: the Fieldset class to use; defaults to \Zend\Form\Fieldset
     * - name: what name to provide the fieldset, if any
     * - options: an array, Traversable, or ArrayAccess object of element options
     * - attributes: an array, Traversable, or ArrayAccess object of element
     *   attributes to assign
     * - elements: an array or Traversable object where each entry is an array
     *   or ArrayAccess object containing the keys:
     *   - flags: (optional) array of flags to pass to FieldsetInterface::add()
     *   - spec: the actual element specification, per {@link configureElement()}
     *
     * @param  FieldsetInterface             $fieldset
     * @param  array|Traversable|ArrayAccess $spec
     * @throws Exception\DomainException
     * @return FieldsetInterface
     */
    
public function configureFieldset(FieldsetInterface $fieldset$spec)
    {
        
$spec     $this->validateSpecification($spec__METHOD__);
        
$fieldset $this->configureElement($fieldset$spec);

        if (isset(
$spec['object'])) {
            
$this->prepareAndInjectObject($spec['object'], $fieldset__METHOD__);
        }

        if (isset(
$spec['hydrator'])) {
            
$this->prepareAndInjectHydrator($spec['hydrator'], $fieldset__METHOD__);
        }

        if (isset(
$spec['elements'])) {
            
$this->prepareAndInjectElements($spec['elements'], $fieldset__METHOD__);
        }

        if (isset(
$spec['fieldsets'])) {
            
$this->prepareAndInjectFieldsets($spec['fieldsets'], $fieldset__METHOD__);
        }

        return 
$fieldset;
    }

    
/**
     * Configure a form based on the provided specification
     *
     * Specification follows that of {@link configureFieldset()}, and adds the
     * following keys:
     *
     * - input_filter: input filter instance, named input filter class, or
     *   array specification for the input filter factory
     * - hydrator: hydrator instance or named hydrator class
     *
     * @param  FormInterface                  $form
     * @param  array|Traversable|ArrayAccess  $spec
     * @return FormInterface
     */
    
public function configureForm(FormInterface $form$spec)
    {
        
$spec $this->validateSpecification($spec__METHOD__);
        
$form $this->configureFieldset($form$spec);

        if (isset(
$spec['input_filter'])) {
            
$this->prepareAndInjectInputFilter($spec['input_filter'], $form__METHOD__);
        }

        if (isset(
$spec['validation_group'])) {
            
$this->prepareAndInjectValidationGroup($spec['validation_group'], $form__METHOD__);
        }

        return 
$form;
    }

    
/**
     * Validate a provided specification
     *
     * Ensures we have an array, Traversable, or ArrayAccess object, and returns it.
     *
     * @param  array|Traversable|ArrayAccess $spec
     * @param  string $method Method invoking the validator
     * @return array|ArrayAccess
     * @throws Exception\InvalidArgumentException for invalid $spec
     */
    
protected function validateSpecification($spec$method)
    {
        if (
is_array($spec)) {
            return 
$spec;
        }

        if (
$spec instanceof Traversable) {
            
$spec ArrayUtils::iteratorToArray($spec);
            return 
$spec;
        }

        if (!
$spec instanceof ArrayAccess) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects an array, or object implementing Traversable or ArrayAccess; received "%s"',
                
$method,
                (
is_object($spec) ? get_class($spec) : gettype($spec))
            ));
        }

        return 
$spec;
    }

    
/**
     * Takes a list of element specifications, creates the elements, and injects them into the provided fieldset
     *
     * @param  array|Traversable|ArrayAccess $elements
     * @param  FieldsetInterface $fieldset
     * @param  string $method Method invoking this one (for exception messages)
     * @return void
     */
    
protected function prepareAndInjectElements($elementsFieldsetInterface $fieldset$method)
    {
        
$elements $this->validateSpecification($elements$method);

        foreach (
$elements as $elementSpecification) {
            
$flags = isset($elementSpecification['flags']) ? $elementSpecification['flags'] : array();
            
$spec  = isset($elementSpecification['spec'])  ? $elementSpecification['spec']  : array();

            if (!isset(
$spec['type'])) {
                
$spec['type'] = 'Zend\Form\Element';
            }

            
$element $this->create($spec);
            
$fieldset->add($element$flags);
        }
    }

    
/**
     * Takes a list of fieldset specifications, creates the fieldsets, and injects them into the master fieldset
     *
     * @param  array|Traversable|ArrayAccess $fieldsets
     * @param  FieldsetInterface $masterFieldset
     * @param  string $method Method invoking this one (for exception messages)
     * @return void
     */
    
public function prepareAndInjectFieldsets($fieldsetsFieldsetInterface $masterFieldset$method)
    {
        
$fieldsets $this->validateSpecification($fieldsets$method);

        foreach (
$fieldsets as $fieldsetSpecification) {
            
$flags = isset($fieldsetSpecification['flags']) ? $fieldsetSpecification['flags'] : array();
            
$spec  = isset($fieldsetSpecification['spec'])  ? $fieldsetSpecification['spec']  : array();

            
$fieldset $this->createFieldset($spec);
            
$masterFieldset->add($fieldset$flags);
        }
    }

    
/**
     * Prepare and inject an object
     *
     * Takes a string indicating a class name, instantiates the class
     * by that name, and injects the class instance as the bound object.
     *
     * @param  string           $objectName
     * @param  FieldsetInterface $fieldset
     * @param  string           $method
     * @throws Exception\DomainException
     * @return void
     */
    
protected function prepareAndInjectObject($objectNameFieldsetInterface $fieldset$method)
    {
        if (!
is_string($objectName)) {
            throw new 
Exception\DomainException(sprintf(
                
'%s expects string class name; received "%s"',
                
$method,
                (
is_object($objectName) ? get_class($objectName) : gettype($objectName))
            ));
        }

        if (!
class_exists($objectName)) {
            throw new 
Exception\DomainException(sprintf(
                
'%s expects string class name to be a valid class name; received "%s"',
                
$method,
                
$objectName
            
));
        }

        
$fieldset->setObject(new $objectName);
    }

    
/**
     * Prepare and inject a named hydrator
     *
     * Takes a string indicating a hydrator class name (or a concrete instance), try first to instantiates the class
     * by pulling it from service manager, and injects the hydrator instance into the form.
     *
     * @param  string|array|Hydrator\HydratorInterface $hydratorOrName
     * @param  FieldsetInterface                       $fieldset
     * @param  string                                  $method
     * @return void
     * @throws Exception\DomainException If $hydratorOrName is not a string, does not resolve to a known class, or
     *                                   the class does not implement Hydrator\HydratorInterface
     */
    
protected function prepareAndInjectHydrator($hydratorOrNameFieldsetInterface $fieldset$method)
    {
        if (
is_object($hydratorOrName) && $hydratorOrName instanceof Hydrator\HydratorInterface) {
            
$fieldset->setHydrator($hydratorOrName);
            return;
        }

        if (
is_array($hydratorOrName)) {
            if (!isset(
$hydratorOrName['type'])) {
                throw new 
Exception\DomainException(sprintf(
                    
'%s expects array specification to have a type value',
                    
$method
                
));
            }
            
$hydratorOptions = (isset($hydratorOrName['options'])) ? $hydratorOrName['options'] : array();
            
$hydratorOrName $hydratorOrName['type'];
        } else {
            
$hydratorOptions = array();
        }

        if (
is_string($hydratorOrName)) {
            
$hydrator $this->getHydratorFromName($hydratorOrName);
        }

        if (!
$hydrator instanceof Hydrator\HydratorInterface) {
            throw new 
Exception\DomainException(sprintf(
                
'%s expects a valid implementation of Zend\Form\Hydrator\HydratorInterface; received "%s"',
                
$method,
                
$hydratorOrName
            
));
        }

        if (!empty(
$hydratorOptions) && $hydrator instanceof Hydrator\HydratorOptionsInterface) {
            
$hydrator->setOptions($hydratorOptions);
        }

        
$fieldset->setHydrator($hydrator);
    }

    
/**
     * Prepare an input filter instance and inject in the provided form
     *
     * If the input filter specified is a string, assumes it is a class name,
     * and attempts to instantiate it. If the class does not exist, or does
     * not extend InputFilterInterface, an exception is raised.
     *
     * Otherwise, $spec is passed on to the attached InputFilter Factory
     * instance in order to create the input filter.
     *
     * @param  string|array|Traversable $spec
     * @param  FormInterface $form
     * @param  string $method
     * @return void
     * @throws Exception\DomainException for unknown InputFilter class or invalid InputFilter instance
     */
    
protected function prepareAndInjectInputFilter($specFormInterface $form$method)
    {
        if (
is_string($spec)) {
            if (!
class_exists($spec)) {
                throw new 
Exception\DomainException(sprintf(
                    
'%s expects string input filter names to be valid class names; received "%s"',
                    
$method,
                    
$spec
                
));
            }
            
$filter = new $spec;
            if (!
$filter instanceof InputFilterInterface) {
                throw new 
Exception\DomainException(sprintf(
                    
'%s expects a valid implementation of Zend\InputFilter\InputFilterInterface; received "%s"',
                    
$method,
                    
$spec
                
));
            }
            
$form->setInputFilter($filter);
            return;
        }

        
$factory $this->getInputFilterFactory();
        
$filter  $factory->createInputFilter($spec);
        
$form->setInputFilter($filter);
    }

    
/**
     * Prepare a validation group and inject in the provided form
     *
     * Takes an array of elements names
     *
     * @param  string|array|Traversable $spec
     * @param  FormInterface $form
     * @param  string $method
     * @return void
     * @throws Exception\DomainException if validation group given is not an array
     */
    
protected function prepareAndInjectValidationGroup($specFormInterface $form$method)
    {
        if (!
is_array($spec)) {
            if (!
class_exists($spec)) {
                throw new 
Exception\DomainException(sprintf(
                    
'%s expects an array for validation group; received "%s"',
                    
$method,
                    
$spec
                
));
            }
        }

        
$form->setValidationGroup($spec);
    }

    
/**
     * Try to pull hydrator from service manager, or instantiates it from its name
     *
     * @param  string $hydratorName
     * @return mixed
     * @throws Exception\DomainException
     */
    
protected function getHydratorFromName($hydratorName)
    {
        
$serviceLocator $this->getFormElementManager()->getServiceLocator();

        if (
$serviceLocator && $serviceLocator->has($hydratorName)) {
            return 
$serviceLocator->get($hydratorName);
        }

        if (!
class_exists($hydratorName)) {
            throw new 
Exception\DomainException(sprintf(
                
'Expects string hydrator name to be a valid class name; received "%s"',
                
$hydratorName
            
));
        }

        
$hydrator = new $hydratorName;
        return 
$hydrator;
    }
}