<?php
/**
 * Project:
 * Contenido Content Management System
 *
 * Description:
 * Contenido Extension Chainer (CEC).
 * See "docs/techref/plugins/Contenido Extension Chainer.pdf" for more details about CEC.
 *
 * Requirements:
 * @con_php_req 5.0
 *
 *
 * @package     Contenido Backend classes
 * @subpackage  CEC
 * @version     1.2.0
 * @author      Timo A. Hummel
 * @author      Murat Purc <murat@purc.de>
 * @copyright   four for business AG <www.4fb.de>
 * @license     http://www.contenido.org/license/LIZENZ.txt
 * @link        http://www.4fb.de
 * @link        http://www.contenido.org
 * @since       file available since contenido release <= 4.6
 *
 * {@internal
 *   created unknown
 *   modified 2008-06-30, Dominik Ziegler, add security fix
 *   modified 2008-08-28, Murat Purc, add singleton pattern feature
 *   modified 2009-12-30, Murat Purc, redesign of cApiCECRegistry and pApiCECChainItem bearing in mind of
 *                        downwards compatibility and documenting the code, see [#CON-291], also regards to [#CON-256]
 *
 *   $Id$:
 * }}
 *
 */

if(!defined('CON_FRAMEWORK')) {
    die('Illegal call');
}


/**
 * CEC registry class. Used to register chains and chain functions to invoke.
 *
 * Following 3 types of CEC functions/callbacks are supported at the moment:
 * - Callbacks, which should only be invoked. They don't return a value and have no
 *   break conditions, @see CEC_Hook::execute()
 * - Callbacks, which should return a value and/or should modify a passed parameter,
 *   @see CEC_Hook::executeAndReturn()
 * - Callbacks, which should be processed untill a defined break condition achieves,
 *   @see CEC_Hook::executeWhileBreakCondition()
 *
 * @author      Timo A. Hummel
 * @author      Murat Purc <murat@purc.de>
 * @package     Contenido Backend classes
 * @subpackage  CEC
 */
class cApiCECRegistry
{
    /**
     * List of available chains
     * @var  array
     */
    private $_aChains;

    /**
     * Self instance
     * @var  cApiCECRegistry
     */
    private static $_instance = null;


    /**
     * Constructor
     *
     * @return  void
     */
    protected function __construct()
    {
        $this->_aChains = array();
    }


    /**
     * Prevent cloning
     *
     * @return  void
     */
    private function __clone()
    {
        // donut
    }


    /**
     * Returns a instance of cApiCECRegistry
     *
     * @return  cApiCECRegistry
     */
    public static function getInstance()
    {
        if (self::$_instance == null) {
            self::$_instance = new cApiCECRegistry();
        }
        return self::$_instance;
    }


    /**
     * Registers a chain (adds the chain to the internal chain holder)
     *
     * @param   string  $sChainName
     * @param   mixed   First chain parameter
     * @param   mixed   Second chain parameter
     * @param   mixed   Third chain parameter...
     *                  NOTE: The number of parameter is not restricted, you can pass parameter as
     *                        much as you want.
     * @return  void
     */
    public function registerChain($sChainName)
    {
        $aParam = array();
        $iNumArgs = func_num_args();

        for ($iCount = 0; $iCount < $iNumArgs; $iCount++) {
            $aParam[$iCount] = func_get_arg($iCount);
        }

        $this->_addChain($sChainName, $aParam);
    }


    /**
     * Unregisters a chain
     *
     * @param   string  $sChainName
     * @return  void
     */
    public function unregisterChain($sChainName)
    {
        // Check if the chain exists
        if (!$this->isChainRegistered($sChainName)) {
            cWarning(__FILE__, __LINE__, "Chain " . $sChainName . " doesn't exist.");
            return false;
        }

        $functions = array();
        $this->_resetIterator($sChainName);
        $chainFunctions = $this->_aChains[$sChainName]['functions'];
        foreach ($chainFunctions as $pos => $item) {
            $functions[] = $item->getFunctionName();
        }

        foreach ($functions as $p => $func) {
            $this->removeChainFunction($sChainName, $func);
        }

        unset($this->_aChains[$sChainName]);
    }


    /**
     * Checks if a chain is registered or not.
     *
     * @param   string  $sChainName
     * @return  bool
     */
    public function isChainRegistered($sChainName)
    {
        return (isset($this->_aChains[$sChainName]));
    }


    /**
     * Returns list of registered chain names
     *
     * @return  array
     */
    public function getRegisteredChainNames()
    {
        return array_keys($this->_aChains);
    }


    /**
     * Adds the chain to the internal chain holder
     *
     * @param   string  $sChainName   Chain name
     * @param   array   $aParameters  Chain parameter
     * @return  void
     */
    protected function _addChain($sChainName, array $aParameters = array())
    {
        $this->_aChains[$sChainName]['parameters'] = $aParameters;
        $this->_aChains[$sChainName]['functions']  = array();
    }


    /**
     * Adds a chain function which is to invoke.
     *
     * @param   string  $sChainName     Chain name
     * @param   string  $sFunctionName  Name of function/callback to invoke. Feasible values are:
     *                                  - "ClassName->methodName" to invoke a method of a ClassName instance.
     *                                    A instance of the clas will be created here.
     *                                  - "ClassName::methodName" to invoke a static method of ClassName.
     *                                  - "FunctionName" to invoke a function.
     *                                  NOTE: Necessary files must be manually included before or by defined autoloader.
     * @return  bool    True on success, otherwhise false
     */
    public function addChainFunction($sChainName, $sFunctionName)
    {
        // Check if the chain exists
        if (!$this->isChainRegistered($sChainName)) {
            cWarning(__FILE__, __LINE__, "Chain " . $sChainName . " doesn't exist.");
            return false;
        }

        if (strpos($sFunctionName, '->') > 0) {
            // chain function is a method of a object instance
            list($class, $method) = explode('->', $sFunctionName);
            if (!class_exists($class)) {
                cWarning(__FILE__, __LINE__, "Class " . $class . " doesn't exist, can't add " . $sFunctionName . " to chain " . $sChainName);
                return false;
            } elseif (!method_exists($class, $method)) {
                cWarning(__FILE__, __LINE__, "Method " . $method . " in class " . $class . " doesn't exist, can't add " . $sFunctionName . " to chain " . $sChainName);
                return false;
            }
            $call = array(new $class(), $method);
        } elseif (strpos($sFunctionName, '::') > 0) {
            // chain function is static method of a object
            list($class, $method) = explode('::', $sFunctionName);
            if (!class_exists($class)) {
                cWarning(__FILE__, __LINE__, "Class " . $class . " doesn't exist, can't add " . $sFunctionName . " to chain " . $sChainName);
                return false;
            } elseif (!method_exists($class, $method)) {
                cWarning(__FILE__, __LINE__, "Method " . $method . " in class " . $class . " doesn't exist, can't add " . $sFunctionName . " to chain " . $sChainName);
                return false;
            }
            $call = array($class, $method);
        } else {
            // chain function is a function
            if (!function_exists($sFunctionName)) {
                cWarning(__FILE__, __LINE__, "Function " . $sFunctionName . " doesn't exist, can't add to chain " . $sChainName);
                return false;
            }
            $call = $sFunctionName;
        }

        // Last check if the callback is callable
        if (!is_callable($call)) {
            cWarning(__FILE__, __LINE__, "Function " . $sFunctionName . " isn't callable, can't add to chain " . $sChainName);
            return false;
        }

        $oChainItem = new pApiCECChainItem($sChainName, $sFunctionName, $this->_aChains[$sChainName]['parameters']);
        $oChainItem->setCallback($call);
        array_push($this->_aChains[$sChainName]['functions'], $oChainItem);

        return true;
    }


    /**
     * Checks if a chain function exists.
     *
     * @param   string  $sChainName     Chain name
     * @param   string  $sFunctionName  Name of function to check
     * @return  bool
     */
    public function chainFunctionExists($sChainName, $sFunctionName)
    {
        if (!$this->isChainRegistered($sChainName)) {
            return false;
        }

        $this->_resetIterator($sChainName);
        $chainFunctions = $this->_aChains[$sChainName]['functions'];
        foreach ($chainFunctions as $pos => $item) {
            if ($item->getFunctionName() == $sFunctionName) {
                return true;
            }
        }

        return false;
    }


    /**
     * Removes a chain function.
     *
     * @param   string  $sChainName     Chain name
     * @param   string  $sFunctionName  Name of function to remove from chain.
     */
    public function removeChainFunction($sChainName, $sFunctionName)
    {
        if (!$this->isChainRegistered($sChainName)) {
            return;
        }

        $this->_resetIterator($sChainName);
        $chainFunctions = $this->_aChains[$sChainName]['functions'];
        foreach ($this->_aChains[$sChainName]['functions'] as $pos => $item) {
            if ($item->getFunctionName() == $sFunctionName) {
                unset($this->_aChains[$sChainName]['functions'][$pos]);
                return;
            }
        }
    }


    /**
     * Returns the iterator for a desired chain.
     *
     * @TODO:  cIterator should be replaced by ArrayIterator (@see http://www.php.net/spl)
     *         but ArrayIterator uses rewind() instead of reset()...
     *
     * @param   string  $sChainName  Chain name
     * @return  cIterator
     */
    public function getIterator($sChainName)
    {
        return new cIterator($this->_aChains[$sChainName]['functions']);
    }


    /**
     * Resets the chain iterator.
     *
     * @param   string  $sChainName
     * @return  void
     */
    protected function _resetIterator($sChainName)
    {
        $iterator = $this->getIterator($sChainName);
        $iterator->reset();
    }

}


/**
 * CEC chain item class.
 *
 * @author      Timo A. Hummel
 * @author      Murat Purc <murat@purc.de>
 * @package     Contenido Backend classes
 * @subpackage  CEC
 */
class pApiCECChainItem
{
    /**
     * Chain name
     * @var  string
     */
    protected $_sChainName;

    /**
     * Name of function to invoke
     * @var  string
     */
    protected $_sFunctionName;

    /**
     * Callback name. Contains either the function name to invoke, or a indexed array (class/object and method)
     * and it's method to execute.
     * @var  array|string
     */
    protected $_mCallback;

    /**
     * Parameter to pass to the function
     * @var  array
     */
    protected $_aParameters;


    /**
     * Temporary arguments holder
     * @var  array|null
     */
    protected $_mTemporaryArguments;


    /**
     * Constructor, sets the CEC chain item properties.
     *
     * @param   string  $sChainName
     * @param   string  $sFunctionName
     * @param   array   $aParameters
     * @return  void
     */
    public function __construct($sChainName, $sFunctionName, $aParameters)
    {
        $this->setChainName($sChainName);
        $this->setFunctionName($sFunctionName);
        $this->setParameters($aParameters);
        $this->setCallback($this->getFunctionName());
    }


    /**
     * Sets the chain name
     *
     * @param   string  $sChainName
     * @return  void
     */
    public function setChainName($sChainName)
    {
        $this->_sChainName = $sChainName;
    }


    /**
     * Returns the chain name
     *
     * @return  string
     */
    public function getChainName()
    {
        return $this->_sChainName;
    }


    /**
     * Sets the function name
     *
     * @param   string  $sFunctionName
     * @return  void
     */
    public function setFunctionName($sFunctionName)
    {
        $this->_sFunctionName = $sFunctionName;
    }


    /**
     * Returns the function name
     *
     * @return  string
     */
    public function getFunctionName()
    {
        return $this->_sFunctionName;
    }


    /**
     * Sets the callback parameters
     *
     * @param   array  $aParameters
     * @return  void
     */
    public function setParameters(array $aParameters)
    {
        $this->_aParameters = $aParameters;
    }


    /**
     * Returns the function name
     *
     * @return  array
     */
    public function getParameters()
    {
        return $this->_aParameters;
    }


    /**
     * Sets the callback
     *
     * @return  string|array
     */
    public function setCallback($callback)
    {
        if (is_string($callback) || is_array($callback)) {
            $this->_mCallback = $callback;
        } else {
            throw new Exception("Passed argument isn't as expected");
        }
    }


    /**
     * Returns the callback
     *
     * @return  string|array
     */
    public function getCallback()
    {
        return $this->_mCallback;
    }


    /**
     * Another way to set the arguments before invoking execute() method.
     *
     * @param   array  $args
     * @return  void
     */
    public function setTemporaryArguments(array $args=array())
    {
        $this->_mTemporaryArguments = $args;
    }

    /**
     * Will be invoked by execute() method. If temporary arguments where set before,
     * it returns them and resets the property.
     *
     * @param   array  $args
     * @return  void
     */
    public function getTemporaryArguments()
    {
        $args = $this->_mTemporaryArguments;
        $this->_mTemporaryArguments = null;
        return $args;

    }


    /**
     * Invokes the CEC function/callback.
     *
     * @return  mixed  If available, the result of the CEC function/callback
     */
    public function execute()
    {
        // get temporary arguments, if the where set before
        if (!$args = $this->getTemporaryArguments()) {
            // no temporary arguments available, get them by func_get_args()
            $args = func_get_args();
        }
        return call_user_func_array($this->getCallback(), $args);
    }

}
