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

#define DATA_MODEL_FILE @"localstate.archive"
#define ARCHIVE_KEY @"Data"

@interface PithosSyncDaemon (Private)
- (NSString *)pithosStateFilePath;
- (void)saveLocalState;
- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState;
- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState 
             remoteObjectHash:(NSString *)remoteObjectHash 
      remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory;
- (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath;
-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
                                object:(ASIPithosObject *)object 
                         localFilePath:(NSString *)filePath;
- (void)requestFailed:(ASIPithosRequest *)request;

- (void)increaseSyncOperationCount;
- (void)decreaseSyncOperationCount;

@end

@implementation PithosSyncDaemon
@synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates;
@synthesize pithosStateFilePath, tempDownloadsDirPath;

#pragma mark -
#pragma Object Lifecycle

- (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
              containerName:(NSString *)aContainerName 
               timeInterval:(NSTimeInterval)aTimeInterval {
    if ((self = [super init])) {
        directoryPath = [aDirectoryPath copy];
        containerName = [aContainerName copy];
        timeInterval = aTimeInterval;
        
        syncOperationCount = 0;
        newSyncRequested = NO;
        
        activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) {
            NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath];
            NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
            self.storedLocalObjectStates = [unarchiver decodeObjectForKey:ARCHIVE_KEY];
            [unarchiver finishDecoding];
        } else {
            self.storedLocalObjectStates = [NSMutableDictionary dictionary];
        }
        
        queue = [[ASINetworkQueue alloc] init];
        queue.shouldCancelAllRequestsOnFailure = NO;
        [queue go];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(applicationWillTerminate:)
                                                     name:NSApplicationWillTerminateNotification
                                                   object:[NSApplication sharedApplication]];
    
        timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain];
        [timer fire];
    }
    
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [queue cancelAllOperations];
    [queue release];
    [timer invalidate];
    [timer release];
    [tempDownloadsDirPath release];
    [pithosStateFilePath release];
    [storedLocalObjectStates release];
    [remoteObjects release];
    [objects release];
    [lastCompletedSync release];
    [lastModified release];
    [blockHash release];
    [containerName 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 = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:DATA_MODEL_FILE] retain];
    }
    return [pithosStateFilePath copy];
}

- (NSString *)tempDownloadsDirPath {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (!tempDownloadsDirPath || ![fileManager fileExistsAtPath:tempDownloadsDirPath]) {
        // Get the path from user defaults
        tempDownloadsDirPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosTempDownloadsDirPath"];
        if (tempDownloadsDirPath) {
            // Check if the path exists
            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;
            }
        if (!tempDownloadsDirPath) {
            NSString *tempDirTemplate = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Temp Downloads XXXXXX"];
            const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation];
            char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1);
            strcpy(tempDirNameCString, tempDirTemplateCString);
            tempDirNameCString = mkdtemp(tempDirNameCString);
            if (tempDirNameCString != NULL)
                tempDownloadsDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)];
            free(tempDirNameCString);
        }
        if (!tempDownloadsDirPath)
            [[NSUserDefaults standardUserDefaults] setObject:tempDownloadsDirPath forKey:@"PithosTempDownloadsDirPath"];
        [tempDownloadsDirPath retain];
    }
    return [tempDownloadsDirPath copy];
}

#pragma mark -
#pragma mark Sync

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

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

- (void)decreaseSyncOperationCount {
    @synchronized(self) {
        syncOperationCount--;
        if (!syncOperationCount && !syncIncomplete) {
            self.lastCompletedSync = [NSDate date];
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                  message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync]];
        }
    }
}

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

    NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
    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];
            @synchronized(self) {
                syncOperationCount = 0;
            }
            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];
        @synchronized(self) {
            syncOperationCount = 0;
        }
        return;
    }
    
    ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName: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(listRequestFinished:);
    containerRequest.didFailSelector = @selector(listRequestFailed:);
    PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther 
                                                               message:@"Sync: Getting server listing"];
    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:NSOperationQueuePriorityVeryHigh], @"priority", 
                                 [NSNumber numberWithUnsignedInteger:10], @"retries", 
                                 nil];
    [queue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh]];
}

- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState {
    if (currentState.isDirectory)
        // Currently a directory, check previous state
        return (!storedState.isDirectory);
    if (storedState.isDirectory)
        // Previously a directory, currently a file or doesn't exist, state has changed
        return YES;
    if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "] && 
        (![storedState.md5 isEqualToString:@" "] || ![storedState.hashMapHash isEqualToString:@" "]))
        // Currently doesn't exist, previously a file, state has changed
        return YES;
    if (![storedState.md5 isEqualToString:currentState.md5] && ![storedState.hashMapHash isEqualToString:currentState.hashMapHash])
        // Neither hash remained the same, different files, state has changed
        return YES;
    else
        // At least one hash remained the same (the other is either the same or emmpty), state hasn't changed
        return NO;
}

- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState 
             remoteObjectHash:(NSString *)remoteObjectHash 
      remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory {
    if (remoteObjectIsDirectory)
        // Remotely a directory, check previous state
        return (!storedState.isDirectory);
    if (storedState.isDirectory)
        // Previously a directory, remotely a file or doesn't exist, state has changed
        return YES;
    if ([remoteObjectHash length] == 32)
        // Remotely a file, check previous state
        return ![remoteObjectHash isEqualToString:storedState.md5];
    else if ([remoteObjectHash length] == 64)
        // Remotely a file, check previous state
        return ![remoteObjectHash isEqualToString:storedState.hashMapHash];
    else if ([remoteObjectHash isEqualToString:@" "]) {
        // Remotely doesn't exist
        if ([storedState.md5 isEqualToString:@" "] && [storedState.hashMapHash isEqualToString:@" "])
            // Previously didn't exist, state hasn't changed
            return NO;
        else
            // Previously did exist, state has changed
            return YES;
    }
    // Only if the server doesn't respond properly this will be reached, leave as is for now
    return NO;
}

- (void)updateLocalStateWithObject:(ASIPithosObject *)object
                     localFilePath:(NSString *)filePath {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error;
    BOOL isDirectory;
    BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
    PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
    if ([object.hash isEqualToString:@" "]) {
        // Delete local object
        // XXX move to local trash instead
        NSLog(@"Sync::delete local object: %@", filePath);
        BOOL deleteFailed = NO;
        if (fileExists) {
            error = nil;
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
                                                          error:error];
                deleteFailed = YES;
            }
        }
        if (!deleteFailed) {
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                  message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]];
            [storedLocalObjectStates removeObjectForKey:object.name];
            [self saveLocalState];
        }
    } else if ([object.contentType isEqualToString:@"application/directory"]) {
        // Create local directory object
        NSLog(@"Sync::create local directory object: %@", filePath);
        BOOL directoryCreated = NO;
        if (fileExists && !isDirectory) {
            NSLog(@"Sync::delete local file object: %@", filePath);
            error = nil;
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
                                                          error:error];
            }
        }
        if (![fileManager fileExistsAtPath:filePath]) {
            NSLog(@"Sync::local directory object doesn't exist: %@", filePath);
            error = nil;
            if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] 
                                                          error:error];
            } else {
                directoryCreated = YES;
                storedState.isDirectory = YES;
                [self saveLocalState];
            }
        } else if (isDirectory) {
            NSLog(@"Sync::local directory object exists: %@", filePath);
            directoryCreated = YES;
        }
        if (directoryCreated)
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                  message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]];
    } else if (object.bytes == 0) {
        // Create local object with zero length
        NSLog(@"Sync::create local zero length object: %@", filePath);
        BOOL fileCreated = NO;
        if (fileExists && (isDirectory || [PithosUtilities bytesOfFile:filePath])) {
            NSLog(@"Sync::delete local object: %@", filePath);
            error = nil;
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
                                                          error:error];
            }
        }
        if (![fileManager fileExistsAtPath:filePath]) {
            NSLog(@"Sync::local zero length object doesn't exist: %@", filePath);
            error = nil;
            if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] 
                                                          error:error];
            } else {
                fileCreated = YES;
                if ([object.hash length] == 32) {
                    storedState.md5 = object.hash;
                    storedState.hashMapHash = @" ";
                } else if([object.hash length] == 64) {
                    storedState.md5 = @" ";
                    storedState.hashMapHash = object.hash;
                } else {
                    storedState.md5 = @" ";
                    storedState.hashMapHash = @" ";
                }
                storedState.tmpDownloadFile = nil;
                [self saveLocalState];
            }
        } else if (!isDirectory && ![PithosUtilities bytesOfFile:filePath]) {
            NSLog(@"Sync::local zero length object exists: %@", filePath);
            fileCreated = YES;
        }
        if (fileCreated)
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
                                                  message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
    } else if (storedState.tmpDownloadFile == nil) {
        // Create new local object
        [self increaseSyncOperationCount];
        __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName 
                                                                                                          object:object 
                                                                                                      blockIndex:0 
                                                                                                       blockSize:blockSize];
        objectRequest.delegate = self;
        objectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
        objectRequest.didFailSelector = @selector(requestFailed:);
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                   message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                totalBytes:object.bytes 
                                                              currentBytes:0];
        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", 
                                  nil];
        [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
            [activityFacility updateActivity:activity 
                                 withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", 
                                              [[objectRequest.userInfo valueForKey:@"pithosObject"] name], 
                                              (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                  totalBytes:activity.totalBytes 
                                currentBytes:(activity.currentBytes + size)];
        }];
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
    } else {
        // Resume local object download
        [self increaseSyncOperationCount];
        ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithContainerName:containerName 
                                                                                                   objectName:object.name];
        objectRequest.delegate = self;
        objectRequest.didFinishSelector = @selector(downloadObjectHashMapFinished:);
        // The fail method for block download does exactly what we want
        objectRequest.didFailSelector = @selector(requestFailed:);
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                   message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                totalBytes:object.bytes 
                                                              currentBytes:0];
        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", 
                                  nil];
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
    }
}

-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
                                  object:(ASIPithosObject *)object 
                           localFilePath:(NSString *)filePath {
    [self increaseSyncOperationCount];
    if (currentState.isDirectory) {
        // Create remote directory object
        ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName: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(uploadDirectoryObjectFinished:);
        objectRequest.didFailSelector = @selector(requestFailed:);
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
                                                                   message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name]];
        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", 
                                  nil];
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
    } else if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "]) {
        // Delete remote object
        NSString *safeObjectName = [PithosUtilities safeObjectNameForContainerName:@"trash" 
                                                                        objectName:object.name];
        if (safeObjectName) {
            ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:containerName 
                                                                                             objectName:object.name 
                                                                               destinationContainerName:@"trash" 
                                                                                  destinationObjectName:safeObjectName 
                                                                                          checkIfExists:NO];
            if (objectRequest) {
                objectRequest.delegate = self;
                objectRequest.didFinishSelector = @selector(moveObjectToTrashFinished:);
                objectRequest.didFailSelector = @selector(requestFailed:);
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
                                                                           message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name]];
                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", 
                                          nil];
                [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
            } else {
                syncIncomplete = YES;
                [self decreaseSyncOperationCount];
            }
        } else {
            syncIncomplete = YES;
            [self decreaseSyncOperationCount];
        }
    } else {
        // Upload file to remote object
        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 writeObjectDataRequestWithContainerName: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(uploadObjectUsingHashMapFinished:);
            objectRequest.didFailSelector = @selector(requestFailed:);
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
                                                                       message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name]
                                                                    totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue]
                                                                  currentBytes:0];
            [(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", 
              nil]];
            [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
        } else {
            syncIncomplete = YES;
            [self decreaseSyncOperationCount];
        }
    }

}

#pragma mark -
#pragma mark ASIHTTPRequestDelegate

- (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
    NSLog(@"Sync::list request finished: %@", containerRequest.url);
    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 listObjectsRequestWithContainerName: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(listRequestFinished:);
                newContainerRequest.didFailSelector = @selector(listRequestFailed:);
                newContainerRequest.userInfo = newContainerRequest.userInfo;
                [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
                [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
                return;
            }
        }
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
        NSError *error = nil;
        NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error];
        if (error) {
            [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"];
            @synchronized(self) {
                // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccesfully
                syncOperationCount = 0;
                if (newSyncRequested)
                    [self sync];
            }
            return;
        }
        for (NSString *objectName in subPaths) {
            if (![storedLocalObjectStates objectForKey:objectName]) {
                [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName];
            }
        }
        [self saveLocalState];

        for (NSString *objectName in remoteObjects) {
            if (![storedLocalObjectStates objectForKey:objectName])
                [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName];
        }

        for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
            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 = [PithosLocalObjectState nullObjectState];
            BOOL isDirectory;
            if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
                if (isDirectory) {
                    currentLocalObjectState.isDirectory = YES;
                } else {
                    currentLocalObjectState.md5 = (NSString *)FileMD5HashCreateWithPath((CFStringRef)filePath, 
                                                                                        FileHashDefaultChunkSizeForReadingData);
                    currentLocalObjectState.hashMapHash = [HashMapHash calculateHashMapHash:[HashMapHash calculateObjectHashMap:filePath 
                                                                                                                  withBlockHash:blockHash 
                                                                                                                   andBlockSize:blockSize]];
                }
            }
            if (currentLocalObjectState.isDirectory)
                object.contentType = @"application/directory";
            
            NSString *remoteObjectHash = @" ";
            BOOL remoteObjectIsDirectory = NO;
            ASIPithosObject *remoteObject = [remoteObjects objectForKey:objectName];
            if (remoteObject) {
                remoteObjectHash = remoteObject.hash;
                if ([remoteObject.contentType isEqualToString:@"application/directory"]) {
                    remoteObjectIsDirectory = YES;
                    object.contentType = @"application/directory";
                }
            }
            NSLog(@"Sync::remote object is directory: %d", remoteObjectIsDirectory);
            
            BOOL localStateHasChanged = [self localStateHasChanged:storedLocalObjectState currentState:currentLocalObjectState];
            BOOL serverStateHasChanged = [self serverStateHasChanged:storedLocalObjectState 
                                                    remoteObjectHash:remoteObjectHash 
                                             remoteObjectIsDirectory:remoteObjectIsDirectory];
            NSLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
            // XXX shouldn't we first do all the deletes? in order not to face a dir that becomes a file and vice versa
            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.hash = remoteObjectHash;
                    [self updateLocalStateWithObject:object localFilePath:filePath];
                }
            } 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 (remoteObjectIsDirectory && currentLocalObjectState.isDirectory) {
                        // Both did the same change (directory)
                        storedLocalObjectState.isDirectory = YES;
                        [self saveLocalState];
                    } else if ([remoteObjectHash isEqualToString:currentLocalObjectState.md5] || 
                               [remoteObjectHash isEqualToString:currentLocalObjectState.hashMapHash]) {
                        // Both did the same change (object edit or delete)
                        if ([remoteObjectHash length] == 32) 
                            storedLocalObjectState.md5 = remoteObjectHash;
                        else if ([remoteObjectHash length] == 64)
                            storedLocalObjectState.hashMapHash = remoteObjectHash;
                        else if ([remoteObjectHash isEqualToString:@" "])
                            [storedLocalObjectStates removeObjectForKey:object.name];
                        [self saveLocalState];
                    } else {
                        // Conflict, we ask the user which change to keep
                        NSString *informativeText;
                        NSString *firstButtonText;
                        NSString *secondButtonText;
                        
                        if ([remoteObjectHash isEqualToString:@" "]) {
                            // 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.md5 isEqualToString:@" "] && [currentLocalObjectState.hashMapHash isEqualToString:@" "]) {
                            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.hash = remoteObjectHash;
                            [self updateLocalStateWithObject:object localFilePath:filePath];
                        } if (choice == NSAlertSecondButtonReturn) {
                            [self updateServerStateWithCurrentState:currentLocalObjectState 
                                                             object:object 
                                                      localFilePath:filePath];
                        }
                    }
                }
            }
        }
        @synchronized(self) {
            [self decreaseSyncOperationCount];
            if (newSyncRequested && !syncOperationCount)
                [self sync];
        }
    } else {
        NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
        if (retries > 0) {
            ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
            [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
            [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
        } else {
            [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
            @synchronized(self) {
                // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
                syncOperationCount = 0;
                if (newSyncRequested)
                    [self sync];
            }
        }
    }
}

- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
    if ([containerRequest isCancelled]) {
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
        [objects release];
        objects = nil;
        @synchronized(self) {
            syncOperationCount = 0;
        }
        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];
        [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
        [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
    } else {
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
        [objects release];
        objects = nil;
        @synchronized(self) {
            // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
            syncOperationCount = 0;
            if (newSyncRequested)
                [self sync];
        }
    }
}

- (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
    NSLog(@"Sync::download object block finished: %@", objectRequest.url);
    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) {
            [activityFacility endActivity:activity 
                              withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
            @synchronized(self) {
                syncIncomplete = YES;
                [self decreaseSyncOperationCount];
                if (newSyncRequested && !syncOperationCount)
                    [self sync];
            }
            return;
        }
        
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
        if ((storedState.tmpDownloadFile == nil) || ![fileManager fileExistsAtPath:storedState.tmpDownloadFile]) {
            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) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", storedState.tmpDownloadFile] 
                                                          error:nil];
                [activityFacility endActivity:activity 
                                  withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                @synchronized(self) {
                    syncIncomplete = YES;
                    [self decreaseSyncOperationCount];
                    if (newSyncRequested && !syncOperationCount)
                        [self sync];
                }
                return;
            }
            close(fileDescriptor);
            storedState.tmpDownloadFile = tempFilePath;
            [self saveLocalState];
        }
        

        NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
        NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile];
        [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]) {
                error = nil;
                // XXX don't delete but move to local trash instead
                [fileManager removeItemAtPath:filePath error:&error];
                if (error != nil) {
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
                                                            message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
                                                              error:error];
                    [activityFacility endActivity:activity 
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                    @synchronized(self) {
                        syncIncomplete = YES;
                        [self decreaseSyncOperationCount];
                        if (newSyncRequested && !syncOperationCount)
                            [self sync];
                    }
                    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) {
                    [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"]];
                    @synchronized(self) {
                        syncIncomplete = YES;
                        [self decreaseSyncOperationCount];
                        if (newSyncRequested && !syncOperationCount)
                            [self sync];
                    }
                    return;
                }
            }
            // Move file from tmp download
            error = nil;
            [fileManager moveItemAtPath:storedState.tmpDownloadFile toPath:filePath error:&error];
            if (error != nil) {
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpDownloadFile, filePath] 
                                                          error:error];
                [activityFacility endActivity:activity 
                                  withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                @synchronized(self) {
                    syncIncomplete = YES;
                    [self decreaseSyncOperationCount];
                    if (newSyncRequested && !syncOperationCount)
                        [self sync];
                }
                return;
            }
            [activityFacility endActivity:activity 
                              withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
                               totalBytes:activity.totalBytes 
                             currentBytes:activity.totalBytes];

            if ([object.hash length] == 32) {
                storedState.md5 = object.hash;
                storedState.hashMapHash = @" ";
            } else if([object.hash length] == 64) {
                storedState.md5 = @" ";
                storedState.hashMapHash = object.hash;
            } else {
                storedState.md5 = @" ";
                storedState.hashMapHash = @" ";
            }
            
            storedState.tmpDownloadFile = nil;
            [self saveLocalState];
            
            @synchronized(self) {
                [self decreaseSyncOperationCount];
                if (newSyncRequested && !syncOperationCount)
                    [self sync];
            }
            return;
        } else {
            if (newSyncRequested) {
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                                  withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
                @synchronized(self) {
                    syncIncomplete = YES;
                    [self decreaseSyncOperationCount];
                    if (!syncOperationCount)
                        [self sync];
                }
                return;
            } else {
                __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName 
                                                                                                                     object:object 
                                                                                                                 blockIndex:missingBlockIndex 
                                                                                                                  blockSize:blockSize];
                newObjectRequest.delegate = self;
                newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
                newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
                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:@"Downloading '%@' (%.0f%%)", 
                                                      object.name, 
                                                      (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                          totalBytes:activity.totalBytes 
                                        currentBytes:(activity.currentBytes + size)];
                }];
                [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
            }
        }
    } else if (objectRequest.responseStatusCode == 412) {
        // The object has changed on the server
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
        @synchronized(self) {
            syncIncomplete = YES;
            [self decreaseSyncOperationCount];
            if (newSyncRequested && !syncOperationCount)
                [self sync];
        }
    } else {
        [self requestFailed:objectRequest];
    }
}

- (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
    NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
    if (objectRequest.responseStatusCode == 200) {
        if (newSyncRequested) {
            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                              withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
            @synchronized(self) {
                syncIncomplete = YES;
                [self decreaseSyncOperationCount];
                if (!syncOperationCount)
                    [self sync];
            }
            return;
        } else {
            ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
            PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
            if ([PithosUtilities bytesOfFile:storedState.tmpDownloadFile] > object.bytes)
                [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile] truncateFileAtOffset:object.bytes];
            PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpDownloadFile 
                                                                    blockSize:blockSize 
                                                                    blockHash:blockHash 
                                                                   withHashes:[objectRequest hashes]];
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
            [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 objectBlockDataRequestWithContainerName:containerName 
                                                                                                                 object:object 
                                                                                                             blockIndex:missingBlockIndex 
                                                                                                              blockSize:blockSize];
            newObjectRequest.delegate = self;
            newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
            newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
            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"];
            [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", 
                                                  object.name, 
                                                  (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                      totalBytes:activity.totalBytes 
                                    currentBytes:(activity.currentBytes + size)];
            }];
            [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
        }
    } else {
        [self requestFailed:objectRequest];
    }
}

- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
    NSLog(@"Sync::upload directory object finished: %@", objectRequest.url);
    if (objectRequest.responseStatusCode == 201) {
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
        storedState.isDirectory = YES;
        [self saveLocalState];
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        @synchronized(self) {
            [self decreaseSyncOperationCount];
            if (newSyncRequested && !syncOperationCount)
                [self sync];
        }
    } else {
        [self requestFailed:objectRequest];
    }
}

- (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
    NSLog(@"Sync::move object to trash finished: %@", objectRequest.url);
    if (objectRequest.responseStatusCode == 201) {
        [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
        [self saveLocalState];
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
        @synchronized(self) {
            [self decreaseSyncOperationCount];
            if (newSyncRequested && !syncOperationCount)
                [self sync];
        }
    } else {
        [self requestFailed:objectRequest];
    }
}

- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
    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 (objectRequest.responseStatusCode == 201) {
        NSLog(@"Sync::object created: %@", objectRequest.url);
        NSString *eTag = [objectRequest eTag];
        if ([eTag length] == 32)
            storedState.md5 = eTag;
        else if([eTag length] == 64)
            storedState.hashMapHash = eTag;
        [self saveLocalState];
        [activityFacility endActivity:activity 
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
                           totalBytes:totalBytes 
                         currentBytes:totalBytes];
        @synchronized(self) {
            [self decreaseSyncOperationCount];
            if (newSyncRequested && !syncOperationCount)
                [self sync];
        }
    } else if (objectRequest.responseStatusCode == 409) {
        if (newSyncRequested) {
            [activityFacility endActivity:activity 
                              withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
            @synchronized(self) {
                syncIncomplete = YES;
                [self decreaseSyncOperationCount];
                if (!syncOperationCount)
                    [self sync];
            }
            return;
        } else {
            NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
            if (iteration == 0) {
                NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
                [activityFacility endActivity:activity 
                                  withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
                syncIncomplete = YES;
                [self decreaseSyncOperationCount];
                return;
            }
            NSLog(@"Sync::object is missing hashes: %@", objectRequest.url);
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
                                                      withMissingHashesResponse:[objectRequest responseString]];
            if (totalBytes >= [missingBlocks count]*blockSize)
                currentBytes = totalBytes - [missingBlocks count]*blockSize;
            [activityFacility updateActivity:activity 
                                 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
                                  totalBytes:totalBytes 
                                currentBytes:currentBytes];
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
            __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName 
                                                                                                                        blockSize:blockSize 
                                                                                                                          forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
                                                                                                                missingBlockIndex:missingBlockIndex 
                                                                                                                   sharingAccount:nil];
            newContainerRequest.delegate = self;
            newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
            newContainerRequest.didFailSelector = @selector(requestFailed:);
            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"];
            [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
                [activityFacility updateActivity:activity 
                                     withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                      totalBytes:activity.totalBytes 
                                    currentBytes:(activity.currentBytes + size)];
            }];
            [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
        }
    } else {
        [self requestFailed:objectRequest];
    }
}

- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
    NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
    if (containerRequest.responseStatusCode == 202) {
        ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
        PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
        NSUInteger totalBytes = activity.totalBytes;
        NSUInteger currentBytes = activity.currentBytes + blockSize;
        if (currentBytes > totalBytes)
            currentBytes = totalBytes;
        [activityFacility updateActivity:activity 
                             withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
                              totalBytes:totalBytes 
                            currentBytes:currentBytes];
        NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
        NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
        missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
        if (missingBlockIndex == NSNotFound) {
            NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
            ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName: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(uploadObjectUsingHashMapFinished:);
            newObjectRequest.didFailSelector = @selector(requestFailed:);
            newObjectRequest.userInfo = containerRequest.userInfo;
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
            [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
        } else {
            if (newSyncRequested) {
                [activityFacility endActivity:activity 
                                  withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
                @synchronized(self) {
                    syncIncomplete = YES;
                    [self decreaseSyncOperationCount];
                    if (!syncOperationCount)
                        [self sync];
                }
                return;
            } else {
                __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName
                                                                                                                            blockSize:blockSize
                                                                                                                              forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
                                                                                                                    missingBlockIndex:missingBlockIndex 
                                                                                                                       sharingAccount:nil];
                newContainerRequest.delegate = self;
                newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
                newContainerRequest.didFailSelector = @selector(requestFailed:);
                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:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
                                          totalBytes:activity.totalBytes 
                                        currentBytes:(activity.currentBytes + size)];
                }];
                [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
            }
        }
    } else {
        [self requestFailed:containerRequest];
    }
}

- (void)requestFailed:(ASIPithosRequest *)request {
    if ([request isCancelled]) {
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                          withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
        syncIncomplete = YES;
        [self decreaseSyncOperationCount];
        return;
    }
    if (newSyncRequested) {
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                          withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
        @synchronized(self) {
            syncIncomplete = YES;
            [self decreaseSyncOperationCount];
            if (!syncOperationCount)
                [self sync];
        }
        return;
    }
    NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
    if (retries > 0) {
        ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request];
        [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
        [queue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
    } else {
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                          withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
        syncIncomplete = YES;
        [self decreaseSyncOperationCount];
    }
}


@end
