<?php
/**
 * Copyright 2001-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  Notification
 */

/**
 * The Horde_Notification package provides a subject-observer pattern for
 * raising and showing messages of different types and to different listeners.
 *
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  Notification
 */
class Horde_Notification_Handler
{
    /**
     * Decorators.
     *
     * @var array
     */
    protected $_decorators = array();

    /**
     * Forces immediate attachment of a notification to a listener.
     *
     * @var boolean
     */
    protected $_forceAttach = false;

    /**
     * Additional handle definitions.
     *
     * @var array
     */
    protected $_handles = array(
        'default' => array(
            '*' => 'Horde_Notification_Event'
        )
    );

    /**
     * Hash containing all attached listener objects.
     *
     * @var array
     */
    protected $_listeners = array();

    /**
     * The storage location where we store the messages.
     *
     * @var Horde_Notification_Storage
     */
    protected $_storage;

    /**
     * Initialize the notification system.
     *
     * @param Horde_Notification_Storage $storage  The storage object to use.
     */
    public function __construct(Horde_Notification_Storage_Interface $storage)
    {
        $this->_storage = $storage;
    }

    /**
     * Registers a listener with the notification object and includes
     * the necessary library file dynamically.
     *
     * @param string $listener  The name of the listener to attach. These
     *                          names must be unique; further listeners with
     *                          the same name will be ignored.
     * @param array $params     A hash containing any additional configuration
     *                          or connection parameters a listener driver
     *                          might need.
     * @param string $class     The class name from which the driver was
     *                          instantiated if not the default one. If given
     *                          you have to include the library file
     *                          containing this class yourself. This is useful
     *                          if you want the listener driver to be
     *                          overriden by an application's implementation
     *
     * @return Horde_Notification_Listener  The listener object.
     * @throws Horde_Exception
     */
    public function attach($listener, $params = null, $class = null)
    {
        if ($ob = $this->getListener($listener)) {
            return $ob;
        }

        if (is_null($class)) {
            $class = 'Horde_Notification_Listener_' . Horde_String::ucfirst(Horde_String::lower($listener));
        }

        if (class_exists($class)) {
            $this->_listeners[$listener] = new $class($params);
            if (!$this->_storage->exists($listener)) {
                $this->_storage->set($listener, array());
            }
            $this->_addTypes($listener);
            return $this->_listeners[$listener];
        }

        throw new Horde_Exception(sprintf('Notification listener %s not found.', $class));
    }

    /**
     * Remove a listener from the notification list.
     *
     * @param string $listner  The name of the listener to detach.
     *
     * @throws Horde_Exception
     */
    public function detach($listener)
    {
        if ($ob = $this->getListener($listener)) {
            unset($this->_listeners[$ob->getName()]);
            $this->_storage->clear($ob->getName());
        } else {
            throw new Horde_Exception(sprintf('Notification listener %s not found.', $listener));
        }
    }

    /**
     * Clear any notification events that may exist in a listener.
     *
     * @param string $listener  The name of the listener to flush. If null,
     *                          clears all unattached events.
     */
    public function clear($listener = null)
    {
        if (is_null($listener)) {
            $this->_storage->clear('_unattached');
        } elseif ($ob = $this->getListener($listener)) {
            $this->_storage->clear($ob->getName());
        }
    }

    /**
     * Returns the current Listener object for a given listener type.
     *
     * @param string $type  The listener type.
     *
     * @return mixed  A Horde_Notification_Listener object, or null if
     *                $type listener is not attached.
     */
    public function get($type)
    {
        foreach ($this->_listeners as $listener) {
            if ($listener->handles($type)) {
                return $listener;
            }
        }

        return null;
    }

    /**
     * Returns a listener object given a listener name.
     *
     * @param string $listener  The listener name.
     *
     * @return mixed  Either a Horde_Notification_Listener or null.
     */
    public function getListener($listener)
    {
        $listener = Horde_String::lower(basename($listener));
        return empty($this->_listeners[$listener])
            ? null
            : $this->_listeners[$listener];
    }

    /**
     * Adds a type handler to a given Listener.
     * To change the default listener, use the following:
     * <pre>
     *   $ob->addType('default', '*', $classname);
     * </pre>
     *
     * @param string $listener  The listener name.
     * @param string $type      The listener type.
     * @param string $class     The Event class to use.
     */
    public function addType($listener, $type, $class)
    {
        $this->_handles[$listener][$type] = $class;

        if (isset($this->_listeners[$listener])) {
            $this->_addTypes($listener);
        }
    }

    /**
     * Adds any additional listener types to a given Listener.
     *
     * @param string $listener  The listener name.
     */
    protected function _addTypes($listener)
    {
        if (isset($this->_handles[$listener])) {
            foreach ($this->_handles[$listener] as $type => $class) {
                $this->_listeners[$listener]->addType($type, $class);
            }
        }
    }

    /**
     * Add a decorator.
     *
     * @param Horde_Notification_Handler_Decorator_Base $decorator  The
     *                                                              Decorator
     *                                                              object.
     */
    public function addDecorator(Horde_Notification_Handler_Decorator_Base $decorator)
    {
        $this->_decorators[] = $decorator;
    }

    /**
     * Add an event to the Horde message stack.
     *
     * @param mixed $event    Horde_Notification_Event object or message
     *                        string.
     * @param string $type    The type of message.
     * @param array $flags    Array of optional flags that will be passed to
     *                        the registered listeners.
     * @param array $options  Additional options:
     * <pre>
     * 'immediate' - (boolean) If true, immediately tries to attach to a
     *               listener. If no listener exists for this type, the
     *               message will be dropped.
     *               DEFAULT: false (message will be attached to available
     *               handler at the time notify() is called).
     * </pre>
     */
    public function push($event, $type = null, array $flags = array(),
                         $options = array())
    {
        if ($event instanceof Horde_Notification_Event) {
            $event->flags = $flags;
            $event->type = $type;
        } else {
            $class = (!is_null($type) && ($listener = $this->get($type)))
                ? $listener->handles($type)
                : $this->_handles['default']['*'];

            /* Transparently create a Horde_Notification_Event object. */
            $event = new $class($event, $type, $flags);
        }

        foreach ($this->_decorators as $decorator) {
            $decorator->push($event, $options);
        }

        if (!$this->_forceAttach && empty($options['immediate'])) {
            $this->_storage->push('_unattached', $event);
        } else {
            if ($listener = $this->get($event->type)) {
                $this->_storage->push($listener->getName(), $event);
            }
        }
    }

    /**
     * Passes the message stack to all listeners and asks them to
     * handle their messages.
     *
     * @param array $options  An array containing display options for the
     *                        listeners. Any options not contained in this
     *                        list will be passed to the listeners.
     * <pre>
     * listeners - (array) The list of listeners to notify.
     * raw - (boolean) If true, does not call the listener's notify()
     *       function.
     * </pre>
     */
    public function notify(array $options = array())
    {
        /* Convert the 'listeners' option into the format expected by the
         * notification handler. */
        if (!isset($options['listeners'])) {
            $listeners = array_keys($this->_listeners);
        } elseif (!is_array($options['listeners'])) {
            $listeners = array($options['listeners']);
        } else {
            $listeners = $options['listeners'];
        }

        $events = array();
        $unattached = $this->_storage->exists('_unattached')
            ? $this->_storage->get('_unattached')
            : array();

        /* Pass the message stack to all listeners and asks them to handle
         * their messages. */
        foreach ($listeners as $listener) {
            $listener = Horde_String::lower($listener);

            if (isset($this->_listeners[$listener])) {
                $instance = $this->_listeners[$listener];
                $name = $instance->getName();

                foreach (array_keys($unattached) as $val) {
                    if ($unattached[$val] instanceof Horde_Notification_Event
                        && $instance->handles($unattached[$val]->type)) {
                        $this->_storage->push($name, $unattached[$val]);
                        unset($unattached[$val]);
                    }
                }

                foreach ($this->_decorators as $decorator) {
                    $this->_forceAttach = true;
                    try {
                        $decorator->notify($this, $instance);
                    } catch (Horde_Notification_Exception $e) {
                        $this->push($e);
                    }
                    $this->_forceAttach = false;
                }

                if (!$this->_storage->exists($name)) {
                    continue;
                }

                $tmp = $this->_storage->get($name);
                if (empty($options['raw'])) {
                    $instance->notify($tmp, $options);
                }
                $this->_storage->clear($name);

                $events = array_merge($events, $tmp);
            }
        }

        if (empty($unattached)) {
            $this->_storage->clear('_unattached');
        } else {
            $this->_storage->set('_unattached', $unattached);
        }

        return $events;
    }

    /**
     * Return the number of notification messages in the stack.
     *
     * @author David Ulevitch <davidu@everydns.net>
     *
     * @param string $my_listener  The name of the listener.
     *
     * @return integer  The number of messages in the stack.
     */
    public function count($my_listener = null)
    {
        $count = 0;

        if (!is_null($my_listener)) {
            if ($ob = $this->get($my_listener)) {
                $count = count($this->_storage->get($ob->getName()));

                if ($this->_storage->exists('_unattached')) {
                    foreach ($this->_storage->get('_unattached') as $val) {
                        if ($ob->handles($val->type)) {
                            ++$count;
                        }
                    }
                }
            }
        } else {
            if ($this->_storage->exists('_unattached')) {
                $count = count($this->_storage->get('_unattached'));
            }

            foreach ($this->_listeners as $val) {
                if ($this->_storage->exists($val->getName())) {
                    $count += count($this->_storage->get($val->getName()));
                }
            }
        }

        return $count;
    }

}
