<?php

/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.2.3
 * Item Marking
 * Owner: Matt "Oh Lord, why did I get assigned this?" Mecham
 * Last Updated: $Date: 2011-06-07 15:07:17 -0400 (Tue, 07 Jun 2011) $
 * </pre>
 *
 * @author 		$Author: mmecham $
 * @copyright	(c) 2001 - 2009 Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/community/board/license.html
 * @package		IP.Board
 * @link		http://www.invisionpower.com
 * @since		9th March 2005 11:03
 * @version		$Revision: 8994 $
 *
 * @todo 	[Future] Perhaps find more ways to reduce the size of items_read
 * @todo 	[Future] Maybe add an IN_DEV tool to warn when markers have been cancelled out / size drastically reduced
 */

class classItemMarking
{
	/**#@+
	 * Registry objects
	 *
	 * @access	protected
	 * @var		object
	 */	
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;
	/**#@-*/
	
	/**
	 * Dead session data
	 *
	 * @access	protected
	 * @var		array
	 */
	protected $_deadSessionData	= array();
	
	/**
	 * Module apps
	 *
	 * @access	protected
	 * @var		array
	 */
	protected $_moduleApps		= array();
	
	/**
	 * Cache for internal use
	 *
	 * @access	protected
	 * @var		array
	 */
	protected $_cache				= array();
	
	/**
	 * Cookie Data
	 *
	 * @access	protected
	 * @var		array
	 */
	protected $_cookie			= array();
	
	/**
	 * Item Markers Data
	 *
	 * @access	protected
	 * @var		array
	 */
	protected $_itemMarkers		= array();
	
	/**
	 * Last cleared time for key
	 *
	 * @access	protected
	 * @var		int
	 */
	protected $_keyLastCleared	= 0;
	
	/**
	 * DB cut off time stamp
	 *
	 * @access	protected
	 * @var		int
	 */
	protected $_dbCutOff			= 0;
	
	/**
	 * Cookie counter
	 *
	 * @access	protected
	 * @var		int
	 */
	protected $_cookieCounter		= 0;
	
	/**
	 * Last save time
	 *
	 * @access	protected
	 * @var		int
	 */
	protected $_lastSaved			= 0;
	
	/**
	 * Saved session already?
	 * 
	 * @access	protected
	 * @var		bool
	 */
	protected $_savedStorage     = false;
	
	/**
	 * Instant save on/off
	 * 
	 * @access	protected
	 * @var		bool
	 */
	protected $_instantSave     = true;
	
	/**
	 * Changes made flag
	 * 
	 * @access	protected
	 * @var		bool
	 */
	protected $_changesMade     = false;
	
	/**
	 * Enable cookies?
	 *
	 */
	const ALLOWCOOKIES			 = true;
	
	/**
	 * Can compress cookies
	 */
	protected $_canCompressCookies = false;
	
	/**
	 * Disable all marking for guests
	 */
	protected $_noGuestMarking     = false;
	
	protected $_setSaveCookie      = false;
	
	/**
	 * Method constructor
	 *
	 * @access	public
	 * @param	object		Registry Object
	 * @return	@e void
	 */
	public function __construct( ipsRegistry $registry )
	{
		$_NOW = IPSDebug::getMemoryDebugFlag();
	
		/* Make object */
		$this->registry   =  $registry;
		$this->DB	      =  $this->registry->DB();
		$this->settings   =& $this->registry->fetchSettings();
		$this->request    =& $this->registry->fetchRequest();
		$this->member     =  $this->registry->member();
		$this->memberData =& $this->registry->member()->fetchMemberData();
		$this->cache	  =  $this->registry->cache();
		$this->caches     =& $this->registry->cache()->fetchCaches();
		
		/* Test for cookie compression */
		if ( function_exists('gzdeflate') AND function_exists('gzinflate') )
		{
			$this->_canCompressCookies = true;
		}
		
 		/* Task? */
		if ( IPS_IS_TASK === TRUE )
		{
			return;
		}
		
		/* Search engine? */
		if ( $this->member->is_not_human === TRUE )
		{
			return;
		}
	
		/* Use cookie marking for guests */
		if ( ! $this->memberData['member_id'] )
		{
			$this->settings['topic_marking_enable'] = 0;
			
			if ( $this->_noGuestMarking )
			{
				return;
			}
		}
		
		/* Build */
		$this->memberData['item_markers'] = $this->_generateIncomingItemMarkers();

		/* Generate last saved time */
		$this->_lastSaved = time();

		/* Set Up */
		if ( is_array( $this->memberData['item_markers'] ) )
		{
			foreach( $this->memberData['item_markers'] as $app => $key )
			{
				foreach( $this->memberData['item_markers'][ $app ] as $key => $data )
				{
					if ( $app AND $key )
					{
						if ( $data['item_last_saved'] )
						{
							$data['item_last_saved'] = intval( $data['item_last_saved'] );
							
							if ( $data['item_last_saved'] < $this->_lastSaved )
							{
								$this->_lastSaved = $data['item_last_saved'];
							}
						}
						
						$this->_itemMarkers[ $app ][ $key ] = $data;
					}
				}
			}
		}

		/* Fetch cookies */
		foreach( IPSLib::getEnabledApplications() as $_app => $data )
		{
			$_value  = $this->_inflateCookie( IPSCookie::get( 'itemMarking_' . $_app ) );
			$_value2 = $this->_inflateCookie( IPSCookie::get( 'itemMarking_' . $_app . '_items' ) );
			
			$this->_cookie[ 'itemMarking_' . $_app ]           = ( is_array( $_value ) )  ? $_value  : array();
			$this->_cookie[ 'itemMarking_' . $_app . '_items'] = ( is_array( $_value2 ) ) ? $_value2 : array();
			
			/* Clean up  */
			if ( $this->_cookie[ 'itemMarking_' . $_app . '_items'] )
			{
				$_items = ( is_array( $this->_cookie[ 'itemMarking_' . $_app . '_items'] ) ) ? $this->_cookie[ 'itemMarking_' . $_app . '_items'] : unserialize( $this->_cookie[ 'itemMarking_' . $_app . '_items'] );
			
				$this->_cookieCounter = 0;
				arsort( $_items, SORT_NUMERIC );
		
				$_newData = array_filter( $_items, array( $this, 'arrayFilterCookieItems' ) );
				
				$this->_cookie[ 'itemMarking_' . $_app . '_items'] = $_newData;
				
				IPSDebug::addMessage( 'Cookie loaded: itemMarking_' . $_app . '_items' . ' - ' . serialize( $_newData ) );
				IPSDebug::addMessage( 'Cookie loaded: itemMarking_' . $_app . ' - ' . serialize( $_value ) );
			}
		}

		IPSDebug::setMemoryDebugFlag( "Topic markers initialized", $_NOW );
	}
	
	/**
	* Compressed the cookie
	*
	* @access	protected
	* @param	mixed		Data (array or string)
	* @return	string		compressed data
	*/
	protected function _compressCookie( $data )
	{
		if ( $this->_canCompressCookies )
		{
			if ( is_array( $data ) )
			{
				$data = serialize( $data );
			}
			
			$data = strtr( base64_encode( addslashes( @gzcompress( $data ) ) ), '+/=', '-_,' );
		}
		
		return $data;
	}
	
	/**
	* Inflate the cookie
	*
	* @access	protected
	* @param	mixed		Compressed string
	* @return	string		Uncompressed data
	*/
	protected function _inflateCookie( $data )
	{
		/* already inflated? */
		if ( ! $data OR is_array( $data ) )
		{
			return $data;
		}
		
		if ( $this->_canCompressCookies )
		{
			$data = @gzuncompress( stripslashes( base64_decode( strtr( $data, '-_,', '+/=' ) ) ) );
			
			if ( substr( $data, 0, 2 ) == 'a:' )
			{
				$data = unserialize( $data );
			}
		}
		
		return $data;
	}
		
	/**
	 * Disable instant save
	 *
	 * @access	public
	 * @return	@e void
	 */
	public function disableInstantSave()
	{
		$this->_instantSave = false;
	}
	
	/**
	 * Enable instant save (Default mode anyway)
	 *
	 * @access	public
	 * @return	@e void
	 */
	public function enableInstantSave()
	{
		$this->_instantSave = true;
	}
	
	/**
	 * Fetch status
	 *
	 * @access	public
	 * @return	@e void
	 */
	public function fetchInstantSaveStatus()
	{
		return ( $this->_instantSave === true ) ? true : false;
	}
	
	/**
	 * Add to cookie
	 *
	 * @access	protected
	 * @param	string 		Key
	 * @param	array
	 * @return	@e void
	 */
	protected function _updateCookieData( $key, $array )
	{
		$this->_cookie[ $key ] = $array;
	}
	
	/**
	 * Fetch cookie data
	 *
	 * @access	protected
	 * @param	key
	 * @return	array
	 */
	protected function _fetchCookieData( $key )
	{
		if ( ! self::ALLOWCOOKIES )
		{
			return array();
		}
		
		if ( is_array( $this->_cookie[ $key ] ) )
		{
			return $this->_cookie[ $key ];
		}
		else
		{
			return array();
		}
	}
	
	/**
	 * Public method to get cookie
	 *
	 * @access	public
	 * @param	string		APP
	 * @param	string		Type of key (items, read)
	 * @return	mixed
	 */
	public function fetchCookieData( $app, $type )
	{
		if ( $app )
		{
			$key = ( $type != 'items' ) ? 'itemMarking_' . $app : 'itemMarking_' . $app . '_items';
			
			return $this->_fetchCookieData( $key );
		}
	}
	
	/**
	 * Set the flag to save cookies in mydestruct
	 *
	 * @access	protected
	 * @return	@e void
	 */
	protected function _setSaveCookie()
	{
		$this->_setSaveCookie = true;
	}
	
	/**
	 * Save cookie
	 *
	 * @access	protected
	 * @param	string		Key name (leave blank to save out all cookies)
	 * @return	@e void
	 */
	protected function _saveCookie( $key='' )
	{
		if ( ! self::ALLOWCOOKIES )
		{
			return;
		}

		if ( $key AND is_array( $this->_cookie[ $key ] ) )
		{
			IPSCookie::set( $key, $this->_cookie[ $key ], 1 );
		}
		else
		{
			foreach( $this->_cookie as $k => $v )
			{ 
				if ( is_array( $v ) AND ! count( $v ) )
				{
					/* Do we have a cookie? */
					$test = IPSCookie::get( $k );
					
					if ( $test )
					{ 
						/* set a blank, non sticky cookie */
						IPSCookie::set( $k, '-', 0, -1 );
					}
					else
					{
						continue;
					}
				}
				else
				{
					IPSDebug::addMessage( 'Cookie SAVED: ' . $k . ' - ' .$this->_compressCookie( $v ) );
					IPSCookie::set( $k, $this->_compressCookie( $v ), 1 );
				}
			}
		}
	}
	
	/**
	 * Check the read status of an item
	 *
	 * Forum example (forumID is mapped to :item_app_key_1). itemID is the topic ID
	 *
	 * <code>$read = $itemMarking->isRead( array( 'forumID' => 2, 'itemID' => 99, 'itemLastUpdate' => 1989098989 ) );</code>
	 *
	 * @access	public
	 * @param	array 		Array of data
	 * @param	string		Optional app
	 * @return	boolean     TRUE is read / FALSE is unread
	 */
	public function isRead( $data, $app='' )
	{
		$app         = ( $app ) ? $app : IPS_APP_COMPONENT;
		$data        = $this->_fetchModule( $app )->convertData( $data );
		$_data       = $data;
		$times       = array();
		$cookie      = $this->_fetchCookieData( 'itemMarking_' . $app );
		$cookieItems = $this->_fetchCookieData( 'itemMarking_' . $app . '_items');
		
		if ( ! $this->memberData['member_id'] AND $this->_noGuestMarking )
		{
			return 1;
		}

		unset( $_data['itemLastUpdate'] );
	
		$times[] = $this->fetchTimeLastMarked( $_data, $app );
		
		if ( $data['itemID'] )
		{
			if ( isset( $cookieItems[ $data['itemID'] ] ) )
			{
				$times[] = intval( $cookieItems[ $data['itemID'] ] );
			}
			
			/* Update the main row? */
			if ( $this->settings['topic_marking_enable'] AND isset( $cookieItems[ $data['itemID'] ] ) AND $cookieItems[ $data['itemID'] ] > $times[1] )
			{
				$mainKey = $this->_findMainRowByKey( $data, $app );
				
				if ( $mainKey )
				{
					/* Mark markers as having changed */
					$this->_changesMade = TRUE;
					
					$this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'][ $data['itemID'] ] = $cookieItems[ $data['itemID'] ];
				}
			}
		}
	
		return ( IPSLib::fetchHighestNumber( $times ) >= $data['itemLastUpdate'] ) ? TRUE : FALSE; 
	}
	
	/**
	 * Mark as read
	 *
	 * Forum example (forumID is mapped to :item_app_key_1). itemID is the topic ID
	 *
	 * <code>$itemMarking->markAsRead( array( 'forumID' => 34, 'itemID' => 99 ) );</code>
	 * <code>$itemMarking->markAsRead( array( 'forumID' => 34 ) );</code>
	 * If you pass in a timestamp, it will set that as the read date so a topic, for example
	 * won't appear read until you're on the last page
	 * <code>$itemMarking->markAsRead( array( 'forumID' => 34, 'itemID' => 99, 'markDate' => 123456790 ) );</code>
	 * 
	 * If you pass a "containerLastActivityDate" timestamp it will compare the markDate against it and not run the fetchUnreadCount query
	 * <code>$itemMarking->markAsRead( array( 'forumID' => 34, 'itemID' => 99, 'markDate' => 123456790, 'containerLastActivityDate' => 1234567890 ) );</code>
	 * @access	public
	 * @param	array
	 * @param 	string	[App]
	 * @return	@e void
	 */
	public function markRead( $data, $app='' )
	{
		$app         			   = ( $app ) ? $app : IPS_APP_COMPONENT;
		$origData   			   = $data;
		$data        			   = $this->_fetchModule( $app )->convertData( $data );
		$cookie      			   = $this->_fetchCookieData( 'itemMarking_' . $app );
		$cookieItems 			   = $this->_fetchCookieData( 'itemMarking_' . $app . '_items');
		$mainKey     			   = $this->_findMainRowByKey( $data, $app );
		$markDate    			   = ( ! empty( $data['markDate'] ) ) ? intval( $data['markDate'] ) : time();
		$containerLastActivityDate = ( ! empty( $data['containerLastActivityDate'] ) ) ? intval( $data['containerLastActivityDate'] ) : false;
		
		/* Search engine? */
		if ( $this->member->is_not_human === TRUE )
		{
			return;
		}
		
		if ( ! $this->memberData['member_id'] AND $this->_noGuestMarking )
		{
			return;
		}
		
		/* Reset flag */
		$this->_savedStorage = FALSE;
		
		/* Mark markers as having changed */
		$this->_changesMade = TRUE;
		
		if ( $data['itemID'] )
		{
			/* Cookie */
			$cookieItems[ $data['itemID'] ] = $markDate;
			$this->_updateCookieData( 'itemMarking_' . $app . '_items', $cookieItems );
			
			$_cookieGreset = isset( $cookie['greset'][ $this->_makeKey( $data, TRUE ) ] ) ? intval( $cookie['greset'][ $this->_makeKey( $data, TRUE ) ] ) : 0;
			$_readItems    = $cookieItems; 
			
			/* Do we need to clean up? */
			if ( $this->settings['topic_marking_enable'] )
			{
				/* DB */
				$this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'][ $data['itemID'] ] = $markDate;
				
				/* Check the cookie again.. */
				if ( $_cookieGreset AND $_cookieGreset > $this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] )
				{
					$this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] = $_cookieGreset;
				}
				
				/* Overwrite read items */
				if ( is_array( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) )
				{
					foreach( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] as $i => $d )
					{
						$_readItems[ $i ] = $d;
					}
				}
			}
			
			/* Check last reset time */
			$_lastReset = IPSLib::fetchHighestNumber( array( $_cookieGreset, isset( $cookie['global']) ? $cookie['global'] : 0, intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] ), intval( $this->memberData['_cache']['gb_mark__'. $app ] ) ) );
			$_doQuery   = ( $this->settings['topic_marking_enable'] OR ( ! $this->settings['topic_marking_enable'] AND ! $this->settings['topic_marking_noquery'] ) );
			
			/* Fetch unread items */
			if ( $_doQuery === true )
			{
				$_oldest	  = IPS_UNIX_TIME_NOW - ( $this->settings['topic_marking_keep_days'] * 86400 );
				$_lastReset   = ( $_lastReset >= $_oldest ) ? $_lastReset : $_oldest;
				
				$_unreadCount = $this->_fetchModule( $app )->fetchUnreadCount( $origData, $_readItems, $_lastReset );
			
				if ( $_unreadCount['count'] > 0 )
				{
					if ( $this->settings['topic_marking_enable'] )
					{
						$this->_itemMarkers[ $app ][ $mainKey ]['item_unread_count'] = $_unreadCount['count'];
					}
				}
				else
				{
					if ( $this->settings['topic_marking_enable'] )
					{
						/* Update the last global reset time and clear the read array */
						$this->_itemMarkers[ $app ][ $mainKey ]['item_read_array']   = array();
						$this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] = time();
						$this->_itemMarkers[ $app ][ $mainKey ]['item_unread_count'] = 0;
					}
					
					/* Cookie */
					$cookie['greset'][ $this->_makeKey( $data, TRUE ) ] = time();
					$this->_updateCookieData( 'itemMarking_' . $app, $cookie );
					$this->_updateCookieData( 'itemMarking_' . $app . '_items', array() );
				}
			}
		}
		else
		{
			if ( $this->settings['topic_marking_enable'] )
			{
				/* Update the last global reset time and clear the read array */
				$this->_itemMarkers[ $app ][ $mainKey ]['item_read_array']   = array();
				$this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] = time();
				$this->_itemMarkers[ $app ][ $mainKey ]['item_unread_count'] = 0;
			}
			
			/* Cookie */
			$cookie['greset'][ $this->_makeKey( $data, TRUE ) ] = time();
			$this->_updateCookieData( 'itemMarking_' . $app, $cookie );
			$this->_updateCookieData( 'itemMarking_' . $app . '_items', array() );
		}
		
		/* Add in global update */
		$this->_itemMarkers[ $app ][ $mainKey ]['item_last_update'] = time();
		
		/* Save cookie */
		$this->_setSaveCookie();
		
		/* Save markers */
		if ( $this->fetchInstantSaveStatus() )
		{
			$this->_saveStorage();
		}
	}
	
	/**
	 * Marks everything within an app as read
	 *
	 * @access	public
	 * @param	string	[App Optional. If ommited, all apps are marked as read]
	 * @return	@e void
	 */
	public function markAppAsRead( $app='' )
	{
		/* Search engine? */
		if ( $this->member->is_not_human === TRUE )
		{
			return;
		}
		
		/* Cookie */
		$cookie['global'] = time();
		$cookie['greset'] = array();
		
		/* Reset flag */
		$this->_savedStorage = FALSE;
		
		/* Mark markers as having changed */
		$this->_changesMade = TRUE;
		
		/* One app or all? */
		if ( $app )
		{
			/* Reset member cache */
			IPSMember::packMemberCache( $this->memberData['member_id'], array('gb_mark__' . $app => time() ) );
			
			/* Mark rows for deletion */
			$this->DB->update( 'core_item_markers', array( 'item_is_deleted' => 1 ), "item_app='" . $app . "' AND item_member_id=" . intval( $this->memberData['member_id'] ) );
			
			/* Update the last global reset time and clear the read array */
			$this->_itemMarkers[ $app ][ $_key ]['item_read_array']   = array();
			$this->_itemMarkers[ $app ][ $_key ]['item_global_reset'] = time();
			$this->_itemMarkers[ $app ][ $_key ]['item_unread_count'] = 0;
			$this->_itemMarkers[ $app ][ $_key ]['item_last_update']  = time();
			
			/* Save markers */
			if ( $this->fetchInstantSaveStatus() )
			{
				$this->_saveStorage();
			}
			
			/* Update cookies */
			$this->_updateCookieData( 'itemMarking_' . $app . '_items', array() );
			$this->_updateCookieData( 'itemMarking_' . $app, $cookie );
		}
		else
		{
			/* Do 'em all */
			$cache = array();
			
			/* Bug #23130 If DB marking is off, _itemMarkers is empty, so we just assume forums */
			if ( ! count( $this->_itemMarkers ) )
			{
				$this->_itemMarkers = array( 'forums' => array() );
			}
			
			foreach( $this->_itemMarkers as $app => $data )
			{
				/* Only process is the app actually does topic marking */
				$f = IPSLib::getAppDir( $app ) . '/extensions/coreExtensions.php';
				
				if ( is_file( $f ) )
				{
					$classToLoad = IPSLib::loadLibrary( $f, 'itemMarking__'.$app, $app );
					
					if ( !class_exists( $classToLoad ) )
					{
						continue;
					}
				}
				
				$cache[ 'gb_mark__' . $app ] = time();
				
				/* Update cookies */
				$this->_updateCookieData( 'itemMarking_' . $app . '_items', array() );
				$this->_updateCookieData( 'itemMarking_' . $app, $cookie );
			}
			
			if ( count( $cache ) )
			{
				/* Reset member cache */
				IPSMember::packMemberCache( $this->memberData['member_id'], $cache );
			}
			
			/* Delete temporary storage */
			$this->DB->delete( 'core_item_markers_storage', 'item_member_id=' . intval( $this->memberData['member_id'] ) );
			
			/* Mark rows for deletion */
			$this->DB->update( 'core_item_markers', array( 'item_is_deleted' => 1 ), 'item_member_id=' . intval( $this->memberData['member_id'] ) );
			
			/* Reset internal array */
			$this->_itemMarkers = array();
			
			/* Reset flag */
			$this->_savedStorage = TRUE;
		}
		
		/* Save cookie */
		$this->_setSaveCookie();
	}
	
	/**
	 * Fetch the last time an item was marked
	 * checks app reset... then the different key resets.. then an individual key
	 *
	 * In this example: itemID is within 'item_read_array'.
	 * $lastReset = $this->fetchTimeLastMarked( array( 'forumID' => 2, 'itemID' => '99' ) );
	 *
	 * @access	public
	 * @param	array 		Data
	 * @param	string		Optional app
	 * @return	int 		Timestamp item was last marked or 0 if unread
	 */
	public function fetchTimeLastMarked( $data, $app='' )
	{
		$app   = ( $app ) ? $app : IPS_APP_COMPONENT;
		$data  = $this->_fetchModule( $app )->convertData( $data );

		$times = array();
		
		$times[] = intval( $this->_findLatestGlobalReset( $data, $app ) );

		if ( isset( $data['itemID'] ) )
		{
			$times[] = intval( $this->_findLatestItemReset( $data, $app ) );
		}
		
		return IPSLib::fetchHighestNumber( $times );
	}
	
	/**
	 * Fetch the unread count
	 *
	 * In this example we are retrieving the number of unread items for a forum id 2
	 * $unreadCount = $this->fetchUnreadCount( array( 'forumID' => 2 ) );
	 *
	 * @access	public
	 * @param	array 		Data
	 * @param	string		App
	 * @return	int 		Number of unread items left
	 */
	public function fetchUnreadCount( $data, $app='' )
	{
		$app   = ( $app ) ? $app : IPS_APP_COMPONENT;
		$data  = $this->_fetchModule( $app )->convertData( $data );

		$times = array();
		
		if ( isset( $data['item_app_key_1'] ) )
		{
			$mainKey  = $this->_findMainRowByKey( $data, $app );
			
			return intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_unread_count'] );
		}

		return 0;
	}
	
	/**
	 * Fetches the oldest unread timestamp
	 *
	 * In this example we are retrieving the number of unread items for a forum id 2
	 * $unreadCount = $this->fetchOldestUnreadTimestamp( array( 'forumID' => 2 ) );
	 *
	 * @access	public
	 * @param	array 		Data
	 * @param	string		App
	 * @return	int 		Number of unread items left
	 */
	public function fetchOldestUnreadTimestamp( $data, $app='' )
	{
		$app   = ( $app ) ? $app : IPS_APP_COMPONENT;
		$data  = $this->_fetchModule( $app )->convertData( $data );
		$time  = IPS_UNIX_TIME_NOW;
		
		if ( ! isset( $data['item_app_key_1'] ) )
		{
			if ( is_array( $this->_itemMarkers[ $app ] ) AND count( $this->_itemMarkers[ $app ] ) )
			{
				foreach( $this->_itemMarkers[ $app ] as $key => $_data )
				{
					if ( $_data['item_global_reset'] && ( $_data['item_global_reset'] < $time ) )
					{
						$time = $_data['item_global_reset'];
					}
				}
			}
		}
		else
		{
			$mainKey  = $this->_findMainRowByKey( $data, $app );
			$_data    = $this->_itemMarkers[ $app ][ $mainKey ];
			
			if ( $_data['item_global_reset'] && ( $_data['item_global_reset'] < $time ) )
			{
				$time = $_data['item_global_reset'];
			}
		}
		
		return $time;
	}

	
	/**
	 * Fetch read IDs since last global reset
	 * Returns IDs read based on incoming data
	 *
	 * <code>$readIDs = $itemMarking->fetchReadIds( array( 'forumID' => 2 ) );</code>
	 * @access		public
	 * @param		array 		Array of data
	 * @param		strng		[App]
	 * @return		array 		Array of read item IDs
	 */
	public function fetchReadIds( $data, $app='', $keysOnly=true )
	{
		$app      = ( $app ) ? $app : IPS_APP_COMPONENT;
		$origData = $data;
		$data     = $this->_fetchModule( $app )->convertData( $data );
		$mainKey  = $this->_findMainRowByKey( $data, $app );
		
		if ( isset( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) AND is_array($this->_itemMarkers[ $app ][ $mainKey ]['item_read_array']) )
		{
			return ( $keysOnly ) ? array_keys( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) : $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'];
		}
		else
		{
			return array();
		}
	}
	
	/**
	 * Fetch read ID
	 * If forum was cleared, it may not exist as global reset takes precidence. 
	 * <code>$readIDs = $itemMarking->fetchReadId( array( 'forumID' => 2, 'itemId' => 2 ) );</code>
	 * @access		public
	 * @param		array 		Array of data
	 * @param		strng		[App]
	 * @return		int			0 or timestamp
	 */
	public function fetchItemReadTime( $data, $app='' )
	{
		$app      = ( $app ) ? $app : IPS_APP_COMPONENT;
		$origData = $data;
		$data     = $this->_fetchModule( $app )->convertData( $data );
		$mainKey  = $this->_findMainRowByKey( $data, $app );
		
		if ( isset( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) AND is_array($this->_itemMarkers[ $app ][ $mainKey ]['item_read_array']) )
		{
			return intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'][ $data['itemID'] ] );
		}
		else
		{
			return 0;
		}
	}
	
	/**
	 * Save my markers to DB
	 *
	 * Saves the current values in $this->_itemMarkers back to the DB
	 *
	 * @access	public
	 * @return	int		Number of rows saved
	 * @deprecated 3.1.0	Is this really deprecated?  It's still being called external to this class by sessions and markasread functions
	 */
	public function writeMyMarkersToDB()
	{
		if ( ! $this->settings['topic_marking_enable'] )
		{
			return 0;
		}
		
		/* Reset flag */
		$this->_savedStorage = FALSE;
		
		/* Mark markers as having changed */
		$this->_changesMade = TRUE;
		
		$_saved = 0;
		
		if ( $this->memberData['member_id'] AND is_array( $this->_itemMarkers ) AND $this->settings['topic_marking_enable'] )
		{
			foreach( $this->_itemMarkers as $app => $data )
			{
				foreach( $this->_itemMarkers[ $app ] as $_key => $_data )
				{
					/* Update internal marker array */
					$this->_itemMarkers[ $app ][ $_key ]['item_last_saved'] = time();
					
					$this->_save( $this->memberData['member_id'], $_key, $_data, $app );
					$_saved++;
				}
			}
		}
	
		return $_saved;
	}
		
	/**
	 * Find latest Item reset
	 *
	 * @access	protected
	 * @param	array 		Array of data (DB field names)
	 * @param	string 		App to check
	 * @return	int			Timestamp
	 */
	protected function _findLatestItemReset( $data, $app )
	{
		$cookie      = $this->_fetchCookieData( 'itemMarking_' . $app );
		$cookieItems = $this->_fetchCookieData( 'itemMarking_' . $app . '_items');
		$_times      = array();
		
		if ( ! $this->memberData['member_id'] AND $this->_noGuestMarking )
		{
			return time();
		}
		
		$mainKey = $this->_findMainRowByKey( $data, $app );
		$mainRow = $this->_itemMarkers[ $app ][ $mainKey ];
		
		/* Got a DB field? */
		if ( isset( $mainRow['item_read_array'] ) AND is_array( $mainRow['item_read_array'] ) )
		{
			if( isset( $mainRow['item_read_array'][ $data['itemID'] ] ) )
			{
				$_times[] = intval( $mainRow['item_read_array'][ $data['itemID'] ] );
			}
		}
		
		/* Got a cookie field? */
		if ( isset( $cookieItems[ $data['itemID'] ] ) )
		{
			$_times[] = $cookieItems[ $data['itemID'] ];
		}
		
		$_time = IPSLib::fetchHighestNumber( $_times );
		
		return $_time;
	}
		
	/**
	 * Find latest Global reset
	 *
	 * @access	protected
	 * @param	array 		Array of data (DB field names)
	 * @param	string 		App to check
	 * @return	int			Timestamp
	 */
	protected function _findLatestGlobalReset( $data, $app )
	{
		$_time       = 0;
		$_times      = array();
		$_key        = $this->_makeKey( $data );
		$cookie      = $this->_fetchCookieData( 'itemMarking_' . $app );
		$cookieItems = $this->_fetchCookieData( 'itemMarking_' . $app . '_items');
		
		if ( ! $this->memberData['member_id'] AND $this->_noGuestMarking )
		{
			return time();
		}

		$mainKey = $this->_findMainRowByKey( $data, $app );
		
		/* Got all fields? */
		if ( isset( $this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] ) )
		{
			$_times[] = $this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'];
		}
		
		if ( isset( $cookie['greset'][ $this->_makeKey( $data, TRUE ) ] ) )
		{
			$_times[] = intval( $cookie['greset'][ $this->_makeKey( $data, TRUE ) ] );
		}
		
		if ( isset( $cookie['global'] ) )
		{
			$_times[] = intval( $cookie['global'] );
		}
		
		if ( isset( $this->memberData['_cache']['gb_mark__'. $app ] ) )
		{
			$_times[] = $this->memberData['_cache']['gb_mark__'.  $app ];
		}
		
		/* Get most recent time */
		$_time = IPSLib::fetchHighestNumber( $_times );
		
		return $_time;
	}
	
	/**
	 * Find a particular row
	 *
	 * @access	protected
	 * @param	array 		Array of data
	 * @param	string		App
	 * @return	array 		Array of data: core_item_marking row, effectively
	 */
	protected function _findMainRowByKey( $data, $app )
	{
		/* Not interested in this for the main row */
		unset( $data['itemID'], $data['itemLastUpdate'] );
		
		$_key  = $this->_makeKey( $data );
		
		if ( ! isset( $this->_itemMarkers[ $app ] ) OR ! is_array( $this->_itemMarkers[ $app ] ) )
		{
			/* Mark markers as having changed */
			$this->_changesMade = TRUE;
			
			/* Add in extra items */
			$data['item_app']		 = $app;
			$data['item_key']		 = $_key;
			$data['item_member_id']	 = $this->memberData['member_id'];
			$data['item_read_array'] = array();
			$this->_itemMarkers[ $app ]          = array();
			$this->_itemMarkers[ $app ][ $_key ] = $data;
			
			IPSDebug::addMessage( "Item Marking Key Created! $_key" );
			
			return $_key;
		}
		
		if ( ! empty( $this->_itemMarkers[ $app ][ $_key ] ) AND is_array( $this->_itemMarkers[ $app ][ $_key ] ) )
		{
			/* Make sure it contains the app & key */
			$this->_itemMarkers[ $app ][ $_key ]['item_app']		= $app;
			$this->_itemMarkers[ $app ][ $_key ]['item_key']		= $_key;
			$this->_itemMarkers[ $app ][ $_key ]['item_member_id']	= $this->memberData['member_id'];
			
			/* Make sure read IDs are unserialized */
			if ( isset( $this->_itemMarkers[ $app ][ $_key ]['item_read_array'] ) AND ! is_array( $this->_itemMarkers[ $app ][ $_key ]['item_read_array'] ) )
			{
				$this->_itemMarkers[ $app ][ $_key ]['item_read_array'] = unserialize( $this->_itemMarkers[ $app ][ $_key ]['item_read_array'] );
			}
			
			return $_key;
		}
		else
		{
			/* Mark markers as having changed */
			$this->_changesMade = TRUE;
			
			/* Make sure it contains the app & key */
			$this->_itemMarkers[ $app ][ $_key ]['item_app']		= $app;
			$this->_itemMarkers[ $app ][ $_key ]['item_key']		= $_key;
			$this->_itemMarkers[ $app ][ $_key ]['item_member_id']	= $this->memberData['member_id'];
			$this->_itemMarkers[ $app ][ $_key ]['item_read_array'] = array();
			$this->_itemMarkers[ $app ][ $_key ] = $data;
			
			IPSDebug::addMessage( "Item Marking Key returned! $_key" );

			return $_key;
		}
		
		/* Mark markers as having changed */
		$this->_changesMade = TRUE;
		
		/* Create a new key ... */
		$data['item_app']		 = $app;
		$data['item_key']		 = $_key;
		$data['item_member_id']	 = $this->memberData['member_id'];
		$data['item_read_array'] = array();
		
		$this->_itemMarkers[ $app ]          = array();
		$this->_itemMarkers[ $app ][ $_key ] = $data;
		
		return $_key;
	}
	
	/**
	 * Fetch module class
	 *
	 * @access	protected
	 * @param	string			App to fetch
	 * @return	object
	 */
	protected function _fetchModule( $app )
	{
		$app = ( $app ) ? $app : IPS_APP_COMPONENT;
		
		if ( isset( $this->_moduleApps[ $app ] ) && is_object( $this->_moduleApps[ $app ] ) )
		{
			return $this->_moduleApps[ $app ];
		}
		else
		{
			$_file = IPSLib::getAppDir( $app ) . '/extensions/coreExtensions.php';
			
			if ( is_file( $_file ) )
			{
				$classToLoad = IPSLib::loadLibrary( $_file, 'itemMarking__'.$app, $app );
				
				if( class_exists( $classToLoad ) )
				{
					$this->_moduleApps[ $app ] = new $classToLoad( $this->registry );
					
					return $this->_moduleApps[ $app ];
				}
				else
				{
					throw new Exception( "No itemMarking class available for $app" );
				}
			}
			else
			{
				throw new Exception( "No itemMarking class available for $app" );
			}
		}
	}

	/**
	 * Make a new key
	 *
	 * @access	protected
	 * @param   array
	 * @param	bool		Is cookie?
	 * @return	string
	 */
	protected function _makeKey( $data, $isCookie=FALSE )
	{
		/* Not interested in this for the main row */
		unset( $data['itemID'], $data['itemLastUpdate'], $data['containerLastActivityDate'], $data['markDate'] );
		
		return ( $isCookie === TRUE ) ? $data['item_app_key_1'] : md5( serialize( $data ) );
	}
	
	/**
	 * Manual clean up
	 * Currently run via a task and in the __myDestruct method, but can be run from anywhere
	 *
	 * @access	public
	 * @param	int 		Number of items to limit
	 * @param	int			Number of hours (ago) to check
	 * @param	boolean		Check for active sessions first
	 * @return	int			Number of items cleaned
	 */
	public function manualCleanUp( $limit=0, $hours=6, $checkSessions=TRUE )
	{
		$sessions   = array();
		$markers    = array();
		$_hoursAgo  = time() - ( 3600 * intval( $hours ) );
		$_limit	    = ( $limit ) ? array( 0, $limit ) : '';
		
		/* If we're not DB tracking, ignore... */
		if ( ! $this->settings['topic_marking_enable'] )
		{
			return FALSE;
		}
		
		/* Grab all active sessions */
		if ( $checkSessions === TRUE )
		{
			$this->DB->build( array( 'select' => 'id, member_id, running_time',
									 'from'   => 'sessions' ) );
								
			$this->DB->execute();
		
			while( $row = $this->DB->fetch() )
			{
				$sessions[ $row['member_id'] ] = $row;
			}
		}
		else if ( $this->memberData['member_id'] )
		{
			/* Make sure our row isn't interupted */
			$sessions[ $this->memberData['member_id'] ] = $this->memberData['member_id'];
		}
		
		/* Now grab 100 inactive markers */
		$this->DB->build( array( 'select' => 'item_member_id, item_last_updated, item_last_saved',
								 'from'   => 'core_item_markers_storage',
								 'where'  => 'item_last_saved < ' . $_hoursAgo,
								 'order'  => 'item_last_saved ASC',
								 'limit'  => $_limit ) );
								
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			/* Not a current session? */
			if ( ! $sessions[ $row['item_member_id'] ] )
			{
				$markers[] = $row['item_member_id'];
			}
		}
		
		/* Now save 'em */
		if ( count( $markers ) )
		{
			$this->_save( $markers );
		}

		return intval( count( $markers ) );
	}
	
	/**
	 * Update topic markers
	 *
	 * @access	protected
	 * @param	array		Array of Member IDs
	 * @return	@e void
	 */
	protected function _save( $memberIDs )
	{
		if ( ! $this->settings['topic_marking_enable'] )
		{
			return FALSE;
		}
		
		$tmpMarkers  = array();
		$markingData = array();
		
		if ( is_array( $memberIDs ) AND count( $memberIDs ) )
		{
			/* Load temp markers */
			$this->DB->build( array( 'select' => '*',
								 	 'from'   => 'core_item_markers_storage',
								     'where'  => 'item_member_id IN (' . implode( ',', $memberIDs ) . ')' ) );
								
			$this->DB->execute();
			
			while( $row = $this->DB->fetch() )
			{
				$markingData[ $row['item_member_id'] ] = $row;
			}
			
			/* Now loop through and process */
			foreach( $markingData as $memberID => $dbData )
			{
				$itemMarkers = unserialize( $dbData['item_markers'] );
				
				if ( is_array( $itemMarkers ) AND count( $itemMarkers ) )
				{
					foreach( $itemMarkers as $app => $data )
					{
						foreach( $itemMarkers[ $app ] as $key => $data )
						{
							if ( $key AND $app AND $memberID )
							{
								$row = array( 'item_key'          => $key,
											  'item_member_id'    => $memberID,
											  'item_app'	      => $app,
											  'item_last_saved'   => time(),
											  'item_read_array'   => ( is_array( $data['item_read_array'] ) ) ? serialize( $data['item_read_array'] ) : $data['item_read_array'],
											  'item_global_reset' => intval( $data['item_global_reset'] ),
											  'item_last_update'  => intval( $data['item_last_update'] ),
											  'item_is_deleted'   => 0 );
								
								/* Ensure $data is valid */
								foreach( $data as $k => $v )
								{
									if ( ! strstr( $k, 'item_' ) )
									{
										unset( $data[ $k ] );
									}
								}
								
								/* Overwrite $data with the values in $row */
								$new_array = array_merge( $data, $row );
								
								/* Weed out useless rows */
								if ( ! $new_array['item_global_reset'] AND ! $new_array['item_unread_count'] AND ( ! $new_array['item_read_array'] OR $new_array['item_read_array'] == 'a:0:{}' ) )
								{
									$new_array['item_is_deleted'] = 1;
									IPSDebug::addLogMessage( "Item Markers NOT saved", 'marker_skipped-' . $memberID, $new_array );
								}
								
								$this->DB->replace( 'core_item_markers', $new_array, array( 'item_key', 'item_app', 'item_member_id' ) );
							}
						}
					}
				}
			}
			
			/* Now remove them */
			$this->DB->delete( 'core_item_markers_storage', 'item_member_id IN (' . implode( ',', $memberIDs ) . ')' );
		}
	}
	
	/**
	 * Manual destructor called by ips_MemberRegistry::__myDestruct()
	 * Gives us a chance to do anything we need to do before other
	 * classes are culled
	 *
	 * @access	public
	 * @return	@e void
	 */
	public function __myDestruct()
	{
		/* Task? */
		if ( IPS_IS_TASK === TRUE )
		{
			return;
		}
		
		/* Search engine? */
		if ( $this->member->is_not_human === TRUE )
		{
			return;
		}
		
		/* Save cookies */
		if ( $this->_setSaveCookie )
		{
			$this->_saveCookie();
		}
		
		/* Save the storage table if not done so already */
		$this->_saveStorage();
		
		/* Grab sessions to be removed */
		if ( $this->settings['topic_marking_enable'] )
		{
			/* Check every 5 mins */
			$cache  = $this->caches['systemvars'];
			
			/* Fail safe */
			if ( ! $cache['itemMarkerClean'] )
			{
				$cache['itemMarkerClean'] = 0;
			}

			if ( time() - 300 > $cache['itemMarkerClean'] )
			{
				/* session_expiration is seconds */
				$_time = ceil( $this->settings['session_expiration'] / 3600 );
			
				/* less than an hour? */
				if ( $_time < 1 )
				{
					$_time = 1;
				}
				
				/* Save Cache BEFORE running clean up to stop race conditions */
				$cache['itemMarkerClean'] = time();
				$this->cache->setCache( 'systemvars', $cache, array( 'array' => 1, 'donow' => 1 ) );
			
				/* Clean 'em up */
				$_c = $this->manualCleanUp( 50, ++$_time, FALSE );
			
				if ( $_c )
				{
					IPSDebug::addLogMessage( "Manually cleaned up: " . $_c . ' hit by ID ' . $this->memberData['member_id'], 'markerClean' );
				}
			}
		}
	}
	
	/**
	 * Update markers storage table
	 *
	 * @access	protected
	 * @return	bool
	 */
	protected function _saveStorage()
	{
		/* Already stored and no further updates? */
		if ( $this->_savedStorage )
		{
			return false;
		}
		
		/* Mark markers changed? */
		if ( $this->_changesMade !== TRUE )
		{
			return false;
		}
		
		/* Update storage table */
		if( ( $this->memberData['member_id'] AND $this->settings['topic_marking_enable'] ) AND count($this->_itemMarkers) AND is_array($this->_itemMarkers) )
		{ 
			$this->DB->replace( 'core_item_markers_storage', array( 'item_member_id'    => $this->memberData['member_id'],
																	'item_markers'      => serialize( $this->_itemMarkers ),
																	'item_last_saved'   => time() ), array( 'item_member_id' ) );
		}
		
		$this->_savedStorage = TRUE;
		
		return true;
	}
	
	/**
	 * Build item markers.
	 * Ok, this function attempts to gather the corret item markers. 
	 * The function also loads data from the markers DB and attempts to build a 'merged' set of markers
	 * based on all the data it has.
	 *
	 * @access	protected
	 * @return	array
	 */
	protected function _generateIncomingItemMarkers()
	{
		$items = NULL;
		
		/* Not playing? */
		if ( ! $this->settings['topic_marking_enable'] )
		{
			return array();
		}
		
		/* Not a member */
		if ( ! $this->memberData['member_id'] )
		{
			return array();
		}
		
		/* Already got some data? */
		if ( $this->memberData['item_markers'] )
		{
			$items = ( is_array( $this->memberData['item_markers'] ) ) ? $this->memberData['item_markers'] : unserialize( $this->memberData['item_markers'] );
		}
		
		if ( ! is_array( $items ) OR ! count( $items ) )
		{
			/* Ensure they are written back correctly */
			$this->_changesMade = TRUE;
			
			$this->DB->build( array( 'select' => '*',
									 'from'   => 'core_item_markers',
									 'where'  => 'item_member_id=' . $this->memberData['member_id'] ) );

			$itemMarking = $this->DB->execute();

			while( $row = $this->DB->fetch( $itemMarking ) )
			{
				if ( ! $row['item_is_deleted'] )
				{
					$items[ $row['item_app'] ][ $row['item_key'] ] = $row;
				}
			}
		}
		
		/* Check data */
		if ( is_array( $items ) AND count( $items ) )
		{
			foreach( $items as $app => $item )
			{
				foreach( $item as $key => $_data )
				{
					if ( $app AND $key )
					{
						/* Ensure INT values */
						$items[ $app ][ $key ]['item_last_update']  = intval( $_data['item_last_update'] );
						$items[ $app ][ $key ]['item_global_reset'] = intval( $_data['item_global_reset'] );
						$items[ $app ][ $key ]['item_unread_count'] = intval( $_data['item_unread_count'] );
						$items[ $app ][ $key ]['item_member_id']    = intval( $_data['item_member_id'] );
						$items[ $app ][ $key ]['item_last_saved']   = intval( $_data['item_last_saved'] );
					
						/* Ensure read array is unserialized correctly */
						if ( isset( $_data['item_read_array'] ) AND ! is_array( $_data['item_read_array'] ) )
						{
							$items[ $app ][ $key ]['item_read_array'] = unserialize( $_data['item_read_array'] );
						}
						
						/* Now clean it up */
						if ( isset( $_data['item_read_array'] ) AND is_array( $items[ $app ][ $key ]['item_read_array'] ) )
						{
							/* Remove items that are older than the last marked time */
							if ( $items[ $app ][ $key ]['item_global_reset'] )
							{
								$this->_keyLastCleared = intval( $_data['item_global_reset'] );
								$items[ $app ][ $key ]['item_read_array'] = array_filter( $items[ $app ][ $key ]['item_read_array'], array( $this, 'arrayFilterRemoveAlreadyClearedItems' ) );
							}
						}
						else
						{
							$items[ $app ][ $key ]['item_read_array'] = array();
						}
					}
				}
			}
		}
		else
		{
			return array();
		}

		return $items;
	}
	
	/**
	 * Array sort Used to remove out of date topic marking entries
	 *
	 * @access	public
	 * @param	mixed
	 * @return	mixed
	 * @since	2.0
	 */
    public function arrayFilterRemoveAlreadyClearedItems( $var )
	{
		return $var > $this->_keyLastCleared;
	}
	
	/**
	 * Array sort Used to remove out of date topic marking entries
	 *
	 * @access	public
	 * @param	mixed
	 * @return	mixed
	 * @since	2.0
	 */
    public function arrayFilterCleanReadItems( $var )
	{
		return $var > $this->_dbCutOff;
	}
		
	/**
	 * Array sort Used to make sure there are no more than 50 last read items
	 *
	 * @access	public
	 * @param	mixed
	 * @return	mixed
	 * @since	2.0
	 */
    public function arrayFilterCookieItems( $var )
	{
		$this->_cookieCounter++;
		
		return ( $this->_cookieCounter <= 50 ) ? TRUE : FALSE;
	}

}