<?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\View\Helper;

use 
Zend\Form\ElementInterface;
use 
Zend\Form\Element\Select as SelectElement;
use 
Zend\Form\Exception;
use 
Zend\Stdlib\ArrayUtils;

class 
FormSelect extends AbstractHelper
{
    
/**
     * Attributes valid for the current tag
     *
     * Will vary based on whether a select, option, or optgroup is being rendered
     *
     * @var array
     */
    
protected $validTagAttributes;

    protected 
$validSelectAttributes = array(
        
'name'      => true,
        
'autofocus' => true,
        
'disabled'  => true,
        
'form'      => true,
        
'multiple'  => true,
        
'required'  => true,
        
'size'      => true
    
);

    protected 
$validOptionAttributes = array(
        
'disabled' => true,
        
'selected' => true,
        
'label'    => true,
        
'value'    => true,
    );

    protected 
$validOptgroupAttributes = array(
        
'disabled' => true,
        
'label'    => true,
    );

    
/**
     * Render a form <select> element from the provided $element
     *
     * @param  ElementInterface $element
     * @throws Exception\InvalidArgumentException
     * @throws Exception\DomainException
     * @return string
     */
    
public function render(ElementInterface $element)
    {
        if (!
$element instanceof SelectElement) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s requires that the element is of type Zend\Form\Element\Select',
                
__METHOD__
            
));
        }

        
$name   $element->getName();
        if (empty(
$name) && $name !== 0) {
            throw new 
Exception\DomainException(sprintf(
                
'%s requires that the element has an assigned name; none discovered',
                
__METHOD__
            
));
        }

        
$options $element->getValueOptions();

        if ((
$emptyOption $element->getEmptyOption()) !== null) {
            
$options = array('' => $emptyOption) + $options;
        }

        
$attributes $element->getAttributes();
        
$value      $this->validateMultiValue($element->getValue(), $attributes);

        
$attributes['name'] = $name;
        if (
array_key_exists('multiple'$attributes) && $attributes['multiple']) {
            
$attributes['name'] .= '[]';
        }
        
$this->validTagAttributes $this->validSelectAttributes;

        return 
sprintf(
            
'<select %s>%s</select>',
            
$this->createAttributesString($attributes),
            
$this->renderOptions($options$value)
        );
    }

    
/**
     * Render an array of options
     *
     * Individual options should be of the form:
     *
     * <code>
     * array(
     *     'value'    => 'value',
     *     'label'    => 'label',
     *     'disabled' => $booleanFlag,
     *     'selected' => $booleanFlag,
     * )
     * </code>
     *
     * @param  array $options
     * @param  array $selectedOptions Option values that should be marked as selected
     * @return string
     */
    
public function renderOptions(array $options, array $selectedOptions = array())
    {
        
$template      '<option %s>%s</option>';
        
$optionStrings = array();
        
$escapeHtml    $this->getEscapeHtmlHelper();

        foreach (
$options as $key => $optionSpec) {
            
$value    '';
            
$label    '';
            
$selected false;
            
$disabled false;

            if (
is_scalar($optionSpec)) {
                
$optionSpec = array(
                    
'label' => $optionSpec,
                    
'value' => $key
                
);
            }

            if (isset(
$optionSpec['options']) && is_array($optionSpec['options'])) {
                
$optionStrings[] = $this->renderOptgroup($optionSpec$selectedOptions);
                continue;
            }

            if (isset(
$optionSpec['value'])) {
                
$value $optionSpec['value'];
            }
            if (isset(
$optionSpec['label'])) {
                
$label $optionSpec['label'];
            }
            if (isset(
$optionSpec['selected'])) {
                
$selected $optionSpec['selected'];
            }
            if (isset(
$optionSpec['disabled'])) {
                
$disabled $optionSpec['disabled'];
            }

            if (
ArrayUtils::inArray($value$selectedOptions)) {
                
$selected true;
            }

            if (
null !== ($translator $this->getTranslator())) {
                
$label $translator->translate(
                    
$label$this->getTranslatorTextDomain()
                );
            }

            
$attributes compact('value''selected''disabled');
            
$this->validTagAttributes $this->validOptionAttributes;
            
$optionStrings[] = sprintf(
                
$template,
                
$this->createAttributesString($attributes),
                
$escapeHtml($label)
            );
        }

        return 
implode("\n"$optionStrings);
    }

    
/**
     * Render an optgroup
     *
     * See {@link renderOptions()} for the options specification. Basically,
     * an optgroup is simply an option that has an additional "options" key
     * with an array following the specification for renderOptions().
     *
     * @param  array $optgroup
     * @param  array $selectedOptions
     * @return string
     */
    
public function renderOptgroup(array $optgroup, array $selectedOptions = array())
    {
        
$template '<optgroup%s>%s</optgroup>';

        
$options = array();
        if (isset(
$optgroup['options']) && is_array($optgroup['options'])) {
            
$options $optgroup['options'];
            unset(
$optgroup['options']);
        }

        
$this->validTagAttributes $this->validOptgroupAttributes;
        
$attributes $this->createAttributesString($optgroup);
        if (!empty(
$attributes)) {
            
$attributes ' ' $attributes;
        }

        return 
sprintf(
            
$template,
            
$attributes,
            
$this->renderOptions($options$selectedOptions)
        );
    }

    
/**
     * Invoke helper as functor
     *
     * Proxies to {@link render()}.
     *
     * @param  ElementInterface|null $element
     * @return string|FormSelect
     */
    
public function __invoke(ElementInterface $element null)
    {
        if (!
$element) {
            return 
$this;
        }

        return 
$this->render($element);
    }

    
/**
     * Ensure that the value is set appropriately
     *
     * If the element's value attribute is an array, but there is no multiple
     * attribute, or that attribute does not evaluate to true, then we have
     * a domain issue -- you cannot have multiple options selected unless the
     * multiple attribute is present and enabled.
     *
     * @param  mixed $value
     * @param  array $attributes
     * @return array
     * @throws Exception\DomainException
     */
    
protected function validateMultiValue($value, array $attributes)
    {
        if (
null === $value) {
            return array();
        }

        if (!
is_array($value)) {
            return (array) 
$value;
        }

        if (!isset(
$attributes['multiple']) || !$attributes['multiple']) {
            throw new 
Exception\DomainException(sprintf(
                
'%s does not allow specifying multiple selected values when the element does not have a multiple attribute set to a boolean true',
                
__CLASS__
            
));
        }

        return 
$value;
    }
}