#import "XMPPRoomHybridStorage.h"
#import "XMPPRoomPrivate.h"
#import "XMPPCoreDataStorageProtected.h"
#import "NSXMLElement+XEP_0203.h"
#import "XMPPLogging.h"

#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

// Log levels: off, error, warn, info, verbose
#if DEBUG
  static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#else
  static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif

#define AssertPrivateQueue() \
            NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue");


@interface XMPPRoomHybridStorage ()
{
	// Protected variables are listed in the header file.
	// These are the private variables.
	
	NSString *messageEntityName;
	Class occupantClass;
	
	NSTimeInterval maxMessageAge;
	NSTimeInterval deleteInterval;
	
	NSMutableSet *pausedMessageDeletion;
	
	dispatch_time_t lastDeleteTime;
	dispatch_source_t deleteTimer;
}

- (void)performDelete;
- (void)destroyDeleteTimer;
- (void)updateDeleteTimer;
- (void)createAndStartDeleteTimer;

@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@implementation XMPPRoomHybridStorage

static XMPPRoomHybridStorage *sharedInstance;

+ (instancetype)sharedInstance
{
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		
		sharedInstance = [[XMPPRoomHybridStorage alloc] initWithDatabaseFilename:nil storeOptions:nil];
	});
	
	return sharedInstance;
}

- (void)commonInit
{
	XMPPLogTrace();
	[super commonInit];
	
	// This method is invoked by all public init methods of the superclass
	
	occupantsGlobalDict = [[NSMutableDictionary alloc] init];
	
	messageEntityName = NSStringFromClass([XMPPRoomMessageHybridCoreDataStorageObject class]);
	occupantClass = [XMPPRoomOccupantHybridMemoryStorageObject class];
	
	maxMessageAge  = (60 * 60 * 24 * 7); // 7 days
	deleteInterval = (60 * 5);           // 5 days
	
	pausedMessageDeletion = [[NSMutableSet alloc] init];
	
	autoRecreateDatabaseFile = YES;
}

/**
 * Documentation from the superclass (XMPPCoreDataStorage):
 * 
 * Override me, if needed, to provide customized behavior.
 * 
 * This method is queried to get the name of the ManagedObjectModel within the app bundle.
 * It should return the name of the appropriate file (*.xdatamodel / *.mom / *.momd) sans file extension.
 * 
 * The default implementation returns the name of the subclass, stripping any suffix of "CoreDataStorage".
 * E.g., if your subclass was named "XMPPExtensionCoreDataStorage", then this method would return "XMPPExtension".
 * 
 * Note that a file extension should NOT be included.
**/
- (NSString *)managedObjectModelName
{
	// Optional hook
	// 
	// The default implementation would return "XMPPPRoomHybridStorage".
	// We prefer a slightly shorter version.
	
	return @"XMPPRoomHybrid";
}

- (void)dealloc
{
	[self destroyDeleteTimer];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@synthesize messageEntityName;
@synthesize occupantClass;

- (NSTimeInterval)maxMessageAge
{
	__block NSTimeInterval result = 0;
	
	dispatch_block_t block = ^{
		result = maxMessageAge;
	};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_sync(storageQueue, block);
	
	return result;
}

- (void)setMaxMessageAge:(NSTimeInterval)age
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		NSTimeInterval oldMaxMessageAge = maxMessageAge;
		NSTimeInterval newMaxMessageAge = age;
		
		maxMessageAge = age;
		
		// There are several cases we need to handle here.
		// 
		// 1. If the maxAge was previously enabled and it just got disabled,
		//    then we need to stop the deleteTimer. (And we might as well release it.)
		// 
		// 2. If the maxAge was previously disabled and it just got enabled,
		//    then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
		// 
		// 3. If the maxAge was increased,
		//    then we don't need to do anything.
		// 
		// 4. If the maxAge was decreased,
		//    then we should do an immediate delete.
		
		BOOL shouldDeleteNow = NO;
		
		if (oldMaxMessageAge > 0.0)
		{
			if (newMaxMessageAge <= 0.0)
			{
				// Handles #1
				[self destroyDeleteTimer];
			}
			else if (oldMaxMessageAge > newMaxMessageAge)
			{
				// Handles #4
				shouldDeleteNow = YES;
			}
			else
			{
				// Handles #3
				// Nothing to do now
			}
		}
		else if (newMaxMessageAge > 0.0)
		{
			// Handles #2
			shouldDeleteNow = YES;
		}
		
		if (shouldDeleteNow)
		{
			[self performDelete];
			
			if (deleteTimer)
				[self updateDeleteTimer];
			else
				[self createAndStartDeleteTimer];
		}
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_async(storageQueue, block);
}

- (NSTimeInterval)deleteInterval
{
	__block NSTimeInterval result = 0;
	
	dispatch_block_t block = ^{
		result = deleteInterval;
	};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_sync(storageQueue, block);
	
	return result;
}

- (void)setDeleteInterval:(NSTimeInterval)interval
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		deleteInterval = interval;
		
		// There are several cases we need to handle here.
		// 
		// 1. If the deleteInterval was previously enabled and it just got disabled,
		//    then we need to stop the deleteTimer. (And we might as well release it.)
		// 
		// 2. If the deleteInterval was previously disabled and it just got enabled,
		//    then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
		// 
		// 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date.
		// 
		// 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date.
		//    (Plus we might need to do an immediate delete.)
		
		if (deleteInterval > 0.0)
		{
			if (deleteTimer == NULL)
			{
				// Handles #2
				// 
				// Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
				// if a delete is needed the timer will fire immediately.
				
				[self createAndStartDeleteTimer];
			}
			else
			{
				// Handles #3
				// Handles #4
				// 
				// Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
				// if a save is needed the timer will fire immediately.
				
				[self updateDeleteTimer];
			}
		}
		else if (deleteTimer)
		{
			// Handles #1
			
			[self destroyDeleteTimer];
		}
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_async(storageQueue, block);
}

- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		[pausedMessageDeletion addObject:[roomJID bareJID]];
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_async(storageQueue, block);
}

- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		[pausedMessageDeletion removeObject:[roomJID bareJID]];
		[self performDelete];
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block();
	else
		dispatch_async(storageQueue, block);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Internal API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)performDelete
{
	if (maxMessageAge <= 0.0) return;
	
	NSDate *minLocalTimestamp = [NSDate dateWithTimeIntervalSinceNow:(maxMessageAge * -1.0)];
	
	NSManagedObjectContext *moc = [self managedObjectContext];
	NSEntityDescription *messageEntity = [self messageEntity:moc];
	
	NSPredicate *predicate;
	if ([pausedMessageDeletion count] > 0)
	{
		predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@ AND roomJIDStr NOT IN %@",
		                                                  minLocalTimestamp, pausedMessageDeletion];
	}
	else
	{
		predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@", minLocalTimestamp];
	}
	
	NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
	[fetchRequest setEntity:messageEntity];
	[fetchRequest setPredicate:predicate];
	[fetchRequest setFetchBatchSize:saveThreshold];
	
	NSError *error = nil;
	NSArray *oldMessages = [moc executeFetchRequest:fetchRequest error:&error];
	
	if (error)
	{
		XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error);
	}
	
	NSUInteger unsavedCount = [self numberOfUnsavedChanges];
	
	for (XMPPRoomMessageHybridCoreDataStorageObject *oldMessage in oldMessages)
	{
		[moc deleteObject:oldMessage];
		
		if (++unsavedCount >= saveThreshold)
		{
			[self save];
			unsavedCount = 0;
		}
	}
	
	lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0);
}

- (void)destroyDeleteTimer
{
	if (deleteTimer)
	{
		dispatch_source_cancel(deleteTimer);
		#if !OS_OBJECT_USE_OBJC
		dispatch_release(deleteTimer);
		#endif
		deleteTimer = NULL;
	}
}

- (void)updateDeleteTimer
{
	if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0))
	{
		uint64_t interval = deleteInterval * NSEC_PER_SEC;
		dispatch_time_t startTime;
		
		if (lastDeleteTime > 0)
			startTime = dispatch_time(lastDeleteTime, interval);
		else
			startTime = dispatch_time(DISPATCH_TIME_NOW, interval);
		
		dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0);
	}
}

- (void)createAndStartDeleteTimer
{
	if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0))
	{
		deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, storageQueue);
		
		dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool {
			
			[self performDelete];
			
		}});
		
		[self updateDeleteTimer];
		
		if (deleteTimer)
			dispatch_resume(deleteTimer);
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Protected API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Optional override hook.
**/
- (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream
{
	NSDate *remoteTimestamp = [message delayedDeliveryDate];
	
	if (remoteTimestamp == nil)
	{
		// When the xmpp server sends us a room message, it will always timestamp delayed messages.
		// For example, when retrieving the discussion history, all messages will include the original timestamp.
		// If a message doesn't include such timestamp, then we know we're getting it in "real time".
		
		return NO;
	}
	
	// Does this message already exist in the database?
	// How can we tell if two XMPPRoomMessages are the same?
	// 
	// 1. Same streamBareJidStr
	// 2. Same jid
	// 3. Same text
	// 4. Approximately the same timestamps
	// 
	// This is actually a rather difficult question.
	// What if the same user sends the exact same message multiple times?
	// 
	// If we first received the message while already in the room, it won't contain a remoteTimestamp.
	// Returning to the room later and downloading the discussion history will return the same message,
	// this time with a remote timestamp.
	// 
	// So if the message doesn't have a remoteTimestamp,
	// but it's localTimestamp is approximately the same as the remoteTimestamp,
	// then this is enough evidence to consider the messages the same.
	// 
	// Note: Predicate order matters. Most unique key should be first, least unique should be last.
	
	NSManagedObjectContext *moc = [self managedObjectContext];
	NSEntityDescription *messageEntity = [self messageEntity:moc];
	
	NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
	
	XMPPJID *messageJID = [message from];
	NSString *messageBody = [[message elementForName:@"body"] stringValue];
	
	NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60];
	NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60];
	
	NSString *predicateFormat = @"    body == %@ "
	                            @"AND jidStr == %@ "
	                            @"AND streamBareJidStr == %@ "
	                            @"AND "
	                            @"("
	                            @"     (remoteTimestamp == %@) "
	                            @"  OR (remoteTimestamp == NIL && localTimestamp BETWEEN {%@, %@})"
	                            @")";
	
	NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat,
	                             messageBody, messageJID, streamBareJidStr,
	                             remoteTimestamp, minLocalTimestamp, maxLocalTimestamp];
	
	NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
	[fetchRequest setEntity:messageEntity];
	[fetchRequest setPredicate:predicate];
	[fetchRequest setFetchLimit:1];
		
	NSError *error = nil;
	NSArray *results = [moc executeFetchRequest:fetchRequest error:&error];
	
	if (error)
	{
		XMPPLogError(@"%@: %@ - Fetch error: %@", THIS_FILE, THIS_METHOD, error);
	}
	
	return ([results count] > 0);
}

/**
 * Optional override hook for general extensions.
 * 
 * @see insertMessage:outgoing:forRoom:stream:
**/
- (void)didInsertMessage:(XMPPRoomMessageHybridCoreDataStorageObject *)message
{
	// Override me if you're extending the XMPPRoomMessageHybridCoreDataStorageObject class
	// to add additional properties, which you can set here.
	// 
	// At this point the standard properties have already been set.
	// So you can, for example, access the XMPPMessage via message.message.
}

/**
 * Optional override hook for complete customization.
 * Override me if you need to do specific custom work when inserting a message in a room.
 * 
 * @see didInsertMessage:
**/
- (void)insertMessage:(XMPPMessage *)message
             outgoing:(BOOL)isOutgoing
              forRoom:(XMPPRoom *)room
               stream:(XMPPStream *)xmppStream
{
	// Extract needed information
	
	XMPPJID *roomJID = room.roomJID;
	XMPPJID *messageJID = isOutgoing ? room.myRoomJID : [message from];
	
	NSDate *localTimestamp;
	NSDate *remoteTimestamp;
	
	if (isOutgoing)
	{
		localTimestamp = [[NSDate alloc] init];
		remoteTimestamp = nil;
	}
	else
	{
		remoteTimestamp = [message delayedDeliveryDate];
		if (remoteTimestamp) {
			localTimestamp = remoteTimestamp;
		}
		else {
			localTimestamp = [[NSDate alloc] init];
		}
	}
	
	NSString *messageBody = [[message elementForName:@"body"] stringValue];
	
	NSManagedObjectContext *moc = [self managedObjectContext];
	NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
	
	NSEntityDescription *messageEntity = [self messageEntity:moc];
	
	// Add to database
	
	XMPPRoomMessageHybridCoreDataStorageObject *roomMessage = (XMPPRoomMessageHybridCoreDataStorageObject *)
	    [[NSManagedObject alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:nil];
	
	roomMessage.message = message;
	roomMessage.roomJID = roomJID;
	roomMessage.jid = messageJID;
	roomMessage.nickname = [messageJID resource];
	roomMessage.body = messageBody;
	roomMessage.localTimestamp = localTimestamp;
	roomMessage.remoteTimestamp = remoteTimestamp;
	roomMessage.isFromMe = isOutgoing;
	roomMessage.streamBareJidStr = streamBareJidStr;
	
	[moc insertObject:roomMessage];      // Hook if subclassing XMPPRoomMessageHybridCDSO (awakeFromInsert)
	[self didInsertMessage:roomMessage]; // Hook if subclassing XMPPRoomHybridStorage
}

/**
 * Optional override hook for general extensions.
 * 
 * @see insertOccupantWithPresence:room:stream:
**/
- (void)didInsertOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
{
	// Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class
	// to add additional properties, which you can set here.
	// 
	// At this point the standard properties have already been set.
	// So you can, for example, access the XMPPPresence via occupant.presece.
}

/**
 * Optional override hook for general extensions.
 *
 * @see updateOccupant:withPresence:room:stream:
**/
- (void)didUpdateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
{
	// Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class,
	// and you have additional properties that may need to be updated.
	// 
	// At this point the standard properties have already been updated.
}

/**
 * Optional override hook for general extensions.
**/
- (void)willRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
{
	// Override me if you have any custom work to do before an occupant leaves (is removed from storage).
}

/**
 * Optional override hook for general extensions.
**/
- (void)didRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
{
	// Override me if you have any custom work to do after an occupant leaves (is removed from storage).
}

/**
 * Optional override hook for complete customization.
 * Override me if you need to do custom work when inserting an occupant in a room.
**/ 
- (XMPPRoomOccupantHybridMemoryStorageObject *)insertOccupantWithPresence:(XMPPPresence *)presence
                                                                     room:(XMPPRoom *)room
                                                                   stream:(XMPPStream *)xmppStream
{
	XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream];
	XMPPJID *roomJid = room.roomJID;
	
	NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
	if (occupantsRoomsDict == nil)
	{
		occupantsRoomsDict = [[NSMutableDictionary alloc] init];
		occupantsGlobalDict[streamFullJid] = occupantsRoomsDict;
	}
	
	NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
	if (occupantsRoomDict == nil)
	{
		occupantsRoomDict = [[NSMutableDictionary alloc] init];
		occupantsRoomsDict[roomJid] = occupantsRoomDict;
	}
	
	XMPPRoomOccupantHybridMemoryStorageObject *occupant = (XMPPRoomOccupantHybridMemoryStorageObject *)
	    [[self.occupantClass alloc] initWithPresence:presence streamFullJid:streamFullJid];
	
	occupantsRoomDict[occupant.jid] = occupant;
	
	return occupant;
}

/**
 * Optional override hook for complete customization.
 * Override me if you need to do custom work when updating an occupant in a room.
**/
- (void)updateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
          withPresence:(XMPPPresence *)presence
                  room:(XMPPRoom *)room
                stream:(XMPPStream *)stream
{
	
	[occupant updateWithPresence:presence];
}

/**
 * Optional override hook for complete customization.
 * Override me if you need to do custom work when removing an occupant from a room.
**/
- (void)removeOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant
          withPresence:(XMPPPresence *)presence
                  room:(XMPPRoom *)room
                stream:(XMPPStream *)stream
{
	// Remove from dictionary
	
	XMPPJID *streamFullJid = [self myJIDForXMPPStream:stream];
	XMPPJID *roomJid = occupant.roomJID;
	
	NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
	NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
	
	[occupantsRoomDict removeObjectForKey:occupant.jid]; // Remove occupant
	if ([occupantsRoomDict count] == 0)
	{
		[occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room if now empty
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc
{
	// This method should be thread-safe.
	// So be sure to access the entity name through the property accessor.
	
	if (moc == nil)
	{
		XMPPLogWarn(@"%@: %@ - Invalid parameter, moc is nil", THIS_FILE, THIS_METHOD);
		return nil;
	}
	
	NSString *entityName = self.messageEntityName;
	return [NSEntityDescription entityForName:entityName inManagedObjectContext:moc];
}

- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID
                                       stream:(XMPPStream *)xmppStream
                                    inContext:(NSManagedObjectContext *)inMoc
{
	if (roomJID == nil) return nil;
	
	// It's possible to use our internal managedObjectContext only because we're not returning a NSManagedObject.
	
	__block NSDate *result = nil;
	
	dispatch_block_t block = ^{ @autoreleasepool {
		
		NSManagedObjectContext *moc = inMoc ? : [self managedObjectContext];
		
		NSEntityDescription *entity = [self messageEntity:moc];
		
		NSPredicate *predicate;
		if (xmppStream)
		{
			NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
			
			NSString *predicateFormat = @"roomJIDStr == %@ AND streamBareJidStr == %@";
			predicate = [NSPredicate predicateWithFormat:predicateFormat, roomJID, streamBareJidStr];
		}
		else
		{
			predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", roomJID];
		}
		
		NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"localTimestamp" ascending:NO];
		NSArray *sortDescriptors = @[sortDescriptor];
		
		NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
		[fetchRequest setEntity:entity];
		[fetchRequest setPredicate:predicate];
		[fetchRequest setSortDescriptors:sortDescriptors];
		[fetchRequest setFetchLimit:1];
		
		NSError *error = nil;
		XMPPRoomMessageHybridCoreDataStorageObject *message = (XMPPRoomMessageHybridCoreDataStorageObject *)
		    [[moc executeFetchRequest:fetchRequest error:&error] lastObject];

		if (error)
		{
			XMPPLogError(@"%@: %@ - fetchRequest error: %@", THIS_FILE, THIS_METHOD, error);
		}
		else
		{
			result = [message.localTimestamp copy];
		}
	}};
	
	if (inMoc == nil)
		dispatch_sync(storageQueue, block);
	else
		block();
	
	return result;
}

- (XMPPRoomOccupantHybridMemoryStorageObject *)occupantForJID:(XMPPJID *)occupantJid stream:(XMPPStream *)xmppStream
{
	if (occupantJid == nil) return nil;
	
	__block XMPPRoomOccupantHybridMemoryStorageObject *occupant = nil;
	
	void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool {
		
		XMPPJID *roomJid = [occupantJid bareJID];
		
		if (xmppStream)
		{
			XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream];
			
			NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
			NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
			
			occupant = occupantsRoomDict[occupantJid];
		}
		else
		{
			for (XMPPJID *streamFullJid in occupantsGlobalDict)
			{
				NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
				NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
								
				occupant = occupantsRoomDict[occupantJid];
				if (occupant) break;
			}
		}
		
		if (shouldCopy)
		{
			occupant = [occupant copy];
		}
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block(NO);
	else
		dispatch_sync(storageQueue, ^{ block(YES); });
	
	return occupant;
}

- (NSArray *)occupantsForRoom:(XMPPJID *)roomJid stream:(XMPPStream *)xmppStream
{
	roomJid = [roomJid bareJID]; // Just in case a full jid is accidentally passed
	
	__block NSArray *results = nil;
	
	void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool {
		
		if (xmppStream)
		{
			XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream];
			
			NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
			NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
			
			results = [occupantsRoomDict allValues];
		}
		else
		{
			for (XMPPJID *streamFullJid in occupantsGlobalDict)
			{
				NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
				NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid];
				
				if (occupantsRoomDict)
				{
					results = [occupantsRoomDict allValues];
					break;
				}
			}
		}
		
		if (shouldCopy)
		{
			NSArray *temp = results;
			results = [[NSArray alloc] initWithArray:temp copyItems:YES];
		}
	}};
	
	if (dispatch_get_specific(storageQueueTag))
		block(NO);
	else
		dispatch_sync(storageQueue, ^{ block(YES); });
	
	return results;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPRoomStorage Protocol
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
{
	XMPPLogTrace();
	
	dispatch_queue_t roomQueue = room.moduleQueue;
	XMPPStream *xmppStream = room.xmppStream;
	
	[self scheduleBlock:^{
		
		XMPPJID *from = [presence from];
		
		if ([[presence type] isEqualToString:@"unavailable"])
		{
			XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream];
			if (occupant)
			{
				// Occupant did leave - remove
				
				[self willRemoveOccupant:occupant];
				[self removeOccupant:occupant withPresence:presence room:room stream:xmppStream];
				[self didRemoveOccupant:occupant];
				
				// Notify delegate(s)
				
				XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy];
				dispatch_async(roomQueue, ^{ @autoreleasepool {
					
					GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
					    (GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[room multicastDelegate];
					
					[roomMulticastDelegate xmppRoomHybridStorage:self
					                            occupantDidLeave:occupantCopy];
				}});
			}
		}
		else
		{
			XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream];
			if (occupant == nil)
			{
				// Occupant did join - add
				
				occupant = [self insertOccupantWithPresence:presence room:room stream:xmppStream];
				if (occupant == nil)
				{
					// Subclasses may choose to ignore occupants for whatever reason.
					return;
				}
				
				[self didInsertOccupant:occupant];
				
				// Notify delegate(s)
				
				XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy];
				dispatch_async(roomQueue, ^{ @autoreleasepool {
				
					GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
					    (GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[room multicastDelegate];
					
					[roomMulticastDelegate xmppRoomHybridStorage:self
					                             occupantDidJoin:occupantCopy];
				}});
			}
			else
			{
				// Occupant did update - move
				
				[self updateOccupant:occupant withPresence:presence room:room stream:xmppStream];
				[self didUpdateOccupant:occupant];
				
				// Notify delegate(s)
				
				XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy];
				dispatch_async(roomQueue, ^{ @autoreleasepool {
					
					GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
					    (GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[room multicastDelegate];
					
					[roomMulticastDelegate xmppRoomHybridStorage:self
					                           occupantDidUpdate:occupantCopy];
				}});
			}
		}
	}];
}

- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
	XMPPLogTrace();
	
	XMPPStream *xmppStream = room.xmppStream;
	
	[self scheduleBlock:^{
		
		[self insertMessage:message outgoing:YES forRoom:room stream:xmppStream];
	}];
}

- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
	XMPPLogTrace();
	
	XMPPJID *myRoomJID = room.myRoomJID;
	XMPPJID *messageJID = [message from];
	
	if ([myRoomJID isEqualToJID:messageJID])
	{
		if (![message wasDelayed])
		{
			// Ignore - we already stored message in handleOutgoingMessage:room:
			return;
		}
	}
	
	XMPPStream *xmppStream = room.xmppStream;
	
	[self scheduleBlock:^{
		
		if ([self existsMessage:message forRoom:room stream:xmppStream])
		{
			XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD);
		}
		else
		{
			[self insertMessage:message outgoing:NO forRoom:room stream:xmppStream];
		}
	}];
}

- (void)handleDidLeaveRoom:(XMPPRoom *)room
{
	XMPPLogTrace();
	
	XMPPJID *roomJid = room.roomJID;
	XMPPStream *xmppStream = room.xmppStream;
	
	[self scheduleBlock:^{
		
		XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream];
		
		NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid];
		
		[occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room (and all associated occupants)
	}];
}

@end
