<?php
/**
 * @file		composite.php 	Provides classes to manage the like system
 *~TERABYTE_DOC_READY~
 * $Copyright: (c) 2001 - 2011 Invision Power Services, Inc.$
 * $License: http://www.invisionpower.com/company/standards.php#license$
 * $Author: mmecham $
 * @since		11th October 2010
 * $LastChangedDate: 2013-08-14 06:45:49 -0400 (Wed, 14 Aug 2013) $
 * @version		v3.4.6
 * $Revision: 12334 $
 * 
 * <b>Example Usage:</b>
 * @code
 * $app  = 'gallery';
 * $area = 'images';
 * $like = classes_like::bootstrap( $app, $area );
 * print $like->isLiked( $relId );
 * $html	= $like->render( 'summary', $relId );
 * print $html;
 * 
 * RESULT: Matt, Joe, Bob and 5 others like this
 * @endcode
 */

if ( ! defined( 'IN_IPB' ) )
{
	print "<h1>Incorrect access</h1>You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files.";
	exit();
}

/**
 *
 * @class		classes_like_registry
 * @brief		Quick registry class for common methods
 * @author		Matt
 */
class classes_like_registry
{
	/**
	 * App key
	 *
	 * @var		$app
	 */
	static protected $app	= null;
	
	/**
	 * Area key
	 *
	 * @var		$area
	 */
	static protected $area	= null;
	
	/**
	 * Total count
	 *
	 * @var		$count
	 */
	static protected $count	= null;
	
	/**
	 * Cached bootstrap loaders
	 *
	 * @var		$bootstraps
	 */
	static protected $bootstraps = array();
	
	/**
	 * Get application key
	 * 
	 * @return	@e string
	 */
	static public function getApp()
	{
		return self::$app;
	}
	
	/**
	 * Set application key
	 * 
	 * @param	string		$app		Application key
	 * @return	@e void
	 */
	static public function setApp( $app )
	{
		self::$app = $app;
	}
	
	/**
	 * Get area key
	 * 
	 * @return	@e string
	 */
	static public function getArea()
	{
		return self::$area;
	}
	
	/**
	 * Set area key
	 * 
	 * @param	string		$area		Area key
	 * @return	@e void
	 */
	static public function setArea( $area )
	{
		self::$area = $area;
	}
	
	/**
	 * Get total count (saves on queries)
	 * 
	 * @return	@e integer
	 */
	static public function getTotalCount()
	{
		return self::$count;
	}
	
	/**
	  * Set total count (saves on queries)
	 * 
	 * @param	integer		$count		Likes count
	 * @return	@e void
	 */
	static public function setTotalCount( $count )
	{
		self::$count = $count;
	}
	
	/**
	 * Build key, either for cache table or full table
	 *
	 * @param	integer		$relId		Relationship ID
	 * @param	integer		$memberId	Member ID
	 * @return	@e binary	Binary key
	 */
	static public function getKey( $relId, $memberId=null )
	{
		/* Check */
		if ( empty( $relId ) AND empty( $memberId ) )
		{		
			trigger_error( "Relationship ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
		
		if ( $memberId === null )
		{
			/* For relative item only */
			return md5( self::$app . ';' . self::$area . ';' . $relId );
		}
		else if ( ! empty( $memberId ) )
		{
			if ( $relId === null )
			{
				/* For member speciifc relative item */
				return md5( self::$app . ';' . self::$area . ';' . $memberId );
			}
			else
			{
				/* For member speciifc relative item */
				return md5( self::$app . ';' . self::$area . ';' . $relId . ';' . $memberId );
			}
		}
		else
		{
			trigger_error( "Member ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
	}
	
	/**
	 * Build key for SQL query
	 *
	 *
	 * @param	integer		$relId			Relationship ID
	 * @param	string		$memberField	Member Field or value
	 * @return	@e binary	Binary key
	 */
	static public function getKeyForSql( $relId, $memberField=NULL )
	{		
		/* Check */
		if ( empty( $relId ) AND empty( $memberField ) )
		{		
			//var_dump(IPSDebug::prettifyBackTrace(debug_backtrace()));exit;
			trigger_error( "Relationship ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
		
		if ( $memberField === null )
		{
			/* For relative item only */
			return ipsRegistry::DB()->buildMd5Statement( ipsRegistry::DB()->buildConcat( array(
				array( self::$app, 'string' ),
				array( ';', 'string' ),
				array( self::$area, 'string' ),
				array( ';', 'string' ),
				array( $relId, 'string' ),
				) ) );
		}
		else if ( ! empty( $memberField ) )
		{
			if ( $relId === null )
			{
				/* For member speciifc relative item */
				return ipsRegistry::DB()->buildMd5Statement( ipsRegistry::DB()->buildConcat( array(
					array( self::$app, 'string' ),
					array( ';', 'string' ),
					array( self::$area, 'string' ),
					array( ';', 'string' ),
					array( $memberField ),
					) ) );
			}
			else
			{
				/* For member speciifc relative item */
				return ipsRegistry::DB()->buildMd5Statement( ipsRegistry::DB()->buildConcat( array(
							array( self::$app, 'string' ),
							array( ';', 'string' ),
							array( self::$area, 'string' ),
							array( ';', 'string' ),
							array( $relId, 'string' ),
							array( ';', 'string' ),
							array( $memberField ),
							) ) );
			}
		}
		else
		{
			trigger_error( "Member ID missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
	}
}


/**
 *
 * @class		classes_like_meta
 * @brief		Simple static class for fetching meta data
 * @author		Matt
 */
class classes_like_meta
{
	/**
	 * Retrieve the content title
	 *
	 * @param	array		$like		Like data
	 * @return	@e string	Title of content liked
	 */
	static public function getTitleFromId( $like )
	{
		if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
		{
			trigger_error( "You must have like_app, like_rel_id and like_area set", E_USER_WARNING );
		}
		
		$_bootstrap	= classes_like::bootstrap( $like['like_app'], $like['like_area'] );
		
		return $_bootstrap->getTitleFromId( $like['like_rel_id'] );
	}

	/**
	 * Retrieve the content URL
	 *
	 * @param	array		$like		Like data
	 * @return	@e string	URL of content liked
	 */
	static public function getUrlFromId( $like )
	{
		if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
		{
			trigger_error( "You must have like_app, like_rel_id and like_area set", E_USER_WARNING );
		}
		
		$_bootstrap	= classes_like::bootstrap( $like['like_app'], $like['like_area'] );
		
		return $_bootstrap->getUrlFromId( $like['like_rel_id'] );
	}
	
	/**
	 * Fetch all meta data from various like rows
	 * 
	 * @param	array		$likes		Like rows
	 * @return	@e array
	 */
	static public function get( array $likes )
	{
		$apps    = array();
		$process = array();
		$lookUp  = array();
		
		foreach( $likes as $id => $like )
		{
			if ( empty( $like['like_app'] ) OR empty( $like['like_area'] ) OR empty( $like['like_rel_id'] ) )
			{
				continue;
			}
			
			$apps[ $like['like_app'] . '-' . $like['like_area'] ][ $id ] = $like;
			$lookUp[ $like['like_app'] . '-' . $like['like_area'] . '-' . $like['like_rel_id'] ] = $like['like_id'];
		}
		
		/* Process based on apps */
		foreach( $apps as $app => $data )
		{
			list( $_app, $_area ) = explode( '-', $app );
			
			$_bootstrap	= classes_like::bootstrap( $_app, $_area );
			
			$relIds = array();
			
			foreach( $data as $_k => $_v )
			{
				$relIds[] = $_v['like_rel_id'];
			}
		
			/* Bulk fetch */
			$process[ $app ] = $_bootstrap->getMeta( $relIds );
		}
		
		/* Fold up array */
		foreach( $process as $app => $data )
		{
			list( $_app, $_area ) = explode( '-', $app );
			
			foreach( $data as $relId => $_like )
			{
				if ( $relId )
				{
					$like_id = $lookUp[ $_app . '-' . $_area . '-' . $relId ];
					
					if ( $like_id AND is_array( $_like ) AND is_array( $likes[ $like_id ] ) )
					{
						$likes[ $like_id ] = array_merge( $likes[ $like_id ], $_like );
					}
				}
			}
		}
		
		return $likes;
	}
}


/**
 *
 * @class		classes_like
 * @brief		Factory class, loads composite and child class
 * @author		Matt
 */
class classes_like
{
	/**
	 * Applications objects
	 *
	 * @var $apps
	 */
	static protected $apps;
	
	/**
	 * Bootstrap function to load the required file/class and setup the initial variables
	 *
	 * @param	string		$app		Application key
	 * @param	string		$area		Area
	 * @return	@e object	Like object for the loaded key (app.area)
	 */
	static public function bootstrap( $app=null, $area=null )
	{
		if ( $app === null OR $area === null )
		{
			trigger_error( "App or area missing from classes_like", E_USER_WARNING );
		}
		
		/* Pointless comment! */
		if( $area != 'default' )
		{
			$_file	= IPSLib::getAppDir( $app ) . '/extensions/like/' . $area . '.php';
			$_class	= 'like_' . $app . '_' . $area . '_composite';
		}

		$_key	= md5( $app . $area );
		
		/* Get from cache if already cached */
		if( isset( self::$apps[ $_key ] ) )
		{
			return self::$apps[ $_key ];
		}
		
		/* Otherwise create object and cache */
		if ( ! is_file( $_file ) )
		{
			$_file = IPSLib::getAppDir( $app ) . '/extensions/like/default.php';
			$_class = 'like_' . $app . '_composite';
			
			if ( ! is_file( $_file ) )
			{
				throw new Exception( "No like class available for $app - $area" );
			}
		}
				
		$classToLoad = IPSLib::loadLibrary( $_file, $_class, $app );
		
		if ( class_exists( $classToLoad ) )
		{
			classes_like_registry::setApp( $app );
			classes_like_registry::setArea( $area );
			
			self::$apps[ $_key ] = new $classToLoad();
			self::$apps[ $_key ]->init();
		}
		else
		{
			throw new Exception( "No like class available for $app - $area" );
		}
		
		return self::$apps[ $_key ];
	}
}


/**
 *
 * @class		classes_like_composite
 * @brief		Composite class, holds main functionality
 * @author		Matt
 */
abstract class classes_like_composite
{
	/**
	 * Registry Object Shortcuts
	 *
	 * @var		$registry
	 * @var		$DB
	 * @var		$settings
	 * @var		$request
	 * @var		$lang
	 * @var		$member
	 * @var		$memberData
	 * @var		$cache
	 * @var		$caches
	 */
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;	
	
	/**
	 * Cache object
	 *
	 * @var		$likeCache
	 */
	protected $likeCache = null;
	
	/**
	 * Application key
	 *
	 * @var		$_app
	 */
	protected $_app;
	
	/**
	 * Area key
	 *
	 * @var		$_area
	 */
	protected $_area;
	
	/**
	 * Can bypass permission checks
	 *
	 * @var		$_area
	 */
	protected $_bypassPermissionChecks = false;

	
	/**
	 * Init all. Yes, it is.
	 *
	 * @return	@e void
	 */
	public function init()
	{
		$this->_app  = classes_like_registry::getApp();
		$this->_area = classes_like_registry::getArea();
		
		if ( empty( $this->_app ) OR empty( $this->_area ) )
		{
			trigger_error( "Missing area or app variable in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
		
		/* Fetch cache class */
		$this->likeCache = classes_like_cache::getInstance();
		
		/* Set a default cache expiration of 24 hours */
		$this->likeCache->setExpiration( 86400 );
	}
	
	/**
	 * Toggle visibility
	 * 
	 * @param	mixed		$relId		Relationship ID or array ids
	 * @param	boolean		$visible	Visible (true) or not (false)
	 * @return	@e boolean
	 */
	public function toggleVisibility( $relId, $visible=true )
	{
		if ( empty( $relId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		/* Update any rows pointing to this rel id to be visible/invisible */
		if( is_array($relId) )
		{
			if( count($relId) )
			{
				$this->DB->update( 'core_like', array( 'like_visible' => $visible ? 1 : 0 ), "like_app='{$this->_app}' AND like_area='{$this->_area}' AND like_rel_id IN (" . implode( ',', $relId ) . ')' );
			}
		}
		else
		{
			$this->DB->update( 'core_like', array( 'like_visible' => $visible ? 1 : 0 ), "like_app='{$this->_app}' AND like_area='{$this->_area}' AND like_rel_id=" . $relId );
		}

		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relId );
		
		return true;
	}
	
	/**
	 * Add a like
	 * 
	 * @param	integer		$relId			Relationship ID
	 * @param	integer		$memberID		Member ID of user being added
	 * @param	array		$notifyOpts		Notification options
	 * @param	integer		$isAnon			Anonymous flag
	 * @return	@e boolean
	 */
	public function add( $relId, $memberId, array $notifyOpts, $isAnon=0 )
	{
		if ( empty( $relId ) OR empty( $memberId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		/* first check to ensure we've not already like'd this item */
		if ( $this->isLiked( $relId, $memberId, true ) )
		{
			/* if any one cares to check, then we're all good */
			return false;
		}
		
		$memberData = IPSMember::load( $memberId );
		
		/* Build */
		$save = array(  'like_id'          => classes_like_registry::getKey( $relId, $memberId ),
					    'like_lookup_id'   => classes_like_registry::getKey( $relId ),
						'like_lookup_area' => classes_like_registry::getKey( null, $memberId ),
						'like_app'         => $this->_app,
						'like_area'        => $this->_area,
						'like_rel_id'      => $relId,
					 	'like_member_id'   => $memberId,
						'like_added'       => IPS_UNIX_TIME_NOW,
						'like_is_anon'	   => intval($isAnon),
						'like_notify_do'   => $notifyOpts['like_notify_do'],
						'like_notify_meta' => $notifyOpts['like_notify_meta'],
						'like_notify_freq' => ( $notifyOpts['like_notify_do'] ) ? $notifyOpts['like_notify_freq'] : '',
						'like_visible'	   => 1,
						'like_notify_sent' => 0 );
		
		/* Do we have permission ? */
		if ( ! $this->notificationCanSend( array_merge( $save, $memberData ) ) )
		{
			return false;
		}
		
		$notifyOpts = $this->_cleanNotifyOptions($notifyOpts);
		
		/* Save to deebee */
		$this->DB->insert( 'core_like', $save );
		
		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relId );
		
		return true;
	}
	
	/**
	 * Function to let plugins determine if a notification should not be sent.  Return false to send notification, or true to skip sending it.
	 * 
	 * @param	array	Notification data
	 * @return	@e bool
	 */
 	public function excludeNotification( $row )
 	{
 		return false;
 	}
	
	/**
	 * Send notifications to anyone subscribed to item
	 *
	 * @param	integer		$relId				Relationship ID
	 * @param	array		$type				Types of notifications to send (Possible: immediate, offline, daily, weekly)
	 * @param	array		$notificationOpts	Notification options [Keys: notification_key, notification_url, email_template, email_subject, email_lang_file, email_lang_app, build_message_array, from (optional), ignore_data( type => ids )]
	 * @param	array		$sentToIds			Pass a value by reference to get the ID numbers of members that were sent a notification
	 * @return	@e boolean
	 * @see		allowedFrequencies()
	 */
	public function sendNotifications( $relId, $type, $notificationOpts=array(), &$sentToIds=array() )
	{
		$sentToIds = array();
		
		if ( empty( $relId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		if ( isset( $notificationOpts['notification_bypass_permission_checks'] ) && $notificationOpts['notification_bypass_permission_checks'] === true )
		{
			$this->_bypassPermissionChecks = true;
		}
		else
		{
			/* Make sure this is reset (Not used currently) */
			$this->_bypassPermissionChecks = false;
		}
		
		$type	= is_array($type) ? $type : array( $type );
		
		/* Use session expiration, unless it is longer than 1 hour, then limit to 1 hour */
		$_limit	= $this->settings['session_expiration'];
		
		if( $_limit > 3600 )
		{
			$_limit	= 3600;
		}
		
		$expire = IPS_UNIX_TIME_NOW - $_limit;
				
		/* Fetch data by relID */
		$data = $this->getDataByRelationshipId( $relId, false, (array) $notificationOpts['ignore_data'], $type );
				
		/* Remove ourselves from notifications... */
		if ( $this->memberData['member_id'] && isset($data[ $this->memberData['member_id'] ]) && $notificationOpts['includeOwner'] !== true )
		{
			unset($data[ $this->memberData['member_id'] ]);
		}
		
		/* If no users, forget it */
		if ( is_null($data) )
		{
			return false;
		}
		
		$classToLoad	= IPSLib::loadLibrary( IPS_ROOT_PATH . '/sources/classes/member/notifications.php', 'notifications' );
		$notifyLibrary	= new $classToLoad( $this->registry );
		
		$htmlMessage = array();
		$textMessage = array();
		
		foreach( $data as $row )
		{
			if ( ! $row['like_notify_freq'] OR ! in_array( $row['like_notify_freq'], $type ) )
			{
				continue;
			}
			
			/* make sure user is offline */
			if ( $row['like_notify_freq'] == 'offline' AND $row['last_activity'] > $expire )
			{
				continue;
			}
			
			/* Custom content type exclusion rules (e.g. in forums only send one notification per visit for offline types) */
			if( $this->excludeNotification( $row ) )
			{
				continue;
			}
			
			/* Can we actually send this one? */
			if ( $this->notificationCanSend( $row ) === false )
			{
				continue;
			}

			/* Extra protection in case a ghost record is in the table for a member that doesn't exist
				@link	http://community.invisionpower.com/tracker/issue-36793-error-when-editing-some-marketplace-files */
			if( !$row['like_member_id'] )
			{
				continue;
			}

			/* Expensively process HTML/Text versions once per language.. <#NAME#> is converted to -members:members_display_name- for processing later */
			if ( ! isset( $htmlMessage[ $row['language'] ] ) )
			{				
				$buildMessage = array();
					
				if ( is_array( $notificationOpts['build_message_array'] ) and count( $notificationOpts['build_message_array'] ) )
				{
					foreach( $notificationOpts['build_message_array'] as $k => $v )
					{
						$buildMessage[ $k ] = $v;
					}
				}
				
				$buildMessage['UNSUBCRIBE_URL'] 						   = '-member:unsubscribe_url-';
				$notificationOpts['build_message_array']['UNSUBCRIBE_URL'] = '-member:unsubscribe_url-';
				
				IPSText::getTextClass('email')->setPlainTextTemplate( IPSText::getTextClass('email')->getTemplate( $notificationOpts['email_template'], $row['language'], trim($notificationOpts['email_lang_file']), trim($notificationOpts['email_lang_app']) ) );
				
				$textMessage[ $row['language'] ] = IPSText::getTextClass('email')->buildPlainTextContent( $buildMessage );
				$htmlMessage[ $row['language'] ] = IPSText::getTextClass('email')->buildHtmlContent( $buildMessage );
			}
			
			$thisTextMessage = $textMessage[ $row['language'] ];
			$thisHtmlMessage = $htmlMessage[ $row['language'] ];
			
			/* Add in unsubscribe link */
			$unsubKey = $row['like_app'] . ';' . $row['like_area'] . ';' . $row['like_rel_id'] . ';' . $row['like_member_id'] . ';' . $row['member_id'] . ';' . $row['email'];
			$unsubUrl = $this->registry->output->buildSEOUrl( 'app=core&amp;module=global&amp;section=like&amp;do=unsubscribe&amp;key=' . IPSText::base64_encode_urlSafe( $unsubKey ), 'publicNoSession', 'unsubscribe', 'likeunsubscribe' );
			
			/* Now process the dynamic replacements */
			if ( is_array( $notificationOpts['build_message_array'] ) and count( $notificationOpts['build_message_array'] ) )
			{
				foreach( $notificationOpts['build_message_array'] as $k => $v )
				{
					if ( preg_match( '/\-member:(.+?)\-/', $v, $_matches ) )
					{
						if ( $_matches[1] == 'unsubscribe_url' )
						{
							$thisHtmlMessage = str_replace( $_matches[0], $unsubUrl, $thisHtmlMessage );
							$thisTextMessage = str_replace( $_matches[0], $unsubUrl, $thisTextMessage );
						}
						else
						{
							$thisHtmlMessage = str_replace( $_matches[0], $row[ $_matches[1] ], $thisHtmlMessage );
							$thisTextMessage = str_replace( $_matches[0], $row[ $_matches[1] ], $thisTextMessage );
						}
					}
				}
			}
			
			$sentToIds[] = $row['like_member_id'];
			
			$notifyLibrary->setMember( $row );
			$notifyLibrary->setFrom( $notificationOpts['from'] ? $notificationOpts['from'] : $this->memberData );
			$notifyLibrary->setNotificationKey( $notificationOpts['notification_key'] );
			$notifyLibrary->setNotificationUrl( $notificationOpts['notification_url'] );
			$notifyLibrary->setNotificationTitle( $notificationOpts['email_subject'] ? $notificationOpts['email_subject'] : IPSText::getTextClass('email')->subject );
			$notifyLibrary->setNotificationText( $thisTextMessage );
			$notifyLibrary->setNotificationHtml( $thisHtmlMessage );
			
			if ( ! empty( $notificationOpts['email_only_subject'] ) && $notificationOpts['email_only_subject'] != IPSText::getTextClass('email')->subject )
			{
				$notifyLibrary->setEmailSubject( $notificationOpts['email_only_subject'], $row['language'], trim($notificationOpts['email_lang_file']), trim($notificationOpts['email_lang_app']) );
			}
			try
			{
				$notifyLibrary->sendNotification();
			}
			catch( Exception $e ){ }
			
			//-----------------------------------------
			// Update sent timestamp
			//-----------------------------------------
			
			$this->DB->update( 'core_like', array( 'like_notify_sent' => IPS_UNIX_TIME_NOW ), "like_id='" . classes_like_registry::getKey( $row['like_rel_id'], $row['like_member_id'] ) . "'" );
		}
		
		return ( is_array( $sentToIds ) ) ? array_unique( $sentToIds ) : true;
	}
	
	/**
	 * Send digest notifications to anyone subscribed to item
	 *
	 * @param	string		$type		Types of notifications to send (Possible: daily, weekly)
	 * @param	integer		$sendMax	Null (use ipsRegistry::$setting or send INT only)
	 * @return	@e boolean
	 * @see		allowedFrequencies()
	 */
	public function sendDigestNotifications( $type, $sendMax=null )
	{	
		/* Fetch data */
		$blackhole = time(); // mktime( 0, 0 ); - http://community.invisionpower.com/tracker/issue-35320-daily-digests-every-2-days
		// The below has been changed based on troubleshooting in a ticket, 10.17.2012.  With the code below not commented out, for a weekly digest we grab 15-7 days ago, rather than 8-0 days ago to send.
		$time      = $blackhole; //( $type == 'daily' ) ? $blackhole - ( 86400 ) : $blackhole - ( 86400 * 7 );
		$data      = $this->getDataByAreaAndLastSentOlderThanDate( $time, array( $type ), false, $sendMax );

		if ( ! count( $data ) )
		{
			return false;
		}
		
		/* Check for outdated notifications and update them as required */
		$oldestPossDate = 0;
		
		if ( $type == 'daily' )
		{
			/* Grab 4 days to account for timezone differences */
			$oldestPossDate = $blackhole - ( 86400 * 4 );
		}
		else
		{
			/* Grab 10 days to ensure we don't update rows ready to be sent */
			$oldestPossDate = $blackhole - ( 86400 * 10 );
		}
		
		if ( $oldestPossDate )
		{
			$this->DB->update( 'core_like', array( 'like_notify_sent' => IPS_UNIX_TIME_NOW ),
							   'like_app=\'' . classes_like_registry::getApp() . '\' AND like_area=\'' . classes_like_registry::getArea() . '\' AND like_notify_sent < ' . $oldestPossDate . " AND like_notify_freq='{$type}'" );
		}
		
		$classToLoad   = IPSLib::loadLibrary( IPS_ROOT_PATH . '/sources/classes/member/notifications.php', 'notifications' );
		$notifyLibrary = new $classToLoad( $this->registry );
		
		/* Array structure will be $data[ $memberId ][ $likeId ] = array(); */
		foreach( $data as $memberId => $_rows )
		{
			foreach( $_rows as $row )
			{
				if ( ! $row['like_notify_freq'] OR $row['like_notify_freq'] != $type )
				{
					continue;
				}
				
				/* Make sure we don't send huge digests if this is their first email since subscribing */
				$row['like_notify_sent']	= $row['like_notify_sent'] ? $row['like_notify_sent'] : $row['like_added'];

				/* Want to fetch the data from the plug in files? */
				$notificationOpts = $this->buildNotificationData( $row, $type );
					
				if ( ! is_array( $notificationOpts ) OR ! count( $notificationOpts ) )
				{
					/* This will get hit if topic hasn't had a post since notification was last sent - to prevent the same row from getting picked up each time this runs
						we need to update the last sent timestamp for the like */
					$this->DB->update( 'core_like', array( 'like_notify_sent' => IPS_UNIX_TIME_NOW ), 'like_id=\'' . classes_like_registry::getKey( $row['like_rel_id'], $row['like_member_id'] ) . '\'' );

					continue;
				}

				$buildMessage	= array();
				
				if ( is_array( $notificationOpts['build_message_array'] ) and count( $notificationOpts['build_message_array'] ) )
				{
					foreach( $notificationOpts['build_message_array'] as $k => $v )
					{
						if ( preg_match( '/\-member:(.+?)\-/', $v, $_matches ) )
						{
							$v = str_replace( $_matches[0], $row[$_matches[1]], $v );
						}
						
						$buildMessage[$k] = $v;
					}
				}

				/* Add in unsubscribe link */
				$key = $row['like_app'] . ';' . $row['like_area'] . ';' . $row['like_rel_id'] . ';' . $row['like_member_id'] . ';' . $row['member_id'] . ';' . $row['email'];
				$buildMessage['UNSUBCRIBE_URL'] = $this->registry->output->buildSEOUrl( 'app=core&amp;module=global&amp;section=like&amp;do=unsubscribe&amp;key=' . IPSText::base64_encode_urlSafe( $key ), 'publicNoSession', 'unsubscribe', 'likeunsubscribe' );

				IPSText::getTextClass('email')->setPlainTextTemplate( IPSText::getTextClass('email')->getTemplate( $notificationOpts['email_template'], $row['language'], trim($notificationOpts['email_lang_file']), trim($notificationOpts['email_lang_app']) ) );
				IPSText::getTextClass('email')->buildPlainTextContent( $buildMessage );
				
				$notifyLibrary->setMember( $row );
				$notifyLibrary->setFrom( $notificationOpts['from'] ? $notificationOpts['from'] : $this->memberData );
				$notifyLibrary->setNotificationKey( $notificationOpts['notification_key'] );
				$notifyLibrary->setNotificationUrl( $notificationOpts['notification_url'] );
				$notifyLibrary->setNotificationText( IPSText::getTextClass('email')->getPlainTextContent() );
				$notifyLibrary->setNotificationTitle( $notificationOpts['email_subject'] ? $notificationOpts['email_subject'] : IPSText::getTextClass('email')->subject );
				$notifyLibrary->setNotificationHtml( IPSText::getTextClass('email')->buildHtmlContent( $buildMessage ) );
				
				try
				{
					$notifyLibrary->sendNotification();
				}
				catch( Exception $e ){ }
								
				/* Update sent timestamp */
				$this->DB->update( 'core_like', array( 'like_notify_sent' => IPS_UNIX_TIME_NOW ), 'like_id=\'' . classes_like_registry::getKey( $row['like_rel_id'], $row['like_member_id'] ) . '\'' );
			}
		}
		
		return true;
	}
	
	/**
	 * Removes all likes based on member ID
	 * 
	 * @param	integer		$memberId		Member ID
	 * @return	@e boolean
	 */
	public function removeByMemberId( $memberId )
	{
		if ( empty( $memberId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		$where = "like_app='{$this->_app}' AND like_area='{$this->_area}' AND like_member_id=" . intval($memberId);
		
		/* Get rel ids */
		$relIds	= array();
		
		$this->DB->build( array( 'select' => 'like_rel_id', 'from' => 'core_like', 'where' => $where ) );
		$this->DB->execute();
		
		while( $r = $this->DB->fetch() )
		{
			$relIds[]	= $r['like_rel_id'];
		}

		/* Delete likes */
		$this->DB->delete( 'core_like', $where );
									
		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relIds );
		
		return true;
	}
	
	/**
	 * Removes a like
	 * 
	 * @param	mixed		$relId		Relationship IDs (int|array)
	 * @param	integer		$memberId	Optional - If supplied, it'll remove that member rel. or it will remove all
	 * @return	@e boolean
	 */
	public function remove( $relId, $memberId=null )
	{
		if ( empty( $relId ) )
		{
			trigger_error( "Data missing in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING  );
		}
		
		$where = '';
		
		if ( is_numeric($relId) )
		{
			$where = "='" . classes_like_registry::getKey( $relId, $memberId ) . "'";
		}
		elseif ( is_array($relId) )
		{
			$relId = IPSLib::cleanIntArray($relId);
			$keys  = array();
			
			if ( count($relId) )
			{
				foreach( $relId as $id )
				{
					$keys[] = "'" . classes_like_registry::getKey( $id, $memberId ) . "'";
				}
				
				$where = " IN (" . implode( ",", $keys ) . ")";
			}
		}
		
		/* Nowhere to be found? */
		if ( $where == '' )
		{
			return false;
		}
		
		/* Still here? Delete then! */
		if ( $memberId === null )
		{
			$this->DB->delete( 'core_like', "like_lookup_id " . $where );
		}
		else
		{
			/* Not liked? Save us the bother, then */
			if ( ! $this->isLiked($relId, $memberId) )
			{
				/* if any one cares to check, then we're all good */
				return false;
			}
			
			$this->DB->delete( 'core_like', "like_id " . $where );
		}
												
		/* Flag cache as stale */
		$this->likeCache->isNowStale( $relId );
		
		return true;
	}
	
	/**
	 * Updates a member's ID in the like rows
	 *
	 * @param	integer	Old Id
	 * @param	integer	New Id
	 */
	public function updateMemberId( $oldId, $newId )
	{
		$_likes	= array();
		
		/* We might be merging */
		$this->DB->build( array( 'select' => 'like_lookup_id', 'from' => 'core_like', 'where' => "like_member_id=" . $newId ) );
		$this->DB->execute();
			
		while( $r = $this->DB->fetch() )
		{
			$_likes[]	= $r['like_lookup_id'];
		}
		/* not done */
		if ( count( $_likes ) )
		{
			$this->DB->update( 'core_like'		 , array( 'like_member_id' => $newId ), "like_member_id=" . $oldId . " AND like_lookup_id NOT IN('" . implode( "','", array_map( 'addslashes', $_likes ) ) . "')" );
			$this->DB->delete( 'core_like_cache' , "like_cache_id IN('" . implode( "','", array_map( 'addslashes', $_likes ) ) . "')" );
		}
		else
		{
			$this->DB->update( 'core_like'		 , array( 'like_member_id' => $newId ), "like_member_id=" . $oldId );
		}
		
		/* Now remove duplicates */
		$dupes = array();
		
		$this->DB->build( array( 'select' => 'like_id, count(*) as cnt',
								 'from'   => 'core_like',
								 'where'  => 'like_member_id=' . $newId,
								 'group'  => 'like_app, like_area, like_member_id, like_rel_id',
								 'order'  => 'cnt desc' ) );
		$this->DB->execute();
		
		while( $row = $this->DB->fetch() )
		{
			if ( $row['cnt'] < 2 )
			{
				break;
			}
			else
			{
				$dupes[] = $row['like_id'];
			}
		}
		
		/* Do it? */
		if ( count( $dupes ) )
		{
			$this->DB->delete( 'core_like' , "like_id IN('" . implode( "','", array_map( 'addslashes', $dupes ) ) . "')" );
		}
		
		/* Now re-update so that hashes are correct */
		$update = "like_id=MD5( CONCAT( like_app, ';', like_area, ';', like_rel_id, ';', like_member_id ) ), " .
				  "like_lookup_area=MD5( CONCAT( like_app, ';', like_area, ';', like_member_id ) )";
		
		$where  = "like_member_id=" . $newId;
		
		$this->DB->update( 'core_like', $update, $where, false, true );
	}
	
	/**
	 * Return number of people who 'like' the item
	 * 
	 * @param	integer		$relId		Relationship ID
	 * @return	@e integer
	 */
	public function getCount( $relId )
	{
		$cache = $this->likeCache->get( $relId );

		return $cache['count'];
	}
	
	/**
	 * Render a view
	 * 
	 * @param	string		$view		Type of View
	 * @param	integer		$relId		Relationship ID
	 * @param	array		$opts		Options
	 * @param	integer		$memberId	Member ID
	 * @return	@e string	Formatted HTML
	 * @todo	If $cache['count'] is 0, we can probably skip the getDataByMemberIdAndRelationshipId() call as it will be empty
	 */
	public function render( $view, $relId, $opts=array(), $memberId=null )
	{
		/* In the future we could abstract this out to its own class 
		 *  At this time we only have a few views so it would be overkill */
		if ( $view == 'summary' )
		{
			$data   = array( 'names' => array(), 'count' => 0, 'formatted' => '', 'iLike' => false, 'iLikeAnon' => false );
			$cache  = $this->likeCache->get( $relId, $memberId );
			$max	= 0;
			
			/* Fetch data for us? */
			if ( $this->memberData['member_id'] && ( $cache['count'] + $cache['count_anon'] ) > 0 )
			{
				/* We can't use isLiked or it runs twice the query and adding a cache to the function doesn't work either */
				$_me = $this->getDataByMemberIdAndRelationshipId( $relId, $this->memberData['member_id'] );
				
				if ( is_array($_me) && count($_me) && $_me['like_id'] )
				{
					/* Now mix up the data */
					$data['names'][] = array( 'name' => $this->lang->words['fave_moi'], 'seo' => $this->memberData['members_seo_name'], 'id' => $this->memberData['member_id'] );
					
					/* Flag as me */
					$data['iLike'] = true;
					
					/* Anonymous? Let the template know */
					if ( $_me['like_is_anon'] )
					{
						$data['iLikeAnon'] = true;
					}
				}
			}
			
			/* We had the total count set from cache.get - doesn't count anonymous members thought! */
			if ( classes_like_registry::getTotalCount() )
			{
				if ( is_array( $cache['members'] ) )
				{
					$i   = 1;
					$max = 3;
					
					foreach( $cache['members'] as $mid => $mdata )
					{
						$_last = ( $i == $cache['count'] || $i == $max ) ? 1 : 0;
						
						/* Is this you? */
						if ( $mid == $this->memberData['member_id'] )
						{
							continue;
						}
						
						/* Push it on */
						$data['names'][] = array( 'name' => $mdata['n'], 'seo' => $mdata['s'], 'id' => $mid, 'last' => $_last );
						
						$i++;
						
						if ( $i > $max )
						{
							/* Done thanks */
							break;
						}
					}
				}
			}
			
			/* Finish off */
			$data['totalCount']  = $cache['count'] + $cache['count_anon'];
			$data['anonCount']   = $cache['count_anon'];
			$data['othersCount'] = ( $cache['count'] > $max ) ? $cache['count'] - $max : 0;
			$data['app']		 = $this->_app;
			$data['area']		 = $this->_area;
			$data['formatted']   = $this->_formatNameString( $data );
			$data['vernacular']	 = $this->getVernacular();
			
			if ( IPS_IS_AJAX )
			{
				$_template	= $this->templatePrefix() . 'likeSummaryContents';
				
				/* Got an override template? */
				if ( method_exists( $this->registry->output->getTemplate( $this->skin() ), $_template ) )
				{
					return $this->registry->output->getTemplate( $this->skin() )->$_template( $data, $relId, $opts );
				}
				else
				{
					/* Fallback on default template */
					return $this->registry->output->getTemplate('global_other')->likeSummaryContents( $data, $relId, $opts );
				}
			}
			else
			{
				$_template	= $this->templatePrefix() . 'likeSummary';
								
				/* Got an override template? */
				if ( method_exists( $this->registry->output->getTemplate( $this->skin() ), $_template ) )
				{
					return $this->registry->output->getTemplate( $this->skin() )->$_template( $data, $relId, $opts );
				}
				else
				{
					/* Fallback on default template */
					return $this->registry->output->getTemplate('global_other')->likeSummary( $data, $relId, $opts );
				}
			}
		}
		else if ( $view == 'more' )
		{
			/* We need some counts here because we need them! */
			$cache				 = $this->likeCache->get( $relId, $memberId );
			$cache['totalCount'] = $cache['count'] + $cache['count_anon'];
			$cache['anonCount']	 = $cache['count_anon'];
			
			/* Fetch members who have wanted to like this item */
			$data		= $this->getDataByRelationshipId( $relId );
			$_template	= $this->templatePrefix() . 'likeMoreDialogue';
			
			/* Sort out some numbers if we're following anonymously */
			if ( $this->memberData['member_id'] && isset($data[ $this->memberData['member_id'] ]) && $data[ $this->memberData['member_id'] ]['like_is_anon'] )
			{
				$cache['count_anon']--;
				$cache['anonCount']--;
			}
			
			/* Got an override template? */
			if ( method_exists( $this->registry->output->getTemplate( $this->skin() ), $_template ) )
			{
				return $this->registry->output->getTemplate( $this->skin() )->$_template( $data, $relId, $cache );
			}
			else
			{
				/* Fallback on default template */
				return $this->registry->output->getTemplate('global_other')->likeMoreDialogue( $data, $relId, $cache );
			}
		}
	}

	/**
	 * Fetch form data for set dialogue
	 * 
	 * @param	integer		$relId		Relationship ID
	 * @return	@e array
	 */
	public function getDataForSetDialogue( $relid )
	{
		$return = array( 'frequencies' => $this->allowedFrequencies(),
						 'notifyType'  => $this->getNotifyType(),
						 'vernacular'  => $this->getVernacular()
						);
		
		return $return;
	}
	
	/**
	 * Has this user made this item a like already dudes?
	 * 
	 * @param	mixed		$relId		Relationship IDs (int|array)
	 * @param	integer		$memberId	Member ID
	 * @param	boolean		$checkNotVisibleToo	return a match even if not visible to the user
	 * @return	@e boolean
	 */
	public function isLiked( $relId, $memberId, $checkNotVisibleToo=false )
	{
		/* Grab the data */
		return ( $this->getDataByMemberIdAndRelationshipId( $relId, $memberId, $checkNotVisibleToo ) === null ) ? false : true;
	}
	
	/**
	 * Get data based on a relationship ID and a member ID
	 *
	 * @param	mixed		$relId		Relationship IDs (int|array)
	 * @param	integer		$memberId	Member ID
	 * @param	boolean		$checkNotVisibleToo	return a match even if not visible to the user
	 * @return	@e mixed	Array of likes data OR null
	 */
	public function getDataByMemberIdAndRelationshipId( $relId, $memberId, $checkNotVisibleToo=false )
	{
		$where	= '';
		$data	= null;
		
		if ( is_numeric( $relId ) )
		{
			$where = "like_id='" . classes_like_registry::getKey( $relId, $memberId ) . "'";
		}
		else if ( is_array( $relId ) )
		{
			$relId = IPSLib::cleanIntArray( $relId );
			$keys  = array();
			
			foreach( $relId as $id )
			{
				$keys[] = "'" . classes_like_registry::getKey( $id, $memberId ) . "'";
			}
			
			if ( ! count( $keys ) )
			{
				return null;
			}
			
			$where = 'like_id IN (' . implode( ',', $keys ) . ')';
			
		}
		
		if ( $checkNotVisibleToo === false )
		{
			$where .= ' AND like_visible=1';
		}
		
		$this->DB->build( array( 'select' => '*',
					   			 'from'   => 'core_like',
								 'where'  => $where ) );
		
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$data[ $row['like_rel_id'] ] = $row;
		}
		
		/* Just the one? */
		if ( is_numeric($relId) && count($data) )
		{
			$data = array_shift($data);
		}
		
		return is_array($data) ? $data : null;
	}
	
	/**
	 * Get data based on a member ID
	 *
	 * @param	integer		$memberId		Member ID
	 * @param	integer		$limit			Max results
	 * @return	@e mixed 	Array of likes data OR null
	 */
	public function getDataByMemberIdAndArea( $memberId, $limit=500 )
	{
		$data = null;
		
		$this->DB->build( array( 'select' => '*',
					   			 'from'   => 'core_like',
								 'where'  => 'like_lookup_area=\'' . classes_like_registry::getKey( null, $memberId ) . '\' AND like_visible=1',
								 'order'  => 'like_added DESC',
								 'limit'  => array( 0, $limit ) ) );
		
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$data[ $row['like_rel_id'] ] = $row;
		}	
		
		return ( is_array( $data ) ) ? $data : null;
	}
	
	/**
	 * Get data based on a relationship ID
	 *
	 * @param	integer		$relId		Relationship ID
	 * @param	boolean		$skipAnon	Skip anonymous rows flag
	 * @param	array
	 * @param	array		Type of follow (immediate, none, offline, daily, etc)
	 * @return	@e mixed	Array of likes data OR null
	 */
	public function getDataByRelationshipId( $relId, $skipAnon=true, $ignoreData=array(), $followType=array() )
	{
		/* Init */
		$mids	    = array();
		$members    = array();
		$rows       = array();
		$followType	= is_array( $followType ) ? $followType : array( $followType );

		/* Determine where clause */
		$where		= array( "like_visible=1", "like_is_anon" . ( $skipAnon ? '=0' : ' IN(0,1)' ) );

		/*if( $this->memberData['member_id'] )
		{
			$where[]	= "(like_lookup_id='" . classes_like_registry::getKey( $relId ) . "' OR like_id='" . classes_like_registry::getKey( $relId, $this->memberData['member_id'] ) . "')";
		}
		else
		{*/
			$where[]	= "like_lookup_id='" . classes_like_registry::getKey( $relId ) . "'";
		//}

		/* Add in follow type */
		if ( count( $followType ) )
		{
			$where[]	= "like_notify_freq IN ('" . implode( "','", $followType ) ."')";
		}
		
		/* Fetch data */
		$this->DB->build( array( 'select' => '*',
					   			 'from'   => 'core_like',
								 'where'  => implode( " AND ", $where ),
								 'order'  => 'like_added DESC',
								 'limit'  => array( 0, 500 ) ) );
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$data[ $row['like_member_id'] ] = $row;
			$mids[ $row['like_member_id'] ] = intval( $row['like_member_id'] );
		}
		
		/* Just the one? */
		if ( count( $mids ) )
		{
			$members = IPSMember::load( $mids, 'all' );
		
			foreach( $members as $i => $d )
			{
				$_m = IPSMember::buildProfilePhoto( $d );
				$data[ $i ] = array_merge( (array) $_m, (array) $data[ $i ] );
			}
		}
		
		/* Are we giving this bloke a good ignoring? */
		if ( !empty( $ignoreData ) )
		{
			$ignoreWhere = array();
			foreach ( $ignoreData as $type => $ids )
			{
				$ignoreWhere[] = "( {$type}=1 AND " . $this->DB->buildWherePermission( $ids, 'ignore_ignore_id', FALSE ) . ')';
			}
			
			$this->DB->build( array( 'select' => '*', 'from' => 'ignored_users', 'where' => implode( ' OR ', $ignoreWhere ) ) );
			$this->DB->execute();
			while ( $row = $this->DB->fetch() )
			{
				if ( isset( $data[ $row['ignore_owner_id'] ] ) )
				{
					unset( $data[ $row['ignore_owner_id'] ] );
				}
			}
			
		}
				
		return is_array($data) ? $data : null;
	}
	
	/**
	 * Get data based on an area and last sent greater than date [unix timestampe]
	 *
	 * @param	integer		$date			Unix timestamp
	 * @param	array		$types			Array of notification types (optional)
	 * @param	array		$parseMembers	Parse extra data for each member and build display photo (false is default)
	 * @param	integer		$sendMax	Null (use ipsRegistry::$setting or send INT only)
	 * @return	@e mixed	Array of likes data OR null
	 * @see		allowedFrequencies()	 
	 */
	public function getDataByAreaAndLastSentOlderThanDate( $date, $types=array(), $parseMembers=false, $sendMax=null )
	{
		/* Init */
		$mids	 = array();
		$members = array();
		$rows    = array();
		$joins   = array();
		$where   = ( is_array( $types ) ) ? ' AND l.like_notify_freq IN (\'' . implode( "','", $types ) . '\')' : '';
		$sendMax = ( $sendMax !== null )  ? $sendMax : ipsRegistry::$settings['like_notifications_limit'];
		
		/* figure out joins */
		$joins[] = array( 'select' => 'm.*',
						  'from'   => array( 'members' => 'm' ),
						  'where'  => 'm.member_id=l.like_member_id',
					      'type'   => 'left' );
		
		$moreJoins = $this->getDataJoins();
		
		if ( is_array( $moreJoins ) AND count( $moreJoins ) )
		{
			foreach( $moreJoins as $join )
			{
				$joins[] = $join;
			}
		}
		
		/* Prevent it going back EONS AND EONS */
		$oldestPossDate = 0;
		
		if ( in_array( 'weekly', $types ) )
		{
			$oldestPossDate = $date - ( 86400 * 7 );
		}
		else
		{
			$oldestPossDate = $date - ( 86400 );
		}
		
		if ( $oldestPossDate )
		{
			$where .= ' AND ( CASE WHEN l.like_notify_sent > 0 THEN l.like_notify_sent ELSE l.like_added END ) < ' . intval( $oldestPossDate );
		}
		
		/* Fetch data */	
		$this->DB->build( array( 'select'   => 'l.*',
					   			 'from'     => array( 'core_like' => 'l' ),
								 'where'    => 'l.like_notify_do=1 AND l.like_app=\'' . classes_like_registry::getApp() . '\' AND l.like_area=\'' . classes_like_registry::getArea() . '\' AND l.like_visible=1 AND ( CASE WHEN l.like_notify_sent > 0 THEN l.like_notify_sent ELSE l.like_added END ) < ' . intval( $date ) . $where,
								 'order'    => 'l.like_notify_sent ASC',
								 'limit'    => array( 0, $sendMax ),
								 'add_join' => $joins ) );
		
						
		$o = $this->DB->execute();
		
		while( $row = $this->DB->fetch( $o ) )
		{
			$row['like_member_id']			= intval($row['like_member_id']);
			$mids[ $row['like_member_id'] ]	= $row['like_member_id'];
			
			/* Need to apply secondary groups and grab g_perm_id if $parseMembers is false (default)
				@link http://community.invisionpower.com/tracker/issue-34691-digest-notifications-not-going-out/ */
			$row['mgroup_others']	= ($row['mgroup_others'] != '') ? IPSText::cleanPermString($row['mgroup_others']) : '';
			$row					= array_merge( $row, $this->caches['group_cache'][ $row['member_group_id'] ] );
			$row					= $this->registry->member()->setUpSecondaryGroups( $row );
			
			if( $row['topic_last_post'] )
			{
				$row['last_post']	= $row['topic_last_post'];
			}
			
			/* @link http://community.invisionpower.com/tracker/issue-32204-dailyweekly-notifications */
			$data[ $row['like_member_id'] ][ $row['like_id'] ]	= $row;
		}
				
		/* Just the one? */
		if ( $parseMembers && count($mids) )
		{
			$members = IPSMember::load( $mids, 'all' );
		
			foreach( $members as $i => $d )
			{
				$_m = IPSMember::buildProfilePhoto( $d );
				
				foreach( $data[ $i ] as $likeId => $likeData )
				{
					$data[ $i ][ $likeId ] = array_merge( (array) $_m, (array) $data[ $i ][ $likeId ] );
				}
			}
		}
		
		return is_array($data) ? $data : null;
	}
	
	/**
	 * Clean the notification options to make sure they're all nice and fluffy
	 * 
	 * @param	array		$notifyOpts		Notification options
	 * @return	@e array	Cleaned notification options array
	 */
	protected function _cleanNotifyOptions( array $notifyOpts )
	{
		if ( isset( $notifyOpts['like_notify_do'] ) )
		{
			$notifyOpts['like_notify_do'] = intval( $notifyOpts['like_notify_do'] );
		}
		else
		{
			$notifyOpts['like_notify_do'] = 0;
		}
		
		if ( ! isset( $notifyOpts['like_notify_meta'] ) )
		{
			$notifyOpts['like_notify_meta'] = '';
		}
		
		if ( isset( $notifyOpts['like_notify_freq'] ) )
		{
			$notifyOpts['like_notify_freq'] = ( in_array( $notifyOpts['like_notify_freq'], $this->allowedFrequencies() ) ) ? $notifyOpts['like_notify_freq'] : '';
		}
		else
		{
			$notifyOpts['like_notify_freq'] = 0;
		}
		
		return $notifyOpts;
	}
	
    /**
	 * Return an array of acceptable frequencies
	 * Possible: immediate, offline, daily, weekly
	 * 
	 * @return	@e array
	 */
	public function allowedFrequencies()
	{
		return array( 'immediate', 'offline', 'daily', 'weekly' );
	}
	
	/**
	 * Return types of notification available for this item
	 * 
	 * @return	@e array	array( key, human readable )
	 */
	public function getNotifyType()
	{
		return array( 'comments', ipsRegistry::getClass('class_localization')->words['gbl_comments_like'] );
	}
	
	/**
	 * Fetch the template group
	 * 
	 * @return	@e string
	 */
	public function skin()
	{
		return 'global_other';
	}

	/**
	 * Fetch the template prefix.  This allows you to have two follow implementations in
	 * one skin file (i.e. skin_calendars -> eventLikeMoreDialog() and skin_calendars -> calendarLikeMoreDialog())
	 * 
	 * @return	@e string
	 */
	public function templatePrefix()
	{
		return '';
	}
	
	/**
	 * Gets the vernacular (like or follow)
	 *
	 * @return	@e string
	 */
	public function getVernacular()
	{
		return 'like';
	}
	
	/**
	 * Builds the notification data via the app class
	 * 
	 * @param	array		$data		like_ DB data and like owner member data
	 * @param	string		$type		Types of notifications to send
	 * @return	@e array	array( notification_key, notification_url, email_template, email_subject, build_message_array )
	 * @see		allowedFrequencies()
	 */
	public function buildNotificationData( $data, $type )
	{
		return array();
	}
	
	/**
	 * Check notifications that are to be sent to make sure they're valid and that
	 * 
	 * @param	array		$metaData		like_ DB data and like owner member data
	 * @return	@e boolean
	 */
	public function notificationCanSend( $metaData )
	{
		return true;
	}
	
	/**
	 * Fetches joins for fetching data
	 * 
	 * @param	string		$field		DB field name (defaults to 'l.like_rel_id')
	 * @return	@e array
	 */
	public function getDataJoins( $field='l.like_rel_id' )
	{
		return array();
	}
	
	/**
	 * Return the title based on the passed id
	 * 
	 * @param	mixed		$relId		Relationship ID or array of IDs
	 * @return	@e mixed	Title or array of titles
	 */
	public function getTitleFromId( $relId )
	{
		$meta = $this->getMeta( $relId, array( 'title' ) );
		
		if ( is_numeric( $relId ) )
		{
			return $meta[ $relId ]['like.title'];
		}
		else
		{
			$return = array();
			
			foreach( $meta as $id => $data )
			{
				$return[ $id ] = $data['like.title'];
			}
			
			return $return;
		}
	}

	/**
	 * Return the URL based on the passed id
	 * 
	 * @param	mixed		$relId		Relationship ID or array of IDs
	 * @return	@e mixed	URL or array of URLs
	 */
	public function getUrlFromId( $relId )
	{
		$meta = $this->getMeta( $relId, array( 'url' ) );
		
		if ( is_numeric( $relId ) )
		{
			return $meta[ $relId ]['like.url'];
		}
		else
		{
			$return = array();
			
			foreach( $meta as $id => $data )
			{
				$return[ $id ] = $data['like.url'];
			}
			
			return $return;
		}
	}
	
	/**
	 * Formats the Bob, Bill, Joe and 2038 Others Hate You
	 * 
	 * @param	array		$data		Likes data
	 * @return	@e string	Formatted names
	 */
	protected function _formatNameString( array $data )
	{
		$langString  = '';
		$seeMoreLink = 'app=core&amp;module=global&amp;section=like&amp;do=more';
		
		if ( ! is_array( $data['names'] ) OR ! count( $data['names'] ) )
		{
			return false;
		}
		/* Format up the names */
		$i      = 0;
		$_names = array();
		
		foreach( $data['names'] as $name )
		{
			if ( $this->memberData['member_id'] AND $this->memberData['member_id'] == $name['id'] )
			{
				$_names[$i] = $name['name'];
			}
			else
			{
				$_names[$i] = IPSMember::makeProfileLink($name['name'], $name['id'], $name['seo'] );
			}
			
			$i++;
		}

		/* More than one? */
		if ( $data['totalCount'] > 1 )
		{
			/* Joe and Matt love you */
			if ( $data['totalCount'] == 2 )
			{
				$_n = $_names[0] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[1];
				
				$langString = sprintf( $this->lang->words['fave_formatted_many'], $_n );
			}
			/* Joe, Matt and Mike love you more */
			else if ( $data['totalCount'] == 3 )
			{
				$_n = $_names[0] . ', ' . $_names[1] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_many'], $_n );
			}
			/* Joe, Matt, Mike and 1 more love you */
			else if ( $data['totalCount'] == 4 )
			{
				$_n = $_names[0] . ', ' . $_names[1] . ' ' . $this->lang->words['fave_and'] . ' ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_one_more'], $_n, $seeMoreLink );
			}
			/* Joe, Matt, Mike and 5 more are indifferent to your redonkulous comments */
			else
			{
				$_n = $_names[0] . ', ' . $_names[1] . ', ' . $_names[2];
				
				$langString = sprintf( $this->lang->words['fave_formatted_more'], $_n, $seeMoreLink, $data['othersCount'] );
			}
		}
		else
		{
			/* Just the one and it might be you! */	
			if ( $data['names'][0]['id'] == $this->memberData['member_id'] )
			{
				$langString = $this->lang->words['fave_formatted_me'];
			}
			else
			{
				$langString = sprintf( $this->lang->words['fave_formatted_one'], $_names[0] );
			}
		}

		return $langString;
	}
	
	/**
	 * Replace all likes on one item with another
	 * Used when merging two items
	 *
	 * @param	int		$id1			The ID of the item being deleted
	 * @param	int		$id2			The ID of the item being kept
	 */
	public function merge( $id1, $id2 )
	{
		/* We need to tell the driver not to escape our md5 compilation statements */
		$_fieldEncapsulate = $this->DB->fieldEncapsulate;
		$this->DB->fieldEncapsulate = '';
		$this->DB->manual_addslashes = TRUE;
		
		/* Run the query - this looks a bit nuts, but basically we're setting any records for
		likes on the old item to now be likes on the new, unelss the user has already liked
		the new, in which case do nothing and we'll delete the left-overs in a minute */
		$this->DB->update(
			array( 'core_like' => 'a' ),
			$this->DB->compileUpdateString( array(
				'a.like_id'			=> classes_like_registry::getKeyForSql( $id2, 'a.like_member_id' ),
				'a.like_lookup_id'	=> classes_like_registry::getKeyForSql( $id2 ),
				'a.like_lookup_area'=> classes_like_registry::getKeyForSql( null, 'a.like_member_id' ),
				'a.like_rel_id'		=> intval( $id2 ),
			) ),
			"a.like_app='{$this->_app}' AND a.like_area='{$this->_area}' AND a.like_rel_id={$id1} AND b.like_id " . $this->DB->buildIsNull(),
			false,
			true,
			array( array(
				'from'	=> array( 'core_like' => 'b' ),
				'where'	=> "b.like_app='{$this->_app}' AND b.like_area='{$this->_area}' AND b.like_rel_id={$id2}",
				) )
			);
			
		/* Reset the DB driver */
		$this->DB->manual_addslashes = FALSE;
		$this->DB->fieldEncapsulate = $_fieldEncapsulate;
		
		/* Remove any stragglers (would happen if user was subscribed to both topics */
		$this->remove( $id1 );
		
		/* Reset the cache */
		classes_like_cache::getInstance()->isNowStale( $id1 );
		classes_like_cache::getInstance()->isNowStale( $id2 );
	}
}

/**
 *
 * @class		classes_like_cache
 * @brief		Favorites cache class
 * @author		Matt
 */
class classes_like_cache
{
	/**
	 * Registry Object Shortcuts
	 *
	 * @var		$registry
	 * @var		$DB
	 * @var		$settings
	 * @var		$request
	 * @var		$lang
	 * @var		$member
	 * @var		$memberData
	 * @var		$cache
	 * @var		$caches
	 */
	protected $registry;
	protected $DB;
	protected $settings;
	protected $request;
	protected $lang;
	protected $member;
	protected $memberData;
	protected $cache;
	protected $caches;	
	
	/**
	 * Cache expiration time in seconds
	 *
	 * @var		$_expire
	 */
	protected $_expire = 0;
	
	/**
	 * Application key
	 *
	 * @var		$_app
	 */
	protected $_app;
	
	/**
	 * Area key
	 *
	 * @var		$_area
	 */
	protected $_area;
	
	/**
	 * Instance of an object
	 *
	 * @var		$instance
	 */
	private static $instance = null;
	
	/**
	 * Singleton
	 *
	 * @return	@e object
	 */
	public static function getInstance()
	{
		if ( self::$instance === null )
		{
			self::$instance = new self();
		}
		
		return self::$instance;
	}
	
	/**
	 * Constructor
	 *
	 * @return	@e void
	 */
	public function __construct()
	{
		/* Fetch registry like a good boy */
		$this->registry   =  ipsRegistry::instance();
		$this->DB         =  $this->registry->DB();
		$this->settings   =& $this->registry->fetchSettings();
		$this->request    =& $this->registry->fetchRequest();
		$this->lang       =  $this->registry->getClass('class_localization');
		$this->member     =  $this->registry->member();
		$this->memberData =& $this->registry->member()->fetchMemberData();
		$this->cache      =  $this->registry->cache();
		$this->caches     =& $this->registry->cache()->fetchCaches();
		
		$this->_app  = classes_like_registry::getApp();
		$this->_area = classes_like_registry::getArea();
		
		if ( empty( $this->_app ) OR empty( $this->_area ) )
		{
			trigger_error( "Missing area or app variable in " . __CLASS__ . '::' . __FUNCTION__, E_USER_WARNING );
		}
	}
	
	/**
	 * Set the cache expiration
	 *
	 * @param	integer		$seconds		Time in seconds
	 * @return	@e void
	 */
	public function setExpiration( $seconds )
	{
		$this->_expire = intval( $seconds );
	}
	
	/**
	 * Fetch an item from the cache
	 * 
	 * @param	integer		$relId		Relationship ID
	 * @param	integer		$memberId 	Member ID
	 * @return	@e array
	 */
	public function get( $relId, $memberId=null )
	{
		$cache = array();
		
		/* Possible future expansion */
		if ( $memberId === null )
		{
			$cache = $this->DB->buildAndFetch( array( 'select' => '*',
													  'from'   => 'core_like_cache',
													  'where'  => 'like_cache_id=\'' .  classes_like_registry::getKey( $relId ) . '\'' ) );
			
			if ( !is_array($cache) OR $cache['like_cache_expire'] <= IPS_UNIX_TIME_NOW )
			{ 
				/* We don't have a valid cache, so create one and return the data */
				$cache = $this->create( $relId, $memberId );
			}
			
			$tmp = unserialize($cache['like_cache_data']);
			$cache['members']	= $tmp['members'];
			$cache['count']		= intval($tmp['count']);
			$cache['count_anon']= intval($tmp['count_anon']);
			
			/* Set total count for use elsewhere */
			classes_like_registry::setTotalCount( $cache['count'] );
		}
		
		return $cache;
	}
	
	/**
	 * Creates/updates the relationship ID's cache
	 * 
	 * @param	integer		$relId		Relationship ID
	 * @param	integer		$memberId 	Member ID
	 * @return	@e array	Array of stored data
	 */
	public function create( $relId, $memberId=null )
	{
		/* Init vars */
		$store   = array();
		$items   = array();
		$members = array();
		$mids	 = array();
		$data    = array( 'members' => null, 'count' => 0 );
		
		/* Possible future expansion */
		if ( $memberId === null )
		{
			/* Count all public first */
			$cou = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as nt',
									 				'from'   => 'core_like',
									 				'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon=0 AND like_visible=1' ) );
			
			$data['count'] = intval( $cou['nt'] );
			
			/* Count all anonymous second */
			$ano = $this->DB->buildAndFetch( array( 'select' => 'COUNT(*) as nt',
									 				'from'   => 'core_like',
									 				'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon!=0 AND like_visible=1' ) );
			
			$data['count_anon'] = intval( $ano['nt'] );
			
			/* Fetch all items for this app:key:$relId */
			$this->DB->build( array( 'select' => '*',
									 'from'   => 'core_like',
									 'where'  => 'like_lookup_id=\'' . classes_like_registry::getKey( $relId ) . '\' AND like_is_anon=0 AND like_visible=1',
									 'order'  => 'like_added DESC',
									 'limit'  => array( 0, 5 )  ) );
									 
			$o = $this->DB->execute();
			
			while( $row = $this->DB->fetch($o) )
			{
				$items[ $row['like_rel_id'] ]   = $row;
				$mids[ $row['like_member_id'] ] = $row['like_member_id'];
			}
						
			/* Load members */
			$members = IPSMember::load( $mids, 'core' );
			
			/* We don't need all the member's infos */
			foreach( $members as $mid => $mdata )
			{
				$data['members'][ $mid ] = array( 'n' => $mdata['members_display_name'], 's' => $mdata['members_seo_name'] );
			}
		
			/* Build save array */
			$store = array( 'like_cache_id'     => classes_like_registry::getKey( $relId ),
						    'like_cache_app'    => $this->_app,
						    'like_cache_area'   => $this->_area,
						    'like_cache_rel_id' => $relId,
						    'like_cache_data'   => serialize( $data ),
						    'like_cache_expire' => IPS_UNIX_TIME_NOW + $this->_expire );
			
			/* Update the cache */
			$this->DB->replace( 'core_like_cache', $store, array( 'like_cache_id' ) );
		}
		else
		{
			/* We know there's a previous cache row, so delete it */
			$this->DB->delete( 'core_like_cache', 'like_cache_id=\'' . classes_like_registry::getKey( $relId ) . '\'' );
		}
		
		return $store;
	}
	
	/**
	 * Deletes a cache
	 * 
	 * @param	mixed		$relId		Relationship ID or array of IDs
	 * @param	integer		$memberId 	Member ID
	 * @return	@e void
	 */
	public function delete( $relId, $memberId=null )
	{
		/* Possible future expansion */
		if ( $memberId === null )
		{
			$where = '';
			
			if ( is_numeric($relId) )
			{
				$where = "='" . classes_like_registry::getKey( $relId ) . "'";
			}
			elseif ( is_array($relId) )
			{
				$relId = IPSLib::cleanIntArray($relId);
				$keys  = array();
				
				foreach( $relId as $id )
				{
					$keys[] = "'" . classes_like_registry::getKey( $id ) . "'";
				}
				
				if ( ! count($keys) )
				{
					return null;
				}
				
				$where = " IN (" . implode( ",", $keys ) . ")";
			}
			
			$this->DB->delete( 'core_like_cache', 'like_cache_id ' . $where );
		}
	}
	
	/**
	 * Flags a cache as stale. We choose to delete, but the cache class should make the call
	 * not the application
	 * 
	 * @param	mixed		$relId		Relationship ID or array of IDs
	 * @param	integer		$memberId 	Member ID
	 * @return	@e void
	 */
	public function isNowStale( $relId, $memberId = null )
	{
		return $this->delete( $relId, $memberId );
	}
}