<?php
/**
 * Default renderer for HTML_QuickForm2
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2010, 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: Default.php 303432 2010-09-16 20:27:28Z avb $
 * @link       http://pear.php.net/package/HTML_QuickForm2
 */

/**
 * Abstract base class for QuickForm2 renderers
 */
//require_once 'HTML/QuickForm2/Renderer.php';

/**
 * Default renderer for QuickForm2
 *
 * Mostly a direct port of Default renderer from QuickForm 3.x package.
 *
 * While almost everything in this class is defined as public, its properties
 * and those methods that are not published (i.e. not in array returned by
 * exportMethods()) will be available to renderer plugins only.
 *
 * The following methods are published:
 *   - {@link reset()}
 *   - {@link setTemplateForClass()}
 *   - {@link setTemplateForId()}
 *   - {@link setErrorTemplate()}
 *   - {@link setElementTemplateForGroupClass()}
 *   - {@link setElementTemplateForGroupId()}
 *
 * @category   HTML
 * @package    HTML_QuickForm2
 * @author     Alexey Borzov <avb@php.net>
 * @author     Bertrand Mansion <golgote@mamasam.com>
 * @version    Release: 0.5.0
 */
class HTML_QuickForm2_Renderer_Default extends HTML_QuickForm2_Renderer
{
   /**
    * Whether the form contains required elements
    * @var  bool
    */
    public $hasRequired = false;

   /**
    * HTML generated for the form
    * @var  array
    */
    public $html = array(array());

   /**
    * HTML for hidden elements if 'group_hiddens' option is on
    * @var  string
    */
    public $hiddenHtml = '';

   /**
    * Array of validation errors if 'group_errors' option is on
    * @var  array
    */
    public $errors = array();

   /**
    * Default templates for elements of the given class
    * @var  array
    */
    public $templatesForClass = array(
        'html_quickform2_element_inputhidden' => '<div style="display: none;">{element}</div>',
        'html_quickform2' => '<div class="quickform">{errors}<form{attributes}><div>{hidden}{content}</div></form><qf:reqnote><div class="reqnote">{reqnote}</div></qf:reqnote></div>',
        'html_quickform2_container_fieldset' => '<fieldset{attributes}><qf:label><legend id="{id}-legend">{label}</legend></qf:label>{content}</fieldset>',
        'special:error' => array(
            'prefix'    => '<div class="errors"><qf:message><p>{message}</p></qf:message><ul><li>',
            'separator' => '</li><li>',
            'suffix'    => '</li></ul><qf:message><p>{message}</p></qf:message></div>'
        ),
        'html_quickform2_element' => '<div class="row"><label for="{id}" class="element"><qf:required><span class="required">* </span></qf:required>{label}</label><div class="element<qf:error> error</qf:error>"><qf:error><span class="error">{error}<br /></span></qf:error>{element}</div></div>',
        'html_quickform2_container_group' => '<div class="row"><label class="element"><qf:required><span class="required">* </span></qf:required>{label}</label><div class="element group<qf:error> error</qf:error>"><qf:error><span class="error">{error}<br /></span></qf:error>{content}</div></div>'
    );

   /**
    * Custom templates for elements with the given IDs
    * @var  array
    */
    public $templatesForId = array();

   /**
    * Default templates for elements in groups of the given classes
    *
    * Array has the form ('group class' => ('element class' => 'template', ...), ...)
    *
    * @var  array
    */
    public $elementTemplatesForGroupClass = array(
        'html_quickform2_container' => array(
            'html_quickform2_element' => '{element}',
            'html_quickform2_container_fieldset' => '<fieldset{attributes}><qf:label><legend id="{id}-legend">{label}</legend></qf:label>{content}</fieldset>'
        )
    );

   /**
    * Custom templates for grouped elements in the given group IDs
    *
    * Array has the form ('group id' => ('element class' => 'template', ...), ...)
    *
    * @var  array
    */
    public $elementTemplatesForGroupId = array();

   /**
    * Array containing IDs of the groups being rendered
    * @var  array
    */
    public $groupId = array();

    protected function exportMethods()
    {
        return array(
            'reset',
            'setTemplateForClass',
            'setTemplateForId',
            'setErrorTemplate',
            'setGroupedTemplateForClass',
            'setElementTemplateForGroupClass',
            'setElementTemplateForGroupId'
        );
    }

   /**
    * Sets template for form elements that are instances of the given class
    *
    * When searching for a template to use, renderer will check for templates
    * set for element's class and its parent classes, until found. Thus a more
    * specific template will override a more generic one.
    *
    * @param    string  Class name
    * @param    mixed   Template to use for elements of that class
    * @return   HTML_QuickForm2_Renderer_Default
    */
    public function setTemplateForClass($className, $template)
    {
        $this->templatesForClass[strtolower($className)] = $template;
        return $this;
    }

   /**
    * Sets template for form element with the given id
    *
    * If a template is set for an element via this method, it will be used.
    * In the other case a generic template set by {@link setTemplateForClass()}
    * or {@link setGroupedTemplateForClass()} will be used.
    *
    * @param    string  Element's id
    * @param    mixed   Template to use for rendering of that element
    * @return   HTML_QuickForm2_Renderer_Default
    */
    public function setTemplateForId($id, $template)
    {
        $this->templatesForId[$id] = $template;
        return $this;
    }

   /**
    * Sets template for rendering validation errors
    *
    * This template will be used if 'group_errors' option is set to true.
    * The template array should contain 'prefix', 'suffix' and 'separator'
    * keys.
    *
    * @param    array   Template for validation errors
    * @return   HTML_QuickForm2_Renderer_Default
    */
    public function setErrorTemplate(array $template)
    {
        return $this->setTemplateForClass('special:error', $template);
    }

   /**
    * Sets grouped elements templates using group class
    *
    * Templates set via {@link setTemplateForClass()} will not be used for
    * grouped form elements. When searching for a template to use, the renderer
    * will first consider template set for a specific group id, then the
    * group templates set by group class.
    *
    * @param    string  Group class name
    * @param    string  Element class name
    * @param    mixed   Template
    * @return   HTML_QuickForm2_Renderer_Default
    */
    public function setElementTemplateForGroupClass($groupClass, $elementClass, $template)
    {
        $this->elementTemplatesForGroupClass[strtolower($groupClass)][strtolower($elementClass)] = $template;
        return $this;
    }

   /**
    * Sets grouped elements templates using group id
    *
    * Templates set via {@link setTemplateForClass()} will not be used for
    * grouped form elements. When searching for a template to use, the renderer
    * will first consider template set for a specific group id, then the
    * group templates set by group class.
    *
    * @param    string  Group id
    * @param    string  Element class name
    * @param    mixed   Template
    * @return   HTML_QuickForm2_Renderer_Default
    */
    public function setElementTemplateForGroupId($groupId, $elementClass, $template)
    {
        $this->elementTemplatesForGroupId[$groupId][strtolower($elementClass)] = $template;
        return $this;
    }

   /**
    * Resets the accumulated data
    *
    * This method is called automatically by startForm() method, but should
    * be called manually before calling other rendering methods separately.
    *
    * @return HTML_QuickForm2_Renderer_Default
    */
    public function reset()
    {
        $this->html        = array(array());
        $this->hiddenHtml  = '';
        $this->errors      = array();
        $this->hasRequired = false;
        $this->groupId     = array();

        return $this;
    }

   /**
    * Returns generated HTML
    *
    * @return string
    */
    public function __toString()
    {
        return (isset($this->html[0][0])? $this->html[0][0]: '') .
               $this->hiddenHtml;
    }

   /**
    * Renders a generic element
    *
    * @param    HTML_QuickForm2_Node    Element being rendered
    */
    public function renderElement(HTML_QuickForm2_Node $element)
    {
        $elTpl = $this->prepareTemplate($this->findTemplate($element), $element);
        $this->html[count($this->html) - 1][] = str_replace(array('{element}', '{id}'),
                                                            array($element, $element->getId()), $elTpl);
    }

   /**
    * Renders a hidden element
    *
    * @param    HTML_QuickForm2_Node    Hidden element being rendered
    */
    public function renderHidden(HTML_QuickForm2_Node $element)
    {
        if ($this->options['group_hiddens']) {
            $this->hiddenHtml .= $element->__toString();
        } else {
            $this->html[count($this->html) - 1][] = str_replace('{element}', $element,
                                                                $this->findTemplate($element));
        }
    }

   /**
    * Starts rendering a generic container, called before processing contained elements
    *
    * @param    HTML_QuickForm2_Node    Container being rendered
    */
    public function startContainer(HTML_QuickForm2_Node $container)
    {
        $this->html[]    = array();
        $this->groupId[] = false;
    }

   /**
    * Finishes rendering a generic container, called after processing contained elements
    *
    * @param    HTML_QuickForm2_Node    Container being rendered
    */
    public function finishContainer(HTML_QuickForm2_Node $container)
    {
        array_pop($this->groupId);

        $cTpl  = str_replace(
            array('{attributes}', '{id}'),
            array($container->getAttributes(true), $container->getId()),
            $this->prepareTemplate($this->findTemplate($container, '{content}'), $container)
        );
        $cHtml  = array_pop($this->html);
        $break  = HTML_Common2::getOption('linebreak');
        $indent = str_repeat(HTML_Common2::getOption('indent'), count($this->html));
        $this->html[count($this->html) - 1][] = str_replace(
            '{content}', $break . $indent . implode($break . $indent, $cHtml), $cTpl
        );
    }

   /**
    * Starts rendering a group, called before processing grouped elements
    *
    * @param    HTML_QuickForm2_Node    Group being rendered
    */
    public function startGroup(HTML_QuickForm2_Node $group)
    {
        $this->html[]    = array();
        $this->groupId[] = $group->getId();
    }

   /**
    * Finishes rendering a group, called after processing grouped elements
    *
    * @param    HTML_QuickForm2_Node    Group being rendered
    */
    public function finishGroup(HTML_QuickForm2_Node $group)
    {
        $gTpl = str_replace(
            array('{attributes}', '{id}'),
            array($group->getAttributes(true), array_pop($this->groupId)),
            $this->prepareTemplate($this->findTemplate($group, '{content}'), $group)
        );

        $separator = $group->getSeparator();
        $elements  = array_pop($this->html);
        if (!is_array($separator)) {
            $content = implode((string)$separator, $elements);
        } else {
            $content    = '';
            $cSeparator = count($separator);
            for ($i = 0, $count = count($elements); $i < $count; $i++) {
                $content .= (0 == $i? '': $separator[($i - 1) % $cSeparator]) .
                            $elements[$i];
            }
        }

        $this->html[count($this->html) - 1][] = str_replace('{content}', $content, $gTpl);
    }

   /**
    * Starts rendering a form, called before processing contained elements
    *
    * @param    HTML_QuickForm2_Node    Form being rendered
    */
    public function startForm(HTML_QuickForm2_Node $form)
    {
        $this->reset();
    }

   /**
    * Finishes rendering a form, called after processing contained elements
    *
    * @param    HTML_QuickForm2_Node    Form being rendered
    */
    public function finishForm(HTML_QuickForm2_Node $form)
    {
        $formTpl = str_replace(
            array('{attributes}', '{hidden}', '{errors}'),
            array($form->getAttributes(true), $this->hiddenHtml,
                  $this->outputGroupedErrors()),
            $this->findTemplate($form, '{content}')
        );
        $this->hiddenHtml = '';

        // required note
        if (!$this->hasRequired || $form->toggleFrozen() ||
            empty($this->options['required_note']))
        {
            $formTpl = preg_replace('!<qf:reqnote>.*</qf:reqnote>!isU', '', $formTpl);
        } else {
            $formTpl = str_replace(
                array('<qf:reqnote>', '</qf:reqnote>', '{reqnote}'),
                array('', '', $this->options['required_note']),
                $formTpl
            );
        }

        $break         = HTML_Common2::getOption('linebreak');
        $script        = $this->getJavascriptBuilder()->getFormJavascript($form->getId());
        $this->html[0] = array(
            str_replace('{content}', $break . implode($break, $this->html[0]), $formTpl) .
            (empty($script)? '': $break . $script)
        );
    }

   /**
    * Creates a error list if 'group_errors' option is true
    *
    * @return   string  HTML with a list of all validation errors
    */
    public function outputGroupedErrors()
    {
        if (empty($this->errors)) {
            return '';
        }
        if (!empty($this->options['errors_prefix'])) {
            $errorHtml = str_replace(array('<qf:message>', '</qf:message>', '{message}'),
                                     array('', '', $this->options['errors_prefix']),
                                     $this->templatesForClass['special:error']['prefix']);
        } else {
            $errorHtml = preg_replace('!<qf:message>.*</qf:message>!isU', '',
                                      $this->templatesForClass['special:error']['prefix']);
        }
        $errorHtml .= implode($this->templatesForClass['special:error']['separator'], $this->errors);
        if (!empty($this->options['errors_suffix'])) {
            $errorHtml .= str_replace(array('<qf:message>', '</qf:message>', '{message}'),
                                      array('', '', $this->options['errors_suffix']),
                                      $this->templatesForClass['special:error']['suffix']);
        } else {
            $errorHtml .= preg_replace('!<qf:message>.*</qf:message>!isU', '',
                                      $this->templatesForClass['special:error']['suffix']);
        }
        return $errorHtml;
    }

   /**
    * Finds a proper template for the element
    *
    * Templates are scanned in a predefined order. First, if a template was
    * set for a specific element by id, it is returned, no matter if the
    * element belongs to a group. If the element does not belong to a group,
    * we try to match a template using the element class.
    * But, if the element belongs to a group, templates are first looked up
    * using the containing group id, then using the containing group class.
    * When no template is found, the provided default template is returned.
    *
    * @param    HTML_QuickForm2_Node    Element being rendered
    * @param    string                  Default template to use if not found
    * @return   string  Template
    */
    public function findTemplate(HTML_QuickForm2_Node $element, $default = '{element}')
    {
        if (!empty($this->templatesForId[$element->getId()])) {
            return $this->templatesForId[$element->getId()];
        }
        $class          = strtolower(get_class($element));
        $groupId        = end($this->groupId);
        $elementClasses = array();
        do {
            if (empty($groupId) && !empty($this->templatesForClass[$class])) {
                return $this->templatesForClass[$class];
            }
            $elementClasses[$class] = true;
        } while ($class = strtolower(get_parent_class($class)));

        if (!empty($groupId)) {
            if (!empty($this->elementTemplatesForGroupId[$groupId])) {
                while (list($elClass) = each($elementClasses)) {
                    if (!empty($this->elementTemplatesForGroupId[$groupId][$elClass])) {
                        return $this->elementTemplatesForGroupId[$groupId][$elClass];
                    }
                }
            }

            $group = $element->getContainer();
            $grClass = strtolower(get_class($group));
            do {
                if (!empty($this->elementTemplatesForGroupClass[$grClass])) {
                    reset($elementClasses);
                    while (list($elClass) = each($elementClasses)) {
                        if (!empty($this->elementTemplatesForGroupClass[$grClass][$elClass])) {
                            return $this->elementTemplatesForGroupClass[$grClass][$elClass];
                        }
                    }
                }
            } while ($grClass = strtolower(get_parent_class($grClass)));
        }
        return $default;
    }

   /**
    * Processes the element's template, adding label(s), required note and error message
    *
    * @param    string                  Element template
    * @param    HTML_QuickForm2_Node    Element being rendered
    * @return   string  Template with some substitutions done
    */
    public function prepareTemplate($elTpl, HTML_QuickForm2_Node $element)
    {
        // if element is required
        $elTpl = $this->markRequired($elTpl, $element->isRequired());
        $elTpl = $this->outputError($elTpl, $element->getError());
        return $this->outputLabel($elTpl, $element->getLabel());
    }

   /**
    * Marks element required or removes "required" block
    *
    * @param    string  Element template
    * @param    bool    Whether element is required
    * @return   string  Template with processed "required" block
    */
    public function markRequired($elTpl, $required)
    {
        if ($required) {
            $this->hasRequired = true;
            $elTpl = str_replace(array('<qf:required>', '</qf:required>'),
                                  array('', ''), $elTpl);
        } else {
            $elTpl = preg_replace('!<qf:required>.*</qf:required>!isU', '', $elTpl);
        }
        return $elTpl;
    }

   /**
    * Outputs element error, removes empty error blocks
    *
    * @param    string  Element template
    * @param    string  Validation error for the element
    * @return   string  Template with error substitutions done
    */
    public function outputError($elTpl, $error)
    {
        if ($error && !$this->options['group_errors']) {
            $elTpl = str_replace(array('<qf:error>', '</qf:error>', '{error}'),
                                  array('', '', $error), $elTpl);
        } else {
            if ($error && $this->options['group_errors']) {
                $this->errors[] = $error;
            }
            $elTpl = preg_replace('!<qf:error>.*</qf:error>!isU', '', $elTpl);
        }
        return $elTpl;
    }

   /**
    * Outputs element's label(s), removes empty label blocks
    *
    * @param    string  Element template
    * @param    mixed   Element label(s)
    * @return   string  Template with label substitutions done
    */
    public function outputLabel($elTpl, $label)
    {
        $mainLabel = is_array($label)? array_shift($label): $label;
        $elTpl     = str_replace('{label}', $mainLabel, $elTpl);
        if (false !== strpos($elTpl, '<qf:label>')) {
            if ($mainLabel) {
                $elTpl = str_replace(array('<qf:label>', '</qf:label>'), array('', ''), $elTpl);
            } else {
                $elTpl = preg_replace('!<qf:label>.*</qf:label>!isU', '', $elTpl);
            }
        }
        if (is_array($label)) {
            foreach($label as $key => $text) {
                $key   = is_int($key)? $key + 2: $key;
                $elTpl = str_replace(array('<qf:label_' . $key . '>', '</qf:label_' . $key . '>', '{label_' . $key . '}'),
                                     array('', '', $text), $elTpl);
            }
        }
        if (strpos($elTpl, '{label_')) {
            $elTpl = preg_replace('!<qf:label_([^>]+)>.*</qf:label_\1>!isU', '', $elTpl);
        }
        return $elTpl;
    }
}
?>