<?php
/**
 * Handles a Container that can be repeated multiple times in the form
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2012, Alexey Borzov <avb@php.net>,
 *                          Bertrand Mansion <golgote@mamasam.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The names of the authors may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category HTML
 * @package  HTML_QuickForm2
 * @author   Alexey Borzov <avb@php.net>
 * @author   Bertrand Mansion <golgote@mamasam.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @version  SVN: $Id: Repeat.php 325694 2012-05-15 07:46:13Z avb $
 * @link     http://pear.php.net/package/HTML_QuickForm2
 */

/** Base class for all HTML_QuickForm2 containers */
require_once LAB_PATH_KERNEL.'/classes/HTML/QuickForm2/Container.php';
/** Javascript aggregator and builder class */
require_once LAB_PATH_KERNEL.'/classes/HTML/QuickForm2/JavascriptBuilder.php';

/**
 * Javascript builder used when rendering a repeat prototype
 *
 * Instead of returning form setup code and client-side rules as normal
 * Javascript code, it returns them as Javascript string literals. These are
 * expected to be eval()'d when adding a new repeat item.
 *
 * This class is not intended for normal use.
 *
 * @category HTML
 * @package  HTML_QuickForm2
 * @author   Alexey Borzov <avb@php.net>
 * @author   Bertrand Mansion <golgote@mamasam.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @version  Release: 2.0.0
 * @link     http://pear.php.net/package/HTML_QuickForm2
 */
class HTML_QuickForm2_Container_Repeat_JavascriptBuilder
    extends HTML_QuickForm2_JavascriptBuilder
{
    /**
     * Fake "current form" ID
     * @var string
     */
    protected $formId = 'repeat';

    /**
     * Empty list of javascript libraries, base one(s) are in original builder
     * @var array
     */
    protected $libraries = array();


    /**
     * Returns rules and element setup code as Javascript string literals
     *
     * @return array array('rules', 'setup code')
     */
    public function getFormJavascriptAsStrings()
    {
        return array(
            self::encode(
                empty($this->rules['repeat'])
                ? '' : "[\n" . implode(",\n", $this->rules['repeat']) . "\n]"
            ),
            self::encode(
                empty($this->scripts['repeat'])
                ? '' : implode("\n", $this->scripts['repeat'])
            )
        );
    }

    /**
     * Passes Javascript libraries added by repeat prototype
     *
     * @param HTML_QuickForm2_JavascriptBuilder $recipient original Javascript builder
     */
    public function passLibraries(HTML_QuickForm2_JavascriptBuilder $recipient)
    {
        foreach ($this->libraries as $name => $library) {
            $recipient->addLibrary(
                $name, $library['file'], $library['webPath'], $library['absPath']
            );
        }
    }
}



/**
 * Handles a Container that can be repeated multiple times in the form
 *
 * This element accepts a Container (a Fieldset, a Group, but not another
 * Repeat) serving as a "prototype" and repeats it several times. Repeated
 * items can be dynamically added / removed via Javascript, with the benefit
 * that server-side part automatically knows about these changes and that
 * server-side and client-side validation can be easily leveraged.
 *
 * Example:
 * <code>
 * $group  = new HTML_QuickForm2_Container_Group()
 * $repeat = $form->addRepeat('related');
 *                ->setPrototype($group);
 * // repeat indexes will be automatically appended to elements in prototype
 * $group->addHidden('related_id');
 * $group->addText('related_title');
 * // this is identical to $group->addCheckbox('related_active');
 * $repeat->addCheckbox('related_active');
 *
 * // value of this field will be used to find the indexes of repeated items
 * $repeat->setIndexField('related_id');
 * </code>
 *
 * @category HTML
 * @package  HTML_QuickForm2
 * @author   Alexey Borzov <avb@php.net>
 * @author   Bertrand Mansion <golgote@mamasam.com>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @version  Release: 2.0.0
 * @link     http://pear.php.net/package/HTML_QuickForm2
 */
class HTML_QuickForm2_Container_Repeat extends HTML_QuickForm2_Container
{
    /**
     * Key to replace by actual item index in elements' names / ids / values
     */
    const INDEX_KEY = ':idx:';

    /**
     * Regular expression used to check for valid indexes
     */
    const INDEX_REGEXP = '/^[a-zA-Z0-9_]+$/';

    /**
     * Field used to search for available indexes
     * @var string
     */
    protected $indexField = null;

    /**
     * Available indexes
     * @var array
     */
    protected $itemIndexes = array();

    /**
     * Errors for (repeated) child elements set during validate() call
     * @var array
     */
    protected $childErrors = array();

    /**
     * Whether getDataSources() should return Container's data sources
     *
     * This is done to prevent useless updateValue() activity in child
     * elements when their values are not going to be needed.
     *
     * @var bool
     */
    protected $passDataSources = false;

    /**
     * Returns the element's type
     *
     * @return   string
     */
    public function getType()
    {
        return 'repeat';
    }

    /**
     * Sets the element's value (not implemented)
     *
     * @param mixed $value element's value
     *
     * @throws HTML_QuickForm2_Exception
     */
    public function setValue($value)
    {
        throw new HTML_QuickForm2_Exception('Not implemented');
    }

    /**
     * Class constructor
     *
     * Repeat element can understand the following keys in $data parameter:
     *   - 'prototype': a Container to be repeated. Passed to {@link setPrototype()}.
     *
     * @param string       $name       Element name
     * @param string|array $attributes Attributes (either a string or an array)
     * @param array        $data       Additional element data
     */
    public function __construct($name = null, $attributes = null, array $data = array())
    {
        if (!empty($data['prototype'])) {
            $this->setPrototype($data['prototype']);
        }
        unset($data['prototype']);
        parent::__construct($name, $attributes, $data);
    }

    /**
     * Sets the Container that will be used as a prototype for repeating
     *
     * @param HTML_QuickForm2_Container $prototype prototype container
     *
     * @return HTML_QuickForm2_Container_Repeat
     */
    public function setPrototype(HTML_QuickForm2_Container $prototype)
    {
        if (!empty($this->elements[0])) {
            parent::removeChild($this->elements[0]);
            $this->elements = array();
        }
        parent::appendChild($prototype);
        return $this;
    }

    /**
     * Returns the prototype Container
     *
     * @return HTML_QuickForm2_Container prototype
     * @throws HTML_QuickForm2_NotFoundException if prototype was not set
     */
    protected function getPrototype()
    {
        if (empty($this->elements[0])) {
            throw new HTML_QuickForm2_NotFoundException(
                "Repeat element needs a prototype, use setPrototype()"
            );
        }
        return $this->elements[0];
    }

    /**
     * Appends an element to the prototype container
     *
     * Elements are kept in the prototype rather than directly in repeat
     *
     * @param HTML_QuickForm2_Node $element Element to add
     *
     * @return   HTML_QuickForm2_Node     Added element
     * @throws   HTML_QuickForm2_InvalidArgumentException
     */
    public function appendChild(HTML_QuickForm2_Node $element)
    {
        return $this->getPrototype()->appendChild($element);
    }

    /**
     * Removes the element from the prototype container
     *
     * Elements are kept in the prototype rather than directly in repeat
     *
     * @param HTML_QuickForm2_Node $element Element to remove
     *
     * @return   HTML_QuickForm2_Node     Removed object
     * @throws   HTML_QuickForm2_NotFoundException
     */
    public function removeChild(HTML_QuickForm2_Node $element)
    {
        return $this->getPrototype()->removeChild($element);
    }


    /**
     * Inserts an element to the prototype container
     *
     * Elements are kept in the prototype rather than directly in repeat
     *
     * @param HTML_QuickForm2_Node $element   Element to insert
     * @param HTML_QuickForm2_Node $reference Reference to insert before
     *
     * @return   HTML_QuickForm2_Node     Inserted element
     */
    public function insertBefore(
        HTML_QuickForm2_Node $element, HTML_QuickForm2_Node $reference = null
    ) {
        return $this->getPrototype()->insertBefore($element, $reference);
    }


    /**
     * Returns the data sources for this element
     *
     * @return array
     * @see $passDataSources
     */
    protected function getDataSources()
    {
        if (!$this->passDataSources) {
            return array();
        } else {
            return parent::getDataSources();
        }
    }

    /**
     * Sets a field to check for available indexes
     *
     * Form data sources will be searched for this field's value, indexes present
     * in the array will be used for repeated elements. Use the field that will be
     * always present in submit data: checkboxes, multiple selects and fields that
     * may be disabled are bad choices
     *
     * @param string $field field name
     *
     * @return HTML_QuickForm2_Container_Repeat
     */
    public function setIndexField($field)
    {
        $this->indexField = $field;
        $this->updateValue();
        return $this;
    }

    /**
     * Tries to guess a field name to use for getting indexes of repeated items
     *
     * @return bool Whether we were able to guess something
     * @see setIndexField()
     */
    private function _guessIndexField()
    {
        $this->appendIndexTemplates();
        $this->passDataSources = false;
        /* @var $child HTML_QuickForm2_Node */
        foreach ($this->getRecursiveIterator(RecursiveIteratorIterator::LEAVES_ONLY) as $child) {
            $name = $child->getName();
            if (false === ($pos = strpos($name, '[' . self::INDEX_KEY . ']'))
                || $child->getAttribute('disabled')
            ) {
                continue;
            }
            // The list is somewhat future-proof for HTML5 input elements
            if ($child instanceof HTML_QuickForm2_Element_Input
                && !($child instanceof HTML_QuickForm2_Element_InputButton
                     || $child instanceof HTML_QuickForm2_Element_InputCheckable
                     || $child instanceof HTML_QuickForm2_Element_InputFile
                     || $child instanceof HTML_QuickForm2_Element_InputImage
                     || $child instanceof HTML_QuickForm2_Element_InputReset
                     || $child instanceof HTML_QuickForm2_Element_InputSubmit)
                || ($child instanceof HTML_QuickForm2_Element_Select
                    && !$child->getAttribute('multiple'))
                || $child instanceof HTML_QuickForm2_Element_Textarea
            ) {
                $this->indexField = substr($name, 0, $pos);
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the indexes for repeated items
     *
     * @return array
     */
    public function getIndexes()
    {
        if (null === $this->indexField && $this->_guessIndexField()) {
            $this->updateValue();
        }
        return $this->itemIndexes;
    }

    /**
     * Sets the indexes for repeated items
     *
     * As is the case with elements' values, the indexes will be updated
     * from data sources, so use this after all possible updates were done.
     *
     * @param array $indexes
     *
     * @return HTML_QuickForm2_Container_Repeat
     */
    public function setIndexes(array $indexes)
    {
        $hash = array();
        foreach ($indexes as $index) {
            if (preg_match(self::INDEX_REGEXP, $index)) {
                $hash[$index] = true;
            }
        }
        $this->itemIndexes = array_keys($hash);
        return $this;
    }

    /**
     * Called when the element needs to update its value from form's data sources
     *
     * Behaves similar to Element::updateValue(), the field's value is used to
     * deduce indexes taken by repeat items.
     *
     * @see setIndexField()
     * @throws HTML_QuickForm2_Exception
     */
    protected function updateValue()
    {
        // check that we are not added to another Repeat
        // done here instead of in setContainer() for reasons outlined in InputFile
        $container = $this->getContainer();
        while (!empty($container)) {
            if ($container instanceof self) {
                throw new HTML_QuickForm2_Exception(
                    "Repeat element cannot be added to another Repeat element"
                );
            }
            $container = $container->getContainer();
        }

        if (null === $this->indexField && !$this->_guessIndexField()) {
            return;
        }
        /* @var HTML_QuickForm2_DataSource $ds */
        foreach (parent::getDataSources() as $ds) {
            if (null !== ($value = $ds->getValue($this->indexField))) {
                $this->setIndexes(array_keys($value));
                return;
            }
        }
    }

    /**
     * Appends the template to elements' names and ids that will be later replaced by index
     *
     * Default behaviour is to append '[:idx:]' to element names and '_:idx:' to
     * element ids. If the string ':idx:' is already present in the attribute,
     * then it will not be changed.
     *
     * Checkboxes and radios may contain ':idx:' in their 'value' attribute,
     * in this case their 'name' attribute is left alone. Names of groups are
     * also not touched.
     */
    protected function appendIndexTemplates()
    {
        $this->passDataSources = true;
        /* @var HTML_QuickForm2_Node $child */
        foreach ($this->getRecursiveIterator() as $child) {
            $id = $child->getId();
            if (false === strpos($id, self::INDEX_KEY)) {
                $child->setId($id . '_' . self::INDEX_KEY);
            }
            $name = $child->getName();
            // checkboxes and radios can have index inside "value" attribute instead,
            // group names should not be touched
            if (strlen($name) && false === strpos($name, self::INDEX_KEY)
                && (!$child instanceof HTML_QuickForm2_Container || !$child->prependsName())
                && (!$child instanceof HTML_QuickForm2_Element_InputCheckable
                    || false === strpos($child->getAttribute('value'), self::INDEX_KEY))
            ) {
                $child->setName($name . '[' . self::INDEX_KEY . ']');
            }
        }
    }

    /**
     * Backs up child attributes
     *
     * @param bool $backupId    whether to backup id attribute
     * @param bool $backupError whether to backup error message
     *
     * @return array backup array
     */
    protected function backupChildAttributes($backupId = false, $backupError = false)
    {
        $this->appendIndexTemplates();
        $backup = array();
        $key    = 0;
        /* @var HTML_QuickForm2_Node $child */
        foreach ($this->getRecursiveIterator() as $child) {
            $backup[$key++] = array(
                'name' => $child->getName(),
            ) + (
                $child instanceof HTML_QuickForm2_Element_InputCheckable
                ? array('valueAttr' => $child->getAttribute('value')) : array()
            ) + (
                $child instanceof HTML_QuickForm2_Container
                ? array() : array('value' => $child->getValue())
            ) + (
                $backupId ? array('id' => $child->getId()) : array()
            ) + (
                $backupError ? array('error' => $child->getError()) : array()
            );
        }
        return $backup;
    }

    /**
     * Restores child attributes from backup array
     *
     * @param array $backup backup array
     *
     * @see backupChildAttributes()
     */
    protected function restoreChildAttributes(array $backup)
    {
        $key = 0;
        /* @var HTML_QuickForm2_Node $child */
        foreach ($this->getRecursiveIterator() as $child) {
            if (array_key_exists('value', $backup[$key])) {
                $child->setValue($backup[$key]['value']);
            }
            if (false !== strpos($backup[$key]['name'], self::INDEX_KEY)) {
                $child->setName($backup[$key]['name']);
            }
            if ($child instanceof HTML_QuickForm2_Element_InputCheckable
                && false !== strpos($backup[$key]['valueAttr'], self::INDEX_KEY)
            ) {
                $child->setAttribute('value', $backup[$key]['valueAttr']);
            }
            if (array_key_exists('id', $backup[$key])) {
                $child->setId($backup[$key]['id']);
            }
            if (array_key_exists('error', $backup[$key])) {
                $child->setError($backup[$key]['error']);
            }
            $key++;
        }
        $this->passDataSources = false;
    }

    /**
     * Replaces a template in elements' attributes by a numeric index
     *
     * @param int   $index  numeric index
     * @param array $backup backup array, contains attributes with templates
     *
     * @see backupChildAttributes()
     */
    protected function replaceIndexTemplates($index, array $backup)
    {
        $this->passDataSources = true;
        $key = 0;
        /* @var HTML_QuickForm2_Node $child */
        foreach ($this->getRecursiveIterator() as $child) {
            if (false !== strpos($backup[$key]['name'], self::INDEX_KEY)) {
                $child->setName(str_replace(self::INDEX_KEY, $index, $backup[$key]['name']));
            }
            if ($child instanceof HTML_QuickForm2_Element_InputCheckable
                && false !== strpos($backup[$key]['valueAttr'], self::INDEX_KEY)
            ) {
                $child->setAttribute(
                    'value', str_replace(self::INDEX_KEY, $index, $backup[$key]['valueAttr'])
                );
            }
            if (array_key_exists('id', $backup[$key])) {
                $child->setId(str_replace(self::INDEX_KEY, $index, $backup[$key]['id']));
            }
            if (array_key_exists('error', $backup[$key])) {
                $child->setError();
            }
            $key++;
        }
    }

    /**
     * Returns the array containing child elements' values
     *
     * Iterates over all available repeat indexes to get values
     *
     * @param bool $filtered Whether child elements should apply filters on values
     *
     * @return   array|null
     */
    protected function getChildValues($filtered = false)
    {
        $backup = $this->backupChildAttributes();
        $values = array();
        foreach ($this->getIndexes() as $index) {
            $this->replaceIndexTemplates($index, $backup);
            $values = self::arrayMerge(
                $values, parent::getChildValues($filtered)
            );
        }
        $this->restoreChildAttributes($backup);
        return empty($values) ? null : $values;
    }

    /**
     * Performs the server-side validation
     *
     * Iterates over all available repeat indexes and calls validate() on
     * prototype container.
     *
     * @return   boolean Whether the repeat and all repeated items are valid
     */
    protected function validate()
    {
        $backup = $this->backupChildAttributes(false, true);
        $valid  = true;
        $this->childErrors = array();
        foreach ($this->getIndexes() as $index) {
            $this->replaceIndexTemplates($index, $backup);
            $valid = $this->getPrototype()->validate() && $valid;
            /* @var HTML_QuickForm2_Node $child */
            foreach ($this->getRecursiveIterator() as $child) {
                if (strlen($error = $child->getError())) {
                    $this->childErrors[spl_object_hash($child)][$index] = $error;
                }
            }
        }
        $this->restoreChildAttributes($backup);
        foreach ($this->rules as $rule) {
            if (strlen($this->error)) {
                break;
            }
            if ($rule[1] & HTML_QuickForm2_Rule::SERVER) {
                $rule[0]->validate();
            }
        }
        return !strlen($this->error) && $valid;
    }

    /**
     * Generates Javascript code to initialize repeat behaviour
     *
     * @param HTML_QuickForm2_Container_Repeat_JavascriptBuilder $evalBuilder
     *        Javascript builder returning JS string literals
     *
     * @return string javascript
     */
    private function _generateInitScript(
        HTML_QuickForm2_Container_Repeat_JavascriptBuilder $evalBuilder
    ) {
        $myId     = HTML_QuickForm2_JavascriptBuilder::encode($this->getId());
        $protoId  = HTML_QuickForm2_JavascriptBuilder::encode($this->getPrototype()->getId());
        $triggers = HTML_QuickForm2_JavascriptBuilder::encode(
            $this->getJavascriptTriggers()
        );
        list ($rules, $scripts) = $evalBuilder->getFormJavascriptAsStrings();

        return "new qf.elements.Repeat(document.getElementById({$myId}), {$protoId}, "
               . "{$triggers},\n{$rules},\n{$scripts}\n);";
    }

    /**
     * Renders the container using the given renderer
     *
     * Container will be output N + 1 times, where N are visible items and 1 is
     * the hidden prototype used by Javascript code to create new items.
     *
     * @param HTML_QuickForm2_Renderer $renderer renderer to use
     *
     * @return   HTML_QuickForm2_Renderer
     */
    public function render(HTML_QuickForm2_Renderer $renderer)
    {
        $backup      = $this->backupChildAttributes(true, true);
        $hiddens     = $renderer->getOption('group_hiddens');
        $jsBuilder   = $renderer->getJavascriptBuilder();
        $evalBuilder = new HTML_QuickForm2_Container_Repeat_JavascriptBuilder();

        $renderer->setJavascriptBuilder($evalBuilder)
            ->setOption('group_hiddens', false)
            ->startContainer($this);

        // first, render a (hidden) prototype
        $this->getPrototype()->addClass('repeatItem repeatPrototype');
        $this->getPrototype()->render($renderer);
        $this->getPrototype()->removeClass('repeatPrototype');

        // restore original JS builder
        $evalBuilder->passLibraries($jsBuilder);
        $renderer->setJavascriptBuilder($jsBuilder);

        // next, render all available rows
        foreach ($this->getIndexes() as $index) {
            $this->replaceIndexTemplates($index, $backup);
            /* @var HTML_QuickForm2_Node $child */
            foreach ($this->getRecursiveIterator() as $child) {
                if (isset($this->childErrors[$hash = spl_object_hash($child)])
                    && isset($this->childErrors[$hash][$index])
                ) {
                    $child->setError($this->childErrors[$hash][$index]);
                }
            }
            $this->getPrototype()->render($renderer);
        }
        $this->restoreChildAttributes($backup);

        // only add javascript if not frozen
        if (!$this->toggleFrozen()) {
            $jsBuilder->addLibrary('repeat', 'quickform-repeat.js');
            $jsBuilder->addElementJavascript($this->_generateInitScript($evalBuilder));
            $this->renderClientRules($jsBuilder);
        }

        $renderer->finishContainer($this);
        $renderer->setOption('group_hiddens', $hiddens);
        return $renderer;
    }
}
?>