#import "XMPP.h"
#import "XMPPRosterPrivate.h"
#import "XMPPRosterMemoryStorage.h"
#import "XMPPRosterMemoryStoragePrivate.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 XMPPRosterMemoryStorage ()

@property (readonly) dispatch_queue_t parentQueue;

@end

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

@implementation XMPPRosterMemoryStorage

- (id)init
{
	if ((self = [super init]))
	{
		userClass = [XMPPUserMemoryStorageObject class];
		resourceClass = [XMPPResourceMemoryStorageObject class];
		
		roster = [[NSMutableDictionary alloc] init];
	}
	return self;
}

- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue
{
	NSParameterAssert(aParent != nil);
	NSParameterAssert(queue != NULL);
	
	@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
			
			return YES;
		}
	}
	
	return NO;
}

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

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

@synthesize userClass;
@synthesize resourceClass;

- (XMPPRoster *)parent
{
	XMPPRoster *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
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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

- (XMPPUserMemoryStorageObject *)_userForJID:(XMPPJID *)jid
{
	AssertPrivateQueue();
	
	XMPPUserMemoryStorageObject *result = roster[[jid bareJID]];
	
	if (result)
	{
		return result;
	}
	
	XMPPJID *myBareJID = [myJID bareJID];
	XMPPJID *bareJID = [jid bareJID];
	
	if ([bareJID isEqualToJID:myBareJID])
	{
		return myUser;
	}
	
	return nil;
}

- (XMPPResourceMemoryStorageObject *)_resourceForJID:(XMPPJID *)jid
{
	AssertPrivateQueue();
	
	XMPPUserMemoryStorageObject *user = [self _userForJID:jid];
	return (XMPPResourceMemoryStorageObject *)[user resourceForJID:jid];
}

- (NSArray *)_unsortedUsers
{
	AssertPrivateQueue();
	
	return [roster allValues];
}

- (NSArray *)_unsortedAvailableUsers
{
	AssertPrivateQueue();
	
	NSArray *allUsers = [roster allValues];
	
	NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]];
	
	for (id <XMPPUser> user in allUsers)
	{
		if ([user isOnline])
		{
			[result addObject:user];
		}
	}
	
	return result;
}

- (NSArray *)_unsortedUnavailableUsers
{
	AssertPrivateQueue();
	
	NSArray *allUsers = [roster allValues];
	
	NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]];
	
	for (id <XMPPUser> user in allUsers)
	{
		if (![user isOnline])
		{
			[result addObject:user];
		}
	}
	
	return result;
}

- (NSArray *)_sortedUsersByName
{
	AssertPrivateQueue();
	
	return [[roster allValues] sortedArrayUsingSelector:@selector(compareByName:)];
}

- (NSArray *)_sortedUsersByAvailabilityName
{
	AssertPrivateQueue();
	
	return [[roster allValues] sortedArrayUsingSelector:@selector(compareByAvailabilityName:)];
}

- (NSArray *)_sortedAvailableUsersByName
{
	AssertPrivateQueue();
	
	return [[self _unsortedAvailableUsers] sortedArrayUsingSelector:@selector(compareByName:)];
}

- (NSArray *)_sortedUnavailableUsersByName
{
	AssertPrivateQueue();
	
	return [[self _unsortedUnavailableUsers] sortedArrayUsingSelector:@selector(compareByName:)];
}

- (NSArray *)_sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself
{
	AssertPrivateQueue();
	
	// Add all the resouces from all the available users in the roster
	// 
	// Remember: There may be multiple resources per user
	
	NSArray *availableUsers = [self unsortedAvailableUsers];
	
	NSMutableArray *result = [NSMutableArray arrayWithCapacity:[availableUsers count]];
	
	for (id<XMPPUser> user in availableUsers)
	{
		[result addObjectsFromArray:[user allResources]];
	}
	
	if (includeResourcesForMyUserExcludingMyself)
	{
		// Now add all the available resources from our own user account (excluding ourselves)
		
		NSArray *myResources = [myUser allResources];
		
		for (id<XMPPResource> resource in myResources)
		{
			if (![myJID isEqualToJID:[resource jid]])
			{
				[result addObject:resource];
			}
		}
	}
	
	return [result sortedArrayUsingSelector:@selector(compare:)];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Roster Management
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (XMPPUserMemoryStorageObject *)myUser
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return myUser;
	}
	else
	{
		__block XMPPUserMemoryStorageObject *result;
		
		dispatch_sync(parentQueue, ^{
			result = [myUser copy];
		});
		
		return result;
	}
}

- (XMPPResourceMemoryStorageObject *)myResource
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return (XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID];
	}
	else
	{
		__block XMPPResourceMemoryStorageObject *result;
		
		dispatch_sync(parentQueue, ^{
			XMPPResourceMemoryStorageObject *resource =
			    (XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID];
			result = [resource copy];
		});
		
		return result;
	}
}

- (XMPPUserMemoryStorageObject *)userForJID:(XMPPJID *)jid
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _userForJID:jid];
	}
	else
	{
		__block XMPPUserMemoryStorageObject *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			XMPPUserMemoryStorageObject *user = [self _userForJID:jid];
			result = [user copy];
			
		}});
		
		return result;
	}
}

- (XMPPResourceMemoryStorageObject *)resourceForJID:(XMPPJID *)jid
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _resourceForJID:jid];
	}
	else
	{
		__block XMPPResourceMemoryStorageObject *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			XMPPResourceMemoryStorageObject *resource = [self _resourceForJID:jid];
			result = [resource copy];
			
		}});
		
		return result;
	}
}

- (NSArray *)sortedUsersByName
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _sortedUsersByName];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _sortedUsersByName];
			
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)sortedUsersByAvailabilityName
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _sortedUsersByAvailabilityName];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _sortedUsersByAvailabilityName];
			
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)sortedAvailableUsersByName
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _sortedAvailableUsersByName];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _sortedAvailableUsersByName];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)sortedUnavailableUsersByName
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _sortedUnavailableUsersByName];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _sortedUnavailableUsersByName];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)unsortedUsers
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _unsortedUsers];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _unsortedUsers];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)unsortedAvailableUsers
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _unsortedAvailableUsers];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _unsortedAvailableUsers];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)unsortedUnavailableUsers
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _unsortedUnavailableUsers];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _unsortedUnavailableUsers];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

- (NSArray *)sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself
{
	// This is a public method, so it may be invoked on any thread/queue.
	
	if (self.parentQueue == NULL)
	{
		// Haven't been attached to parent yet
		return nil;
	}
	
	if (dispatch_get_specific(parentQueueTag))
	{
		return [self _sortedResources:includeResourcesForMyUserExcludingMyself];
	}
	else
	{
		__block NSArray *result;
		
		dispatch_sync(parentQueue, ^{ @autoreleasepool {
			
			NSArray *temp = [self _sortedResources:includeResourcesForMyUserExcludingMyself];
			result = [[NSArray alloc] initWithArray:temp copyItems:YES];
			
		}});
		
		return result;
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPRosterStorage Protocol
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version
{
	XMPPLogTrace();
	AssertParentQueue();
	
	isRosterPopulation = YES;
	
	myJID = self.parent.xmppStream.myJID;
	
	myUser = [[self.userClass alloc] initWithJID:myJID];
}

- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	isRosterPopulation = NO;
	
	[[self multicastDelegate] xmppRosterDidPopulate:self]; 
	[[self multicastDelegate] xmppRosterDidChange:self];
}

- (void)handleRosterItem:(NSXMLElement *)item xmppStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	NSString *jidStr = [item attributeStringValueForName:@"jid"];
	XMPPJID *jid = [[XMPPJID jidWithString:jidStr] bareJID];
	
	if (isRosterPopulation)
	{
		XMPPUserMemoryStorageObject *newUser =
		    (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item];
		
		roster[jid] = newUser;
		
		XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
	}
	else
	{
		NSString *subscription = [item attributeStringValueForName:@"subscription"];
		
		if ([subscription isEqualToString:@"remove"])
		{
			XMPPUserMemoryStorageObject *user = roster[jid];
			if (user)
			{
				[roster removeObjectForKey:jid];
				
				XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
				
				[[self multicastDelegate] xmppRoster:self didRemoveUser:user];
				[[self multicastDelegate] xmppRosterDidChange:self];
			}
		}
		else
		{
			XMPPUserMemoryStorageObject *user = roster[jid];
			if (user)
			{
				[user updateWithItem:item];
				
				XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
				
				[[self multicastDelegate] xmppRoster:self didUpdateUser:user];
				[[self multicastDelegate] xmppRosterDidChange:self];
			}
			else
			{
				XMPPUserMemoryStorageObject *newUser =
				    (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item];
				
				roster[jid] = newUser;
				
				XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
				
				[[self multicastDelegate] xmppRoster:self didAddUser:newUser];
				[[self multicastDelegate] xmppRosterDidChange:self];
				
			}
		}
	}
}

- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	int change = XMPP_USER_NO_CHANGE;
	
	XMPPUserMemoryStorageObject *user = nil;
	XMPPResourceMemoryStorageObject *resource = nil;
	
	XMPPJID *jidKey = [[presence from] bareJID];
	
	user = roster[jidKey];
	if (user == nil)
	{
		// Not a presence element from anyone in our roster (that we know of).
		
		if ([[myJID bareJID] isEqualToJID:jidKey])
		{
			// It's a presence element for our user, either our resource or another resource.
			
			user = myUser;
		}
		else if([parent allowRosterlessOperation])
		{
			// Unknown user (this is the first time we've encountered them).
			// This happens if the roster is in rosterlessOperation mode.
			
			user = (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithJID:jidKey];
			
			roster[jidKey] = user;
			
			[[self multicastDelegate] xmppRoster:self didAddUser:user];
			[[self multicastDelegate] xmppRosterDidChange:self];
		}
	}
	
	change = [user updateWithPresence:presence resourceClass:self.resourceClass andGetResource:&resource];
	
	XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
	
	if (change == XMPP_USER_ADDED_RESOURCE)
		[[self multicastDelegate] xmppRoster:self didAddResource:resource withUser:user];
	
	if (change == XMPP_USER_UPDATED_RESOURCE)
		[[self multicastDelegate] xmppRoster:self didUpdateResource:resource withUser:user];
	
	if (change == XMPP_USER_REMOVED_RESOURCE)
		[[self multicastDelegate] xmppRoster:self didRemoveResource:resource withUser:user];
	
	if (change != XMPP_USER_NO_CHANGE)
		[[self multicastDelegate] xmppRosterDidChange:self];
}

- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	XMPPJID *jidKey = [jid bareJID];
	XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];
	
	return (rosterUser != nil);
}

#if TARGET_OS_IPHONE
- (void)setPhoto:(UIImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
#else
- (void)setPhoto:(NSImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
#endif
{
	XMPPLogTrace();
	AssertParentQueue();
	
	XMPPJID *jidKey = [jid bareJID];
	XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];
	
	if (rosterUser)
	{
		[rosterUser setPhoto:photo];
	}
}

- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator])
	{
		[user clearAllResources];
	}
	
	[[self multicastDelegate] xmppRosterDidChange:self];
}

- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream
{
	XMPPLogTrace();
	AssertParentQueue();
	
	[roster removeAllObjects];
	
	myUser = nil;
	
	[[self multicastDelegate] xmppRosterDidChange:self];
}

- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream
{
    XMPPLogTrace();
	AssertParentQueue();
	
    NSMutableArray *results = [NSMutableArray array];
    
	for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator])
	{
        [results addObject:[user.jid bareJID]];
	}
    
    return results;
}

- (void)getSubscription:(NSString **)subscription
					ask:(NSString **)ask
			   nickname:(NSString **)nickname
				 groups:(NSArray **)groups
				 forJID:(XMPPJID *)jid
			 xmppStream:(XMPPStream *)stream
{

	XMPPLogTrace();
	AssertParentQueue();

	XMPPJID *jidKey = [jid bareJID];
	XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];

	if(rosterUser)
	{
		if(subscription)
		{
			*subscription = rosterUser.subscription;
		}

		if(ask)
		{
			*ask = rosterUser.ask;
		}

		if(nickname)
		{
			*nickname = rosterUser.nickname;
		}
		
		if(groups)
		{
			*groups = rosterUser.groups;
		}
	}
}

@end
