/*
 *  FSDirectoryMonitor.h
 *  Created by Martin Hwasser on 12/19/11.
 */

#import "FSDirectoryMonitor.h"
#import <CoreFoundation/CoreFoundation.h>
#import <fcntl.h>

#define kMHDirectoryMonitorPollingTimes 6

@interface FSDirectoryMonitor ()
- (void)startPollingChanges;
- (NSMutableSet *)metadataForContentsOfDirectory;
@end

@implementation FSDirectoryMonitor

- (id)initWithPath:(NSString *)path {
    if (self = [super init]) {
        _monitoringPath = [path copy];
    }
    return self;
}

- (void)dealloc {
    [self stopMonitoring];
}

#pragma mark - Public methods

- (void)stopMonitoring {
    _metadata = nil;
    _delegate = nil;
    
    if (_source) {
        dispatch_source_cancel(_source);
        _source = NULL;
    }
    _monitoringQueue = NULL;
}

- (BOOL)startMonitoringWithDelegate:(id<FSDirectoryMonitorDelegate>)delegate {
    // Already monitoring
	if (_source) {
        return NO;
    }
    
	// Open an event-only file descriptor associated with the directory
	const int fd = open([_monitoringPath fileSystemRepresentation], O_EVTONLY);
    
	if (fd < 0) {
        return NO;
    }
    
    void (^cleanup)() = ^{
        [self stopMonitoring];
        close(fd);
    };
    
    // Get a background priority queue
    _monitoringQueue = dispatch_queue_create("fs.directory_monitor", 0);
	if (NULL == _monitoringQueue) {
        cleanup();
		return NO;
	}
    
	// Monitor the directory for writes
	_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, DISPATCH_VNODE_WRITE, _monitoringQueue);
	if (NULL == _source) {
        cleanup();
		return NO;
	}
    
    dispatch_async(_monitoringQueue, ^{
        @autoreleasepool {
            _metadata = [self metadataForContentsOfDirectory];
            [self startPollingChanges];
        }
    });
    
	// Call startPollingChanges on event callback
	dispatch_source_set_event_handler(_source, ^{
        @autoreleasepool {
            [self startPollingChanges];
        }
    });
    
    // Dispatch source destructor
	dispatch_source_set_cancel_handler(_source, cleanup);
    
	// Sources are create in suspended state, so resume it
	dispatch_resume(_source);
    
    _delegate = delegate;
    
    // Everything was OK
    return YES;
}

#pragma mark - Private methods

- (NSMutableSet *)metadataForContentsOfDirectory {
    NSArray * const contents = [theFileManager contentsOfDirectoryAtPath:_monitoringPath error:NULL];
    const NSUInteger numberOfSubpaths = [contents count];
    NSMutableSet *metadata = [NSMutableSet setWithCapacity:numberOfSubpaths];
    
    for (NSUInteger i = 0; i < numberOfSubpaths; ++ i) {
        @autoreleasepool {
            NSString *fileName = [contents objectAtIndex:i];
            
            if (![fileName hasPrefix:@"."]) {
                NSString *filePath = [_monitoringPath stringByAppendingPathComponent:fileName];
                NSDictionary *fileAttributes = [theFileManager attributesOfItemAtPath:filePath error:nil];
                const UInt64 fileSize = [[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
                // The fileName and fileSize will be our hash key
                NSString *fileHash = [[NSString alloc] initWithFormat:@"%@:%llu", fileName, fileSize];
                
                // Add it to our metadata list
                [metadata addObject:fileHash];
            }
        }
    }
    
    return metadata;
}

- (void)pollDirectoryForChangesWithMetadata:(NSMutableSet *)oldMetadata {
    NSMutableSet *newMetadata = [self metadataForContentsOfDirectory];
    
    // Check if metadata has changed
    if ([newMetadata isEqualToSet:oldMetadata]) {
        _pollingTimesLeft --;
    } else {
        // Reset retries when it's still changing
        _pollingTimesLeft = kMHDirectoryMonitorPollingTimes;
        _directoryChanged = YES;
        
        NSMutableSet *changedItems = [[NSMutableSet alloc] init];
        
        [oldMetadata minusSet:newMetadata];
        
        for (NSString *fileHash in oldMetadata) {
            NSArray *hashComponents = [fileHash componentsSeparatedByString:@":"];
            
            [changedItems addObject:[hashComponents objectAtIndex:0]];
        }
        
        // Changes just occurred
        dispatch_sync(dispatch_get_main_queue(), ^{
            [_delegate directoryDidChangeItems:changedItems];
        });
        
        }
    
    if (0 < _pollingTimesLeft) {
        // Either the directory is changing or
        // we should try again as more changes may occur
        const NSInteger product = _pollingTimesLeft * (_pollingTimesLeft - 1);
        const double timeInterval = 0.1f + (double)product / 100.f;
        const int64_t delta = (int64_t)(timeInterval * (double)NSEC_PER_SEC);
        const dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delta);
        
        dispatch_after(popTime, _monitoringQueue, ^{
            @autoreleasepool {
                [self pollDirectoryForChangesWithMetadata:newMetadata];
            }
        });
    } else {
        _metadata = newMetadata;
        
        if (_directoryChanged) {
            _directoryChanged = NO;
            
            // Changes appear to be completed
            // Post a notification informing that the directory did change
            dispatch_sync(dispatch_get_main_queue(), ^{
                [_delegate directoryDidFinishChanging];
            });
        }
    }
}

- (void)startPollingChanges {
    if (_pollingTimesLeft <= 0) {
        _pollingTimesLeft = kMHDirectoryMonitorPollingTimes;
        
        [self pollDirectoryForChangesWithMetadata:_metadata];
    }
}

@end
