<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Event
 * @subpackage Dispatcher
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category   Core
 * @package    Core_Event
 * @subpackage Dispatcher
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */
class  Core_Event_Dispatcher implements Core_Event_Dispatcher_Interface
{
    /**
     * @var array List of event with all their subscribers, priority-sorted.
     */
    protected $_subscribers = array();

    /**
     * @param string $eventName        Event name
     * @param array  $eventSubscribers Event subscribers
     *
     * @return Core_Event_Dispatcher
     */
    public function setEventSubscribers($eventName, array $eventSubscribers)
    {
        $this->_subscribers[$eventName] = array($eventSubscribers);
        return $this;
    }

    /**
     * Does an event have subscribers?
     *
     * @param string $eventName
     *
     * @return boolean
     */
    public function hasEventSubscribers($eventName)
    {
        $tokens         = explode('.', $eventName);
        while ($tokens) {
            $name = implode('.', $tokens);
            if ((isset($this->_subscribers[$name]) && !empty($this->_subscribers[$name]))) {
                return true;
            }
            array_pop($tokens);
        }
        return false;

        return ;
    }

    /**
     * @param string $eventName
     *
     * @return array Subscribers, streamlined.
     */
    public function getEventSubscribers($eventName)
    {
        if (!isset($this->_subscribers[$eventName])) {
            return array();
        }

        // sort priorities by numeric value
        ksort($this->_subscribers[$eventName], SORT_NUMERIC);

        // merge all priorities to one smooth linear array
        return call_user_func_array('array_merge', $this->_subscribers[$eventName]);
    }

    /**
     * Subscribe to an event.
     *
     * @param string   $eventName Event name
     * @param callback $callback  Event subscriber, a callback to be fired when event happens.
     * @param int      $priority  Optional priority of subscription, -10 comes before 10 (Unix nice style)
     * @param string   $filter    Optional filter which will additionaly filter the requests (needs to evaluate to true)
     *
     * @return Core_Event_Dispatcher
     */
    public function subscribe($eventName, $callback, $priority = 0, $filter = null)
    {
        if (!isset($this->_subscribers[$eventName][$priority])) {
            if (!isset($this->_subscribers[$eventName])) {
                $this->_subscribers[$eventName] = array();
            }
            $this->_subscribers[$eventName][$priority] = array();
        }
        $this->_subscribers[$eventName][$priority][] = array(
            'filter'   => $filter,
            'callback' => $callback,
        );
        return $this;
    }

    /**
     * Unsubscribe a subscriber from an event.
     *
     * If no subscriber is passed, unsubscribe all subscribers.
     *
     * @param string   $eventName Event name
     * @param callback $callback  Event subscriber, a callback to be unsubscribed.
     *
     * @return Core_Event_Dispatcher
     */
    public function unsubscribe($eventName, $callback = null)
    {
        if (!$this->hasEventSubscribers($eventName)) {
            return $this;
        }

        if (null === $callback) {
            unset($this->_subscribers[$eventName]);
            return $this;
        }

        foreach ($this->_subscribers[$eventName] as $priority => $eventSubscribers) {
            foreach ($eventSubscribers as $idx => $eventSubscriber) {
                if ($eventSubscriber['callback'] === $callback) {
                    unset($this->_subscribers[$eventName][$priority][$idx]);
                }
            }
        }
        return $this;
    }

    /**
     * Dispatch an event to all subscribers
     *
     * @param Core_Event_Interface $event Event to dispatch.
     *
     * @return Core_Event_Dispatcher
     */
    public function dispatch(Core_Event_Interface $event)
    {
        $this->_dispatch($event);
        return $this;
    }

    /**
     * Dispatch an event to all subscribers until one of them marks it as processed.
     *
     * @param Core_Event_Interface $event
     *
     * @return Core_Event_Dispatcher
     */
    public function dispatchUntil(Core_Event_Interface $event)
    {
        $this->_dispatch($event, true);
        return $this;
    }

    /**
     * Filter a value using event subscribers.
     *
     * @param Core_Event_Interface $event Event to dispatch
     * @param mixed                $value Value to filter
     *
     * @return type
     */
    public function filter(Core_Event_Interface $event, $value)
    {
        return $this->_dispatch($event, false, $value);
    }

    /**
     * Filter a value using event subscribers until one of them marks it as processed.
     *
     * @param Core_Event_Interface $event Event to dispatch
     * @param mixed                $value Value to filter
     *
     * @return type
     */
    public function filterUntil(Core_Event_Interface $event, $value)
    {
        return $this->_dispatch($event, true, $value);
    }

    /**
     *
     * @param callable             $caller Callable to push the event to
     * @param Core_Event_Interface $event  The event in question
     * @param mixed                $value  Optional value to process by the callable
     *
     * @return mixed
     */
    protected function _filterAndCall(array $caller, Core_Event_Interface $event, $value = null)
    {
        if (null !== $caller['filter'] && $this->_callFiltered($caller['filter'], $event)) {
            return $value;
        }

        return call_user_func($caller['callback'], $event, $value);
    }

    /**
     *
     * @param Core_Event_Interface $event           Event to dispatch
     * @param string               $name            Name of the event
     * @param boolean              $stopOnProcessed Do we stop dispatching if event is marked as processed by the handler?
     * @param mixed                $value           Value to filter
     *
     * @return mixed
     */
    protected function _dispatch(Core_Event_Interface $event, $stopOnProcessed = false, $value = null)
    {
        $tokens = explode('.', $event->getName());
        while ($tokens) {
            $name = implode('.', $tokens);
            foreach ($this->getEventSubscribers($name) as $callback) {
                $value = $this->_filterAndCall($callback, $event, $value);

                if ($stopOnProcessed && $event->isProcessed()) {
                    // event got processed, do not invoke any more handlers
                    break;
                }
            }

            if (!$event->isPropagated()) {
                // stop event propagation
                break;
            }
            array_pop($tokens);
        }
        return $value;
    }

    /**
     * @param string               $filter
     * @param Core_Event_Interface $event
     *
     * @return boolean
     */
    protected function _callFiltered($filter, Core_Event_Interface $event)
    {
        $matches = array();
        if (preg_match_all('/([a-zA-Z][a-zA-z0-9-_]*(\.[a-zA-Z][a-zA-z0-9-_]*)+)/us', $filter, $matches)) {
            $tokens = array_unique(current($matches));
            $values = array();

            foreach ($tokens as $token) {
                $attributes = explode('.', $token);
                $attribute  = array_shift($attributes);
                $element    = $this->_getAttribute($event, $attribute);

                // drill down form attributes/elements/etc and fetch the one we want
                foreach($attributes as $attribute) {
                    $element = $this->_getAttribute($element, $attribute);
                }
                $values[$token] = var_export($element, true);
            }

            $expression = 'return (bool) ('. str_replace($tokens, $values, $filter) .');';
            $unfiltered = eval($expression);
            return !$unfiltered;
        }
        return false;
    }

    /**
     * @param type $element
     * @param type $name
     *
     * @return mixed
     */
    protected function _getAttribute($element, $name)
    {
        if (is_object($element)) {
            if (is_callable(array($element, 'get'. $name))) {
                return call_user_func(array($element, 'get'. $name));
            }
        } elseif (is_array($element)) {
            if (array_key_exists($name, $element)) {
                return $element[$name];
            }
        }
        return null;
    }
}
