<?php

require_once __DIR__ . '/../exceptions/class.BusException.php';

/**
 * Generic event bus with subscribe/notify architecture.
 *
 * Every notifier registers itself by this class so that it can receive events. The events represent changes in the underlying
 * MAPI store like changes to folders, e-mails, calendar items, etc.
 *
 * A notifier can register itself for the following events:
 * - OBJECT_SAVE
 * - OBJECT_DELETE
 *
 * - TABLE_SAVE
 * - TABLE_DELETE
 *
 * - REQUEST_START
 * - REQUEST_END
 *
 * To use the REQUEST_* events, make sure you register your notifier with REQUEST_ENTRYID
 * as the dummy entryid.
 *
 * Events:
 *
 * (The parameters that are discussed are passed to notify() and sent to the update() methods of the notifiers)
 *
 * <b>OBJECT_SAVE</b>
 * This event is triggered when a folder object is created or modified. The entryid parameter is the entryid
 * of the object that has been created or modified. The data parameter contains an array of properties of the
 * object, with the following properties: PR_ENTRYID, PR_STORE_ENTRYID
 *
 * <b>OBJECT_DELETE</b>
 * This event is triggered when a folder is deleted. The entryid parameter is the entryid of the object
 * that has been deleted. The data parameter contains an array of properties of the deleted object, with
 * the following properties: PR_ENTRYID, PR_STORE_ENTRYID
 *
 * <b>TABLE_SAVE</b>
 * This event is triggered when a message item is created or modified. The entryid parameter is the entryid of the modified
 * object. The data parameter contains the following properties of the modified message: PR_ENTRYID, PR_PARENT_ENTRYID,
 * PR_STORE_ENTRYID
 *
 * <b>TABLE_DELETE</b>
 * This event is triggered when a message item is deleted. The entryid parameter is the entryid of the deleted object. The
 * data parameter contains the following properties of the deleted message: PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID
 *
 * <b>REQUEST_START</b>
 * This event is triggered for EVERY XML request that is done to the backend, before processing of the XML or other events.
 * The entryid parameter must be the fixed value REQUEST_ENTRYID. The $data parameter is not passed.
 *
 * <b>REQUEST_END</b>
 * This event is triggereed for EVERY XML request that is done to the backend, after all notifiers processing and will be the last
 * event triggered. The entryid parameter must be that fixed value REQUEST_ENTRYID. The $data parameter is not passed.
 *
 * @todo The event names are rather misleading and should be renamed to FOLDER_CHANGE, FOLDER_DELETE, MESSAGE_CHANGE, MESSAGE_DELETE
 * @todo The entryid is passed in $entryid AND in $data in almost all cases, which is rather wasteful
 */
class Bus {
	/**
	 * data which is sent back to the client.
	 */
	private $responseData;

	/**
	 * all registered notifiers.
	 */
	private $registeredNotifiers;

	/**
	 * all registered store notifiers.
	 */
	private $registeredStoreNotifiers;

	/**
	 * all notifier objects.
	 */
	private $notifiers;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->responseData = [];

		$this->registeredNotifiers = [];
		$this->registeredStoreNotifiers = [];
		$this->notifiers = [];
	}

	/**
	 * Register a notifier on the bus.
	 *
	 * This will check if a instance for the given $notifierName already exists, and if
	 * not will instantiate it through the dispatcher. It will then use $this->registerEvent
	 * to register the instance for all correct events on the given $entryid(s)
	 *
	 * After a notifier is registered on the bus, it will receive updates to objects generated by the modules
	 * which are handling the requests. For example a notifier is registered on the inbox can receive changes
	 * of the item count of the inbox by specifying the entryid of the inbox here and the event
	 *
	 * @param string $notifierName The classname of the notifier to register
	 * @param Array/Binary $entryid The entryid or entryids on which the notifier should
	 * be registered
	 * @param bool $store optional If true, the $entryid points to the store and all events within the store are
	 *                    bubbled to this notifier object
	 */
	public function registerNotifier($notifierName, $entryid, $store = false) {
		if (!isset($this->notifiers[$notifierName])) {
			$this->notifiers[$notifierName] = $GLOBALS["dispatcher"]->loadNotifier($notifierName);
		}

		// Obtain the events which are supported by this notifier
		$events = $this->notifiers[$notifierName]->getEvents();

		if (is_array($entryid)) {
			foreach ($entryid as $id) {
				$this->registerEvent($notifierName, $id, $events, $store);
			}
		}
		else {
			$this->registerEvent($notifierName, $entryid, $events, $store);
		}
	}

	/**
	 * Register notifier events on the bus.
	 *
	 * Called by $registerNotifier after the notifier instance was created. This function will then
	 * register the given $notifierName to the provided $entryid and will mark the $events for which
	 * this notifier should be fired.
	 *
	 * Valid values for $events are OBJECT_SAVE, OBJECT_DELETE, TABLE_SAVE, TABLE_DELETE, REQUEST_START and REQUEST_END
	 *
	 * @param string $notifierName The classname of the notifier to register
	 * @param string $entryid      EntryID of an object coupled to the notifier
	 * @param number $events       Bitmask of events that the notifier should receive
	 * @param bool   $store        optional If true, the $entryid points to the store and all events within the store are
	 *                             bubbled to this notifier object
	 */
	public function registerEvent($notifierName, $entryid, $events, $store = false) {
		$entryidCmp = $GLOBALS["entryid"];

		if ($store) {
			$found = false;

			foreach ($this->registeredStoreNotifiers as $key => &$storeNotifier) {
				if ($entryidCmp->compareEntryIds($storeNotifier['entryid'], $entryid)) {
					$storeNotifier[$notifierName] = ['events' => $events];
					$found = true;
					break;
				}
			}
			unset($storeNotifier);

			if (!$found) {
				$this->registeredStoreNotifiers[] = [
					'entryid' => $entryid,
					$notifierName => ['events' => $events],
				];
			}
		}
		else {
			$found = false;

			foreach ($this->registeredNotifiers as $key => &$folderNotifier) {
				if ($entryidCmp->compareEntryIds($folderNotifier['entryid'], $entryid)) {
					$folderNotifier[$notifierName] = ['events' => $events];
					$found = true;
					break;
				}
			}
			unset($folderNotifier);

			if (!$found) {
				$this->registeredNotifiers[] = [
					'entryid' => $entryid,
					$notifierName => ['events' => $events],
				];
			}
		}
	}

	/**
	 * Broadcast an update to notifiers attached to bus.
	 *
	 * This function is used to send an update to other notifiers that have been attached to the bus via register().
	 *
	 * @param string $entryID Entryid of the object for which the event has happened
	 * @param int    $event   Event which should be fired. Can be any of OBJECT_SAVE, OBJECT_DELETE, TABLE_SAVE, TABLE_DELETE, REQUEST_START and REQUEST_END.
	 * @param array  $data    the data which is used to execute the event, which differs for each event type
	 */
	public function notify($entryID, $event, $data = null) {
		$entryidCmp = $GLOBALS["entryid"];

		// Notifiers must be get only ONE update
		$updatedNotifiers = [];

		// Check if there are bubbled notifiers, and if a PR_STORE_ENTRYID was provided
		// on which the notifiers should be executed.
		if ($entryID !== REQUEST_ENTRYID && $data && isset($data[PR_STORE_ENTRYID])) {
			$storeEntryid = bin2hex($data[PR_STORE_ENTRYID]);

			// Update the store notifier
			foreach ($this->registeredStoreNotifiers as $key => &$storeNotifier) {
				if ($entryidCmp->compareEntryIds($storeNotifier['entryid'], $storeEntryid)) {
					foreach ($storeNotifier as $key => $notifier) {
						if (isset($notifier['events']) && ($notifier['events'] & $event)) {
							if (!isset($updatedNotifiers[$key])) {
								if (isset($this->notifiers[$key]) && is_object($this->notifiers[$key])) {
									$this->notifiers[$key]->update($event, $entryID, $data);
									$updatedNotifiers[$key] = true;
								}
							}
						}
					}
					break;
				}
			}
			unset($storeNotifier);
		}

		// Update the notifier
		foreach ($this->registeredNotifiers as $key => &$folderNotifier) {
			if (($entryID === REQUEST_ENTRYID && $folderNotifier['entryid'] === REQUEST_ENTRYID) ||
			$entryidCmp->compareEntryIds($folderNotifier['entryid'], $entryID)) {
				foreach ($folderNotifier as $key => $notifier) {
					if (isset($notifier['events']) && ($notifier['events'] & $event)) {
						if (!isset($updatedNotifiers[$key])) {
							if (isset($this->notifiers[$key]) && is_object($this->notifiers[$key])) {
								$this->notifiers[$key]->update($event, $entryID, $data);
								$updatedNotifiers[$key] = true;
							}
						}
					}
				}
				break;
			}
		}
		unset($folderNotifier);
	}

	/**
	 * Add response data to collected XML response.
	 *
	 * This function is called by all modules when they want to output some XML response data
	 * to the client. It simply copies the XML response data to an internal structure. The data
	 * is later collected via getData() when all modules are done processing, after which the
	 * data is sent to the client.
	 *
	 * The data added is not checked for validity, and is therefore completely free-form. However,
	 *
	 * @param array $data data to be added
	 */
	public function addData($data) {
		foreach ($data as $moduleName => $module) {
			if (isset($this->responseData[$moduleName])) {
				foreach ($module as $moduleId => $action) {
					if (isset($this->responseData[$moduleName][$moduleId])) {
						foreach ($action as $actionType => $actionData) {
							if (isset($this->responseData[$moduleName][$moduleId][$actionType])) {
								if ($this->responseData[$moduleName][$moduleId][$actionType] == $actionData) {
									// in future we can think something about this but for now we throw exception
									throw new BusException(_("Can't add data to bus, same action type is present in bus"));
								}
							}
						}
					}
				}
			}
		}

		$this->responseData = array_replace_recursive($this->responseData, $data);
	}

	/**
	 * Function which returns the data stored via addData().
	 *
	 * @return array response data
	 */
	public function getData() {
		if (empty($this->responseData)) {
			// Helps to avoid unnecessary bus error message on client side when gromox is stopped.
			// we just error_log and send empty zarafa array on client side
			error_log(_("Response data requested from bus but it doesn't have any data."));

			return json_encode(["zarafa" => []]);
		}

		return $this->responseData;
	}

	/**
	 * Reset the bus state and response data, and send resets to all notifier.
	 *
	 * Since the bus is a global, persistent object, the reset() function is called before or after
	 * processing. This makes sure that the on-disk serialized representation of the bus object is as
	 * small as possible.
	 */
	public function reset() {
		$this->responseData = [];

		foreach ($this->notifiers as $key => $notifier) {
			$this->notifiers[$key]->reset();
		}
	}
}
