//
//  PithosAccountNode.m
//  pithos-macos
//
// Copyright 2011-2013 GRNET S.A. All rights reserved.
//
// Redistribution and use in source and binary forms, with or
// without modification, are permitted provided that the following
// conditions are met:
// 
//   1. Redistributions of source code must retain the above
//      copyright notice, this list of conditions and the following
//      disclaimer.
// 
//   2. Redistributions in binary form must reproduce the above
//      copyright notice, this list of conditions and the following
//      disclaimer in the documentation and/or other materials
//      provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// 
// The views and conclusions contained in the software and
// documentation are those of the authors and should not be
// interpreted as representing official policies, either expressed
// or implied, of GRNET S.A.

#import "PithosAccountNode.h"
#import "PithosContainerNode.h"
#import "ASIPithos.h"
#import "ASIPithosAccountRequest.h"
#import "ASIPithosAccount.h"
#import "ASIPithosContainer.h"
#import "ASIDownloadCache.h"
#import "PithosAccount.h"
#import "PithosUtilities.h"
#import "PithosActivityFacility.h"

static NSImage *sharedIcon = nil;

@implementation PithosAccountNode
@synthesize pithosAccount, accountRequest, applyMetadataAccountRequest, refreshMetadataAccountRequest, translatedGroups;

+ (void)initialize {
	if (self == [PithosAccountNode class])
        sharedIcon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)];
}

#pragma mark -
#pragma mark Object Lifecycle

- (void)dealloc {
    [accountRequest clearDelegatesAndCancel];
    [refreshMetadataAccountRequest clearDelegatesAndCancel];
    [applyMetadataAccountRequest clearDelegatesAndCancel];
}

#pragma mark -
#pragma mark Internal

- (void)updateGroups {
    if (!pithosAccount) {
        self.translatedGroups = [NSMutableDictionary dictionary];
    } else if (pithosAccountManager) {
        NSMutableSet *UUIDs = [NSMutableSet set];
        for (NSString *groupName in pithosAccount.groups) {
            [UUIDs addObjectsFromArray:[pithosAccount.groups objectForKey:groupName]];
        }
        [UUIDs removeObject:@""];
        [UUIDs removeObject:@"*"];
        if (UUIDs.count) {
            [pithosAccountManager updateUserCatalogForDisplaynames:nil UUIDs:[UUIDs allObjects]];
        }
        
        NSMutableDictionary *newTranslatedGroups = [NSMutableDictionary dictionaryWithCapacity:pithosAccount.groups.count];
        for (NSString *groupName in pithosAccount.groups) {
            NSMutableArray *groupUsers = [NSMutableArray array];
            for (NSString *UUID in [pithosAccount.groups objectForKey:groupName]) {
                [groupUsers addObject:[pithosAccountManager displaynameForUUID:UUID safe:YES]];
            }
            [newTranslatedGroups setObject:groupUsers forKey:groupName];
        }
        self.translatedGroups = newTranslatedGroups;
    } else {
        self.translatedGroups = [pithosAccount.groups copy];
    }
}

#pragma mark -
#pragma mark Actions

- (void)reset {
    [accountRequest clearDelegatesAndCancel];
    self.accountRequest = nil;
    [refreshMetadataAccountRequest clearDelegatesAndCancel];
    self.refreshMetadataAccountRequest = nil;
    [applyMetadataAccountRequest clearDelegatesAndCancel];
    self.applyMetadataAccountRequest = nil;
    children = nil;
    newChildren = nil;
    self.pithosAccount = nil;
    freshness = PithosNodeStateRefreshNeeded;
    forcedRefresh = YES;
    [self postChildrenUpdatedNotificationName];
    [self children];
}

#pragma mark -
#pragma mark Properties

- (void)setPithosAccount:(ASIPithosAccount *)aPithosAccount {
    if (![pithosAccount isEqualTo:aPithosAccount]) {
        pithosAccount = aPithosAccount;
        [self updateGroups];
    }
}

- (NSString *)url {
    return [NSString stringWithFormat:@"@account@%@%@",
            (sharingAccount ? sharingAccount : pithosAccountManager.pithos.authUser),
            (shared ? @"?shared" : @"")];
}

- (NSArray *)children {
    @synchronized(self) {
        switch (freshness) {
            case PithosNodeStateFresh:
                break;
            case PithosNodeStateRefreshNeeded:
                freshness = PithosNodeStateRefreshing;
                self.accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithosAccountManager.pithos
                                                                                         limit:0
                                                                                        marker:nil
                                                                                        shared:shared
                                                                                         until:nil];
                if (sharingAccount)
                    [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos];
                else if (!forcedRefresh)
                    accountRequest.downloadCache = [ASIDownloadCache sharedCache];
                accountRequest.delegate = self;
                accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                           [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                           NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
                                           NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", 
                                           nil];
                [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
                break;
            case PithosNodeStateRefreshing:
                break;
            case PithosNodeStateRefreshFinished:
                if (newChildren) {
                    children = newChildren;
                    newChildren = nil;
                }
                freshness = PithosNodeStateFresh;
            default:
                break;
        }
        return children;
    }
}

- (NSString *)displayName {
    if (displayName == nil) {
        if (!sharingAccount) {
            return @"account";
        } else if (pithosAccountManager) {
            return [pithosAccountManager displaynameForUUID:sharingAccount safe:YES];
        } else {
            return [sharingAccount copy];
        }
    }
    return [displayName copy];
}

- (NSImage *)icon {
    if (icon == nil)
        icon = sharedIcon;
    return icon;
}

#pragma mark -
#pragma mark ASIHTTPRequestDelegate

- (void)accountRequestFailed:(ASIPithosAccountRequest *)request {
    @autoreleasepool {
        NSString *message;
        NSError *error = [accountRequest error];
        if (error)
            message = [NSString stringWithFormat:@"Account listing %@ failed: %@", accountRequest.url, [error localizedDescription]];
        else
            message = [NSString stringWithFormat:@"Account listing %@ failed: (%d) %@", 
                       accountRequest.url, accountRequest.responseStatusCode, accountRequest.responseStatusMessage];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[PithosActivityFacility defaultPithosActivityFacility] startAndEndActivityWithType:PithosActivityOther message:message];
        });
        NSUInteger retries = [[accountRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
        if (retries > 0) {
            ASIPithosAccountRequest *newAccountRequest = (ASIPithosAccountRequest *)[PithosUtilities retryWithUpdatedURLRequest:accountRequest
                                                                                                        andPithosAccountManager:pithosAccountManager];
            [(NSMutableDictionary *)(newAccountRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];

            self.accountRequest = newAccountRequest;
            [[PithosUtilities prepareRequest:accountRequest priority:[[accountRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
        } else {
            newChildren = nil;
            self.accountRequest = nil;
            forcedRefresh = NO;
            @synchronized(self) {
                freshness = PithosNodeStateRefreshNeeded;
            }
        }
    }
}

- (void)accountRequestFinished:(ASIPithosAccountRequest *)request {
    @autoreleasepool {
        DLog(@"List account finished: %@", [accountRequest url]);
        DLog(@"Cached: %d", [accountRequest didUseCachedResponse]);
        if (accountRequest.responseStatusCode == 200) {
            self.pithosAccount = [accountRequest account];
            
            NSMutableArray *containers = [accountRequest.userInfo objectForKey:@"containers"];
            NSArray *someContainers = [accountRequest containers];
            if (containers == nil) {
                containers = [NSMutableArray arrayWithArray:someContainers];
            } else {
                [containers addObjectsFromArray:someContainers];
            }
            if ([someContainers count] < 10000) {
                if (!accountRequest.didUseCachedResponse || ([containers count] != [someContainers count]) || !children) {
                    // Save new children
                    DLog(@"using newChildren");
                    newChildren = [[NSMutableArray alloc] init];
                    NSMutableIndexSet *keptNodes = [NSMutableIndexSet indexSet];
                    for (ASIPithosContainer *container in containers) {
                        PithosContainerNode *node = [[PithosContainerNode alloc] initWithPithosAccountManager:pithosAccountManager
                                                                                              pithosContainer:container];
                        node.parent = self;
                        node.shared = shared;
                        node.sharingAccount = sharingAccount;
                        node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
                        if (children) {
                            NSUInteger oldIndex = [children indexOfObject:node];
                            if (oldIndex != NSNotFound) {
                                // Use the same pointer value, if possible
                                node = [children objectAtIndex:oldIndex];
    //                            node.pithosContainer = container;
                                [node setLimitedPithosContainer:container];
                                [keptNodes addIndex:oldIndex];
                            }
                        }
                        [newChildren addObject:node];
                    }
                    [[children objectsAtIndexes:
                      [[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [children count])] indexesPassingTest:^(NSUInteger idx, BOOL *stop){
                        if ([keptNodes containsIndex:idx])
                            return NO;
                        return YES;
                    }]] makeObjectsPerformSelector:@selector(pithosNodeWillBeRemoved)];
                }
                // Else cache was used and all results were fetched during this request, so existing children can be reused
                self.accountRequest = nil;
                forcedRefresh = NO;
                @synchronized(self) {
                    freshness = PithosNodeStateRefreshFinished;
                }
                [self postChildrenUpdatedNotificationName];
            } else {
                // Do an additional request to fetch more objects
                self.accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithosAccountManager.pithos
                                                                                         limit:0
                                                                                        marker:[[someContainers lastObject] name]
                                                                                        shared:shared
                                                                                         until:nil];
                if (sharingAccount)
                    [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithosAccountManager.pithos];
                else if (!forcedRefresh)
                    accountRequest.downloadCache = [ASIDownloadCache sharedCache];
                accountRequest.delegate = self;
                accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                           [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                           NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
                                           NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector",
                                           containers, @"containers",
                                           nil];
                [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
            }
        } else if (accountRequest.responseStatusCode == 304) {
            // Account is not modified, so existing children can be reused
            self.accountRequest = nil;
            forcedRefresh = NO;
            @synchronized(self) {
                freshness = PithosNodeStateRefreshFinished;
            }
            [self postChildrenUpdatedNotificationName];
        } else {
            [self accountRequestFailed:accountRequest];
        }
    }
}

- (void)accountMetadataRequestFinished:(ASIPithosAccountRequest *)request {
    @autoreleasepool {
        DLog(@"URL: %@", [request url]);
        DLog(@"cached: %d", [request didUseCachedResponse]);
        
        if ([request isEqualTo:applyMetadataAccountRequest]) {
            @synchronized(self) {
                self.applyMetadataAccountRequest = nil;
            }
            [self refreshInfo];
        } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
            self.pithosAccount = [refreshMetadataAccountRequest account];
            @synchronized(self) {
                self.refreshMetadataAccountRequest = nil;
            }
        }
    }
}

- (void)accountMetadataRequestFailed:(ASIPithosAccountRequest *)request {
    @autoreleasepool {
        NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
        if (retries > 0) {
            ASIPithosAccountRequest *newRequest = (ASIPithosAccountRequest *)[PithosUtilities retryWithUpdatedURLRequest:request
                                                                                                 andPithosAccountManager:pithosAccountManager];;
            [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
            if ([request isEqualTo:applyMetadataAccountRequest]) {
                @synchronized(self) {
                    self.applyMetadataAccountRequest = newRequest;
                }
            } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
                @synchronized(self) {
                    self.refreshMetadataAccountRequest = newRequest;
                }
            }
            [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
        } else {
            if ([request isEqualTo:applyMetadataAccountRequest]) {
                [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataAccountRequest];
                @synchronized(self) {
                    self.applyMetadataAccountRequest = nil;
                }
            } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
                [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataAccountRequest];
                @synchronized(self) {
                    self.refreshMetadataAccountRequest = nil;
                }
            }
        }
    }
}

#pragma mark -
#pragma mark Info

- (void)applyInfo {
    @synchronized(self) {
        if (applyMetadataAccountRequest == nil) {
            NSMutableDictionary *groups = [NSMutableDictionary dictionary];
            if (translatedGroups.count) {
                for (NSString *groupName in translatedGroups) {
                    if (!groupName.length ||
                        [groupName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" -_~,;"]].location != NSNotFound) {
                        NSAlert *alert = [[NSAlert alloc] init];
                        [alert setMessageText:@"Invalid Input"];
                        [alert setInformativeText:@"Group names cannot be empty or contain ' ', '-', '_', '~', ',' or ';'."];
                        [alert addButtonWithTitle:@"OK"];
                        [alert runModal];
                        return;
                    }
                }
                if (pithosAccountManager) {
                    NSMutableSet *allGroupUsers = [NSMutableSet set];
                    for (NSMutableArray *groupUsers in [translatedGroups objectEnumerator]) {
                        [allGroupUsers addObjectsFromArray:groupUsers];
                    }
                    [allGroupUsers removeObject:@""];
                    [allGroupUsers removeObject:@"*"];
                    if (allGroupUsers.count) {
                        ASIPithosRequest *userCatalogRequest = [pithosAccountManager updateUserCatalogForDisplaynames:[allGroupUsers allObjects]
                                                                                                                        UUIDs:nil];
                        if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
                            return;
                        } else if (userCatalogRequest.responseStatusCode == 200) {
                            // Check if all users exist.
                            NSDictionary *displaynameCatalog = [userCatalogRequest displaynameCatalog];
                            NSMutableArray *inexistentGroupUsers = [NSMutableArray array];
                            for (NSString *groupUser in allGroupUsers) {
                                if (![displaynameCatalog objectForKey:groupUser]) {
                                    [inexistentGroupUsers addObject:groupUser];
                                }
                            }
                            if (!inexistentGroupUsers.count) {
                                // Create groups.
                                for (NSString *groupName in translatedGroups) {
                                    NSMutableArray *groupUsers = [NSMutableArray array];
                                    for (NSString *groupUser in [translatedGroups objectForKey:groupName]) {
                                        [groupUsers addObject:([groupUser isEqualToString:@"*"] ?
                                                               @"*" : [displaynameCatalog objectForKey:groupUser])];
                                    }
                                    [groups setObject:groupUsers forKey:groupName];
                                }
                            } else {
                                NSAlert *alert = [[NSAlert alloc] init];
                                if (inexistentGroupUsers.count == 1) {
                                    [alert setMessageText:@"Invalid User"];
                                    [alert setInformativeText:[NSString stringWithFormat:@"User '%@' doesn't exist.", [inexistentGroupUsers objectAtIndex:0]]];
                                } else {
                                    [alert setMessageText:@"Invalid Users"];
                                    [alert setInformativeText:[NSString stringWithFormat:@"Users '%@' don't exist.", [inexistentGroupUsers componentsJoinedByString:@"', '"]]];
                                }
                                [alert addButtonWithTitle:@"OK"];
                                [alert runModal];
                                return;
                            }
                        } else {
                            // 404. Since we don't translate to UUIDs, check for invalid chars.
                            BOOL valid = YES;
                            for (NSString *groupUser in allGroupUsers) {
                                if ([groupUser rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ~,;:"]].location != NSNotFound) {
                                    valid = NO;
                                    break;
                                }
                            }
                            if (valid) {
                                [groups addEntriesFromDictionary:translatedGroups];
                            } else {
                                NSAlert *alert = [[NSAlert alloc] init];
                                [alert setMessageText:@"Invalid Input"];
                                [alert setInformativeText:@"Users cannot contain ' ', '~', ',', ';' or ':'."];
                                [alert addButtonWithTitle:@"OK"];
                                [alert runModal];
                                return;
                            }
                        }
                    } else {
                        for (NSString *groupName in translatedGroups) {
                            if ([[translatedGroups objectForKey:groupName] containsObject:@"*"]) {
                                [groups setObject:[NSMutableArray arrayWithObject:@"*"] forKey:groupName];
                            }
                        }
                        if (!groups.count) {
                            [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
                        }
                    }
                } else {
                    [groups addEntriesFromDictionary:translatedGroups];
                }
            } else {
                [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
            }
            
            self.applyMetadataAccountRequest = [ASIPithosAccountRequest updateAccountMetadataRequestWithPithos:pithosAccountManager.pithos
                                                                                                        groups:groups
                                                                                                      metadata:pithosAccount.metadata
                                                                                                        update:NO];
            applyMetadataAccountRequest.delegate = self;
            applyMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            applyMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            applyMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                                    [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
                                                    [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                                    NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
                                                    NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
                                                    nil];
            [[PithosUtilities prepareRequest:applyMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
        }
    }
}

- (void)refreshInfo {
    @synchronized(self) {
        if (refreshMetadataAccountRequest == nil) {
            self.refreshMetadataAccountRequest = [ASIPithosAccountRequest accountMetadataRequestWithPithos:pithosAccountManager.pithos];
            refreshMetadataAccountRequest.delegate = self;
            refreshMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            refreshMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            refreshMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                                      [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
                                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                                      NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
                                                      NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
                                                      nil];
            if (!sharingAccount)
                refreshMetadataAccountRequest.downloadCache = [ASIDownloadCache sharedCache];
            [[PithosUtilities prepareRequest:refreshMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
        }
    }
}

@end
