#import "XMPPRoomMemoryStorage.h"
#import "XMPPRoomPrivate.h"
#import "XMPP.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; // | XMPP_LOG_FLAG_TRACE;
#else
  static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif

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

#define AssertParentQueue() \
        NSAssert(dispatch_get_specific(parentQueueTag), @"Private protocol method: MUST run on parentQueue");

@interface XMPPRoomMemoryStorage ()
{
  #if __has_feature(objc_arc_weak)
	__weak XMPPRoom *parent;
  #else
	__unsafe_unretained XMPPRoom *parent;
  #endif
	dispatch_queue_t parentQueue;
	void *parentQueueTag;
	
	NSMutableArray * messages;
	NSMutableArray * occupantsArray;
	NSMutableDictionary * occupantsDict;
	
	Class messageClass;
	Class occupantClass;
}

@property (readonly) dispatch_queue_t parentQueue;

@end

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

@implementation XMPPRoomMemoryStorage

- (id)init
{
	if ((self = [super init]))
	{
		messages  = [[NSMutableArray alloc] init];
		occupantsArray = [[NSMutableArray alloc] init];
		occupantsDict  = [[NSMutableDictionary alloc] init];
		
		messageClass = [XMPPRoomMessageMemoryStorageObject class];
		occupantClass = [XMPPRoomOccupantMemoryStorageObject class];
	}
	return self;
}

- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue
{
	NSParameterAssert(aParent != nil);
	NSParameterAssert(queue != NULL);
	
	BOOL result = NO;
	
	@synchronized(self)
	{
		if ((parent == nil) && (parentQueue == NULL))
		{
			parent = aParent;
			parentQueue = queue;
			parentQueueTag = &parentQueueTag;
			dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL);
			
			#if !OS_OBJECT_USE_OBJC
			dispatch_retain(parentQueue);
			#endif
			
			result = YES;
		}
	}
	
	return result;
}

- (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)multicastDelegate
{
	return (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)[parent multicastDelegate];
}

- (void)dealloc
{
	#if !OS_OBJECT_USE_OBJC
	if (parentQueue)
		dispatch_release(parentQueue);
	#endif
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@synthesize messageClass;
@synthesize occupantClass;

- (XMPPRoom *)parent
{
	XMPPRoom *result = nil;
	
	@synchronized(self) // synchronized with configureWithParent:queue:
	{
		result = parent;
	}
	
	return result;
}

- (dispatch_queue_t)parentQueue
{
	dispatch_queue_t result = NULL;
	
	@synchronized(self) // synchronized with configureWithParent:queue:
	{
		result = parentQueue;
	}
	
	return result;
}

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

- (BOOL)existsMessage:(XMPPMessage *)message
{
	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;
	}
	
	if ([messages count] == 0)
	{
		// Safety net for binary search algorithm used below
		
		return NO;
	}
	
	
	// Does this message already exist in the messages array?
	// How can we tell if two XMPPRoomMessages are the same?
	// 
	// 1. Same jid
	// 2. Same text
	// 3. Same remoteTimestamp
	// 4. Approximately the same localTimestamps (if existing message doesn't have set remoteTimestamp)
	// 
	// 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.
	
	// Algorithm overview:
	// 
	// Since the clock of the client and server may be out of sync,
	// a localTimestamp and remoteTimestamp may be off by several seconds.
	// So we're going to search a range of messages, bounded by a min and max localTimestamp.
	// 
	// We find the first message that has a localTimestamp >= minLocalTimestamp.
	// We then search from there to the first message that has a localTimestamp > maxLocalTimestamp.
	// 
	// This represents our range of messages to search.
	// Then we can simply iterate over these messages to see if any have the same jid and text.
	
	NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60];
	NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60];
	
	// Use binary search to locate first message with localTimestamp >= minLocalTimestamp.
	
	NSInteger mid;
	NSInteger min = 0;
	NSInteger max = [messages count] - 1;
	
	while (YES)
	{
		mid = (min + max) / 2;
		XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
		
		NSComparisonResult cmp = [minLocalTimestamp compare:[currentMessage localTimestamp]];
		if (cmp == NSOrderedAscending)
		{
			// minLocalTimestamp < currentMessage.localTimestamp
			
			if (mid == min)
				break;
			else
				max = mid - 1;
		}
		else // Descending || Same
		{
			// minLocalTimestamp >= currentMessage.localTimestamp
			
			if (mid == max) {
				mid++;
				break;
			}
			else {
				min = mid + 1;
			}
		}
	}
	
	// The 'mid' variable now points to the index of the first message in the sorted messages array
	// that has a localTimestamp >= minLocalTimestamp.
	// 
	// We're going to start looking for matching messages here,
	// and break if we find a message with localTimestamp <= maxLocalTimestamp.
	
	XMPPJID *messageJid = [message from];
	NSString *messageBody = [[message elementForName:@"body"] stringValue];
	
	NSInteger index;
	for (index = mid; index < [messages count]; index++)
	{
		XMPPRoomMessageMemoryStorageObject *currentMessage = messages[index];
		
		NSComparisonResult cmp = [maxLocalTimestamp compare:[currentMessage localTimestamp]];
		if (cmp != NSOrderedAscending)
		{
			// maxLocalTimestamp >= currentMessage.localTimestamp
			break;
		}
		
		if ([currentMessage.jid isEqualToJID:messageJid])
		{
			if ([currentMessage.body isEqualToString:messageBody])
			{
				if (currentMessage.remoteTimestamp)
				{
					if ([currentMessage.remoteTimestamp isEqualToDate:remoteTimestamp])
					{
						// 1. jid matches
						// 2. body matches
						// 3. remoteTimestamp matches
						// 
						// => Incoming message already exists in the array.
						
						return YES;
					}
				}
				else
				{
					// 1. jid matches
					// 2. body matches
					// 3. existing message in array doesn't have set remoteTimestamp
					// 4. existing message has approximately the same localTimestamp
					// 
					// => Incoming message already exists in the array.
					
					return YES;
				}
			}
		}
	}

	return NO;
}

- (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorageObject *)message
{
	NSUInteger count = [messages count];
	
	if (count == 0)
	{
		[messages addObject:message];
		return 0;
	}
	
	// Shortcut - Most (if not all) messages are inserted at the end
	
	XMPPRoomMessageMemoryStorageObject *lastMessage = messages[count - 1];
	if ([message compare:lastMessage] != NSOrderedAscending)
	{
		[messages addObject:message];
		return count;
	}
	
	// Shortcut didn't work.
	// Find location using binary search algorithm.
	
	NSInteger mid;
	NSInteger min = 0;
	NSInteger max = count - 1;
	
	while (YES)
	{
		mid = (min + max) / 2;
		XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
		
		NSComparisonResult cmp = [message compare:currentMessage];
		if (cmp == NSOrderedAscending)
		{
			// message < currentMessage
			
			if (mid == min)
				break;
			else
				max = mid - 1;
		}
		else // Descending || Same
		{
			// message >= currentMessage
			
			if (mid == max) {
				mid++;
				break;
			}
			else {
				min = mid + 1;
			}
		}
	}
	
	// Algorithm check:
	// 
	// Insert in array[length] at index (index)
	// : min, mid, max (cmp_result)
	// 
	// 
	// Insert in array[3] at index (0)
	// : 0, 1, 2 (asc)
	// : 0, 0, 0 (asc) break
	// 
	// Insert in array[3] at index (1)
	// : 0, 1, 2 (asc)
	// : 0, 0, 0 (dsc) mid++, break
	// 
	// Insert in array[3] at index (2)
	// : 0, 1, 2 (dsc)
	// : 2, 2, 2 (asc) break
	// 
	// Insert in array[3] at index (3)
	// : 0, 1, 2 (dsc)
	// : 2, 2, 2 (dsc) mid++, break
	
	[messages insertObject:message atIndex:mid];
	return (NSUInteger)mid;
}

- (void)addMessage:(XMPPRoomMessageMemoryStorageObject *)roomMsg
{
	NSUInteger index = [self insertMessage:roomMsg];
	
	XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[[roomMsg jid]];
	
	XMPPRoomMessageMemoryStorageObject *roomMsgCopy = [roomMsg copy];
	XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
	NSArray *messagesCopy = [[NSArray alloc] initWithArray:messages copyItems:YES];
	
	[[self multicastDelegate] xmppRoomMemoryStorage:self
	                              didReceiveMessage:roomMsgCopy
	                                   fromOccupant:occupantCopy
	                                        atIndex:index
	                                        inArray:messagesCopy];
}

- (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant
{
	NSUInteger count = [occupantsArray count];
	
	if (count == 0)
	{
		[occupantsArray addObject:occupant];
		return 0;
	}
	
	// Find location using binary search algorithm.
	
	NSInteger mid;
	NSInteger min = 0;
	NSInteger max = count - 1;
	
	while (YES)
	{
		mid = (min + max) / 2;
		XMPPRoomOccupantMemoryStorageObject *currentOccupant = occupantsArray[mid];
		
		NSComparisonResult cmp = [occupant compare:currentOccupant];
		if (cmp == NSOrderedAscending)
		{
			// occupant < currentOccupant
			
			if (mid == min)
				break;
			else
				max = mid - 1;
		}
		else // Descending || Same
		{
			// occupant >= currentOccupant
			
			if (mid == max) {
				mid++;
				break;
			}
			else {
				min = mid + 1;
			}
		}
	}
	
	[occupantsArray insertObject:occupant atIndex:mid];
	return (NSUInteger)mid;
}

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

- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid
{
	XMPPLogTrace();
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return occupantsDict[jid];
	}
	else
	{
		__block XMPPRoomOccupantMemoryStorageObject *occupant = nil;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			occupant = [occupantsDict[jid] copy];
		}});
		
		return occupant;
	}
}

- (NSArray *)messages
{
	XMPPLogTrace();
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return messages;
	}
	else
	{
		__block NSArray *result = nil;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			result = [[NSArray alloc] initWithArray:messages copyItems:YES];
		}});
		
		return result;
	}
}

- (NSArray *)occupants
{
	XMPPLogTrace();
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return occupantsArray;
	}
	else
	{
		__block NSArray *result = nil;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
		}});
		
		return result;
	}
}

- (NSArray *)resortMessages
{
	XMPPLogTrace();
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		[messages sortUsingSelector:@selector(compare:)];
		return messages;
	}
	else
	{
		__block NSArray *result = nil;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			[messages sortUsingSelector:@selector(compare:)];
			result = [[NSArray alloc] initWithArray:messages copyItems:YES];
		}});
		
		return result;
	}
}

- (NSArray *)resortOccupants
{
	XMPPLogTrace();
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		[occupantsArray sortUsingSelector:@selector(compare:)];
		return occupantsArray;
	}
	else
	{
		__block NSArray *result = nil;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			[occupantsArray sortUsingSelector:@selector(compare:)];
			result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
		}});
		
		return result;
	}
}

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

- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
{
	XMPPLogTrace();
	AssertParentQueue();
	
	XMPPJID *from = [presence from];
	
	if ([[presence type] isEqualToString:@"unavailable"])
	{
		XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
		if (occupant)
		{
			// Occupant did leave - remove
			
			NSUInteger index = [occupantsArray indexOfObjectIdenticalTo:occupant];
			
			[occupantsArray removeObjectAtIndex:index];
			[occupantsDict removeObjectForKey:from];
			
			// Notify delegate(s)
			
			XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
			NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
			
			[[self multicastDelegate] xmppRoomMemoryStorage:self
			                               occupantDidLeave:occupantCopy
			                                        atIndex:index
			                                      fromArray:occupantsCopy];
		}
	}
	else
	{
		XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
		if (occupant == nil)
		{
			// Occupant did join - add
			
			occupant = [[self.occupantClass alloc] initWithPresence:presence];
			
			NSUInteger index = [self insertOccupant:occupant];
			occupantsDict[from] = occupant;
			
			// Notify delegate(s)
			
			XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
			NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
			
			[[self multicastDelegate] xmppRoomMemoryStorage:self
			                                occupantDidJoin:occupantCopy
			                                        atIndex:index
			                                        inArray:occupantsCopy];
		}
		else
		{
			// Occupant did update - move
			
			[occupant updateWithPresence:presence];
			
			NSUInteger oldIndex = [occupantsArray indexOfObjectIdenticalTo:occupant];
			[occupantsArray removeObjectAtIndex:oldIndex];
			NSUInteger newIndex = [self insertOccupant:occupant];
			
			// Notify delegate(s)
			
			XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
			NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
			
			[[self multicastDelegate] xmppRoomMemoryStorage:self
			                              occupantDidUpdate:occupantCopy
			                                      fromIndex:oldIndex
			                                        toIndex:newIndex
			                                        inArray:occupantsCopy];
		}
	}
}

- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
	XMPPLogTrace();
	AssertParentQueue();
	
	XMPPJID *msgJID = room.myRoomJID;
	
	XMPPRoomMessageMemoryStorageObject *roomMsg;
	roomMsg = [[self.messageClass alloc] initWithOutgoingMessage:message jid:msgJID];
	
	[self addMessage:roomMsg];
}

- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
	XMPPLogTrace();
	AssertParentQueue();
	
	XMPPJID *myRoomJID = room.myRoomJID;
	XMPPJID *messageJID = [message from];
	
	if ([myRoomJID isEqualToJID:messageJID])
	{
		if (![message wasDelayed])
		{
			// Ignore - we already stored message in handleOutgoingMessage:room:
			return;
		}
	}
	
	if ([self existsMessage:message])
	{
		XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD);
	}
	else
	{
		XMPPRoomMessageMemoryStorageObject *roomMessage = [[self.messageClass alloc] initWithIncomingMessage:message];
		[self addMessage:roomMessage];
	}
}

- (void)handleDidLeaveRoom:(XMPPRoom *)room
{
	XMPPLogTrace();
	AssertParentQueue();
	
	[occupantsDict removeAllObjects];
	[occupantsArray removeAllObjects];
}

@end
