<?php

/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.4.6
 * Item Marking
 * Owner: Matt "Oh Lord, why did I get assigned this?" Mecham
 * Last Updated: $Date: 2012-11-06 15:14:35 -0500 (Tue, 06 Nov 2012) $
 * </pre>
 *
 * @author 		$Author: bfarber $
 * @copyright	(c) 2001 - 2009 Invision Power Services, Inc.
 *
 * @package		IP.Board
 *
 * @since		9th March 2005 11:03
 * @version		$Revision: 11565 $
 *
 * @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;
	
	/**
	 * Instant save on/off
	 * 
	 * @access	protected
	 * @var		bool
	 */
	protected $_instantSave     = true;

	
	/**
	 * Enable cookies?
	 *
	 */
	protected $_useCookies			= true;
	
	/**
	 * Can compress cookies
	 */
	protected $_canCompressCookies = false;
	
	/**
	 * Disable all marking for guests
	 */
	protected $_noGuestMarking     = false;
	
	protected $_setSaveCookie      = false;
	
	protected $_dbFields           = array( 'item_key', 'item_member_id', 'item_app', 'item_last_update', 'item_last_saved', 'item_unread_count', 'item_read_array', 'item_global_reset',
											'item_app_key_1', 'item_app_key_2', 'item_app_key_3', 'item_is_deleted' );
	/**
	 * 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();
			
 		/* Task? */
		if ( IPS_IS_TASK === TRUE )
		{
			return;
		}
		
		/* Search engine? */
		if ( $this->member->is_not_human === TRUE )
		{
			return;
		}
		
		/* Track markers for guests? */
		$this->_noGuestMarking = ( $this->settings['topic_marking_guests'] ) ? 0 : 1;
		
		/* Use cookie marking for guests */
		if ( ! $this->memberData['member_id'] )
		{
			$this->settings['topic_marking_enable'] = 0;
			
			if ( $this->_noGuestMarking )
			{
				return;
			}
		}
		
		/* Switch off cookies for members */
		if ( $this->memberData['member_id'] )
		{
			/* If we've disabled DB marking, best use cookies anyway */
			$this->_useCookies = ( $this->settings['topic_marking_enable'] ) ? false : true;
		}
		
		/* Build */
		$this->memberData['item_markers'] = $this->_generateIncomingItemMarkers();

		/* 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'] );
						}
						
						$this->_itemMarkers[ $app ][ $key ] = $data;
					}
				}
			}
		}

		/* Fetch cookies */
		if ( $this->_useCookies )
		{
			/* Test for cookie compression */
			if ( function_exists('gzdeflate') AND function_exists('gzinflate') )
			{
				$this->_canCompressCookies = true;
			}
		
			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'] : json_decode( $this->_cookie[ 'itemMarking_' . $_app . '_items'], true );
				
					$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 = json_encode( $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, '-_,', '+/=' ) ) ) );

			$data = json_decode( $data, true );
		}
		
		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 ( ! $this->_useCookies )
		{
			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 ( ! $this->_useCookies )
		{
			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 );
				}
			}
		}
	}
	
	/**
	 * Fetch marking data via a join
	 * 
	 * <code>$join = $itemMarking->getSqlJoin( array( 'item_app_key_1' => 'prefix.field' ) );</code>
	 * 
	 * @param array $data
	 * @param string $app
	 */
	public function getSqlJoin( $data, $app='' )
	{
		if ( ! $this->memberData['member_id'] OR ! $this->settings['topic_marking_enable'] )
		{
			return false;
		}
		
		$app = ( $app ) ? $app : IPS_APP_COMPONENT;
		
		/* Set up where */
		$where[] = 'itemmarking.item_member_id=' . intval( $this->memberData['member_id'] );
		$where[] = 'itemmarking.item_app=\'' . $app . "'";
		
		foreach( $data as $key => $value )
		{
			if ( strstr( $key, 'item_app_key_' ) )
			{
				$where[] = 'itemmarking.' . $key . '=' . $value;
			} 
		}
		
		/* Still here? */
		return array( 'select' => 'itemmarking.*',
					  'from'   => array( 'core_item_markers' => 'itemmarking' ),
					  'where'  => implode( ' AND ', $where ) );
	}
	
	/**
	 * Sets internal data based on data fetched from a join
	 * @param array $data
	 */
	public function setFromSqlJoin( $data, $app='' )
	{
		$app     = ( $app ) ? $app : IPS_APP_COMPONENT;
		$data    = $this->_fetchModule( $app )->convertData( $data );
		$_key    = array();
		$_data   = array();
		
		/* Check key */
		if ( empty( $data['item_key'] ) )
		{
			return $data;
		}
		
		foreach( $this->_dbFields as $field )
		{
			if ( isset( $data[ $field ] ) )
			{
				$_data[ $field ] = $data[ $field ];
				
				unset( $data[ $field ] );
			}
		}
		
		if ( count( $_data ) && empty( $_data['item_is_deleted'] ) )
		{
			if ( IPSLib::isSerialized( $_data['item_read_array'] ) )
			{
				$_data['item_read_array'] = unserialize( $_data['item_read_array'] );
			}
			
			$this->_itemMarkers[ $app ][ $_data['item_key'] ] = $_data;
		}
		
		/* Recommend you always use the returned array as it removes the marking data to reduce memory footprint */
		return $data;
	}
	
	/**
	 * 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;
		}
		
		/* Check check for how long we're tracking for */
		if ( $data['itemLastUpdate'] && $this->settings['topic_marking_keep_days'] )
		{
			if ( ( IPS_UNIX_TIME_NOW - ( $this->settings['topic_marking_keep_days'] * 86400 ) ) > $data['itemLastUpdate'] )
			{
				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 )
				{
					$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;
		}
		
		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'] ) ? true : false;
			
			/* 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 DB */
		$this->_save( $app, $mainKey );
	}
	
	/**
	 * 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();
		
		/* 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 cookies */
			$this->_updateCookieData( 'itemMarking_' . $app . '_items', array() );
			$this->_updateCookieData( 'itemMarking_' . $app, $cookie );
		}
		else
		{
			/* Do 'em all */
			$cache = array();
			$apps  = IPSLib::getEnabledApplications( array('itemMarking') );
			
			foreach( $apps as $app => $data )
			{
				$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 );
			}
			
			/* 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();
		}
		
		/* 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;
		
		/* Check check for how long we're tracking for */
		if ( $data['itemLastUpdate'] && $this->settings['topic_marking_keep_days'] )
		{
			if ( ( IPS_UNIX_TIME_NOW - ( $this->settings['topic_marking_keep_days'] * 86400 ) ) > $data['itemLastUpdate'] )
			{
				return IPS_UNIX_TIME_NOW;
			}
		}
		
		unset( $data['itemLastUpdate'] );
		
		$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;

		/* Get all read ids */
		if ( ! isset( $data['item_app_key_1'] ) )
		{
			$_results	= array();

			if ( is_array( $this->_itemMarkers[ $app ] ) AND count( $this->_itemMarkers[ $app ] ) )
			{
				foreach( $this->_itemMarkers[ $app ] as $key => $_data )
				{
					if ( $_data['item_read_array'] )
					{
						/* + is intentionally used as the arrays will preserve their keys, but will not with array_merge */
						$_results	= $_results + $_data['item_read_array'];
					}
				}
			}

			return ( $keysOnly ) ? array_keys( $_results ) : $_results;
		}
		else
		{
			$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'];
			}
		}

		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;
		}
	}
		
	/**
	 * 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 */
		
		/**
		 * @todo	Matt: this code is not used anymore? We already return in the if/else above..
		 */
		$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 ) );
	}
	
	/**
	 * Update topic markers
	 *
	 * @access	protected
	 * @param	string	app
	 * @param	string	mainkey
	 * @return	@e void
	 */
	protected function _save( $app, $mainKey )
	{
		if ( ! $this->settings['topic_marking_enable'] )
		{
			return FALSE;
		}
		
		if ( ! $this->memberData['member_id'] )
		{
			return FALSE;
		}
		
		$row = array( 'item_key'          => $mainKey,
					  'item_member_id'    => $this->memberData['member_id'],
					  'item_app'	      => $app,
					  'item_last_update'  => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_last_update'] ),
					  'item_last_saved'   => time(),
					  'item_read_array'   => ( is_array( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) ) ? serialize( $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'] ) : $this->_itemMarkers[ $app ][ $mainKey ]['item_read_array'],
					  'item_global_reset' => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_global_reset'] ),
					  'item_unread_count' => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_unread_count'] ),
					  'item_app_key_1'	  => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_app_key_1'] ),
					  'item_app_key_2'	  => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_app_key_2'] ),
					  'item_app_key_3'	  => intval( $this->_itemMarkers[ $app ][ $mainKey ]['item_app_key_3'] ),
					  'item_is_deleted'   => 0 );
		
		$this->DB->replace( 'core_item_markers', $row, array( 'item_key', 'item_app', 'item_member_id' ) );
	}
	
	/**
	 * 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();
		}
	}
	
	/**
	 * 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();
		}
		
		/* Not loading on init? */
		try
		{
			if ( method_exists( $this->_fetchModule(), 'loadAllMarkers' ) )
			{
				$result = $this->_fetchModule()->loadAllMarkers();
				
				if ( $result === false )
				{
					return;
				}
			}
		}
		catch( Exception $e )
		{
		}	
		
		$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;
	}

	/** 
    * 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.3.0  Really depricated now!
    */ 
   public function writeMyMarkersToDB()
   {
   	
   }

}