<?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 
Traversable;
use 
Zend\Form\Element\Collection;
use 
Zend\Form\Exception;
use 
Zend\InputFilter\InputFilter;
use 
Zend\InputFilter\InputFilterAwareInterface;
use 
Zend\InputFilter\InputFilterInterface;
use 
Zend\InputFilter\InputFilterProviderInterface;
use 
Zend\InputFilter\InputProviderInterface;
use 
Zend\Stdlib\ArrayUtils;
use 
Zend\Stdlib\Hydrator\HydratorInterface;

class 
Form extends Fieldset implements FormInterface
{
    
/**
     * Seed attributes
     *
     * @var array
     */
    
protected $attributes = array(
        
'method' => 'POST',
    );

    
/**
     * How to bind values to the attached object
     *
     * @var int
     */
    
protected $bindAs FormInterface::VALUES_NORMALIZED;

    
/**
     * Whether or not to bind values to the bound object on successful validation
     *
     * @var int
     */
    
protected $bindOnValidate FormInterface::BIND_ON_VALIDATE;

    
/**
     * Base fieldset to use for hydrating (if none specified, directly hydrate elements)
     *
     * @var FieldsetInterface
     */
    
protected $baseFieldset;

    
/**
     * Data being validated
     *
     * @var null|array|Traversable
     */
    
protected $data;

    
/**
     * @var null|InputFilterInterface
     */
    
protected $filter;

    
/**
     * Whether or not to automatically scan for input filter defaults on
     * attached fieldsets and elements
     *
     * @var bool
     */
    
protected $useInputFilterDefaults true;

    
/**
     * Has the input filter defaults been added already ?
     *
     * @var bool
     */
    
protected $hasAddedInputFilterDefaults false;

    
/**
     * Whether or not validation has occurred
     *
     * @var bool
     */
    
protected $hasValidated false;

    
/**
     * Result of last validation operation
     *
     * @var bool
     */
    
protected $isValid false;

    
/**
     * Is the form prepared ?
     *
     * @var bool
     */
    
protected $isPrepared false;

    
/**
     * Prefer form input filter over input filter defaults
     *
     * @var bool
     */
    
protected $preferFormInputFilter false;

    
/**
     * Are the form elements/fieldsets wrapped by the form name ?
     *
     * @var bool
     */
    
protected $wrapElements false;

    
/**
     * Validation group, if any
     *
     * @var null|array
     */
    
protected $validationGroup;

    
/**
     * Add an element or fieldset
     *
     * If $elementOrFieldset is an array or Traversable, passes the argument on
     * to the composed factory to create the object before attaching it.
     *
     * $flags could contain metadata such as the alias under which to register
     * the element or fieldset, order in which to prioritize it, etc.
     *
     * @param  array|Traversable|ElementInterface $elementOrFieldset
     * @param  array                              $flags
     * @return \Zend\Form\Fieldset|\Zend\Form\FieldsetInterface|\Zend\Form\FormInterface
     */
    
public function add($elementOrFieldset, array $flags = array())
    {
        
// TODO: find a better solution than duplicating the factory code, the problem being that if $elementOrFieldset is an array,
        // it is passed by value, and we don't get back the concrete ElementInterface
        
if (is_array($elementOrFieldset)
            || (
$elementOrFieldset instanceof Traversable && !$elementOrFieldset instanceof ElementInterface)
        ) {
            
$factory $this->getFormFactory();
            
$elementOrFieldset $factory->create($elementOrFieldset);
        }

        
parent::add($elementOrFieldset$flags);

        if (
$elementOrFieldset instanceof Fieldset && $elementOrFieldset->useAsBaseFieldset()) {
            
$this->baseFieldset $elementOrFieldset;
        }

        return 
$this;
    }

    
/**
     * Ensures state is ready for use
     *
     * Marshalls the input filter, to ensure validation error messages are
     * available, and prepares any elements and/or fieldsets that require
     * preparation.
     *
     * @return Form
     */
    
public function prepare()
    {
        if (
$this->isPrepared) {
            return 
$this;
        }

        
$this->getInputFilter();

        
// If the user wants to, elements names can be wrapped by the form's name
        
if ($this->wrapElements()) {
            
$this->prepareElement($this);
        } else {
            foreach (
$this->getIterator() as $elementOrFieldset) {
                if (
$elementOrFieldset instanceof ElementPrepareAwareInterface) {
                    
$elementOrFieldset->prepareElement($this);
                }
            }
        }

        
$this->isPrepared true;
        return 
$this;
    }

    
/**
     * Set data to validate and/or populate elements
     *
     * Typically, also passes data on to the composed input filter.
     *
     * @param  array|\ArrayAccess|Traversable $data
     * @return Form|FormInterface
     * @throws Exception\InvalidArgumentException
     */
    
public function setData($data)
    {
        if (
$data instanceof Traversable) {
            
$data ArrayUtils::iteratorToArray($data);
        }
        if (!
is_array($data)) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects an array or Traversable argument; received "%s"',
                
__METHOD__,
                (
is_object($data) ? get_class($data) : gettype($data))
            ));
        }

        
$this->hasValidated false;
        
$this->data         $data;
        
$this->populateValues($data);

        return 
$this;
    }

    
/**
     * Bind an object to the form
     *
     * Ensures the object is populated with validated values.
     *
     * @param  object $object
     * @param  int $flags
     * @return mixed|void
     * @throws Exception\InvalidArgumentException
     */
    
public function bind($object$flags FormInterface::VALUES_NORMALIZED)
    {
        if (!
in_array($flags, array(FormInterface::VALUES_NORMALIZEDFormInterface::VALUES_RAW))) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects the $flags argument to be one of "%s" or "%s"; received "%s"',
                
__METHOD__,
                
'Zend\Form\FormInterface::VALUES_NORMALIZED',
                
'Zend\Form\FormInterface::VALUES_RAW',
                
$flags
            
));
        }

        if (
$this->baseFieldset !== null) {
            
$this->baseFieldset->setObject($object);
        }

        
$this->bindAs $flags;
        
$this->setObject($object);
        
$this->extract();

        return 
$this;
    }

    
/**
     * Set the hydrator to use when binding an object to the element
     *
     * @param  HydratorInterface $hydrator
     * @return FieldsetInterface
     */
    
public function setHydrator(HydratorInterface $hydrator)
    {
        if (
$this->baseFieldset !== null) {
            
$this->baseFieldset->setHydrator($hydrator);
        }

        return 
parent::setHydrator($hydrator);
    }

    
/**
     * Bind values to the bound object
     *
     * @param array $values
     * @return mixed
     */
    
public function bindValues(array $values = array())
    {
        if (!
is_object($this->object)) {
            if ( 
$this->baseFieldset === null || $this->baseFieldset->allowValueBinding() == false ) {
                return;
            }
        }
        if (!
$this->hasValidated() && !empty($values)) {
            
$this->setData($values);
            if (!
$this->isValid()) {
                return;
            }
        } elseif (!
$this->isValid) {
            return;
        }

        
$filter $this->getInputFilter();

        switch (
$this->bindAs) {
            case 
FormInterface::VALUES_RAW:
                
$data $filter->getRawValues();
                break;
            case 
FormInterface::VALUES_NORMALIZED:
            default:
                
$data $filter->getValues();
                break;
        }

        
$data $this->prepareBindData($data$this->data);

        
// If there is a base fieldset, only hydrate beginning from the base fieldset
        
if ($this->baseFieldset !== null) {
            
$data $data[$this->baseFieldset->getName()];
            
$this->object $this->baseFieldset->bindValues($data);
        } else {
            
$this->object parent::bindValues($data);
        }
    }

    
/**
     * Parse filtered values and return only posted fields for binding
     *
     * @param array $values
     * @param array $match
     * @return array
     */
    
protected function prepareBindData(array $values, array $match)
    {
        
$data = array();
        foreach (
$values as $name => $value) {
            if (!
array_key_exists($name$match)) {
                continue;
            }

            if (
is_array($value) && is_array($match[$name])) {
                
$data[$name] = $this->prepareBindData($value$match[$name]);
            } else {
                
$data[$name] = $value;
            }
        }
        return 
$data;
    }

    
/**
     * Set flag indicating whether or not to bind values on successful validation
     *
     * @param  int $bindOnValidateFlag
     * @return void|Form
     * @throws Exception\InvalidArgumentException
     */
    
public function setBindOnValidate($bindOnValidateFlag)
    {
        if (!
in_array($bindOnValidateFlag, array(self::BIND_ON_VALIDATEself::BIND_MANUAL))) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects the flag to be one of %s::%s or %s::%s',
                
__METHOD__,
                
get_called_class(),
                
'BIND_ON_VALIDATE',
                
get_called_class(),
                
'BIND_MANUAL'
            
));
        }
        
$this->bindOnValidate $bindOnValidateFlag;
        return 
$this;
    }

    
/**
     * Will we bind values to the bound object on successful validation?
     *
     * @return bool
     */
    
public function bindOnValidate()
    {
        return (static::
BIND_ON_VALIDATE === $this->bindOnValidate);
    }

    
/**
     * Set the base fieldset to use when hydrating
     *
     * @param  FieldsetInterface $baseFieldset
     * @return Form
     * @throws Exception\InvalidArgumentException
     */
    
public function setBaseFieldset(FieldsetInterface $baseFieldset)
    {
        
$this->baseFieldset $baseFieldset;
        return 
$this;
    }

    
/**
     * Get the base fieldset to use when hydrating
     *
     * @return FieldsetInterface
     */
    
public function getBaseFieldset()
    {
        return 
$this->baseFieldset;
    }

    
/**
     * Check if the form has been validated
     *
     * @return bool
     */
    
public function hasValidated()
    {
        return 
$this->hasValidated;
    }

    
/**
     * Validate the form
     *
     * Typically, will proxy to the composed input filter.
     *
     * @return bool
     * @throws Exception\DomainException
     */
    
public function isValid()
    {
        if (
$this->hasValidated) {
            return 
$this->isValid;
        }

        
$this->isValid false;

        if (!
is_array($this->data) && !is_object($this->object)) {
            throw new 
Exception\DomainException(sprintf(
                
'%s is unable to validate as there is no data currently set',
                
__METHOD__
            
));
        }

        if (!
is_array($this->data)) {
            
$data $this->extract();
            if (!
is_array($data)) {
                throw new 
Exception\DomainException(sprintf(
                    
'%s is unable to validate as there is no data currently set',
                    
__METHOD__
                
));
            }
            
$this->data $data;
        }

        
$filter $this->getInputFilter();
        if (!
$filter instanceof InputFilterInterface) {
            throw new 
Exception\DomainException(sprintf(
                
'%s is unable to validate as there is no input filter present',
                
__METHOD__
            
));
        }

        
$filter->setData($this->data);
        
$filter->setValidationGroup(InputFilterInterface::VALIDATE_ALL);

        
$validationGroup $this->getValidationGroup();
        if (
$validationGroup !== null) {
            
$this->prepareValidationGroup($this$this->data$validationGroup);
            
$filter->setValidationGroup($validationGroup);
        }

        
$this->isValid $result $filter->isValid();
        
$this->hasValidated true;

        if (
$result && $this->bindOnValidate()) {
            
$this->bindValues();
        }

        if (!
$result) {
            
$this->setMessages($filter->getMessages());
        }

        return 
$result;
    }

    
/**
     * Retrieve the validated data
     *
     * By default, retrieves normalized values; pass one of the
     * FormInterface::VALUES_* constants to shape the behavior.
     *
     * @param  int $flag
     * @return array|object
     * @throws Exception\DomainException
     */
    
public function getData($flag FormInterface::VALUES_NORMALIZED)
    {
        if (!
$this->hasValidated) {
            throw new 
Exception\DomainException(sprintf(
                
'%s cannot return data as validation has not yet occurred',
                
__METHOD__
            
));
        }

        if ((
$flag !== FormInterface::VALUES_AS_ARRAY) && is_object($this->object)) {
            return 
$this->object;
        }

        
$filter $this->getInputFilter();

        if (
$flag === FormInterface::VALUES_RAW) {
            return 
$filter->getRawValues();
        }

        return 
$filter->getValues();
    }

    
/**
     * Set the validation group (set of values to validate)
     *
     * Typically, proxies to the composed input filter
     *
     * @throws Exception\InvalidArgumentException
     * @return Form|FormInterface
     */
    
public function setValidationGroup()
    {
        
$argc func_num_args();
        if (
=== $argc) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects at least one argument; none provided',
                
__METHOD__
            
));
        }

        
$argv func_get_args();
        
$this->hasValidated false;

        if (
$argc 1) {
            
$this->validationGroup $argv;
            return 
$this;
        }

        
$arg array_shift($argv);
        if (
$arg === FormInterface::VALIDATE_ALL) {
            
$this->validationGroup null;
            return 
$this;
        }

        if (!
is_array($arg)) {
            
$arg = (array) $arg;
        }

        
$this->validationGroup $arg;
        return 
$this;
    }

    
/**
     * Retrieve the current validation group, if any
     *
     * @return null|array
     */
    
public function getValidationGroup()
    {
        return 
$this->validationGroup;
    }

    
/**
     * Prepare the validation group in case Collection elements were used (this function also handle the case where elements
     * could have been dynamically added or removed from a collection using JavaScript)
     *
     * @param FieldsetInterface $formOrFieldset
     * @param array             $data
     * @param array             $validationGroup
     */
    
protected function prepareValidationGroup(FieldsetInterface $formOrFieldset, array $data, array &$validationGroup)
    {
        foreach (
$validationGroup as $key => &$value) {
            if (!
$formOrFieldset->has($key)) {
                continue;
            }

            
$fieldset $formOrFieldset->byName[$key];

            if (
$fieldset instanceof Collection) {
                if (!isset(
$data[$key]) && $fieldset->getCount() == 0) {
                    unset (
$validationGroup[$key]);
                    continue;
                }

                
$values = array();

                if (isset(
$data[$key])) {
                    foreach(
array_keys($data[$key]) as $cKey) {
                        
$values[$cKey] = $value;
                    }
                }

                
$value $values;
            } else {
                if (!isset(
$data[$key])) {
                    
$data[$key] = array();
                }
                
$this->prepareValidationGroup($fieldset$data[$key], $validationGroup[$key]);
            }
        }
    }

    
/**
     * Set the input filter used by this form
     *
     * @param  InputFilterInterface $inputFilter
     * @return FormInterface
     */
    
public function setInputFilter(InputFilterInterface $inputFilter)
    {
        
$this->hasValidated                false;
        
$this->hasAddedInputFilterDefaults false;
        
$this->filter                      $inputFilter;
        return 
$this;
    }

    
/**
     * Retrieve input filter used by this form
     *
     * @return null|InputFilterInterface
     */
    
public function getInputFilter()
    {
        if (
$this->object instanceof InputFilterAwareInterface) {
            if (
null == $this->baseFieldset) {
                
$this->filter $this->object->getInputFilter();
            } else {
                
$name $this->baseFieldset->getName();
                if (!
$this->filter instanceof InputFilterInterface || !$this->filter->has($name)) {
                    
$filter = new InputFilter();
                    
$filter->add($this->object->getInputFilter(), $name);
                    
$this->filter $filter;
                }
            }
        }

        if (!isset(
$this->filter)) {
            
$this->filter = new InputFilter();
        }

        if (!
$this->hasAddedInputFilterDefaults
            
&& $this->filter instanceof InputFilterInterface
            
&& $this->useInputFilterDefaults()
        ) {
            
$this->attachInputFilterDefaults($this->filter$this);
            
$this->hasAddedInputFilterDefaults true;
        }

        return 
$this->filter;
    }

    
/**
     * Set flag indicating whether or not to scan elements and fieldsets for defaults
     *
     * @param  bool $useInputFilterDefaults
     * @return Form
     */
    
public function setUseInputFilterDefaults($useInputFilterDefaults)
    {
        
$this->useInputFilterDefaults = (bool) $useInputFilterDefaults;
        return 
$this;
    }

    
/**
     * Should we use input filter defaults from elements and fieldsets?
     *
     * @return bool
     */
    
public function useInputFilterDefaults()
    {
        return 
$this->useInputFilterDefaults;
    }

    
/**
     * Set flag indicating whether or not to prefer the form input filter over element and fieldset defaults
     *
     * @param  bool $preferFormInputFilter
     * @return Form
     */
    
public function setPreferFormInputFilter($preferFormInputFilter)
    {
        
$this->preferFormInputFilter = (bool) $preferFormInputFilter;
        return 
$this;
    }

    
/**
     * Should we use form input filter over element input filter defaults from elements and fieldsets?
     *
     * @return bool
     */
    
public function getPreferFormInputFilter()
    {
        return 
$this->preferFormInputFilter;
    }

    
/**
     * Attach defaults provided by the elements to the input filter
     *
     * @param  InputFilterInterface $inputFilter
     * @param  FieldsetInterface $fieldset Fieldset to traverse when looking for default inputs
     * @return void
     */
    
public function attachInputFilterDefaults(InputFilterInterface $inputFilterFieldsetInterface $fieldset)
    {
        
$formFactory  $this->getFormFactory();
        
$inputFactory $formFactory->getInputFilterFactory();

        if (
$this instanceof InputFilterProviderInterface) {
            foreach (
$this->getInputFilterSpecification() as $name => $spec) {
                
$input $inputFactory->createInput($spec);
                
$inputFilter->add($input$name);
            }
        }

        foreach (
$fieldset->getElements() as $element) {
            
$name $element->getName();

            if (
$this->preferFormInputFilter && $inputFilter->has($name)) {
                continue;
            }

            if (!
$element instanceof InputProviderInterface) {
                if (
$inputFilter->has($name)) {
                    continue;
                }
                
// Create a new empty default input for this element
                
$spec = array('name' => $name'required' => false);
            } else {
                
// Create an input based on the specification returned from the element
                
$spec  $element->getInputSpecification();
            }

            
$input $inputFactory->createInput($spec);
            
$inputFilter->add($input$name);
        }

        foreach (
$fieldset->getFieldsets() as $fieldset) {
            
$name $fieldset->getName();

            if (!
$fieldset instanceof InputFilterProviderInterface) {
                if (!
$inputFilter->has($name)) {
                    
// Add a new empty input filter if it does not exist (or the fieldset's object input filter),
                    // so that elements of nested fieldsets can be recursively added
                    
if ($fieldset->getObject() instanceof InputFilterAwareInterface) {
                        
$inputFilter->add($fieldset->getObject()->getInputFilter(), $name);
                    } else {
                        
$inputFilter->add(new InputFilter(), $name);
                    }
                }

                
$fieldsetFilter $inputFilter->get($name);

                if (!
$fieldsetFilter instanceof InputFilterInterface) {
                    
// Input attached for fieldset, not input filter; nothing more to do.
                    
continue;
                }

                
// Traverse the elements of the fieldset, and attach any
                // defaults to the fieldset's input filter
                
$this->attachInputFilterDefaults($fieldsetFilter$fieldset);
                continue;
            }

            if (
$inputFilter->has($name)) {
                
// if we already have an input/filter by this name, use it
                
continue;
            }

            
// Create an input filter based on the specification returned from the fieldset
            
$spec   $fieldset->getInputFilterSpecification();
            
$filter $inputFactory->createInputFilter($spec);
            
$inputFilter->add($filter$name);

            
// Recursively attach sub filters
            
$this->attachInputFilterDefaults($filter$fieldset);
        }
    }

    
/**
     * Are the form elements/fieldsets names wrapped by the form name ?
     *
     * @param  bool $wrapElements
     * @return Form
     */
    
public function setWrapElements($wrapElements)
    {
        
$this->wrapElements = (bool) $wrapElements;
        return 
$this;
    }

    
/**
     * If true, form elements/fieldsets name's are wrapped around the form name itself
     *
     * @return bool
     */
    
public function wrapElements()
    {
        return 
$this->wrapElements;
    }

    
/**
     * Recursively extract values for elements and sub-fieldsets, and populate form values
     *
     * @return array
     */
    
protected function extract()
    {
        if (
null !== $this->baseFieldset) {
            
$name $this->baseFieldset->getName();
            
$values[$name] = $this->baseFieldset->extract();
            
$this->baseFieldset->populateValues($values[$name]);
        } else {
            
$values parent::extract();
            
$this->populateValues($values);
        }

        return 
$values;
    }
}