//
//  PithosSyncDaemon.m
//  pithos-macos
//
// Copyright 2011-2012 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 "PithosSyncDaemon.h"
#import "PithosAccount.h"
#import "PithosLocalObjectState.h"
#import "PithosActivityFacility.h"
#import "PithosUtilities.h"
#import "ASINetworkQueue.h"
#import "ASIPithosRequest.h"
#import "ASIPithos.h"
#import "ASIPithosContainerRequest.h"
#import "ASIPithosObjectRequest.h"
#import "ASIPithosObject.h"

@interface PithosSyncDaemon (Private)
- (void)resetLocalState;
- (NSString *)pithosStateFilePath;
- (void)saveLocalState;

- (BOOL)moveToTempTrashFile:(NSString *)filePath;
- (void)emptyTempTrash;
- (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath;

- (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath;
- (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
                                   object:(ASIPithosObject *)object 
                            localFilePath:(NSString *)filePath;
- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest;
- (void)requestFailed:(ASIPithosRequest *)request;

- (void)syncOperationStarted;
- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull;

@end

@implementation PithosSyncDaemon
@synthesize directoryPath, pithosAccount, containerName, pithos;
@synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates, currentLocalObjectStates;
@synthesize containerDirectoryPath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;

#pragma mark -
#pragma Object Lifecycle

- (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
              pithosAccount:(PithosAccount *)aPithosAccount 
              containerName:(NSString *)aContainerName 
            resetLocalState:(BOOL)resetLocalState {
    if ((self = [super init])) {
        directoryPath = [aDirectoryPath copy];
        pithosAccount = [aPithosAccount retain];
        containerName = [aContainerName copy];
        self.pithos = pithosAccount.pithos;
        
        activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
        
        if (resetLocalState)
            [self resetLocalState];
        
        networkQueue = [[ASINetworkQueue alloc] init];
        networkQueue.showAccurateProgress = YES;
        networkQueue.shouldCancelAllRequestsOnFailure = NO;
//        networkQueue.maxConcurrentOperationCount = 1;
        
        callbackQueue = [[NSOperationQueue alloc] init];
        [callbackQueue setSuspended:YES];
        callbackQueue.name = [NSString stringWithFormat:@"gr.grnet.pithos.SyncCallbackQueue.%@", pithosAccount.uniqueName];
//        callbackQueue.maxConcurrentOperationCount = 1;
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(applicationWillTerminate:)
                                                     name:NSApplicationWillTerminateNotification
                                                   object:[NSApplication sharedApplication]];
        
        [self startDaemon];
    }
    return self;
}

- (void)resetLocalState {
    self.lastCompletedSync = nil;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    if ([fileManager fileExistsAtPath:self.pithosStateFilePath] && 
        (![fileManager removeItemAtPath:self.pithosStateFilePath error:&error] || error))
        [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                message:[NSString stringWithFormat:@"Cannot remove file at '%@'", self.pithosStateFilePath] 
                                                  error:error];
    if (self.tempDownloadsDirPath) {
        error = nil;
        for (NSString *subPath in [fileManager contentsOfDirectoryAtPath:self.tempDownloadsDirPath error:&error]) {
            if (error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", self.tempDownloadsDirPath] 
                                                          error:error];
                break;
            }
            NSString *subFilePath = [self.tempDownloadsDirPath stringByAppendingPathComponent:subPath];
            if (![fileManager removeItemAtPath:subFilePath error:&error] || error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] 
                                                          error:error];
            }
            error = nil;
        }
    }
}

- (void)resetDaemon {
    @synchronized(self) {
        if (!daemonActive)
            return;
    }
    
    [networkQueue reset];
    [callbackQueue cancelAllOperations];
    [callbackQueue setSuspended:YES];
    [self emptyTempTrash];
    
    @synchronized(self) {            
        daemonActive = NO;
    }
}

- (void)startDaemon {
    @synchronized(self) {
        if (daemonActive)
            return;
    }
    
    syncOperationCount = 0;
    newSyncRequested = NO;
    syncIncomplete = NO;
    syncLate = NO;

    self.containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
            
    if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) {
        NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath];
        NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
        self.storedLocalObjectStates = [unarchiver decodeObjectForKey:containerName];
        [unarchiver finishDecoding];
    } else {
        self.storedLocalObjectStates = [NSMutableDictionary dictionary];
    }

    // In the improbable case of leftover operations
    [networkQueue reset];
    [callbackQueue cancelAllOperations];
    
    [networkQueue go];
    [callbackQueue setSuspended:NO];
    
    @synchronized(self) {
        daemonActive = YES;
    }
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self resetDaemon];
    [callbackQueue release];
    [networkQueue release];
    [tempTrashDirPath release];
    [tempDownloadsDirPath release];
    [pithosStateFilePath release];
    [containerDirectoryPath release];
    [currentLocalObjectStates release];
    [storedLocalObjectStates release];
    [remoteObjects release];
    [objects release];
    [lastCompletedSync release];
    [lastModified release];
    [blockHash release];
    [pithos release];
    [containerName release];
    [pithosAccount release];
    [directoryPath release];
    [super dealloc];
}

#pragma mark -
#pragma mark Observers

- (void)applicationWillTerminate:(NSNotification *)notification {
    [self saveLocalState];
}

#pragma mark -
#pragma mark Properties

- (NSString *)pithosStateFilePath {
    if (!pithosStateFilePath)
        pithosStateFilePath = [[[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
                                 stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
                                stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-PithosLocalObjectStates.archive", 
                                                                pithosAccount.uniqueName]] retain];
    return [[pithosStateFilePath copy] autorelease];
}

- (NSString *)tempDownloadsDirPath {
    if (!tempDownloadsDirPath) {
        tempDownloadsDirPath = [[[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
                                  stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempDownloads", 
                                                                 pithosAccount.uniqueName]] retain];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isDirectory;
        BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory];
        NSError *error = nil;
        if (fileExists && !isDirectory)
            [fileManager removeItemAtPath:tempDownloadsDirPath error:&error];
        if (!error & !fileExists)
            [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error];
        //if (error)
        //    tempDownloadsDirPath = nil;
        // XXX create a dir using mktmps?
    }
    return tempDownloadsDirPath;    
}

- (NSString *)tempTrashDirPath {
    if (!tempTrashDirPath) {
        tempTrashDirPath = [[[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
                                  stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempTrash", 
                                                                 pithosAccount.uniqueName]] retain];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isDirectory;
        BOOL fileExists = [fileManager fileExistsAtPath:tempTrashDirPath isDirectory:&isDirectory];
        NSError *error = nil;
        if (fileExists && !isDirectory)
            [fileManager removeItemAtPath:tempTrashDirPath error:&error];
        if (!error & !fileExists)
            [fileManager createDirectoryAtPath:tempTrashDirPath withIntermediateDirectories:YES attributes:nil error:&error];
        //if (error)
        //    tempTrashDirPath = nil;
        // XXX create a dir using mktmps?
    }
    return tempTrashDirPath;
}

- (void)setDirectoryPath:(NSString *)aDirectoryPath {
    if (aDirectoryPath && ![aDirectoryPath isEqualToString:directoryPath]) {
        [self resetDaemon];
        [self resetLocalState];
        [directoryPath release];
        directoryPath = [aDirectoryPath copy];
    }
}

- (void)setContainerName:(NSString *)aContainerName {
    if (aContainerName && ![aContainerName isEqualToString:containerName]) {
        [self resetDaemon];
        [self resetLocalState];
        [containerName release];
        containerName = [aContainerName copy];
    }    
}

- (void)setPithos:(ASIPithos *)aPithos {
    if (!pithos) {
        pithos = [[ASIPithos pithos] retain];
        pithos.authUser = [[aPithos.authUser copy] autorelease];
        pithos.authToken = [[aPithos.authToken copy] autorelease];
        pithos.storageURLPrefix = [[aPithos.storageURLPrefix copy] autorelease];
        pithos.authURL = [[aPithos.authURL copy] autorelease];
        pithos.publicURLPrefix = [[aPithos.publicURLPrefix copy] autorelease];
    }
    if (aPithos && 
        (![aPithos.authUser isEqualToString:pithos.authUser] || 
         ![aPithos.authToken isEqualToString:pithos.authToken] || 
         ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix])) {
        [self resetDaemon];
        if (![aPithos.authUser isEqualToString:pithos.authUser] || 
            ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix])
            [self resetLocalState];
        pithos.authUser = [[aPithos.authUser copy] autorelease];
        pithos.authToken = [[aPithos.authToken copy] autorelease];
        pithos.storageURLPrefix = [[aPithos.storageURLPrefix copy] autorelease];
        pithos.authURL = [[aPithos.authURL copy] autorelease];
        pithos.publicURLPrefix = [[aPithos.publicURLPrefix copy] autorelease];
    }        
}

#pragma mark -
#pragma mark Sync

- (void)saveLocalState {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
    [archiver encodeObject:storedLocalObjectStates forKey:containerName];
    [archiver finishEncoding];
    [data writeToFile:self.pithosStateFilePath atomically:YES];
    [pool drain];
}

- (void)syncOperationStarted {
    @synchronized(self) {
        syncOperationCount++;
    }
}

- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull {
    @synchronized(self) {
        if (!operationSuccessfull)
            syncIncomplete = YES;
        if (syncOperationCount == 0)
            // XXX This shouldn't happen, maybe change operationCount to a BOOL as operationsPending in browser
            return;
        syncOperationCount--;
        if (syncOperationCount == 0) {
            if (!syncIncomplete) {
                self.lastCompletedSync = [NSDate date];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                          message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync] 
                                                    pithosAccount:pithosAccount];
                    
                });
            }
            [self emptyTempTrash];
            if (newSyncRequested && daemonActive)
                [self sync];
        }
    }
}

- (BOOL)isSyncing {
    @synchronized(self) {
        return ((syncOperationCount > 0) && daemonActive);
    }
}

- (void)syncLate {
    @synchronized(self) {
        if ([self isSyncing])
            syncLate = YES;
    }
}

- (void)sync {
    @synchronized(self) {
        if ([self isSyncing]) {
            // If at least one operation is running return
            newSyncRequested = YES;
            return;
        } else if (daemonActive) {
            // The first operation is the server listing
            [self syncOperationStarted];
            newSyncRequested = NO;
            syncIncomplete = NO;
            syncLate = NO;
        } else {
            return;
        }
    }

    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDirectory;
    NSError *error = nil;
    if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) {
        if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || 
            error) {
            [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
                                                    message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath] 
                                                      error:error];
            [self syncOperationFinishedWithSuccess:NO];
            return;
        }
    } else if (!isDirectory) {
        [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
                                                message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", containerDirectoryPath] 
                                                  error:nil];
        [self syncOperationFinishedWithSuccess:NO];
        return;
    }
    
    ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
                                                                                            containerName:containerName 
                                                                                                    limit:0 
                                                                                                   marker:nil 
                                                                                                   prefix:nil 
                                                                                                delimiter:nil 
                                                                                                     path:nil 
                                                                                                     meta:nil 
                                                                                                   shared:NO 
                                                                                                    until:nil 
                                                                                          ifModifiedSince:lastModified];
    containerRequest.delegate = self;
    containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
    containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
    PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther 
                                                               message:@"Sync: Getting server listing" 
                                                         pithosAccount:pithosAccount];
    containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                 activity, @"activity", 
                                 @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", 
                                 @"Sync: Getting server listing (failed)", @"failedActivityMessage", 
                                 @"Sync: Getting server listing (finished)", @"finishedActivityMessage", 
                                 [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
                                 [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                 NSStringFromSelector(@selector(listRequestFinished:)), @"didFinishSelector", 
                                 NSStringFromSelector(@selector(listRequestFailed:)), @"didFailSelector", 
                                 nil];
    [networkQueue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityHigh]];
}

- (void)emptyTempTrash {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString *trashDirPath = self.tempTrashDirPath;
    if (trashDirPath) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error = nil;
//        NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error];
        NSArray *subPaths = [fileManager contentsOfDirectoryAtPath:trashDirPath error:&error];
        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
                                                          error:error];
            });
            [pool drain];
            return;
        }
        if ([subPaths count]) {
//            NSMutableArray *subURLs = [NSMutableArray arrayWithCapacity:[subPaths count]];
//            for (NSString *subPath in subPaths) {
//                [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]];
//            }
//            [self syncOperationStarted];
//            [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) {
//                if (error) {
//                    dispatch_async(dispatch_get_main_queue(), ^{
//                        [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error" 
//                                                                message:@"Cannot move files to Trash" 
//                                                                  error:error];
//                    });
//                }
//                [self syncOperationFinishedWithSuccess:YES];
//            }];
            for (NSString *subPath in subPaths) {
                NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath];
                error = nil;
                if (![fileManager removeItemAtPath:subFilePath error:&error] || error)
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                                message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] 
                                                                  error:error];
                    });
            }
        }
    }
    [pool drain];
}

- (BOOL)moveToTempTrashFile:(NSString *)filePath {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString *trashDirPath = self.tempTrashDirPath;
    if (!tempTrashDirPath) {
        [pool drain];
        return NO;
    }
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDirectory;
    BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
    NSError *error = nil;
    NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:containerDirectoryPath 
                                                                withString:trashDirPath];
    NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent];
    if (fileExists && isDirectory) {
        NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error];
        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
                                                          error:error];
            });
            [pool drain];
            return NO;
        }
        if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
                                                          error:error];
            });
            [pool drain];
            return NO;
        }
        if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
                                                                 filePath, newFilePath] 
                                                          error:error];
            });
            [pool drain];
            return NO;
        }
        PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
        if (currentState) {
            currentState.filePath = newFilePath;
            [currentLocalObjectStates setObject:currentState forKey:newFilePath];
            [currentLocalObjectStates removeObjectForKey:filePath];        
        } else {
            [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
                                                                                       blockHash:blockHash 
                                                                                       blockSize:blockSize] 
                                         forKey:newFilePath];
        }
        for (NSString *subPath in subPaths) {
            NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath];
            NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:containerDirectoryPath 
                                                                              withString:trashDirPath];
            currentState = [currentLocalObjectStates objectForKey:subFilePath];
            if (currentState) {
                currentState.filePath = newSubFilePath;
                [currentLocalObjectStates setObject:currentState forKey:newSubFilePath];
                [currentLocalObjectStates removeObjectForKey:subFilePath];
            } else {
                [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath 
                                                                                           blockHash:blockHash 
                                                                                           blockSize:blockSize] 
                                             forKey:newSubFilePath];
            }        
        }
    } else if (fileExists && !isDirectory) {
        if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
                                                          error:error];
            });
            [pool drain];
            return NO;
        }
        if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
                                                                 filePath, newFilePath] 
                                                          error:error];
            });
            [pool drain];
            return NO;
        }
        PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
        if (currentState) {
            currentState.filePath = newFilePath;
            [currentLocalObjectStates setObject:currentState forKey:newFilePath];
            [currentLocalObjectStates removeObjectForKey:filePath];        
        } else {
            [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
                                                                                       blockHash:blockHash 
                                                                                       blockSize:blockSize] 
                                         forKey:newFilePath];
        }
    }
    [pool drain];
    return YES;
}

- (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath {
    if ([hash length] != 64)
        return NO;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    PithosLocalObjectState *localState;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDirectory;
    NSError *error = nil;
    for (NSString *localFilePath in [currentLocalObjectStates allKeys]) {
        localState = [currentLocalObjectStates objectForKey:localFilePath];
        if (!localState.isDirectory && [hash isEqualToString:localState.hash] && 
            [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) {
            if ([localFilePath hasPrefix:containerDirectoryPath]) {
                if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error" 
                                                                message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", 
                                                                         localFilePath, filePath] 
                                                                  error:error];
                    });
                } else {
                    [pool drain];
                    return YES;
                }            
            } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) {
                if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
                                                                message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
                                                                         localFilePath, filePath] 
                                                                  error:error];
                    });
                } else {
                    localState.filePath = filePath;
                    [currentLocalObjectStates setObject:localState forKey:filePath];
                    [currentLocalObjectStates removeObjectForKey:localFilePath];
                    [pool drain];
                    return YES;
                }
            }
        }
    }
    [pool drain];
    return NO;
}

- (void)updateLocalStateWithObject:(ASIPithosObject *)object
                     localFilePath:(NSString *)filePath {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error;
    BOOL isDirectory;
    BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
    NSString *fileDirectoryPath = [filePath stringByDeletingLastPathComponent];
    PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
    // Remote updated info
    NSError *remoteError;
    BOOL remoteIsDirectory;
    BOOL remoteObjectExists = [PithosUtilities objectExistsAtPithos:pithos 
                                                      containerName:containerName 
                                                         objectName:object.name 
                                                              error:&remoteError 
                                                        isDirectory:&remoteIsDirectory 
                                                     sharingAccount:nil];
    if (!object || !object.objectHash) {
        // Delete local object
        if (remoteObjectExists) {
            // Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object
            syncIncomplete = YES;
        }
        NSLog(@"Sync::delete local object: %@", filePath);
        if (!fileExists || [self moveToTempTrashFile:filePath]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name] 
                                                pithosAccount:pithosAccount];
            });
            [storedLocalObjectStates removeObjectForKey:object.name];
            [self saveLocalState];
        }
    } else if ([PithosUtilities isContentTypeDirectory:object.contentType]) {
        // Create local directory object
        if (!remoteObjectExists || !remoteIsDirectory) {
            // Remote directory object deleted or changed to a file object in the meantime, mark the sync cycle as incomplete and skip
            syncIncomplete = YES;
            [pool drain];
            return;
        }
        NSLog(@"Sync::create local directory object: %@", filePath);
        BOOL directoryCreated = NO;
        if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath])) {
            NSLog(@"Sync::local directory object doesn't exist: %@", filePath);
            error = nil;
            if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                            message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] 
                                                              error:error];
                });
            } else {
                directoryCreated = YES;
                storedState.filePath = filePath;
                storedState.isDirectory = YES;
                [self saveLocalState];
            }
        } else {
            NSLog(@"Sync::local directory object exists: %@", filePath);
            directoryCreated = YES;
        }
        if (directoryCreated)
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name] 
                                                pithosAccount:pithosAccount];
            });
    } else if (object.bytes == 0) {
        // Create local object with zero length
        if (!remoteObjectExists || remoteIsDirectory) {
            // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
            syncIncomplete = YES;
            [pool drain];
            return;
        }
        NSLog(@"Sync::create local zero length object: %@", filePath);
        BOOL fileCreated = NO;
        if (!fileExists || ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && [self moveToTempTrashFile:filePath])) {
            NSLog(@"Sync::local zero length object doesn't exist: %@", filePath);
            // Create directory of the file, if it doesn't exist
            error = nil;
            if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                            message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
                                                              error:error];
                });
            }
            error = nil;
            if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" 
                                                            message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] 
                                                              error:error];
                });
            } else {
                fileCreated = YES;
                storedState.filePath = filePath;
                storedState.hash = object.objectHash;
                storedState.tmpFilePath = nil;
                [self saveLocalState];
            }
        } else {
            NSLog(@"Sync::local zero length object exists: %@", filePath);
            fileCreated = YES;
        }
        if (fileCreated)
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
                                                pithosAccount:pithosAccount];
            });
    } else if (storedState.tmpFilePath == nil) {
        // Create new local object
        if (!remoteObjectExists || remoteIsDirectory) {
            // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
            syncIncomplete = YES;
            [pool drain];
            return;
        }
        // Create directory of the file, if it doesn't exist
        error = nil;
        if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
                                                          error:error];
            });
        }
        // Check first if a local copy exists
        if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
            storedState.filePath = filePath;
            storedState.hash = object.objectHash;
            [self saveLocalState];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
                                                pithosAccount:pithosAccount];
            });
        } else {
            [self syncOperationStarted];
            __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
                                                                                                containerName:containerName 
                                                                                                       object:object 
                                                                                                   blockIndex:0 
                                                                                                    blockSize:blockSize];
            objectRequest.delegate = self;
            objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                       message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                    totalBytes:object.bytes 
                                                                  currentBytes:0 
                                                                 pithosAccount:pithosAccount];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility updateActivity:activity withMessage:activity.message];
            });
            objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                      object, @"pithosObject", 
                                      [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(blockSize + 0.0)))], @"missingBlocks", 
                                      [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", 
                                      filePath, @"filePath", 
                                      activity, @"activity", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", 
                                      [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                      NSStringFromSelector(@selector(downloadObjectBlockFinished:)), @"didFinishSelector", 
                                      NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
                                      nil];
            [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", 
                                                  [[objectRequest.userInfo objectForKey:@"pithosObject"] name], 
                                                  (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                      totalBytes:activity.totalBytes 
                                    currentBytes:(activity.currentBytes + size)];
            }];
            [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
        }
    } else {
        // Resume local object download
        if (!remoteObjectExists || remoteIsDirectory) {
            // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
            syncIncomplete = YES;
            [pool drain];
            return;
        }
        // Create directory of the file, if it doesn't exist
        error = nil;
        if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
                                                          error:error];
            });
        }
        // Check first if a local copy exists
        if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
            storedState.filePath = filePath;
            storedState.hash = object.objectHash;
            // Delete incomplete temp download
            storedState.tmpFilePath = nil;
            [self saveLocalState];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
                                                pithosAccount:pithosAccount];
            });
        } else {
            [self syncOperationStarted];
            ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos 
                                                                                             containerName:containerName 
                                                                                                objectName:object.name];
            objectRequest.delegate = self;
            objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                       message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                    totalBytes:object.bytes 
                                                                  currentBytes:0 
                                                                 pithosAccount:pithosAccount];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility updateActivity:activity withMessage:activity.message];
            });
            objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                      object, @"pithosObject", 
                                      filePath, @"filePath", 
                                      activity, @"activity", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", 
                                      [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", 
                                      [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                      NSStringFromSelector(@selector(downloadObjectHashMapFinished:)), @"didFinishSelector", 
                                      NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
                                      nil];
            [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
        }
    }
    [pool drain];
}

-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
                                  object:(ASIPithosObject *)object 
                           localFilePath:(NSString *)filePath {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [self syncOperationStarted];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDirectory;
    BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
    if (currentState.isDirectory) {
        // Create remote directory object
        if (!fileExists || !isDirectory) {
            // Local directory object deleted or changed to a file in the meantime, mark the sync cycle as incomplete and skip
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }
        ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
                                                                                           containerName:containerName 
                                                                                              objectName:object.name 
                                                                                                    eTag:nil 
                                                                                             contentType:@"application/directory" 
                                                                                         contentEncoding:nil 
                                                                                      contentDisposition:nil 
                                                                                                manifest:nil 
                                                                                                 sharing:nil 
                                                                                                isPublic:ASIPithosObjectRequestPublicIgnore 
                                                                                                metadata:nil 
                                                                                                    data:[NSData data]];
        objectRequest.delegate = self;
        objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
        objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
                                                                   message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name] 
                                                             pithosAccount:pithosAccount];
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility updateActivity:activity withMessage:activity.message];
        });
        objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                  object, @"pithosObject", 
                                  activity, @"activity", 
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (stopped)", object.name], @"stoppedActivityMessage", 
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (failed)", object.name], @"failedActivityMessage", 
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (finished)", object.name], @"finishedActivityMessage", 
                                  [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                  NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
                                  NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
                                  nil];
        [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
    } else if (![currentState exists]) {
        // Delete remote object
        if (fileExists) {
            // Local object created in the meantime, just mark the sync cycle as incomplete, but do delete the server object
            syncIncomplete = YES;
        }
        NSString *safeName;
        if ([PithosUtilities isContentTypeDirectory:object.contentType])
            safeName = [PithosUtilities safeSubdirNameForPithos:pithos containerName:@"trash" subdirName:object.name];
        else
            safeName = [PithosUtilities safeObjectNameForPithos:pithos containerName:@"trash" objectName:object.name];
        if (safeName) {
            ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
                                                                                   containerName:containerName 
                                                                                      objectName:object.name 
                                                                        destinationContainerName:@"trash" 
                                                                           destinationObjectName:safeName 
                                                                                   checkIfExists:NO];
            if (objectRequest) {
                objectRequest.delegate = self;
                objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
                                                                           message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name] 
                                                                     pithosAccount:pithosAccount];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [activityFacility updateActivity:activity withMessage:activity.message];
                });
                objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                          object, @"pithosObject", 
                                          activity, @"activity", 
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (stopped)", object.name], @"stoppedActivityMessage", 
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (failed)", object.name], @"failedActivityMessage", 
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (finished)", object.name], @"finishedActivityMessage", 
                                          [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
                                          [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                          NSStringFromSelector(@selector(moveObjectToTrashFinished:)), @"didFinishSelector", 
                                          NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
                                          nil];
                [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
            } else {
                [self syncOperationFinishedWithSuccess:NO];
            }
        } else {
            [self syncOperationFinishedWithSuccess:NO];
        }
    } else {
        // Upload file to remote object
        if (!fileExists || isDirectory) {
            // Local file object deleted or changed to a directory in the meantime, mark the sync cycle as incomplete and skip
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }
        NSError *error = nil;
        object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
        if (object.contentType == nil)
            object.contentType = @"application/octet-stream";
        if (error)
            NSLog(@"contentType detection error: %@", error);
        NSArray *hashes = nil;
        ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
                                                                                    containerName:containerName 
                                                                                       objectName:object.name 
                                                                                      contentType:object.contentType 
                                                                                        blockSize:blockSize 
                                                                                        blockHash:blockHash 
                                                                                          forFile:filePath 
                                                                                    checkIfExists:NO 
                                                                                           hashes:&hashes 
                                                                                   sharingAccount:nil];
        if (objectRequest) {
            objectRequest.delegate = self;
            objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
                                                                       message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name]
                                                                    totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
                                                                  currentBytes:0 
                                                                 pithosAccount:pithosAccount];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility updateActivity:activity withMessage:activity.message];
            });
            [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
             [NSDictionary dictionaryWithObjectsAndKeys:
              object, @"pithosObject", 
              filePath, @"filePath", 
              hashes, @"hashes", 
              [NSNumber numberWithUnsignedInteger:10], @"iteration", 
              activity, @"activity", 
              [NSString stringWithFormat:@"Sync: Uploading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
              [NSString stringWithFormat:@"Sync: Uploading '%@' (failed)", object.name], @"failedActivityMessage", 
              [NSString stringWithFormat:@"Sync: Uploading '%@' (100%%)", object.name], @"finishedActivityMessage", 
              [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
              [NSNumber numberWithUnsignedInteger:10], @"retries", 
              NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
              NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
              nil]];
            [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
        } else {
            [self syncOperationFinishedWithSuccess:NO];
        }
    }
    [pool drain];
}

#pragma mark -
#pragma mark ASIHTTPRequestDelegate

- (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
    // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
    NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
                                                                             selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) 
                                                                               object:request] autorelease];
    operation.completionBlock = ^{
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                                  withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
            });
            [self syncOperationFinishedWithSuccess:NO];
        }
        [pool drain];
    };
    [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
    [callbackQueue addOperation:operation];
}

- (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request {
    if (request.isCancelled) {
        // Request has been cancelled 
        // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway
        [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
                   withObject:request];
    } else {
        // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
        NSInvocationOperation *operation = [[[NSInvocationOperation alloc] initWithTarget:self 
                                                                                 selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
                                                                                   object:request] autorelease];
        operation.completionBlock = ^{
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
            if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                                      withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
                });
                [self syncOperationFinishedWithSuccess:NO];
            }
            [pool drain];
        };
        [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
        [callbackQueue addOperation:operation];
    }
}

- (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::list request finished: %@", containerRequest.url);
    if (operation.isCancelled) {
        [self listRequestFailed:containerRequest];
    } else if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) {
        if (containerRequest.responseStatusCode == 200) {
            NSArray *someObjects = [containerRequest objects];
            if (objects == nil) {
                objects = [[NSMutableArray alloc] initWithArray:someObjects];
            } else {
                [objects addObjectsFromArray:someObjects];
            }
            if ([someObjects count] < 10000) {
                self.blockHash = [containerRequest blockHash];
                self.blockSize = [containerRequest blockSize];
                self.lastModified = [containerRequest lastModified];
                self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]];
                for (ASIPithosObject *object in objects) {
                    [remoteObjects setObject:object forKey:object.name];
                }
                [objects release];
                objects = nil;
            } else {
                // Do an additional request to fetch more objects
                ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
                                                                                                           containerName:containerName 
                                                                                                                   limit:0 
                                                                                                                  marker:[[someObjects lastObject] name] 
                                                                                                                  prefix:nil 
                                                                                                               delimiter:nil 
                                                                                                                    path:nil 
                                                                                                                    meta:nil 
                                                                                                                  shared:NO 
                                                                                                                   until:nil 
                                                                                                         ifModifiedSince:lastModified];
                newContainerRequest.delegate = self;
                newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                newContainerRequest.userInfo = containerRequest.userInfo;
                [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
                [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
                [pool drain];
                return;
            }
        }
        
        if (operation.isCancelled) {
            [self listRequestFailed:containerRequest];
            [pool drain];
            return;
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        });
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error = nil;
        NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error];
        if (error) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
                                                          error:error];
                [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                      message:@"Sync: Failed to read contents of sync directory" 
                                                pithosAccount:pithosAccount];
            });
            // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccessfully
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }
        
        if (operation.isCancelled) {
            operation.completionBlock = nil;
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }

        self.currentLocalObjectStates = [NSMutableDictionary dictionaryWithCapacity:[subPaths count]];
        for (NSString *objectName in subPaths) {
            if (operation.isCancelled) {
                operation.completionBlock = nil;
                [self saveLocalState];
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }

            PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:objectName];
            NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
            if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
                // New or modified existing local object, compute current state
                if (!storedLocalObjectState)
                    // For new local object, also create empty stored state
                    [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
                [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath 
                                                                                           blockHash:blockHash 
                                                                                           blockSize:blockSize] 
                                             forKey:filePath];
            } else {
                // Local object hasn't changed, set stored state also to current
                [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath];
            }
        }
        [self saveLocalState];

        if (operation.isCancelled) {
            operation.completionBlock = nil;
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }

        for (NSString *objectName in remoteObjects) {
            if (operation.isCancelled) {
                operation.completionBlock = nil;
                [self saveLocalState];
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }

            if (![storedLocalObjectStates objectForKey:objectName])
                [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
        }
        [self saveLocalState];

        if (operation.isCancelled) {
            operation.completionBlock = nil;
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }

        for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
            if (operation.isCancelled) {
                operation.completionBlock = nil;
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }

            NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
            if ([objectName hasSuffix:@"/"])
                filePath = [filePath stringByAppendingString:@":"];
            ASIPithosObject *object = [ASIPithosObject object];
            object.name = objectName;
            NSLog(@"Sync::object name: %@", objectName);
            
            PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:object.name];
            PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath];
            if (!currentLocalObjectState)
                currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath 
                                                                                 blockHash:blockHash 
                                                                                 blockSize:blockSize];
            if (currentLocalObjectState.isDirectory)
                object.contentType = @"application/directory";
            
            PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState];
            ASIPithosObject *remoteObject = [remoteObjects objectForKey:objectName];
            if (remoteObject) {
                if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) {
                    remoteObjectState.isDirectory = YES;
                    object.contentType = @"application/directory";
                } else {
                    remoteObjectState.hash = remoteObject.objectHash;
                }
            }

            BOOL localStateHasChanged = ![currentLocalObjectState isEqualToState:storedLocalObjectState];
            BOOL serverStateHasChanged = ![remoteObjectState isEqualToState:storedLocalObjectState];
            NSLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
            if (!localStateHasChanged) {
                // Local state hasn't changed
                if (serverStateHasChanged) {
                    // Server state has changed
                    // Update local state to match that of the server 
                    object.bytes = remoteObject.bytes;
                    object.version = remoteObject.version;
                    object.contentType = remoteObject.contentType;
                    object.objectHash = remoteObject.objectHash;
                    [self updateLocalStateWithObject:object localFilePath:filePath];
                } else if (!remoteObject && ![currentLocalObjectState exists]) {
                    // Server state hasn't changed
                    // If the object doesn't exist neither in the server or locally, it should be removed from the stored local objects
                    [storedLocalObjectStates removeObjectForKey:objectName];
                    [self saveLocalState];
                }
            } else {
                // Local state has changed
                if (!serverStateHasChanged) {
                    // Server state hasn't changed
                    [self updateServerStateWithCurrentState:currentLocalObjectState 
                                                     object:object 
                                              localFilePath:filePath];
                } else {
                    // Server state has also changed
                    if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) {
                        // Both did the same change (directory)
                        storedLocalObjectState.filePath = filePath;
                        storedLocalObjectState.isDirectory = YES;
                        [self saveLocalState];
                    } else if ([remoteObjectState isEqualToState:currentLocalObjectState]) {
                        // Both did the same change (object edit or delete)
                        if (![remoteObjectState exists]) {
                            [storedLocalObjectStates removeObjectForKey:object.name];
                        } else {
                            storedLocalObjectState.filePath = filePath;
                            storedLocalObjectState.hash = remoteObjectState.hash;
                        }
                        [self saveLocalState];
                    } else {
                        // Conflict, we ask the user which change to keep
                        NSString *informativeText;
                        NSString *firstButtonText;
                        NSString *secondButtonText;
                        
                        if (![remoteObjectState exists]) {
                            // Remote object has been deleted
                            informativeText = [NSString stringWithFormat:@"'%@' has been modified locally, while it has been deleted from server.", object.name ];
                            firstButtonText = @"Delete local file";
                            secondButtonText = @"Upload file to server";
                        } else if (![currentLocalObjectState exists]) {
                            informativeText = [NSString stringWithFormat:@"'%@' has been modified on the server, while it has been deleted locally.", object.name];
                            firstButtonText = @"Download file from server";
                            secondButtonText = @"Delete file on server";
                        } else {
                            informativeText = [NSString stringWithFormat:@"'%@' has been modifed both locally and on the server.", object.name];
                            firstButtonText = @"Keep server version";
                            secondButtonText = @"Keep local version";
                        }
                        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
                        [alert setMessageText:@"Conflict"];
                        [alert setInformativeText:informativeText];
                        [alert addButtonWithTitle:firstButtonText];
                        [alert addButtonWithTitle:secondButtonText];
                        [alert addButtonWithTitle:@"Do nothing"];
                        NSInteger choice = [alert runModal];
                        if (choice == NSAlertFirstButtonReturn) {
                            object.bytes = remoteObject.bytes;
                            object.version = remoteObject.version;
                            object.contentType = remoteObject.contentType;
                            object.objectHash = remoteObject.objectHash;
                            [self updateLocalStateWithObject:object localFilePath:filePath];
                        } if (choice == NSAlertSecondButtonReturn) {
                            [self updateServerStateWithCurrentState:currentLocalObjectState 
                                                             object:object 
                                                      localFilePath:filePath];
                        }
                    }
                }
            }
        }
        [self syncOperationFinishedWithSuccess:YES];
    } else {
        [self listRequestFailed:containerRequest];
    }
    [pool drain];
}

- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
    if (operation.isCancelled) {
        [objects release];
        objects = nil;
        [pool drain];
        return;        
    }
    if (containerRequest.isCancelled) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
        });
        [objects release];
        objects = nil;
        [self syncOperationFinishedWithSuccess:NO];
        [pool drain];
        return;
    }
    // If the server listing fails, the sync should start over, so just retrying is enough
    NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
    if (retries > 0) {
        ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[[PithosUtilities copyRequest:containerRequest] autorelease];
        [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
        [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
        });
        [objects release];
        objects = nil;
        // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
        [self syncOperationFinishedWithSuccess:NO];
    }
    [pool drain];
}

- (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::download object block finished: %@", objectRequest.url);
    if (operation.isCancelled) {
        [self requestFailed:objectRequest];
    } else if (objectRequest.responseStatusCode == 206) {
        ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error;
        PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
        
        NSString *downloadsDirPath = self.tempDownloadsDirPath;
        if (!downloadsDirPath) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility endActivity:activity 
                                  withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
            });
            [self syncOperationFinishedWithSuccess:NO];
            [pool drain];
            return;
        }
        
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
        if ((storedState.tmpFilePath == nil) || ![fileManager fileExistsAtPath:storedState.tmpFilePath]) {
            NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
            const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
            char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
            strcpy(tempFileNameCString, tempFileTemplateCString);
            int fileDescriptor = mkstemp(tempFileNameCString);
            NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
            free(tempFileNameCString);
            if (fileDescriptor == -1) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" 
                                                            message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] 
                                                              error:nil];
                    [activityFacility endActivity:activity 
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                });
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }
            close(fileDescriptor);
            storedState.tmpFilePath = tempFilePath;
            [self saveLocalState];
        }
        

        NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
        NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath];
        [tempFileHandle seekToFileOffset:missingBlockIndex*blockSize];
        [tempFileHandle writeData:[objectRequest responseData]];
        [tempFileHandle closeFile];

        NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
        missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
        if (missingBlockIndex == NSNotFound) {
            NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
            NSString *dirPath = [filePath stringByDeletingLastPathComponent];
            if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [activityFacility endActivity:activity 
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                });
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            } else if (![fileManager fileExistsAtPath:dirPath]) {
                // File doesn't exist but also the containing directory doesn't exist
                // In most cases this should have been resolved as an update of the corresponding local object,
                // but it never hurts to check
                error = nil;
                [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
                if (error != nil) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                                message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] 
                                                                  error:error];
                        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                    });
                    [self syncOperationFinishedWithSuccess:NO];
                    [pool drain];
                    return;
                }
            }
            // Move file from tmp download
            error = nil;
            [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error];
            if (error != nil) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
                                                            message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] 
                                                              error:error];
                    [activityFacility endActivity:activity 
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                });
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility endActivity:activity 
                                  withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
                                   totalBytes:activity.totalBytes 
                                 currentBytes:activity.totalBytes];
            });

            storedState.filePath = filePath;
            storedState.hash = object.objectHash;
            storedState.tmpFilePath = nil;
            [self saveLocalState];
            [self syncOperationFinishedWithSuccess:YES];
            [pool drain];
            return;
        } else {
            if (newSyncRequested || syncLate || operation.isCancelled) {
                [self requestFailed:objectRequest];
            } else {
                __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
                                                                                                       containerName:containerName 
                                                                                                              object:object 
                                                                                                          blockIndex:missingBlockIndex 
                                                                                                           blockSize:blockSize];
                newObjectRequest.delegate = self;
                newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                newObjectRequest.userInfo = objectRequest.userInfo;
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
                [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
                    [activityFacility updateActivity:activity 
                                         withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", 
                                                      object.name, 
                                                      (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                          totalBytes:activity.totalBytes 
                                        currentBytes:(activity.currentBytes + size)];
                }];
                [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
            }
        }
    } else if (objectRequest.responseStatusCode == 412) {
        // The object has changed on the server
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
        });
        [self syncOperationFinishedWithSuccess:NO];
    } else {
        [self requestFailed:objectRequest];
    }
    [pool drain];
}

- (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
    if (operation.isCancelled) {
        [self requestFailed:objectRequest];
    } else if (objectRequest.responseStatusCode == 200) {
        if (newSyncRequested || syncLate || operation.isCancelled) {
            [self requestFailed:objectRequest];
        } else {
            ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
            PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
            if ([PithosUtilities bytesOfFile:storedState.tmpFilePath] > object.bytes)
                [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath] truncateFileAtOffset:object.bytes];
            PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpFilePath 
                                                                    blockSize:blockSize 
                                                                    blockHash:blockHash 
                                                                   withHashes:[objectRequest hashes]];
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility endActivity:activity 
                                  withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", 
                                               object.name, 
                                               (100*(activity.totalBytes - [missingBlocks count]*blockSize + 0.0)/(activity.totalBytes + 0.0))] 
                                   totalBytes:activity.totalBytes 
                                 currentBytes:(activity.totalBytes - [missingBlocks count]*blockSize)];
            });

            __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
                                                                                                   containerName:containerName 
                                                                                                          object:object 
                                                                                                      blockIndex:missingBlockIndex 
                                                                                                       blockSize:blockSize];
            newObjectRequest.delegate = self;
            newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            newObjectRequest.userInfo = objectRequest.userInfo;
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(downloadObjectBlockFinished:)) forKey:@"didFinishSelector"];
            [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", 
                                                  object.name, 
                                                  (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                      totalBytes:activity.totalBytes 
                                    currentBytes:(activity.currentBytes + size)];
            }];
            [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
        }
    } else {
        [self requestFailed:objectRequest];
    }
    [pool drain];
}

- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::upload directory object finished: %@", objectRequest.url);
    if (operation.isCancelled) {
        [self requestFailed:objectRequest];
    } else if (objectRequest.responseStatusCode == 201) {
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
        storedState.isDirectory = YES;
        [self saveLocalState];
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        });
        [self syncOperationFinishedWithSuccess:YES];
    } else {
        [self requestFailed:objectRequest];
    }
    [pool drain];
}

- (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::move object to trash finished: %@", objectRequest.url);
    if (operation.isCancelled) {
        [self requestFailed:objectRequest];
    } else if (objectRequest.responseStatusCode == 201) {
        [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
        [self saveLocalState];
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        });
        [self syncOperationFinishedWithSuccess:YES];
    } else {
        [self requestFailed:objectRequest];
    }
    [pool drain];
}

- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
    ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
    PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
    PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
    NSUInteger totalBytes = activity.totalBytes;
    NSUInteger currentBytes = activity.currentBytes;
    if (operation.isCancelled) {
        [self requestFailed:objectRequest];
    } else if (objectRequest.responseStatusCode == 201) {
        NSLog(@"Sync::object created: %@", objectRequest.url);
        storedState.hash = [objectRequest objectHash];
        [self saveLocalState];
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:activity 
                              withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
                               totalBytes:totalBytes 
                             currentBytes:totalBytes];
        });
        [self syncOperationFinishedWithSuccess:YES];
    } else if (objectRequest.responseStatusCode == 409) {
        if (newSyncRequested || syncLate || operation.isCancelled) {
            [self requestFailed:objectRequest];
        } else {
            NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
            if (iteration == 0) {
                NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
                dispatch_async(dispatch_get_main_queue(), ^{
                    [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
                });
                [self syncOperationFinishedWithSuccess:NO];
                [pool drain];
                return;
            }
            NSLog(@"Sync::object is missing hashes: %@", objectRequest.url);
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
                                                              withMissingHashes:[objectRequest hashes]];
            if (totalBytes >= [missingBlocks count]*blockSize)
                currentBytes = totalBytes - [missingBlocks count]*blockSize;
            dispatch_async(dispatch_get_main_queue(), ^{
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Sync: Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
                                      totalBytes:totalBytes 
                                    currentBytes:currentBytes];
            });
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
            __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
                                                                                                             containerName:containerName 
                                                                                                                 blockSize:blockSize 
                                                                                                                   forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
                                                                                                         missingBlockIndex:missingBlockIndex 
                                                                                                            sharingAccount:nil];
            newContainerRequest.delegate = self;
            newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            newContainerRequest.userInfo = objectRequest.userInfo;
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"];
            [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Sync: Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                      totalBytes:activity.totalBytes 
                                    currentBytes:(activity.currentBytes + size)];
            }];
            [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
        }
    } else {
        [self requestFailed:objectRequest];
    }
    [pool drain];
}

- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
    NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
    if (operation.isCancelled) {
        [self requestFailed:containerRequest];
    } else if (containerRequest.responseStatusCode == 202) {
        ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
        PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
        NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
        NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
        missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
        if (operation.isCancelled) {
            [self requestFailed:containerRequest];
        } else if (missingBlockIndex == NSNotFound) {
            NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
            ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
                                                                                           containerName:containerName 
                                                                                              objectName:object.name 
                                                                                             contentType:object.contentType 
                                                                                               blockSize:blockSize 
                                                                                               blockHash:blockHash
                                                                                                 forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
                                                                                           checkIfExists:NO 
                                                                                                  hashes:&hashes 
                                                                                          sharingAccount:nil];
            newObjectRequest.delegate = self;
            newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
            newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
            newObjectRequest.userInfo = containerRequest.userInfo;
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
            [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
        } else {
            if (newSyncRequested || syncLate || operation.isCancelled) {
                [self requestFailed:containerRequest];
            } else {
                __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
                                                                                                                 containerName:containerName
                                                                                                                     blockSize:blockSize
                                                                                                                       forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
                                                                                                             missingBlockIndex:missingBlockIndex 
                                                                                                                sharingAccount:nil];
                newContainerRequest.delegate = self;
                newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                newContainerRequest.userInfo = containerRequest.userInfo;
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
                [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
                    [activityFacility updateActivity:activity 
                                         withMessage:[NSString stringWithFormat:@"Sync: Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                          totalBytes:activity.totalBytes 
                                        currentBytes:(activity.currentBytes + size)];
                }];
                [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
            }
        }
    } else {
        [self requestFailed:containerRequest];
    }
    [pool drain];
}

- (void)requestFailed:(ASIPithosRequest *)request {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSOperation *operation = [request.userInfo objectForKey:@"operation"];
    if (operation.isCancelled) {
        [pool drain];
        return;        
    }
    if (request.isCancelled || newSyncRequested || syncLate) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                              withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
        });
        [self syncOperationFinishedWithSuccess:NO];
        [pool drain];
        return;
    }
    NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
    if (retries > 0) {
        ASIPithosRequest *newRequest = (ASIPithosRequest *)[[PithosUtilities copyRequest:request] autorelease];
        [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
        [networkQueue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                              withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
        });
        [self syncOperationFinishedWithSuccess:NO];
    }
    [pool drain];
}


@end
