<?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\Annotation;

use 
ArrayObject;
use 
ReflectionClass;
use 
Zend\Code\Annotation\AnnotationCollection;
use 
Zend\Code\Annotation\AnnotationManager;
use 
Zend\Code\Annotation\Parser;
use 
Zend\Code\Reflection\ClassReflection;
use 
Zend\EventManager\Event;
use 
Zend\EventManager\EventManager;
use 
Zend\EventManager\EventManagerAwareInterface;
use 
Zend\EventManager\EventManagerInterface;
use 
Zend\Form\Exception;
use 
Zend\Form\Factory;
use 
Zend\Form\FormFactoryAwareInterface;
use 
Zend\Stdlib\ArrayUtils;

/**
 * Parses a class' properties for annotations in order to create a form and
 * input filter definition.
 */
class AnnotationBuilder implements EventManagerAwareInterfaceFormFactoryAwareInterface
{
    
/**
     * @var AnnotationManager
     */
    
protected $annotationManager;

    
/**
     * @var EventManagerInterface
     */
    
protected $events;

    
/**
     * @var Factory
     */
    
protected $formFactory;

    
/**
     * @var object
     */
    
protected $entity;

    
/**
     * @var array Default annotations to register
     */
    
protected $defaultAnnotations = array(
        
'AllowEmpty',
        
'Attributes',
        
'ComposedObject',
        
'ErrorMessage',
        
'Exclude',
        
'Filter',
        
'Flags',
        
'Hydrator',
        
'Input',
        
'InputFilter',
        
'Name',
        
'Object',
        
'Options',
        
'Required',
        
'Type',
        
'ValidationGroup',
        
'Validator'
    
);

    
/**
     * Set form factory to use when building form from annotations
     *
     * @param  Factory $formFactory
     * @return AnnotationBuilder
     */
    
public function setFormFactory(Factory $formFactory)
    {
        
$this->formFactory $formFactory;
        return 
$this;
    }

    
/**
     * Set annotation manager to use when building form from annotations
     *
     * @param  AnnotationManager $annotationManager
     * @return AnnotationBuilder
     */
    
public function setAnnotationManager(AnnotationManager $annotationManager)
    {
        
$parser = new Parser\DoctrineAnnotationParser();
        foreach (
$this->defaultAnnotations as $annotationName) {
            
$class __NAMESPACE__ '\\' $annotationName;
            
$parser->registerAnnotation($class);
        }
        
$annotationManager->attach($parser);
        
$this->annotationManager $annotationManager;
        return 
$this;
    }

    
/**
     * Set event manager instance
     *
     * @param  EventManagerInterface $events
     * @return AnnotationBuilder
     */
    
public function setEventManager(EventManagerInterface $events)
    {
        
$events->setIdentifiers(array(
            
__CLASS__,
            
get_class($this),
        ));
        
$events->attach(new ElementAnnotationsListener());
        
$events->attach(new FormAnnotationsListener());
        
$this->events $events;
        return 
$this;
    }

    
/**
     * Retrieve form factory
     *
     * Lazy-loads the default form factory if none is currently set.
     *
     * @return Factory
     */
    
public function getFormFactory()
    {
        if (
$this->formFactory) {
            return 
$this->formFactory;
        }

        
$this->formFactory = new Factory();
        return 
$this->formFactory;
    }

    
/**
     * Retrieve annotation manager
     *
     * If none is currently set, creates one with default annotations.
     *
     * @return AnnotationManager
     */
    
public function getAnnotationManager()
    {
        if (
$this->annotationManager) {
            return 
$this->annotationManager;
        }

        
$this->setAnnotationManager(new AnnotationManager());
        return 
$this->annotationManager;
    }

    
/**
     * Get event manager
     *
     * @return EventManagerInterface
     */
    
public function getEventManager()
    {
        if (
null === $this->events) {
            
$this->setEventManager(new EventManager());
        }
        return 
$this->events;
    }

    
/**
     * Creates and returns a form specification for use with a factory
     *
     * Parses the object provided, and processes annotations for the class and
     * all properties. Information from annotations is then used to create
     * specifications for a form, its elements, and its input filter.
     *
     * @param  string|object $entity Either an instance or a valid class name for an entity
     * @throws Exception\InvalidArgumentException if $entity is not an object or class name
     * @return ArrayObject
     */
    
public function getFormSpecification($entity)
    {
        if (!
is_object($entity)) {
            if ((
is_string($entity) && (!class_exists($entity))) // non-existent class
                
|| (!is_string($entity)) // not an object or string
            
) {
                throw new 
Exception\InvalidArgumentException(sprintf(
                    
'%s expects an object or valid class name; received "%s"',
                    
__METHOD__,
                    
var_export($entity1)
                ));
            }
        }

        
$this->entity      $entity;
        
$annotationManager $this->getAnnotationManager();
        
$formSpec          = new ArrayObject();
        
$filterSpec        = new ArrayObject();

        
$reflection  = new ClassReflection($entity);
        
$annotations $reflection->getAnnotations($annotationManager);

        if (
$annotations instanceof AnnotationCollection) {
            
$this->configureForm($annotations$reflection$formSpec$filterSpec);
        }

        foreach (
$reflection->getProperties() as $property) {
            
$annotations $property->getAnnotations($annotationManager);

            if (
$annotations instanceof AnnotationCollection) {
                
$this->configureElement($annotations$property$formSpec$filterSpec);
            }
        }

        if (!isset(
$formSpec['input_filter'])) {
            
$formSpec['input_filter'] = $filterSpec;
        }

        return 
$formSpec;
    }

    
/**
     * Create a form from an object.
     *
     * @param  string|object $entity
     * @return \Zend\Form\Form
     */
    
public function createForm($entity)
    {
        
$formSpec    ArrayUtils::iteratorToArray($this->getFormSpecification($entity));
        
$formFactory $this->getFormFactory();
        return 
$formFactory->createForm($formSpec);
    }

    
/**
     * Get the entity used to construct the form.
     *
     * @return object
     */
    
public function getEntity()
    {
        return 
$this->entity;
    }

    
/**
     * Configure the form specification from annotations
     *
     * @param  AnnotationCollection $annotations
     * @param  ClassReflection $reflection
     * @param  ArrayObject $formSpec
     * @param  ArrayObject $filterSpec
     * @return void
     * @triggers discoverName
     * @triggers configureForm
     */
    
protected function configureForm($annotations$reflection$formSpec$filterSpec)
    {
        
$name                   $this->discoverName($annotations$reflection);
        
$formSpec['name']       = $name;
        
$formSpec['attributes'] = array();
        
$formSpec['elements']   = array();
        
$formSpec['fieldsets']  = array();

        
$events $this->getEventManager();
        foreach (
$annotations as $annotation) {
            
$events->trigger(__FUNCTION__$this, array(
                
'annotation' => $annotation,
                
'name'        => $name,
                
'formSpec'   => $formSpec,
                
'filterSpec' => $filterSpec,
            ));
        }
    }

    
/**
     * Configure an element from annotations
     *
     * @param  AnnotationCollection $annotations
     * @param  \Zend\Code\Reflection\PropertyReflection $reflection
     * @param  ArrayObject $formSpec
     * @param  ArrayObject $filterSpec
     * @return void
     * @triggers checkForExclude
     * @triggers discoverName
     * @triggers configureElement
     */
    
protected function configureElement($annotations$reflection$formSpec$filterSpec)
    {
        
// If the element is marked as exclude, return early
        
if ($this->checkForExclude($annotations)) {
            return;
        }

        
$events $this->getEventManager();
        
$name   $this->discoverName($annotations$reflection);

        
$elementSpec = new ArrayObject(array(
            
'flags' => array(),
            
'spec'  => array(
                
'name' => $name
            
),
        ));
        
$inputSpec = new ArrayObject(array(
            
'name' => $name,
        ));

        
$event = new Event();
        
$event->setParams(array(
            
'name'        => $name,
            
'elementSpec' => $elementSpec,
            
'inputSpec'   => $inputSpec,
            
'formSpec'    => $formSpec,
            
'filterSpec'  => $filterSpec,
        ));
        foreach (
$annotations as $annotation) {
            
$event->setParam('annotation'$annotation);
            
$events->trigger(__FUNCTION__$this$event);
        }

        
$filterSpec[$name] = $event->getParam('inputSpec');

        
$elementSpec $event->getParam('elementSpec');
        
$type        = (isset($elementSpec['spec']['type']))
            ? 
$elementSpec['spec']['type']
            : 
'Zend\Form\Element';

        
// Compose as a fieldset or an element, based on specification type
        
if (static::isSubclassOf($type'Zend\Form\FieldsetInterface')) {
            if (!isset(
$formSpec['fieldsets'])) {
                
$formSpec['fieldsets'] = array();
            }
            
$formSpec['fieldsets'][] = $elementSpec;
        } else {
            if (!isset(
$formSpec['elements'])) {
                
$formSpec['elements'] = array();
            }
            
$formSpec['elements'][] = $elementSpec;
        }
    }

    
/**
     * Discover the name of the given form or element
     *
     * @param  AnnotationCollection $annotations
     * @param  \Reflector $reflection
     * @return string
     */
    
protected function discoverName($annotations$reflection)
    {
        
$results $this->getEventManager()->trigger('discoverName'$this, array(
            
'annotations' => $annotations,
            
'reflection'  => $reflection,
        ), function (
$r) {
            return (
is_string($r) && !empty($r));
        });
        return 
$results->last();
    }

    
/**
     * Determine if an element is marked to exclude from the definitions
     *
     * @param  AnnotationCollection $annotations
     * @return true|false
     */
    
protected function checkForExclude($annotations)
    {
        
$results $this->getEventManager()->trigger('checkForExclude'$this, array(
            
'annotations' => $annotations,
        ), function (
$r) {
            return (
true === $r);
        });
        return (bool) 
$results->last();
    }

    
/**
     * Checks if the object has this class as one of its parents
     *
     * @see https://bugs.php.net/bug.php?id=53727
     * @see https://github.com/zendframework/zf2/pull/1807
     *
     * @param string $className
     * @param string $type
     * @return bool
     */
    
protected static function isSubclassOf($className$type)
    {
        if (
is_subclass_of($className$type)) {
            return 
true;
        }
        if (
version_compare(PHP_VERSION'5.3.7''>=')) {
            return 
false;
        }
        if (!
interface_exists($type)) {
            return 
false;
        }
        
$r = new ReflectionClass($className);
        return 
$r->implementsInterface($type);
    }
}