<?php
/**
* @version	$Id: event_manager.php 16513 2017-01-20 14:10:53Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kEventManager extends kBase implements kiCacheable {

	/**
	 * Instance of hook manager
	 *
	 * @var kHookManager
	 * @access protected
	 */
	protected $Hooks = null;

	/**
	 * Instance of scheduled task manager
	 *
	 * @var kScheduledTaskManager
	 * @access protected
	 */
	protected $ScheduledTasks = null;

	/**
	 * Instance of request manager
	 *
	 * @var kRequestManager
	 * @access protected
	 */
	protected $Request = null;

	/**
	 * Build events registred for pseudo classes.
	 * key - pseudo class, value - event name
	 *
	 * @var Array
	 * @access protected
	 */
	protected $buildEvents = Array ();

	/**
	 * Event recursion tracking stack
	 *
	 * @var Array
	 * @access protected
	 */
	protected $recursionStack = Array ();

	/**
	 * Creates new instance of kEventManager class
	 *
	 */
	public function __construct()
	{
		parent::__construct();

		$this->Hooks = $this->Application->makeClass('kHookManager');
		$this->ScheduledTasks = $this->Application->makeClass('kScheduledTaskManager');
		$this->Request = $this->Application->makeClass('kRequestManager');
	}

	/**
	 * Sets data from cache to object
	 *
	 * @param Array $data
	 * @access public
	 */
	public function setFromCache(&$data)
	{
		$this->Hooks->setFromCache($data);
		$this->ScheduledTasks->setFromCache($data);

		$this->buildEvents = $data['EventManager.buildEvents'];
	}

	/**
	 * Gets object data for caching
	 *
	 * @return Array
	 * @access public
	 */
	public function getToCache()
	{
		return array_merge(
			$this->Hooks->getToCache(),
			$this->ScheduledTasks->getToCache(),
			Array (
				'EventManager.buildEvents' => $this->buildEvents,
			)
		);
	}

	/**
	 * Returns information about registered scheduled tasks
	 *
	 * @param bool $from_cache
	 * @return Array
	 * @access public
	 */
	public function getScheduledTasks($from_cache = false)
	{
		return $this->ScheduledTasks->getAll($from_cache);
	}

	/**
	 * Add new scheduled task
	 *
	 * @param string $short_name name to be used to store last maintenance run info
	 * @param string $event_string
	 * @param int $run_schedule run schedule like for Cron
	 * @param int $status
	 * @access public
	 */
	public function registerScheduledTask($short_name, $event_string, $run_schedule, $status = STATUS_ACTIVE)
	{
		$this->ScheduledTasks->add($short_name, $event_string, $run_schedule, $status);
	}

	/**
	 * Run registered scheduled tasks with specified event type
	 *
	 * @param bool $from_cron
	 * @access public
	 */
	public function runScheduledTasks($from_cron = false)
	{
		$this->ScheduledTasks->runAll($from_cron);
	}

	/**
	 * Runs scheduled task based on given data
	 *
	 * @param Array $scheduled_task_data
	 * @return bool
	 * @access public
	 */
	public function runScheduledTask($scheduled_task_data)
	{
		return $this->ScheduledTasks->run($scheduled_task_data);
	}

	/**
	 * Registers Hook from sub-prefix event to master prefix event
	 *
	 * Pattern: Observer
	 *
	 * @param string $hook_event
	 * @param string $do_event
	 * @param int $mode
	 * @param bool $conditional
	 * @access public
	 */
	public function registerHook($hook_event, $do_event, $mode = hAFTER, $conditional = false)
	{
		$this->Hooks->registerHook($hook_event, $do_event, $mode, $conditional);
	}

	/**
	 * Registers build event for given pseudo class
	 *
	 * @param string $pseudo_class
	 * @param string $event_name
	 * @access public
	 */
	public function registerBuildEvent($pseudo_class, $event_name)
	{
		$this->buildEvents[$pseudo_class] = $event_name;
	}

	/**
	 * Runs build event for given $pseudo_class instance, when defined
	 *
	 * @param string $prefix_special
	 * @param string $pseudo_class
	 * @param Array $event_params
	 * @access public
	 */
	public function runBuildEvent($prefix_special, $pseudo_class, $event_params)
	{
		if ( !isset($this->buildEvents[$pseudo_class]) ) {
			return ;
		}

		$event = new kEvent($prefix_special . ':' . $this->buildEvents[$pseudo_class], $event_params);
		$this->HandleEvent($event);
	}

	/**
	 * Check if event is called twice, that causes recursion
	 *
	 * @param kEvent $event
	 * @return bool
	 * @access protected
	 */
	protected function isRecursion($event)
	{
		$event_key = $event->getPrefixSpecial() . ':' . $event->Name;

		return in_array($event_key, $this->recursionStack);
	}

	/**
	 * Adds event to recursion stack
	 *
	 * @param kEvent $event
	 * @access protected
	 */
	protected function pushEvent($event)
	{
		$event_key = $event->getPrefixSpecial() . ':' . $event->Name;

		array_push($this->recursionStack, $event_key);
	}

	/**
	 * Removes event from recursion stack
	 *
	 * @access protected
	 */
	protected function popEvent()
	{
		array_pop($this->recursionStack);
	}

	/**
	 * Allows to process any type of event
	 *
	 * @param kEvent $event
	 * @return void
	 * @access public
	 */
	public function HandleEvent($event)
	{
		if ( $this->isRecursion($event) || !$this->verifyEventPrefix($event) ) {
			return;
		}

		$this->pushEvent($event);

		if ( !$event->SkipBeforeHooks ) {
			$this->Hooks->runHooks($event, hBEFORE);

			if ( $event->status == kEvent::erFATAL ) {
				return;
			}
		}

		/** @var kEventHandler $event_handler */
		$event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler');

		$event_handler->processEvent($event);

		if ( $event->status == kEvent::erFATAL ) {
			return;
		}

		if ( !$event->SkipAfterHooks ) {
			$this->Hooks->runHooks($event, hAFTER);
		}

		$this->popEvent();
	}

	/**
	 * Notifies event subscribers, that event has occured
	 *
	 * @param kEvent $event
	 * @return void
	 */
	public function notifySubscribers(kEvent $event)
	{
		if ( $event->status != kEvent::erSUCCESS ) {
			return;
		}

		$cache_key = 'email_to_event_mapping[%EmailTemplateSerial%]';
		$event_mapping = $this->Application->getCache($cache_key);

		if ( $event_mapping === false ) {
			$this->Conn->nextQueryCachable = true;
			$sql = 'SELECT TemplateId, TemplateName, Type, BindToSystemEvent
					FROM ' . $this->Application->getUnitOption('email-template', 'TableName') . '
					WHERE BindToSystemEvent <> ""';
			$event_mapping = $this->Conn->Query($sql, 'BindToSystemEvent');

			$this->Application->setCache($cache_key, $event_mapping);
		}

		$email_template = Array ();

		if ( isset($event_mapping[(string)$event]) ) {
			$email_template = $event_mapping[(string)$event];
		}
		elseif ( isset($event_mapping[$event->Prefix . '.*:' . $event->Name]) ) {
			$email_template = $event_mapping[$event->Prefix . '.*:' . $event->Name];
		}

		if ( !$email_template ) {
			return;
		}

		$where_clause = Array ();
		$where_clause['EmailTemplateId'] = 'EmailTemplateId = ' . $email_template['TemplateId'];

		try {
			$category_ids = Array ();

			/** @var kDBItem $category */
			$category = $this->Application->recallObject('c');

			if ( $category->isLoaded() ) {
				$category_ids = explode('|', substr($category->GetDBField('ParentPath'), 1, -1));
			}
		}
		catch (Exception $e) {
		}

		$where_clause['CategoryId'] = $this->_getSubscriberFilter('CategoryId', $category_ids, true);

		try {
			$item_id = $parent_item_id = false;

			/** @var kDBItem $object */
			$object = $event->getObject();

			if ( $object->isLoaded() ) {
				$item_id = $object->GetID();
				$parent_prefix = $this->Application->getUnitOption($event->Prefix, 'ParentPrefix');

				if ( $parent_prefix ) {
					$parent_item_id = $object->getParentId($parent_prefix);
				}
			}
		}
		catch (Exception $e) {
		}

		$where_clause['ItemId'] = $this->_getSubscriberFilter('ItemId', $item_id);
		$where_clause['ParentItemId'] = $this->_getSubscriberFilter('ParentItemId', $parent_item_id);

		$event_params = Array (
			'EmailTemplateId' => $email_template['TemplateId'],
			'CategoryIds' => $category_ids,
			'ItemId' => $item_id,
			'ParentId' => $parent_item_id,
			'where_clause' => $where_clause,
		);

		$sql_event = new kEvent($event->getPrefixSpecial() . ':OnGetEventSubscribersQuery', $event_params);
		$sql_event->MasterEvent = $event;
		$this->HandleEvent($sql_event);

		$subscribers = $this->Conn->GetIterator($sql_event->getEventParam('sql'));

		if ( !count($subscribers) ) {
			// mapping exists, but nobody has subscribed
			return;
		}

		$send_params = Array (
			'Prefix' => $event->Prefix,
			'Special' => $event->Special,
			'PrefixSpecial' => $event->getPrefixSpecial(),
		);

		$send_method = $email_template['Type'] == EmailTemplate::TEMPLATE_TYPE_FRONTEND ? 'emailUser' : 'emailAdmin';

		foreach ($subscribers as $subscriber_info) {
			$send_params['to_name'] = $subscriber_info['SubscriberEmail'];
			$send_params['to_email'] = $subscriber_info['SubscriberEmail'];
			$this->Application->$send_method($email_template['TemplateName'], $subscriber_info['UserId'], $send_params);
		}
	}

	/**
	 * Returns filter for searching event subscribers
	 *
	 * @param string $field
	 * @param mixed $value
	 * @param bool $is_category
	 * @return string
	 * @access protected
	 */
	protected function _getSubscriberFilter($field, $value, $is_category = false)
	{
		if ( $value ) {
			// send to this item subscribers AND to subscribers to all items
			if ( $is_category ) {
				$clause = 'IF(IncludeSublevels = 1, ' . $field . ' IN (' . implode(',', $value) . '), ' . $field . ' = ' . end($value) . ')';
			}
			else {
				$clause = $field . ' = ' . $this->Conn->qstr($value);
			}

			return $clause . ' OR ' . $field . ' IS NULL';
		}

		// send to subscribers to all items
		return $field . ' IS NULL';
	}

	/**
	 * Checks, that given event is implemented
	 *
	 * @param kEvent $event
	 * @return bool
	 * @access public
	 */
	public function eventImplemented(kEvent $event)
	{
		if ( !$this->verifyEventPrefix($event, true) ) {
			return false;
		}

		/** @var kEventHandler $event_handler */
		$event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler');

		return $event_handler->getEventMethod($event) != '';
	}

	/**
	 * Checks if event prefix is valid
	 *
	 * @param kEvent $event
	 * @param bool $is_fatal
	 * @return string
	 * @access public
	 */
	public function verifyEventPrefix($event, $is_fatal = false)
	{
		if ( !$this->Application->prefixRegistred($event->Prefix) ) {
			// when "l-cdata" is requested, then load "l", that will clone "l-cdata" unit config
			$this->Application->UnitConfigReader->loadConfig($event->Prefix);

			if ( !$this->Application->prefixRegistred($event->Prefix) ) {
				$error_msg = 'Prefix "<strong>' . $event->Prefix . '</strong>" not registred (requested event "<strong>' . $event->Name . '</strong>")';

				if ($is_fatal) {
					throw new Exception($error_msg);
				}
				else {
					trigger_error($error_msg, E_USER_WARNING);
				}

				return false;
			}
		}

		return true;
	}

	/**
	 * Processes request
	 *
	 * @access public
	 */
	public function ProcessRequest()
	{
		$this->Request->process();
	}

	/**
	 * Allows to add new element to opener stack
	 *
	 * @param string $template
	 * @param Array $params
	 * @access public
	 */
	public function openerStackPush($template = '', $params = Array ())
	{
		$this->Request->openerStackPush($template, $params);
	}

	/**
	 * Set's new event for $prefix_special
	 * passed
	 *
	 * @param string $prefix_special
	 * @param string $event_name
	 * @access public
	 */
	public function setEvent($prefix_special,$event_name)
	{
		/** @var Params $actions */
		$actions = $this->Application->recallObject('kActions');

		$actions->Set('events[' . $prefix_special . ']', $event_name);
	}

	/**
	 * Allows to determine, that required event is beeing processed right now
	 *
	 * @param string $event_key Event name in format prefix[.special]:event_name
	 * @return bool
	 * @access public
	 */
	public function eventRunning($event_key)
	{
		return array_search($event_key, $this->recursionStack) !== false;
	}
}