#import "ContactsManager.h"
#import "FutureSource.h"
#import "FutureUtil.h"
#import <AddressBook/AddressBook.h>
#import "Constraints.h"
#import "Environment.h"
#import "NotificationManifest.h"
#import "PhoneNumberDirectoryFilter.h"
#import "PhoneNumberDirectoryFilterManager.h"
#import "PreferencesUtil.h"
#import "Util.h"


//@TODO: Split Up into Seperate components.

#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()

static NSString *const FAVOURITES_DEFAULT_KEY = @"FAVOURITES_DEFAULT_KEY";
static NSString *const KNOWN_USERS_DEFAULT_KEY = @"KNOWN_USERS_DEFAULT_KEY";

typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL*);

@interface ContactsManager () {
	NSMutableArray *_favouriteContactIds;
    NSMutableArray *_knownWhisperUserIds;
    BOOL newUserNotificationsEnabled;
    
    id addressBookReference;
}

@end

@implementation ContactsManager

- (id)init {
    self = [super init];
    if (self) {
        newUserNotificationsEnabled = [self knownUserStoreInitialized];
        _favouriteContactIds = [self loadFavouriteIds];
        _knownWhisperUserIds = [self loadKnownWhisperUsers];
        life = [CancelTokenSource cancelTokenSource];
        observableContactsController = [ObservableValueController observableValueControllerWithInitialValue:nil];
        observableWhisperUsersController = [ObservableValueController observableValueControllerWithInitialValue:nil];
        [self registerNotificationHandlers];
    }
    return self;
}
-(void) doAfterEnvironmentInitSetup {
    [self setupAddressBook];
    [observableContactsController watchLatestValueOnArbitraryThread:^(NSArray *latestContacts) {
        @synchronized(self) {
            [self setupLatestContacts:latestContacts];
        }
    } untilCancelled:[life getToken]];
    
    [observableWhisperUsersController watchLatestValueOnArbitraryThread:^(NSArray *latestUsers) {
        @synchronized(self) {
            [self setupLatestWhisperUsers:latestUsers];
        }
    } untilCancelled:[life getToken]];
}

-(void)dealloc {
    [life cancel];
}

#pragma mark - Notification Handlers
-(void) registerNotificationHandlers{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatedDirectoryHandler:) name:NOTIFICATION_DIRECTORY_UPDATE object:nil];
}

-(void) updatedDirectoryHandler:(NSNotification*) notification {
    [self checkForNewWhisperUsers];
 }

-(void) enableNewUserNotifications{
    newUserNotificationsEnabled = YES;
}

#pragma mark - Address Book callbacks

void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context);
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
    ContactsManager* contactsManager = (__bridge ContactsManager*)context;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [contactsManager pullLatestAddressBook];
        
    });
}

#pragma mark - Setup

-(void) setupAddressBook {
    dispatch_async(ADDRESSBOOK_QUEUE, ^{
        [[ContactsManager asyncGetAddressBook] thenDo:^(id addressBook) {
            addressBookReference = addressBook;
            ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
            ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void*)self);
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
                [self pullLatestAddressBook];
            });
        }];
    });
}

-(void) pullLatestAddressBook{
    CFErrorRef creationError = nil;
    ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
    checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError) localizedDescription]) ;
    ABAddressBookRequestAccessWithCompletion(addressBookRef,  nil);
    [observableContactsController updateValue:[self getContactsFromAddressBook:addressBookRef]];
}

- (void)setupLatestContacts:(NSArray *)contacts {
    if (contacts) {
        latestContactsById = [ContactsManager keyContactsById:contacts];
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self checkForNewWhisperUsers];
        });
    }
}

- (void)setupLatestWhisperUsers:(NSArray *)users {
    if (users) {
        latestWhisperUsersById = [ContactsManager keyContactsById:users];
        
        if (!observableFavouritesController) {
            NSArray *favourites = [self contactsForContactIds:_favouriteContactIds];
            observableFavouritesController = [ObservableValueController observableValueControllerWithInitialValue:favourites];
        }

    }
}

#pragma mark - Observables

-(ObservableValue *) getObservableContacts {
    return observableContactsController;
}

-(ObservableValue *) getObservableWhisperUsers {
    return observableWhisperUsersController;
}

-(ObservableValue *) getObservableFavourites {
    return observableFavouritesController;
}

#pragma mark - Address Book utils

+(Future*) asyncGetAddressBook {
    CFErrorRef creationError = nil;
    ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
    assert((addressBookRef == nil) == (creationError != nil));
    if (creationError != nil) {
        return [Future failed:(__bridge_transfer id)creationError];
    }

    FutureSource *futureAddressBookSource = [FutureSource new];

    id addressBook = (__bridge_transfer id)addressBookRef;
    ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) {
        if (granted) {
            dispatch_async(ADDRESSBOOK_QUEUE,^{
                [futureAddressBookSource trySetResult:addressBook];
            });
        } else {
            [futureAddressBookSource trySetFailure:(__bridge id)requestAccessError];
        }
    });

    return futureAddressBookSource;
}

-(NSArray*) getContactsFromAddressBook:(ABAddressBookRef)addressBook {
    ABRecordRef source = ABAddressBookCopyDefaultSource(addressBook);

    NSArray *allPeople = (__bridge_transfer NSArray *)
                         (ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering(addressBook,
                                                                                    source,
                                                                                    kABPersonSortByFirstName));

    return [allPeople map:^id(id item) {
        return [self contactForRecord:(__bridge ABRecordRef)item];
    }];
}

-(NSArray*)latestContactsWithSearchString:(NSString *)searchString {
    return [[latestContactsById allValues] filter:^int(Contact *contact) {
        return [searchString length] == 0 || [ContactsManager name:[contact fullName] matchesQuery:searchString];
    }];
}

#pragma mark - Contact/Phone Number util

- (Contact *)contactForRecord:(ABRecordRef)record {
    ABRecordID recordID = ABRecordGetRecordID(record);

    NSString *firstName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonFirstNameProperty);
    NSString *lastName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonLastNameProperty);
    NSArray *phoneNumbers = [self phoneNumbersForRecord:record];

    if (!firstName && !lastName) {
        NSString *companyName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonOrganizationProperty);
        if (companyName) {
            firstName = companyName;
        } else if ([phoneNumbers count]) {
            firstName =	[phoneNumbers firstObject];
        }
    }

    NSString *notes = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonNoteProperty);
    NSArray *emails = [ContactsManager emailsForRecord:record];
    NSData *image = (__bridge_transfer NSData*)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
    UIImage *img = [UIImage imageWithData:image];
        
    ContactSearchBlock searchBlock = ^BOOL(NSNumber *obj, NSUInteger idx, BOOL *stop) {
        return [obj intValue] == recordID;
    };

    NSUInteger favouriteIndex = [_favouriteContactIds indexOfObjectPassingTest:searchBlock];

    return [Contact contactWithFirstName:firstName
                             andLastName:lastName
                 andUserTextPhoneNumbers:phoneNumbers
                               andEmails:emails
                                andImage:img
                            andContactID:recordID
                          andIsFavourite:favouriteIndex != NSNotFound
                                andNotes:notes];
}

-(Contact*)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber {
    NSArray *allContacts = [latestContactsById allValues];

    ContactSearchBlock searchBlock = ^BOOL(Contact *contact, NSUInteger idx, BOOL *stop) {
        for (PhoneNumber *number in contact.parsedPhoneNumbers) {
            
            if ([self phoneNumber:number matchesNumber:phoneNumber]) {
                *stop = YES;
                return YES;
            }
        }
        return NO;
    };

    NSUInteger contactIndex = [allContacts indexOfObjectPassingTest:searchBlock];

    if (contactIndex != NSNotFound) {
        return allContacts[contactIndex];
    } else {
        return nil;
    }
}

- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
    return [[phoneNumber1 toE164] isEqualToString:[phoneNumber2 toE164]];
}

- (NSArray *)phoneNumbersForRecord:(ABRecordRef)record {
    ABMultiValueRef numberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);

    @try {
        NSArray *phoneNumbers = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(numberRefs);
        
        if (phoneNumbers == nil) phoneNumbers = @[];
        
        NSMutableArray *numbers = [NSMutableArray array];
        
        for (CFIndex i = 0; i < (NSInteger)[phoneNumbers count]; i++) {
            NSString *phoneNumber = phoneNumbers[(NSUInteger)i];
            [numbers addObject:phoneNumber];
        }
        
        return numbers;
        
    } @finally {
        if (numberRefs) {
            CFRelease(numberRefs);
        }
    }
}

+(NSArray *)emailsForRecord:(ABRecordRef)record {
    ABMultiValueRef emailRefs = ABRecordCopyValue(record, kABPersonEmailProperty);

    @try {
        NSArray *emails = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(emailRefs);
        
        if (emails == nil) emails = @[];
        
        return emails;
        
    } @finally {
        if (emailRefs) {
            CFRelease(emailRefs);
        }
    }
}

+(NSDictionary *)groupContactsByFirstLetter:(NSArray *)contacts matchingSearchString:(NSString *)optionalSearchString {
    require(contacts != nil);

    NSArray *matchingContacts = [contacts filter:^int(Contact *contact) {
        return [optionalSearchString length] == 0 || [self name:[contact fullName] matchesQuery:optionalSearchString];
    }];

    return [matchingContacts groupBy:^id(Contact *contact) {
        NSString *nameToUse = @"";
    
        BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst?YES:NO;
        
        if (firstNameOrdering && [contact firstName] != nil && [[contact firstName] length] > 0) {
            nameToUse = [contact firstName];
        } else if (!firstNameOrdering && [contact lastName] != nil && [[contact lastName] length] > 0){
            nameToUse = [contact lastName];
        } else if ([contact lastName] == nil) {
            if ([[contact fullName] length] > 0) {
                nameToUse = [contact fullName];
            } else {
                return nameToUse;
            }
        } else {
            nameToUse = [contact lastName];
        }
        
        return [[[nameToUse substringToIndex:1] uppercaseString] decomposedStringWithCompatibilityMapping];
    }];
}

+(NSDictionary *)keyContactsById:(NSArray *)contacts {
    return [contacts keyedBy:^id(Contact* contact) {
        return [NSNumber numberWithInt:(int)contact.recordID];
    }];
}

-(Contact *)latestContactWithRecordId:(ABRecordID)recordId {
    @synchronized(self) {
        return [latestContactsById objectForKey:[NSNumber numberWithInt:recordId]];
    }
}

-(NSArray*) recordsForContacts:(NSArray*) contacts{
    return [contacts map:^id(Contact *contact) {
        return [NSNumber numberWithInt:[contact recordID]];
    }];
}

+(BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString {
    NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
    NSArray *queryStrings = [queryString componentsSeparatedByCharactersInSet:whitespaceSet];
    NSArray *nameStrings = [nameString componentsSeparatedByCharactersInSet:whitespaceSet];

    return [queryStrings all:^int(NSString* query) {
        if ([query length] == 0) return YES;
        return [nameStrings any:^int(NSString* nameWord) {
            NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
            return [nameWord rangeOfString:query options:searchOpts].location != NSNotFound;
        }];
    }];
}

+(BOOL)phoneNumber:(PhoneNumber *)phoneNumber matchesQuery:(NSString *)queryString {
    NSString *phoneNumberString = [phoneNumber localizedDescriptionForUser];
    NSString *searchString = [[phoneNumberString componentsSeparatedByCharactersInSet:
                            [[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
                           componentsJoinedByString:@""];

    if ([queryString length] == 0) return YES;
    NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
    return [searchString rangeOfString:queryString options:searchOpts].location != NSNotFound;
}

-(NSArray*) contactsForContactIds:(NSArray *)contactIds {
    NSMutableArray *contacts = [NSMutableArray array];
    for (NSNumber *favouriteId in contactIds) {
        Contact *contact = [self latestContactWithRecordId:[favouriteId integerValue]];
        
        if (contact) {
            [contacts addObject:contact];
        }
    }
    return [contacts copy];
}

#pragma mark - Favourites

-(NSMutableArray *)loadFavouriteIds {
    NSArray *favourites = [[NSUserDefaults standardUserDefaults] objectForKey:FAVOURITES_DEFAULT_KEY];
    return favourites == nil ? [NSMutableArray array] : [favourites mutableCopy];
}

-(void)saveFavouriteIds {
    [[NSUserDefaults standardUserDefaults] setObject:[_favouriteContactIds copy]
                                              forKey:FAVOURITES_DEFAULT_KEY];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [observableFavouritesController updateValue:[self contactsForContactIds:_favouriteContactIds]];
}

-(void)toggleFavourite:(Contact *)contact {
    require(contact != nil);

    contact.isFavourite = !contact.isFavourite;
    if (contact.isFavourite) {
        [_favouriteContactIds addObject:[NSNumber numberWithInt:contact.recordID]];
    } else {
        
        ContactSearchBlock removeBlock = ^BOOL(NSNumber *favouriteNumber, NSUInteger idx, BOOL *stop) {
            return [favouriteNumber integerValue] == contact.recordID;
        };
        
        NSUInteger indexToRemove = [_favouriteContactIds indexOfObjectPassingTest:removeBlock];
        
        if (indexToRemove != NSNotFound) {
            [_favouriteContactIds removeObjectAtIndex:indexToRemove];
        }
    }
    [self saveFavouriteIds];
}

+(NSArray *)favouritesForAllContacts:(NSArray *)contacts {
    return [contacts filter:^int(Contact* contact) {
        return [contact isFavourite];
    }];
}

#pragma mark - Whisper User Management

-(unsigned int) checkForNewWhisperUsers {
	NSArray *currentUsers = [self getWhisperUsersFromContactsArray:[latestContactsById allValues]];
	NSArray *newUsers     = [self getNewItemsFrom:currentUsers comparedTo:[latestWhisperUsersById allValues]];
    
	if([newUsers count] > 0){
		[observableWhisperUsersController updateValue:currentUsers];
	}
    
    NSArray *unacknowledgedUserIds = [self getUnacknowledgedUsersFrom:currentUsers];
    if([unacknowledgedUserIds count] > 0){
        NSArray *unacknowledgedUsers = [self contactsForContactIds: unacknowledgedUserIds];
        if(!newUserNotificationsEnabled){
            [self addContactsToKnownWhisperUsers:unacknowledgedUsers];
        }else{
            NSDictionary *payload = [NSDictionary dictionaryWithObject:unacknowledgedUsers forKey:NOTIFICATION_DATAKEY_NEW_USERS];
            [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NEW_USERS_AVAILABLE object:self userInfo:payload];
        }
    }
    return [newUsers count];
}

-(NSArray*) getUnacknowledgedUsersFrom:(NSArray*) users {
    NSArray *userIds = [self recordsForContacts:users];
    return [self getNewItemsFrom:userIds comparedTo:_knownWhisperUserIds ];
}

-(NSUInteger) getNumberOfUnacknowledgedCurrentUsers{
    NSArray *currentUsers = [self getWhisperUsersFromContactsArray:[latestContactsById allValues]];
    return [[self getUnacknowledgedUsersFrom:currentUsers] count];
}

-(NSArray*) getWhisperUsersFromContactsArray:(NSArray*) contacts {
	return [contacts filter:^int(Contact* contact) {
        return [self isContactRegisteredWithWhisper:contact];
    }];
}

-(NSArray*) getNewItemsFrom:(NSArray*) newArray comparedTo:(NSArray*) oldArray {
	NSMutableSet *newSet = [NSMutableSet setWithArray:newArray];
	NSSet *oldSet = [NSSet setWithArray:oldArray];
	
	[newSet minusSet:oldSet];
	return [newSet allObjects];
}

- (BOOL)isContactRegisteredWithWhisper:(Contact*) contact {
	for(PhoneNumber *phoneNumber in contact.parsedPhoneNumbers){
		if ( [self isPhoneNumberRegisteredWithWhisper:phoneNumber]) {
			return YES;
		}
	}
	return NO;
}

- (BOOL)isPhoneNumberRegisteredWithWhisper:(PhoneNumber *)phoneNumber {
	PhoneNumberDirectoryFilter* directory = [[[Environment getCurrent] phoneDirectoryManager] getCurrentFilter];
	return phoneNumber != nil && [directory containsPhoneNumber:phoneNumber];
}

-(void) addContactsToKnownWhisperUsers:(NSArray*) contacts {
    for( Contact *contact in contacts){
        [_knownWhisperUserIds addObject:@([contact recordID])];
    }
    NSMutableSet *users = [NSMutableSet setWithArray:[latestWhisperUsersById allValues]];
    [users addObjectsFromArray:contacts];
    
    [observableWhisperUsersController updateValue:[users allObjects]];
    [self saveKnownWhisperUsers];
}

-(BOOL) knownUserStoreInitialized{
    NSUserDefaults *d = [[NSUserDefaults standardUserDefaults] objectForKey:KNOWN_USERS_DEFAULT_KEY];
    return  (Nil != d);
}

-(NSMutableArray*) loadKnownWhisperUsers{
    NSArray *knownUsers = [[NSUserDefaults standardUserDefaults] objectForKey:KNOWN_USERS_DEFAULT_KEY];
    return knownUsers == nil ? [NSMutableArray array] : [knownUsers mutableCopy];
}

-(void) saveKnownWhisperUsers{
    _knownWhisperUserIds = [NSMutableArray arrayWithArray:[latestWhisperUsersById allKeys]];
    [[NSUserDefaults standardUserDefaults] setObject:[_knownWhisperUserIds copy] forKey:KNOWN_USERS_DEFAULT_KEY];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

-(void) clearKnownWhisUsers{
    [[NSUserDefaults standardUserDefaults] setObject:[NSArray array] forKey:KNOWN_USERS_DEFAULT_KEY];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

@end
