//
//  MGMPathSubscriber.m
//  Conmote
//
//  Created by Mr. Gecko on 8/14/13.
//  Copyright (c) 2013 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
//
//  Permission to use, copy, modify, and/or distribute this software for any purpose
//  with or without fee is hereby granted, provided that the above copyright notice
//  and this permission notice appear in all copies.
//
//  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
//  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
//  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,
//  OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
//  DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
//  ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

#import "MGMPathSubscriber.h"

@interface MGMPathSubscriberFile : NSObject {
	NSString *path;
	int fileDescriptor;
	u_int options;
}
- (id)initWithPath:(NSString*)thePath options:(u_int)theOptions;
- (NSString *)path;
- (int)fileDescriptor;
- (u_int)options;
- (BOOL)reopen;
@end

@implementation MGMPathSubscriberFile
- (id)initWithPath:(NSString*)thePath options:(u_int)theOptions {
	if ((self = [super init])) {
		path = [thePath copy];
		options = theOptions;
		fileDescriptor = open([path fileSystemRepresentation], O_EVTONLY, 0);
		if (fileDescriptor<0) {
			[self release];
			return nil;
		}
	}
	return self;
}
- (void)dealloc {
	[path release];
	if (fileDescriptor>=0)
		close(fileDescriptor);
	[super dealloc];
}
- (NSString *)path {
	return path;
}
- (int)fileDescriptor {
	return fileDescriptor;
}
- (u_int)options {
	return options;
}

- (BOOL)reopen {
	if (fileDescriptor>=0)
		close(fileDescriptor);
	fileDescriptor = open([path fileSystemRepresentation], O_EVTONLY, 0);
	if (fileDescriptor<0)
		return NO;
	return YES;
}
@end

@interface MGMPathSubscriber (MGMPrivate)
- (void)subscriptionChanged:(FNSubscriptionRef)theSubscription;
- (void)subscriptionFSChanged:(ConstFSEventStreamRef)theSubscription;
- (void)subscriptionFileChanged:(MGMPathSubscriberFile *)theFile;
- (void)sendNotificationForPath:(NSString *)thePath;
@end

static MGMPathSubscriber *MGMSharedPathSubscriber;
NSString * const MGMSubscribedPathChangedNotification = @"MGMSubscribedPathChangedNotification";

void MGMPathSubscriptionChange(FNMessage theMessage, OptionBits theFlags, void *thePathSubscription, FNSubscriptionRef theSubscription) {
	if (theMessage==kFNDirectoryModifiedMessage)
		[(MGMPathSubscriber *)thePathSubscription subscriptionChanged:theSubscription];
	else
		NSLog(@"MGMPathSubscription: Received Unknown message: %u", theMessage);
}

void MGMPathSubscriptionFSChange(ConstFSEventStreamRef streamRef, void *thePathSubscription, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) {
	for (size_t i=0; i<numEvents; i++) {
		if (eventFlags[i] & (0x00000100 /*kFSEventStreamEventFlagItemCreated*/ | 0x00000200 /*kFSEventStreamEventFlagItemRemoved*/ | 0x00000800 /*kFSEventStreamEventFlagItemRenamed*/)) {
			[(MGMPathSubscriber *)thePathSubscription subscriptionFSChanged:streamRef];
			break;
		}
	}
}

@implementation MGMPathSubscriber
+ (id)sharedPathSubscriber {
	if (MGMSharedPathSubscriber==nil)
		MGMSharedPathSubscriber = [MGMPathSubscriber new];
	return MGMSharedPathSubscriber;
}
- (id)init {
	if ((self = [super init])) {
		subscriptions = [NSMutableDictionary new];
		subscriptionUPP = NewFNSubscriptionUPP(MGMPathSubscriptionChange);
		notificationsSending = [NSMutableArray new];
		runLoop = CFRunLoopGetCurrent();
		
		queueDescriptor = kqueue();
		fileWatchLoop = NO;
	}
	return self;
}
- (void)dealloc {
	if (!fileWatchLoop && queueDescriptor>=0)
		close(queueDescriptor);
	fileWatchLoop = NO;
	[self removeAllPaths];
	DisposeFNSubscriptionUPP(subscriptionUPP);
	[subscriptions release];
	[notificationsSending release];
	[super dealloc];
}

- (id<MGMPathSubscriberDelegate>)delegate {
	return delegate;
}
- (void)setDelegate:(id)theDelegate {
	delegate = theDelegate;
}

- (void)addPath:(NSString *)thePath {
	[self addPath:thePath fileOptions:MGMFODelete | MGMFOWrite | MGMFOExtend | MGMFOAttribute | MGMFOLink | MGMFORename | MGMFORevoke];
}
- (void)addPath:(NSString *)thePath fileOptions:(u_int)theOptions {
	NSValue *value = [subscriptions objectForKey:thePath];
	if (value!=nil)
		return;
	
	BOOL directory = NO;
	BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:thePath isDirectory:&directory];
	if (!exists) {
		NSLog(@"MGMPathSubscription: Unable to subscribe to %@ as it does not exist", thePath);
		return;
	}
	if (directory) {
		if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
			FSEventStreamContext context = {0, self, NULL, NULL, NULL};
			FSEventStreamRef stream = FSEventStreamCreate(NULL, &MGMPathSubscriptionFSChange, &context, (CFArrayRef)[NSArray arrayWithObject:thePath], kFSEventStreamEventIdSinceNow, 0.5, 0x00000010 /* kFSEventStreamCreateFlagFileEvents */);
			if (stream==NULL) {
				NSLog(@"MGMPathSubscription: Unable to subscribe to %@", thePath);
				return;
			}
			FSEventStreamScheduleWithRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
			FSEventStreamStart(stream);
			[subscriptions setObject:[NSValue valueWithPointer:stream] forKey:thePath];
		} else {
			FNSubscriptionRef subscription = NULL;
			OSStatus error = FNSubscribeByPath((UInt8 *)[thePath fileSystemRepresentation], subscriptionUPP, self, kFNNotifyInBackground, &subscription);
			if (error!=noErr) {
				NSLog(@"MGMPathSubscription: Unable to subscribe to %@ due to the error %ld", thePath, (long)error);
				return;
			}
			[subscriptions setObject:[NSValue valueWithPointer:subscription] forKey:thePath];
		}
	} else {
		MGMPathSubscriberFile *file = [[[MGMPathSubscriberFile alloc] initWithPath:thePath options:theOptions] autorelease];
		
		if (file==nil) {
			NSLog(@"MGMPathSubscription: Unable to subscribe to %@ due to an error", thePath);
			return;
		}
		
		struct timespec nullts = { 0, 0 };
		struct kevent event;
		EV_SET(&event, [file fileDescriptor], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, theOptions, 0, file);
		
		[subscriptions setObject:file forKey:thePath];
		kevent(queueDescriptor, &event, 1, NULL, 0, &nullts);
		
		if (!fileWatchLoop) {
			fileWatchLoop = YES;
			[NSThread detachNewThreadSelector:@selector(fileWatchThread) toTarget:self withObject:nil];
		}
	}
}
- (void)removePath:(NSString *)thePath {
	NSValue *value = [subscriptions objectForKey:thePath];
	if (value!=nil) {
		if ([value isKindOfClass:[NSValue class]]) {
			if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
				FSEventStreamRef stream = [value pointerValue];
				FSEventStreamStop(stream);
				FSEventStreamUnscheduleFromRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
				FSEventStreamInvalidate(stream);
				FSEventStreamRelease(stream);
				[subscriptions removeObjectForKey:thePath];
			} else {
				FNUnsubscribe([value pointerValue]);
				[subscriptions removeObjectForKey:thePath];
			}
		} else {
			[subscriptions removeObjectForKey:thePath];
		}
	}
}
- (void)removeAllPaths {
	NSArray *keys = [subscriptions allKeys];
	for (int i=0; i<[keys count]; i++) {
		NSValue *value = [subscriptions objectForKey:[keys objectAtIndex:i]];
		if ([value isKindOfClass:[NSValue class]]) {
			if (NSFoundationVersionNumber>=677.00/*NSFoundationVersionNumber10_5*/) {
				FSEventStreamRef stream = [value pointerValue];
				FSEventStreamStop(stream);
				FSEventStreamUnscheduleFromRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
				FSEventStreamInvalidate(stream);
				FSEventStreamRelease(stream);
			} else {
				FNUnsubscribe([value pointerValue]);
			}
		}
	}
	[subscriptions removeAllObjects];
}

- (NSArray *)subscribedPaths {
	return [subscriptions allKeys];
}

- (void)subscriptionChanged:(FNSubscriptionRef)theSubscription {
	NSArray *keys = [subscriptions allKeysForObject:[NSValue valueWithPointer:theSubscription]];
	if ([keys count]>=1) {
		NSString *path = [keys objectAtIndex:0];
		if (![notificationsSending containsObject:path]) {
			[notificationsSending addObject:path];
			[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
		}
	}
}
- (void)subscriptionFSChanged:(ConstFSEventStreamRef)theSubscription {
	NSArray *keys = [subscriptions allKeysForObject:[NSValue valueWithPointer:theSubscription]];
	if ([keys count]>=1) {
		NSString *path = [keys objectAtIndex:0];
		if (![notificationsSending containsObject:path]) {
			[notificationsSending addObject:path];
			[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
		}
	}
}
- (void)subscriptionFileChanged:(MGMPathSubscriberFile *)theFile {
	NSArray *keys = [subscriptions allKeysForObject:theFile];
	if ([keys count]>=1) {
		NSString *path = [keys objectAtIndex:0];
		if (![notificationsSending containsObject:path]) {
			[notificationsSending addObject:path];
			[self performSelector:@selector(sendNotificationForPath:) withObject:path afterDelay:0.5];
		}
	}
}

- (void)sendNotificationForPath:(NSString *)thePath {
	[[NSNotificationCenter defaultCenter] postNotificationName:MGMSubscribedPathChangedNotification object:thePath];
	if ([delegate respondsToSelector:@selector(subscribedPathChanged:)]) [delegate subscribedPathChanged:thePath];
	[notificationsSending removeObject:thePath];
}

- (void)fileWatchThread {
	int status;
	struct kevent event;
	struct timespec timeout = {1, 0};
	int queueFD = queueDescriptor;
	
	while (fileWatchLoop) {
		@try {
			status = kevent(queueFD, NULL, 0, &event, 1, &timeout);
			if (status>0) {
				MGMPathSubscriberFile *file = [(id)event.udata retain];
				if (event.filter==EVFILT_VNODE && event.fflags && file!=nil) {
					if ((event.fflags & NOTE_RENAME)==NOTE_RENAME) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
					if ((event.fflags & NOTE_WRITE)==NOTE_WRITE) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
					if ((event.fflags & NOTE_DELETE)==NOTE_DELETE) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
						
						if ([file reopen]) {
							struct timespec nullts = { 0, 0 };
							struct kevent event;
							EV_SET(&event, [file fileDescriptor], EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, [file options], 0, file);
							
							kevent(queueDescriptor, &event, 1, NULL, 0, &nullts);
						} else {
							[self removePath:[file path]];
						}
					}
					if ((event.fflags & NOTE_ATTRIB)==NOTE_ATTRIB) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
					if ((event.fflags & NOTE_EXTEND)==NOTE_EXTEND) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
					if ((event.fflags & NOTE_LINK)==NOTE_LINK) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
					if ((event.fflags & NOTE_REVOKE)==NOTE_REVOKE) {
						[self performSelectorOnMainThread:@selector(subscriptionFileChanged:) withObject:file waitUntilDone:NO];
					}
				}
				[file release];
			}
		} @catch (NSException *exception) {
			NSLog(@"MGMPathSubscription: Error in fileWatchThread: %@", exception);
		}
	}
	
	if (close(queueFD)==-1)
		NSLog(@"MGMPathSubscription: fileWatchThread couldn't close queue descriptor %d", errno);
	queueDescriptor = -1;
}
@end