<?php namespace ProcessWire;

/**
 * System Notifications for ProcessWire
 *
 * By Avoine and Ryan Cramer
 * 
 * @property int $disabled Notification status (0=off, 1=on)
 * @property int $reverse Notification order (0=newest to oldest, 1=oldest to newest)
 * @property int $placement
 * @property array $activeHooks Active automatic notification hooks (0=404s, 1=logins, 2=logouts)
 * @property int|bool $trackEdits Whether or not to track page edits
 * @property string $systemUserName Name of user that receives system notifications
 * @property int $systemUserID ID of user that receives system notifications
 * @property int $updateDelay Time between updates in ms (default=5000)
 * @property string $iconMessage default icon for message notifications
 * @property string $iconWarning default icon for warning notifications
 * @property string $iconError default icon for error notifications
 * @property string $iconProgress icon for any item that has an active progress bar
 * @property string $iconDebug default icon for debug-mode notification
 * @property int $ghostDelay how long a ghost appears on screen (in ms)
 * @property int $ghostDelayError how long an error ghost appears on screen (in ms)
 * @property int $ghostFadeSpeed speed at which ghosts fade in or out, or blank for no fade
 * @property int $ghostOpacity full opacity of ghost (when fully faded in) 
 * @property int $ghostPos ghost position: 1=left, 2=right
 * @property int $ghostLimit only show 1 summary ghost if there are more than this number
 * @property string $dateFormat date format to use in notifications (anything compatible with wireDate() function)
 *
 */
class SystemNotifications extends WireData implements Module {

	public static function getModuleInfo() {
		return array(
			'title' => 'System Notifications',
			'version' => 12,
			'summary' => 'Adds support for notifications in ProcessWire (currently in development)',
			'autoload' => true, 
			'installs' => 'FieldtypeNotifications', 
			'icon' => 'bell', 
			);
	}
	
	const fieldName = 'notifications';


	/**
	 * System hooks that may be configured as active in this module
	 *
	 * Each consists of: before|after hookToClass::hooktoMethod myHookMethod
	 *
	 */
	protected $systemHooks = array(
		0 => 'after ProcessPageView::pageNotFound hook404', 
		1 => 'after Session::login hookLogin', 
		2 => 'after Session::logoutSuccess hookLogout', 
		);

	/**
	 * Construction SystemNotifications
	 * 
	 */
	public function __construct() {
		$path = dirname(__FILE__) . '/';
		require_once($path . "Notification.php"); 
		require_once($path . "NotificationArray.php");
		require_once($path . "SystemNotificationsConfig.php"); 
	
		// hook method to access notifications, in case field name ever needs to change for some reason
		$this->addHook('User::notifications', $this, 'hookUserNotifications'); 
		$this->set('disabled', false);
	}

	/**
	 * API init: attach hooks
	 * 
	 */
	public function init() {
	
		if($this->disabled) return;
		if($this->activeHooks) foreach($this->activeHooks as $id) {
			if(!isset($this->systemHooks[$id])) continue;
			list($when, $hook, $method) = explode(' ', $this->systemHooks[$id]); 
			if($when == 'before') {
				$this->addHookBefore($hook, $this, $method); 
			} else {
				$this->addHookAfter($hook, $this, $method); 
			}
		}
	}
	
	/**
	 * API ready
	 * 
	 */
	public function ready() {

		if($this->disabled) return;
		
		$page = $this->wire('page'); 
		$config = $this->wire('config'); 
		$user = $this->wire('user');

		if(!$user->isLoggedin()) return;
		$this->testProgressNotification();

		if($this->wire('config')->ajax) {
			$adminPage = $this->wire('pages')->get($config->adminRootPageID); 
			if($page->parents()->has($adminPage)) {
				$ajaxAction = $this->wire('input')->get('Notifications'); 
				if($ajaxAction) $this->ajaxAction($ajaxAction, $user->get(self::fieldName), $user); 
			}
		}

		if($page->template == 'admin') {
			if(!$this->wire('input')->get('modal')) {
				$this->addHookAfter('AdminTheme::getExtraMarkup', $this, 'hookAdminThemeGetExtraMarkup');
			}
			if($page->process == 'ProcessModule' && !$this->disabled) {
				$this->wire()->modules->addHookAfter('isUninstallable', function(HookEvent $event) {
					$class = $event->arguments(0);
					if($class !== $this->className()) return;
					$returnReason = $event->arguments(1);
					if($returnReason) {
						$event->return = 'You must set “Notification status” to “Off” before uninstalling.'; 
					} else {
						$event->return = false;
					}
				}); 
			}
		}
	}
	
	/**
	 * Test out the progress bar notification
	 * 
	 */
	protected function testProgressNotification() {
		
		$session = $this->wire('session');
		/** @var NotificationArray $notifications */
		$notifications = $this->wire('user')->notifications();

		if($this->wire('input')->get('test_progress')) {
			// start new progress bar notification
		
			/** @var Notification $notification */	
			$notification = $this->wire('user')->notifications()->message('Testing progress bar notification');
			$notification->progress = 0; 
			$notification->flag('annoy');
			$value = $session->get($this, 'test_progress'); 
			if(!is_array($value)) $value = array();
			$id = $notification->getID();
			$value[$id] = $id; 
			$session->set($this, 'test_progress', $value);
			$notifications->save();

		} else if(($value = $session->get($this, 'test_progress')) && count($value)) {
			// updating existing progress bar notification(s)
			
			foreach($value as $id) { 
				$notification = $notifications->get($id);
				if(!$notification) continue; 
				
				$notification->progress += 10;
				if($notification->progress < 100) {
					$notification->html = "<p>$notification->progress%</p>";
					continue;
				}
				
				unset($value[$id]); 
				$notification->title = "Your download is now complete!";
				$notification->flag('open'); 
				$notification->flag('email'); 
				$notification->html = 
					"<p>This is just an example for demo purposes and the button below doesn't actually do anything.<br />" . 
					"<button class='ui-button'><strong class='ui-button-text'>Download</strong></button></p>";
			}
		
			$session->set($this, 'test_progress', $value);
			$notifications->save();
		}
	}


	/**
	 * Convert Notification object to array
	 * 
	 * @param Notification $notification
	 * @return array
	 * 
	 */
	protected function notificationToArray(Notification $notification) {

		$html = $notification->html;
		if(!$html && $notification->text) $html = "<p>" . $this->sanitizer->entities($notification->text) . "</p>";

		$a = array(
			'id' => $notification->getID(), 
			'title' => $notification->title, 
			'from' => $notification->from, 
			'created' => $notification->created, 
			'modified' => $notification->modified, 
			'when' => wireDate($this->dateFormat, $notification->modified),
			'href' => $notification->href, 
			'icon' => $notification->icon, 
			'flags' => $notification->flags, 
			'flagNames' => implode(' ', $notification->flagNames), 
			'progress' => $notification->progress, 
			'html' => $html, 
			'qty' => $notification->qty, 
			'expires' => $notification->expires, 
			);
		
		if($a['progress'] > 0 && $a['progress'] < 100) {
			$a['icon'] = $this->iconProgress; 
		}

		if(empty($a['icon'])) {
			if($notification->is("error")) $a['icon'] = $this->iconError;
				else if($notification->is("warning")) $a['icon'] = $this->iconWarning;
				else $a['icon'] = $this->iconMessage;
		}

		return $a; 
	}

	/**
	 * Process an ajax action request
	 * 
	 * @param $action
	 * @param NotificationArray $notifications
	 * @param Page $page
	 * 
	 */
	protected function ajaxAction($action, NotificationArray $notifications, Page $page) {

		$data = array();
		$qty = 0;
		$qtyNew = 0;
		$qtyMessage = 0;
		$qtyWarning = 0;
		$qtyError = 0;
		$time = (int) $this->wire('input')->get('time'); 
		$rm = $this->wire('input')->get('rm'); 
		$rm = $rm ? explode(',', $rm) : array();
		
		if($this->trackEdits) {
			$processKey = $this->wire('input')->get('processKey');
			$this->updateProcessKey($processKey);
		}

		foreach($notifications->sort('-modified') as $notification) {
			/** @var Notification $notification */
			$qty++;
			$a = $this->notificationToArray($notification); 

			if(in_array($a['id'], $rm)) {
				$qty--;
				$notifications->remove($notification); 
				continue; 
			}

			if($time && $notification->modified < $time) {
				continue;
			}
			
			if($notification->is('shown')) {
				continue;
			} else {
				$notification->setFlag('shown');
				$qtyNew++;
			}

			if($notification->flags & Notification::flagError) $qtyError++;
				else if($notification->flags & Notification::flagWarning) $qtyWarning++;
				else $qtyMessage++;
			
			$data[] = $a; 
		}

		if(count($rm) || $qtyNew) {
			$this->wire('pages')->saveField($page, 'notifications', array('quiet' => true));
		}
		
		if($action == 'update') {
			
			$data = array(
				'notifications' => $data, // new notifications only
				'qty' => $qty, // total notifications (new or not)
				'qtyNew' => $qtyNew, // quantity of new notifications, not yet shown
				'qtyMessage' => $qtyMessage,
				'qtyWarning' => $qtyWarning,
				'qtyError' => $qtyError,
				'time' => time(), // time this info was generated
				); 
		}

		header("Content-type: application/json"); 
		echo json_encode($data); 
		exit; 
	}

	/**
	 * Adds markup to admin theme output to initialize notifications
	 * 
	 * @param $event
	 * 
	 */
	public function hookAdminThemeGetExtraMarkup($event) {
		
		if($this->disabled) return;

		$config = $this->wire('config'); 
		$url = $config->urls->SystemNotifications . 'Notifications'; 
		$info = self::getModuleInfo();
		$config->styles->add("$url.css?v=$info[version]"); 
		$jsfile = $config->debug ? "$url.js" : "$url.min.js";
		$config->scripts->add("$jsfile?v=$info[version]"); 
		$qty = count($this->wire('user')->get(self::fieldName)); 
		$ghostLimit = $this->ghostLimit ? $this->ghostLimit : 20; 

		$properties = array(
			// configured property names
			'updateDelay',
			'iconMessage',
			'iconWarning',
			'iconError', 
			'ghostZindex',	
			'ghostDelay',
			'ghostDelayError',
			'ghostFadeSpeed',
			'ghostOpacity',
			'reverse', 
			);
		
		$options = array(
			// runtime property names
			'version' => $info['version'], 
			'updateLast' => time(),
			);

		foreach($properties as $key) {
			$options[$key] = $this->get($key); 
		}
		$options['reverse'] = (bool) ((int) $options['reverse']); 
	
		// options specified in $config->SystemNotifications
		$configDefaults = array(
			'classMessage' => 'NoticeMessage',
			'classWarning' => 'NoticeWarning',
			'classError' => 'NoticeError',
			'classContainer' => 'container',
		);
		$configOptions = $this->wire('config')->SystemNotifications;
		if(!is_array($configOptions)) $configOptions = array();
		$options = array_merge($options, $configDefaults, $configOptions);
	
		$textdomain = '/wire/core/Functions.php'; 
		$options['i18n'] = array(
			'sec' => __('sec', $textdomain),
			'secs' => __('secs', $textdomain),
			'min' => __('min', $textdomain),
			'mins' => __('mins', $textdomain), 
			'hour' => __('hour', $textdomain),
			'hours' => __('hours', $textdomain), 
			'day' => __('day', $textdomain),
			'days' => __('days', $textdomain), 
			'expires' => $this->_('expires'), 
			'now' => __('now', $textdomain), 
			'fromNow' => __('from now', $textdomain), 
			'ago' => __('ago', $textdomain), 
		);

		if($this->trackEdits) {
			$processKey = $this->makeProcessKey();
			if(!empty($processKey)) $options['processKey'] = $processKey;
		}
		
		$ghostClass = $this->ghostPos == 2 ? "NotificationGhostsRight" : "NotificationGhostsLeft";
		
		$out = 	
			"<div id='NotificationMenu' class='NotificationMenu'>" . 
			"<ul id='NotificationList' class='NotificationList'></ul>" . 
			"</div>" . 
			"<ul id='NotificationGhosts' class='NotificationGhosts $ghostClass'></ul>" . 
			"<script>Notifications.init(" . json_encode($options) . "); ";
		
		$notices = $this->wire('notices'); 
		$notifications = array();
		$numSave = 0; // number of notifications that need to be saved after being shown

		// convert runtime Notices to Notifications and bundle into array
		if($this->placement == SystemNotificationsConfig::placementCombined) {
			foreach($notices as $notice) {
				$notification = $this->noticeToNotification($notice);
				if(!$notification) continue;
				$sort = time() + 2592000;
				while(isset($notifications[$sort])) $sort++;
				$notifications[$sort] = $notification;
			}
		}
		
		// bundle user notifications into the same array
		foreach($this->wire('user')->notifications as $notification) {
			if($notification->is('shown')) {
				$notification->setFlag('no-ghost');
			} else {
				$notification->setFlag('shown');
				$numSave++;
			}
			$sort = ((int) $notification->modified) + ((int) $notification->sort);
			while(isset($notifications[$sort])) $sort++;
			$notifications[$sort] = $notification;
		}
	
		if($this->reverse) {
			ksort($notifications);
		} else {
			krsort($notifications);
		}
		
		$numMessages = 0;
		$numWarnings = 0;
		$numErrors = 0;
		$noGhost = count($notifications) > $ghostLimit; 

		foreach($notifications as $notification) {
			if($noGhost) $notification->setFlag("no-ghost"); 
			$notificationJS = json_encode($this->notificationToArray($notification)); 
			$out .= "\nNotifications.add($notificationJS); ";
			if($notification->is("message")) $numMessages++;
				else if($notification->is("warning")) $numWarnings++;
				else if($notification->is("error")) $numErrors++;
			if($noGhost) $notification->removeFlag("no-ghost"); // restore in case saved to DB
			$notification->setFlag('shown'); 
		}
		
		if($noGhost) {
			$ghostTypes = array(
				"message" => $numMessages, 
				"warning" => $numWarnings, 
				"error" => $numErrors
			);
			foreach($ghostTypes as $type => $qty) {
				if(!$qty) continue; 
				if($type == 'message') $title = sprintf($this->_n('%d new message', '%d new messages', $qty), $qty);
					else if($type == 'warning') $title = sprintf($this->_n('%d new warning', '%d new warnings', $qty), $qty);
					else if($type == 'error') $title = sprintf($this->_n('%d new error', '%d new errors', $qty), $qty); 
					else $title = '';
				$icon = $this->get("icon" . ucfirst($type)); 
				$out .= 'Notifications._ghost({"id":"","title":"' . $title . '","icon":"' . $icon . '","flagNames":"' . $type . ' notice"});';
			}
		}
		
		if($numSave) {
			// updates shown flag so notification doesn't pop up another ghost
			$this->wire('user')->notifications->save();
		}

		if($this->placement == SystemNotificationsConfig::placementCombined) {
			$out .= "$('#notices').remove(); "; // in case admin theme still has #notices
		}
		$out .= 
			"$(document).ready(function() { Notifications.render(); });" . 
			"</script>";

		$extras = $event->return;
		$extras['body'] .= $out;
		$extras['masthead'] .=
			"<div id='NotificationBug' class='NotificationBug qty$qty' data-qty='$qty'>" . 
			"<span class='qty fa fa-fw'>$qty</span>" . 
			"<i class='NotificationSpinner fa fa-fw fa-spin fa-spinner'></i>" . 
			"</div>";

		
		$adminTheme = $this->wire('adminTheme'); 
		if($adminTheme) $adminTheme->addBodyClass('NotificationPlacement' . (int) $this->placement); 

		$event->return = $extras; 
	}
	
	/**
	 * Convert ProcessWire runtime "Notice" objects to runtime Notification objects
	 * 
	 * @param Notice $notice
	 * @return Notification|bool Returns Notification or boolean false on error
	 *
	 */
	protected function noticeToNotification(Notice $notice) {

		if($notice instanceof NoticeWarning || ($notice->flags & Notice::warning)) $type = 'warning';
			else if($notice instanceof NoticeError) $type = 'error';
			else $type = 'message';
		
		/** @var NotificationArray $notifications */	
		$notifications = $this->wire('user')->notifications();
		if(!$notifications) return false;
		
		$notification = $notifications->getNew($type, false);
		if(!$notification) return false;
		
		$notification->setFlag('notice', true); 
		
		if($notice->flags & Notice::allowMarkup) $notification->setFlag('markup', true); 
		if($notice->flags & Notice::log) $notification->setFlag('log', true);
		if($notice->flags & Notice::logOnly) $notification->setFlag('log-only', true); 
		if($notice->flags & Notice::debug) {
			$notification->setFlag('debug', true); 
			$notification->icon = $this->iconDebug; 
		}
		if($notice->class) $notification->from = $notice->class; 
		if($notice->timestamp) $notification->created = $notice->timestamp;
		
		$title = strip_tags((string) $notice);

		if(strlen($title) > 100) {

			$title = substr($title, 0, 100); 
			$title = substr($title, 0, strrpos($title, ' ')) . '...'; 
			$notification->title = $title;

			if($notice->flags & Notice::allowMarkup) {
				$notification->html = (string) $notice; 
			} else {
				$notification->text = (string) $notice; 
			}
			
		} else if($notice->flags & Notice::allowMarkup) {
			$notification->title = $notice->text;
			
		} else {
			$notification->title = $title; 
		}

		return $notification;
	}

	/**
	 * Adds automatic notification for every 404
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hook404(HookEvent $event) {

		/** @var Page $page */
		$page = $event->arguments(0);	
		$url = $event->arguments(1); 
		/** @var User $user */
		$user = $this->getSystemUser();
		if(!$user->id) return;
		
		if(isset($_SERVER['HTTP_REFERER'])) {
			$referer = $this->wire('sanitizer')->entities($this->wire('sanitizer')->text($_SERVER['HTTP_REFERER']));
		} else {
			$referer = '';
		}
		if(isset($_SERVER['HTTP_USER_AGENT'])) { 
			$useragent = $this->wire('sanitizer')->entities($this->wire('sanitizer')->text($_SERVER['HTTP_USER_AGENT']));
		} else {
			$useragent = '';
		}
		if(empty($referer)) $referer = "unknown";
		if(empty($useragent)) $useragent = "unknown";
	
		/** @var NotificationArray $notifications */
		$notifications = $user->notifications();
		$notification = $notifications->warning(sprintf($this->_('404 occurred: %s'), $url)); 
		$notification->expires = 30; 
		$notification->html = 
			"<p>" . 
			"<b>Referer:</b> $referer<br />" .
			"<b>Useragent:</b> $useragent<br />" . 
			"<b>IP:</b> " . $this->wire('session')->getIP() . "<br />" . 
			"<b>Page:</b> " . ($page->id ? $page->url : 'Unknown') . "<br />" .
			"<b>User:</b> " . $this->user->name . 
			"</p>";	
		
		$notifications->save();
	}

	/**
	 * Creates a notifications() method with the user
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookUserNotifications(HookEvent $event) {
		$user = $event->object;
		$notifications = $user->get(self::fieldName); 
		if(!$notifications) {
			$this->install();
			$notifications = $user->get(self::fieldName); 
		}
		$event->return = $notifications;
	}

	/**
	 * Automatic notification for logins
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookLogin(HookEvent $event) {
		$user = $this->getSystemUser();
		if(!$user->id) return;
		/** @var NotificationArray $notifications */
		$notifications = $user->notifications();

		$loginUser = $event->return;
		$loginName = $event->arguments(0); 

		if($loginUser && $loginUser->id) {
			$notification = $notifications->message(sprintf($this->_('User logged in: %s'), $loginName));
		} else {
			$notification = $notifications->error(sprintf($this->_('Login failure: %s'), $loginName));
		}
		
		$useragent = $this->wire('sanitizer')->entities($this->wire('sanitizer')->text($_SERVER['HTTP_USER_AGENT']));
		
		$notification->html = 
			"<p>" . 
			"<b>Useragent:</b> $useragent<br />" . 
			"<b>Time:</b> " . date('Y-m-d H:i:s') . "<br />" . 
			"<b>IP:</b> " . $this->wire('session')->getIP() . 
			"</p>";

		$notifications->save();
	}

	/**
	 * Automatic notification for logouts
	 * 
	 * @param HookEvent $event
	 *
	 */
	public function hookLogout(HookEvent $event) {
		$user = $this->getSystemUser();
		if(!$user->id) return;
		$logoutUser = $event->arguments(0);
		/** @var NotificationArray $notifications */
		$notifications = $user->notifications(); 
		$notifications->message(sprintf($this->_('User logged out: %s'), $logoutUser->name));
		$notifications->save();
	}

	/**
	 * Return the user that receives system notifications
	 * 
	 * @return User
	 * 
	 */
	public function getSystemUser() {
		$user = null;
		if($this->systemUserName) $user = $this->wire('users')->get($this->systemUserName); 
		if(!$user || !$user->id) $user = $this->wire('users')->get($this->systemUserID); 
		if(!$user->id) {
			$role = $this->wire('roles')->get('superuser'); 
			$user = $this->wire('users')->get("roles=$role, sort=-created, include=all");
		}
		return $user; 
	}
	
	/**
	 * Install notifications
	 *
	 */
	public function ___install() {
		$fieldtype = $this->modules->get('FieldtypeNotifications'); 
		$field = $this->wire('fields')->get(self::fieldName); 	
		if($field && !$field->type instanceof FieldtypeNotifications) {
			throw new WireException("There is already a field named '" . self::fieldName . "'"); 
		}
		if(!$field) {
			$field = $this->wire(new Field());
			$field->name = self::fieldName;
			$field->label = 'Notifications';
			$field->type = $fieldtype; 
			$field->collapsed = Inputfield::collapsedBlank;
			$field->flags = Field::flagSystem;
			$field->save();
		}
		$fieldgroup = $this->wire('fieldgroups')->get('user'); 
		if(!$fieldgroup->hasField($field)) {
			$fieldgroup->add($field); 
			$fieldgroup->save();
		}

		// make this field one that the user is allowed to configure in their profile
		// $data = $this->wire('modules')->getModuleConfigData('ProcessProfile'); 
		// $data['profileFields'][] = 'notifications';
		// $this->wire('modules')->saveModuleConfigData('ProcessProfile', $data); 

		$notifications = $this->wire('user')->get(self::fieldName); 
		if($notifications) {
			$notifications->message('Hello World')->text('Thank you for installing the Notifications module. This is your first notification!');
			$notifications->save();
		}
	}

	/**
	 * Uninstall notifications
	 * 
	 */
	public function ___uninstall() {
		$fieldgroup = $this->wire('fieldgroups')->get('user'); 
		$field = $this->wire('fields')->get(self::fieldName); 

		if($field) {
			$field->flags = Field::flagSystemOverride;
			$field->flags = 0; 
			if($fieldgroup->hasField($field)) {
				$fieldgroup->remove($field); 
				$fieldgroup->save();
			}
			$this->wire('fields')->delete($field); 
		}

		if($this->wire('modules')->isInstalled('FieldtypeNotifications')) {
			$this->wire('modules')->uninstall('FieldtypeNotifications'); 
		}
	}

	/**
	 * Update the current processKey and its time in the cache
	 * 
	 * This method is called only during ajax requests. 
	 * 
	 * @param string $processKey 
	 * 
	 */
	protected function updateProcessKey($processKey) {
		// NPK.ProcessPageEdit.pageID.userID.windowID
		if(!preg_match('/^NPK\.[A-Za-z]+\.\d+\.\d+\.PW\d+$/', $processKey)) return;
		$this->checkProcessKey($processKey); 
		$times = $this->wire('cache')->get($processKey);
		if($times) {
			list($created, $modified) = explode(':', $times); 
			if($modified) {} // unused
			$value = $created . ":" . time();
		} else {
			$value = time() . ":" . time();
		}
		$this->wire('cache')->save($processKey, $value, ($this->updateDelay / 1000) * 2);
	}

	/**
	 * Create a new processKey for the current request
	 * 
	 * processKey example: NPK.ProcessPageEdit.pageID.userID
	 * Note that it excludes the windowName, which is added at the end after the 
	 * first ajax request. 
	 * 
	 * This method only runs during full requests, not during ajax requests. 
	 * 
	 * @return string
	 * 
	 */
	protected function makeProcessKey() {
		
		$process = $this->wire('process');
		
		if($process && ($process instanceof ProcessPageEdit || $process instanceof ProcessPageType)) {
			// good, we'll use it
			$page = $process->getPage();
			if(!$page || !$page->id) return '';
		} else {
			// we don't track this process
			return '';
		}
	
		// NPK = NotificationProcessKey
		$processKey = "NPK." . 
			$process->className() . 
			"." . $page->id . 
			"." . $this->wire('user')->id; 
	
		// reset because non-ajax request
		$this->wire('session')->remove($this, 'notifiedProcessKeys');

		return $processKey;
	}

	/**
	 * Given a processKey, check for conflicts with other active processKeys
	 * 
	 * @param $processKey
	 * 
	 */
	protected function checkProcessKey($processKey) {
		
		list($prefix, $className, $pageID, $userID, $windowName) = explode('.', $processKey);
		if($userID) {} // unused
		
		// locate all currently active processKeys editing $page
		$processKeys = $this->wire('cache')->get("$prefix.$className.$pageID.*");
		
		$notified = $this->wire('session')->getFor($this, 'notifiedProcessKeys'); 
		if(!is_array($notified)) $notified = array();
		
		foreach($processKeys as $_processKey => $times) {
			
			if(isset($notified[$processKey]) && in_array($_processKey, $notified[$processKey])) continue;
			
			list($created, $modified) = explode(":", $times); 	
			list($_prefix, $_className, $_pageID, $_userID, $_windowName) = explode('.', $_processKey);
			if($modified || $_prefix || $_className || $_pageID) {} // unused
			$recordNotify = false;
			
			if($_userID == $this->wire('user')->id) {
				// same user
				if($_windowName == $windowName) {
					// this is the window we are already in and this is OK to skip
				} else {
					// editing in different window
					$this->wire('user')->notifications()->error(
						sprintf(
							$this->_('Warning: you are editing this page in another window (editing started %s)'), 
								wireDate('%X', (int) $created)), 
							Notification::flagAnnoy | Notification::flagSession);
					$recordNotify = true; 
				}
				
			} else {
				// different user
				$editingUser = $this->wire('users')->get((int) $_userID);
				if($editingUser->id) {
					$this->wire('user')->notifications()->error(
						sprintf($this->_('Warning: user "%s" is currently editing this page (editing started %s)'),
							$editingUser->name, wireDate('%X', (int) $created)),
								Notification::flagAnnoy | Notification::flagSession);
					$recordNotify = true; 
				}
			}
		
			if($recordNotify) {
				if(isset($notified[$processKey])) $notified[$processKey][] = $_processKey;
					else $notified[$processKey] = array($_processKey);
			}
		}
		
		if(count($notified)) {
			$this->wire('session')->setFor($this, 'notifiedProcessKeys', $notified); 
		}
	}


}

