<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\base;

/**
 * Event is the base class for all event classes.
 *
 * It encapsulates the parameters associated with an event.
 * The [[sender]] property describes who raises the event.
 * And the [[handled]] property indicates if the event is handled.
 * If an event handler sets [[handled]] to be true, the rest of the
 * uninvoked handlers will no longer be called to handle the event.
 *
 * Additionally, when attaching an event handler, extra data may be passed
 * and be available via the [[data]] property when the event handler is invoked.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Event extends Object
{
	/**
	 * @var string the event name. This property is set by [[Component::trigger()]].
	 * Event handlers may use this property to check what event it is handling.
	 */
	public $name;
	/**
	 * @var object the sender of this event. If not set, this property will be
	 * set as the object whose "trigger()" method is called.
	 */
	public $sender;
	/**
	 * @var boolean whether the event is handled. Defaults to false.
	 * When a handler sets this to be true, the event processing will stop and
	 * ignore the rest of the uninvoked event handlers.
	 */
	public $handled = false;
	/**
	 * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler.
	 * Note that this varies according to which event handler is currently executing.
	 */
	public $data;

	private static $_events = [];

	/**
	 * Attaches an event handler to a class-level event.
	 *
	 * When a class-level event is triggered, event handlers attached
	 * to that class and all parent classes will be invoked.
	 *
	 * For example, the following code attaches an event handler to `ActiveRecord`'s
	 * `afterInsert` event:
	 *
	 * ~~~
	 * Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
	 *     Yii::trace(get_class($event->sender) . ' is inserted.');
	 * });
	 * ~~~
	 *
	 * The handler will be invoked for EVERY successful ActiveRecord insertion.
	 *
	 * For more details about how to declare an event handler, please refer to [[Component::on()]].
	 *
	 * @param string $class the fully qualified class name to which the event handler needs to attach
	 * @param string $name the event name
	 * @param callback $handler the event handler
	 * @param mixed $data the data to be passed to the event handler when the event is triggered.
	 * When the event handler is invoked, this data can be accessed via [[Event::data]].
	 * @see off()
	 */
	public static function on($class, $name, $handler, $data = null)
	{
		self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data];
	}

	/**
	 * Detaches an event handler from a class-level event.
	 *
	 * This method is the opposite of [[on()]].
	 *
	 * @param string $class the fully qualified class name from which the event handler needs to be detached
	 * @param string $name the event name
	 * @param callback $handler the event handler to be removed.
	 * If it is null, all handlers attached to the named event will be removed.
	 * @return boolean if a handler is found and detached
	 * @see on()
	 */
	public static function off($class, $name, $handler = null)
	{
		$class = ltrim($class, '\\');
		if (empty(self::$_events[$name][$class])) {
			return false;
		}
		if ($handler === null) {
			unset(self::$_events[$name][$class]);
			return true;
		} else {
			$removed = false;
			foreach (self::$_events[$name][$class] as $i => $event) {
				if ($event[0] === $handler) {
					unset(self::$_events[$name][$class][$i]);
					$removed = true;
				}
			}
			if ($removed) {
				self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
			}
			return $removed;
		}
	}

	/**
	 * Returns a value indicating whether there is any handler attached to the specified class-level event.
	 * Note that this method will also check all parent classes to see if there is any handler attached
	 * to the named event.
	 * @param string|object $class the object or the fully qualified class name specifying the class-level event
	 * @param string $name the event name
	 * @return boolean whether there is any handler attached to the event.
	 */
	public static function hasHandlers($class, $name)
	{
		if (empty(self::$_events[$name])) {
			return false;
		}
		if (is_object($class)) {
			$class = get_class($class);
		} else {
			$class = ltrim($class, '\\');
		}
		do {
			if (!empty(self::$_events[$name][$class])) {
				return true;
			}
		} while (($class = get_parent_class($class)) !== false);
		return false;
	}

	/**
	 * Triggers a class-level event.
	 * This method will cause invocation of event handlers that are attached to the named event
	 * for the specified class and all its parent classes.
	 * @param string|object $class the object or the fully qualified class name specifying the class-level event
	 * @param string $name the event name
	 * @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
	 */
	public static function trigger($class, $name, $event = null)
	{
		if (empty(self::$_events[$name])) {
			return;
		}
		if ($event === null) {
			$event = new self;
		}
		$event->handled = false;
		$event->name = $name;

		if (is_object($class)) {
			if ($event->sender === null) {
				$event->sender = $class;
			}
			$class = get_class($class);
		} else {
			$class = ltrim($class, '\\');
		}
		do {
			if (!empty(self::$_events[$name][$class])) {
				foreach (self::$_events[$name][$class] as $handler) {
					$event->data = $handler[1];
					call_user_func($handler[0], $event);
					if ($event instanceof Event && $event->handled) {
						return;
					}
				}
			}
		} while (($class = get_parent_class($class)) !== false);
	}
}
