<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Markup
 * @subpackage Renderer
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */

/**
 * @see Zend_config
 */
require_once 'Zend/Config.php';
/**
 * @see Zend_Filter
 */
require_once 'Zend/Filter.php';
/**
 * @see Zend_Markup_Renderer_TokenConverterInterface
 */
require_once 'Zend/Markup/Renderer/TokenConverterInterface.php';

/**
 * Defines the basic rendering functionality
 *
 * @category   Zend
 * @package    Zend_Markup
 * @subpackage Renderer
 * @copyright  Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Markup_Renderer_RendererAbstract
{
    const TYPE_CALLBACK = 4;
    const TYPE_REPLACE  = 8;
    const TYPE_ALIAS    = 16;

    /**
     * Tag info
     *
     * @var array
     */
    protected $_markups = array();

    /**
     * Parser
     *
     * @var Zend_Markup_Parser_ParserInterface
     */
    protected $_parser;

    /**
     * What filter to use
     *
     * @var bool
     */
    protected $_filter;

    /**
     * Filter chain
     *
     * @var Zend_Filter
     */
    protected $_defaultFilter;

    /**
     * The current group
     *
     * @var string
     */
    protected $_group;

    /**
     * Groups definition
     *
     * @var array
     */
    protected $_groups = array();

    /**
     * Plugin loader for tags
     *
     * @var Zend_Loader_PluginLoader
     */
    protected $_pluginLoader;

    /**
     * The current token
     *
     * @var Zend_Markup_Token
     */
    protected $_token;

    /**
     * Encoding
     *
     * @var string
     */
    protected static $_encoding = 'UTF-8';


    /**
     * Constructor
     *
     * @param array|Zend_Config $options
     *
     * @return void
     */
    public function __construct($options = array())
    {
        if ($options instanceof Zend_Config) {
            $options = $options->toArray();
        }

        if (isset($options['encoding'])) {
            $this->setEncoding($options['encoding']);
        }
        if (isset($options['parser'])) {
            $this->setParser($options['parser']);
        }
        if (!isset($options['useDefaultFilters']) || ($options['useDefaultFilters'] === true)) {
            $this->addDefaultFilters();
        }
        if (isset($options['defaultFilter'])) {
            $this->addDefaultFilter($options['defaultFilter']);
        }
    }

    /**
     * Set the parser
     *
     * @param  Zend_Markup_Parser_ParserInterface $parser
     * @return Zend_Markup_Renderer_RendererAbstract
     */
    public function setParser(Zend_Markup_Parser_ParserInterface $parser)
    {
        $this->_parser = $parser;
        return $this;
    }

    /**
     * Get the parser
     *
     * @return Zend_Markup_Parser_ParserInterface
     */
    public function getParser()
    {
        return $this->_parser;
    }

    /**
     * Get the plugin loader
     *
     * @return Zend_Loader_PluginLoader
     */
    public function getPluginLoader()
    {
        return $this->_pluginLoader;
    }

    /**
     * Set the renderer's encoding
     *
     * @param string $encoding
     *
     * @return void
     */
    public static function setEncoding($encoding)
    {
        self::$_encoding = $encoding;
    }

    /**
     * Get the renderer's encoding
     *
     * @return string
     */
    public static function getEncoding()
    {
        return self::$_encoding;
    }

    /**
     * Add a new markup
     *
     * @param string $name
     * @param string $type
     * @param array $options
     *
     * @return Zend_Markup_Renderer_RendererAbstract
     */
    public function addMarkup($name, $type, array $options)
    {
        if (!isset($options['group']) && ($type ^ self::TYPE_ALIAS)) {
            require_once 'Zend/Markup/Renderer/Exception.php';
            throw new Zend_Markup_Renderer_Exception("There is no render group defined.");
        }

        // add the filter
        if (isset($options['filter'])) {
            if ($options['filter'] instanceof Zend_Filter_Interface) {
                $filter = $options['filter'];
            } elseif ($options['filter'] === true) {
                $filter = $this->getDefaultFilter();
            } else {
                $filter = false;
            }
        } else {
            $filter = $this->getDefaultFilter();
        }

        // check the type
        if ($type & self::TYPE_CALLBACK) {
            // add a callback tag
            if (isset($options['callback'])) {
                if (!($options['callback'] instanceof Zend_Markup_Renderer_TokenConverterInterface)) {
                    require_once 'Zend/Markup/Renderer/Exception.php';
                    throw new Zend_Markup_Renderer_Exception("Not a valid tag callback.");
                }
                if (method_exists($options['callback'], 'setRenderer')) {
                    $options['callback']->setRenderer($this);
                }
            } else {
                $options['callback'] = null;
            }

            $options['type'] = $type;
            $options['filter'] = $filter;

            $this->_markups[$name] = $options;
        } elseif ($type & self::TYPE_ALIAS) {
            // add an alias
            if (empty($options['name'])) {
                require_once 'Zend/Markup/Renderer/Exception.php';
                throw new Zend_Markup_Renderer_Exception(
                        'No alias was provided but tag was defined as such');
            }

            $this->_markups[$name] = array(
                'type' => self::TYPE_ALIAS,
                'name' => $options['name']
            );
        } else {
            if ($type && array_key_exists('empty', $options) && $options['empty']) {
                // add a single replace markup
                $options['type']   = $type;
                $options['filter'] = $filter;

                $this->_markups[$name] = $options;
            } else {
                // add a replace markup
                $options['type']   = $type;
                $options['filter'] = $filter;

                $this->_markups[$name] = $options;
            }
        }
        return $this;
    }

    /**
     * Remove a markup
     *
     * @param string $name
     *
     * @return void
     */
    public function removeMarkup($name)
    {
        unset($this->_markups[$name]);
    }

    /**
     * Remove the default tags
     *
     * @return void
     */
    public function clearMarkups()
    {
        $this->_markups = array();
    }

    /**
     * Render function
     *
     * @param  Zend_Markup_TokenList|string $tokenList
     * @return string
     */
    public function render($value)
    {
        if ($value instanceof Zend_Markup_TokenList) {
            $tokenList = $value;
        } else {
            $tokenList = $this->getParser()->parse($value);
        }

        $root = $tokenList->current();

        $this->_filter = $this->getDefaultFilter();

        return $this->_render($root);
    }

    /**
     * Render a single token
     *
     * @param  Zend_Markup_Token $token
     * @return string
     */
    protected function _render(Zend_Markup_Token $token)
    {
        $return    = '';

        $this->_token = $token;

        // if this tag has children, execute them
        if ($token->hasChildren()) {
            foreach ($token->getChildren() as $child) {
                $return .= $this->_execute($child);
            }
        }

        return $return;
    }

    /**
     * Get the group of a token
     *
     * @param  Zend_Markup_Token $token
     * @return string|bool
     */
    protected function _getGroup(Zend_Markup_Token $token)
    {
        if (!isset($this->_markups[$token->getName()])) {
            return false;
        }

        $tag = $this->_markups[$token->getName()];

        // alias processing
        while ($tag['type'] & self::TYPE_ALIAS) {
            $tag = $this->_markups[$tag['name']];
        }

        return isset($tag['group']) ? $tag['group'] : false;
    }

    /**
     * Execute the token
     *
     * @param  Zend_Markup_Token $token
     * @return string
     */
    protected function _execute(Zend_Markup_Token $token)
    {
        // first return the normal text tags
        if ($token->getType() == Zend_Markup_Token::TYPE_NONE) {
            return $this->_filter($token->getTag());
        }

        // if the token doesn't have a notation, return the plain text
        if (!isset($this->_markups[$token->getName()])) {
            $oldToken  = $this->_token;
            $return = $this->_filter($token->getTag()) . $this->_render($token) . $token->getStopper();
            $this->_token = $oldToken;
            return $return;
        }

        $name   = $this->_getMarkupName($token);
        $markup = (!$name) ? false : $this->_markups[$name];
        $empty  = (is_array($markup) && array_key_exists('empty', $markup) && $markup['empty']);

        // check if the tag has content
        if (!$empty && !$token->hasChildren()) {
            return '';
        }

        // check for the context
        if (is_array($markup) && !in_array($markup['group'], $this->_groups[$this->_group])) {
            $oldToken = $this->_token;
            $return   = $this->_filter($token->getTag()) . $this->_render($token) . $token->getStopper();
            $this->_token = $oldToken;
            return $return;
        }

        // check for the filter
        if (!isset($markup['filter'])
            || (!($markup['filter'] instanceof Zend_Filter_Interface) && ($markup['filter'] !== false))) {
            $this->_markups[$name]['filter'] = $this->getDefaultFilter();
        }

        // save old values to reset them after the work is done
        $oldFilter = $this->_filter;
        $oldGroup  = $this->_group;

        $return = '';

        // set the filter and the group
        $this->_filter = $this->getFilter($name);

        if ($group = $this->_getGroup($token)) {
            $this->_group = $group;
        }

        // callback
        if (is_array($markup) && ($markup['type'] & self::TYPE_CALLBACK)) {
            // load the callback if the tag doesn't exist
            if (!($markup['callback'] instanceof Zend_Markup_Renderer_TokenConverterInterface)) {
                $class = $this->getPluginLoader()->load($name);

                $markup['callback'] = new $class;

                if (!($markup['callback'] instanceof Zend_Markup_Renderer_TokenConverterInterface)) {
                    require_once 'Zend/Markup/Renderer/Exception.php';
                    throw new Zend_Markup_Renderer_Exception("Callback for tag '$name' found, but it isn't valid.");
                }

                if (method_exists($markup['callback'], 'setRenderer')) {
                    $markup['callback']->setRenderer($this);
                }
            }
            if ($markup['type'] && !$empty) {
                $return = $markup['callback']->convert($token, $this->_render($token));
            } else {
                $return = $markup['callback']->convert($token, null);
            }
        } else {
            // replace
            if ($markup['type'] && !$empty) {
                $return = $this->_executeReplace($token, $markup);
            } else {
                $return = $this->_executeSingleReplace($token, $markup);
            }
        }

        // reset to the old values
        $this->_filter = $oldFilter;
        $this->_group  = $oldGroup;

        return $return;
    }

    /**
     * Filter method
     *
     * @param string $value
     *
     * @return string
     */
    protected function _filter($value)
    {
        if ($this->_filter instanceof Zend_Filter_Interface) {
            return $this->_filter->filter($value);
        }
        return $value;
    }

    /**
     * Get the markup name
     *
     * @param Zend_Markup_Token
     *
     * @return string
     */
    protected function _getMarkupName(Zend_Markup_Token $token)
    {
        $name = $token->getName();
        if (empty($name)) {
            return false;
        }

        return $this->_resolveMarkupName($name);
    }

    /**
     * Resolve aliases for a markup name
     *
     * @param string $name
     *
     * @return string
     */
    protected function _resolveMarkupName($name)
    {
        while (($type = $this->_getMarkupType($name))
               && ($type & self::TYPE_ALIAS)
        ) {
            $name = $this->_markups[$name]['name'];
        }

        return $name;
    }

    /**
     * Retrieve markup type
     *
     * @param  string $name
     * @return false|int
     */
    protected function _getMarkupType($name)
    {
        if (!isset($this->_markups[$name])) {
            return false;
        }
        if (!isset($this->_markups[$name]['type'])) {
            return false;
        }
        return $this->_markups[$name]['type'];
    }

    /**
     * Execute a replace token
     *
     * @param  Zend_Markup_Token $token
     * @param  array $tag
     * @return string
     */
    protected function _executeReplace(Zend_Markup_Token $token, $tag)
    {
        return $tag['start'] . $this->_render($token) . $tag['end'];
    }

    /**
     * Execute a single replace token
     *
     * @param  Zend_Markup_Token $token
     * @param  array $tag
     * @return string
     */
    protected function _executeSingleReplace(Zend_Markup_Token $token, $tag)
    {
        return $tag['replace'];
    }

    /**
     * Get the default filter
     *
     * @return void
     */
    public function getDefaultFilter()
    {
        if (null === $this->_defaultFilter) {
            $this->addDefaultFilters();
        }

        return $this->_defaultFilter;
    }

    /**
     * Add a default filter
     *
     * @param string $filter
     *
     * @return void
     */
    public function addDefaultFilter(Zend_Filter_Interface $filter, $placement = Zend_Filter::CHAIN_APPEND)
    {
        if (!($this->_defaultFilter instanceof Zend_Filter)) {
            $defaultFilter = new Zend_Filter();
            $defaultFilter->addFilter($filter);
            $this->_defaultFilter = $defaultFilter;
        }

        $this->_defaultFilter->addFilter($filter, $placement);
    }

    /**
     * Set the default filter
     *
     * @param Zend_Filter_Interface $filter
     *
     * @return void
     */
    public function setDefaultFilter(Zend_Filter_Interface $filter)
    {
        $this->_defaultFilter = $filter;
    }

    /**
     * Get the filter for an existing markup
     *
     * @param string $markup
     *
     * @return Zend_Filter_Interface
     */
    public function getFilter($markup)
    {
        $markup = $this->_resolveMarkupName($markup);

        if (!isset($this->_markups[$markup]['filter'])
            || !($this->_markups[$markup]['filter'] instanceof Zend_Filter_Interface)
        ) {
            if (isset($this->_markups[$markup]['filter']) && $this->_markups[$markup]['filter']) {
                $this->_markups[$markup]['filter'] = $this->getDefaultFilter();
            } else {
                return false;
            }
        }

        return $this->_markups[$markup]['filter'];
    }

    /**
     * Add a filter for an existing markup
     *
     * @param Zend_Filter_Interface $filter
     * @param string $markup
     * @param string $placement
     *
     * @return Zend_Markup_Renderer_RendererAbstract
     */
    public function addFilter(Zend_Filter_Interface $filter, $markup, $placement = Zend_Filter::CHAIN_APPEND)
    {
        $markup = $this->_resolveMarkupName($markup);

        $oldFilter = $this->getFilter($markup);

        // if this filter is the default filter, clone it first
        if ($oldFilter === $this->getDefaultFilter()) {
            $oldFilter = clone $oldFilter;
        }

        if (!($oldFilter instanceof Zend_Filter)) {
            $this->_markups[$markup]['filter'] = new Zend_Filter();

            if ($oldFilter instanceof Zend_Filter_Interface) {
                $this->_markups[$markup]['filter']->addFilter($oldFilter);
            }
        } else {
            $this->_markups[$markup]['filter'] = $oldFilter;
        }

        $this->_markups[$markup]['filter']->addFilter($filter, $placement);

        return $this;
    }

    /**
     * Set the filter for an existing
     *
     * @param Zend_Filter_Interface $filter
     * @param string $markup
     *
     * @return Zend_Markup_Renderer_RendererAbstract
     */
    public function setFilter(Zend_Filter_Interface $filter, $markup)
    {
        $markup = $this->_resolveMarkupName($markup);

        $this->_markups[$markup]['filter'] = $filter;

        return $this;
    }

    /**
     * Add a render group
     *
     * @param string $name
     * @param array $allowedInside
     * @param array $allowsInside
     *
     * @return void
     */
    public function addGroup($name, array $allowedInside = array(), array $allowsInside = array())
    {
        $this->_groups[$name] = $allowsInside;

        foreach ($allowedInside as $group) {
            $this->_groups[$group][] = $name;
        }
    }

    /**
     * Get group definitions
     *
     * @return array
     */
    public function getGroups()
    {
        return $this->_groups;
    }

    /**
     * Set the default filters
     *
     * @return void
     */
    abstract public function addDefaultFilters();

}