//
//  SandboxBroker.m
//  Cog
//
//  Created by Christopher Snowhill on 6/20/22.
//

#import <Foundation/Foundation.h>

#import <Cocoa/Cocoa.h>

#import "SandboxBroker.h"

#import "Logging.h"

#import "Cog-Swift.h"

#import "PlaylistController.h"

static SandboxBroker *kSharedSandboxBroker = nil;

@interface SandboxEntry : NSObject {
	SandboxToken *_token;
	NSInteger _refCount;
	NSURL *_secureUrl;
	NSString *_path;
	BOOL _isFolder;
};

@property(readonly) SandboxToken *token;

@property NSURL *secureUrl;

@property(readonly) NSString *path;

@property NSInteger refCount;

@property(readonly) BOOL isFolder;

- (id)initWithToken:(SandboxToken *)token;
@end

@implementation SandboxEntry
- (id)initWithToken:(SandboxToken *)token {
	SandboxEntry *obj = [super init];
	if(obj) {
		obj->_refCount = 1;
		obj->_secureUrl = nil;
		obj->_token = token;
		obj->_path = token.path;
		obj->_isFolder = token.folder;
	}
	return obj;
}

- (NSInteger)refCount {
	return _refCount;
}

- (void)setRefCount:(NSInteger)refCount {
	_refCount = refCount;
}

- (NSURL *)secureUrl {
	return _secureUrl;
}

- (void)setSecureUrl:(NSURL *)url {
	_secureUrl = url;
}

- (SandboxToken *)token {
	return _token;
}

- (NSString *)path {
	return _path;
}

- (BOOL)isFolder {
	return _isFolder;
}
@end

@implementation SandboxBroker

+ (id)sharedSandboxBroker {
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		kSharedSandboxBroker = [[self alloc] init];
	});
	return kSharedSandboxBroker;
}

+ (NSPersistentContainer *)sharedPersistentContainer {
	return [NSClassFromString(@"PlaylistController") sharedPersistentContainer];
}

+ (NSURL *)urlWithoutFragment:(NSURL *)url {
	if(![url isFileURL]) return url;

	NSString *s = [url path];

	NSRange fragmentRange = [s rangeOfString:@"#"
									 options:NSBackwardsSearch];

	if(fragmentRange.location != NSNotFound) {
		// Chop the fragment.
		NSString *newURLString = [s substringToIndex:fragmentRange.location];

		return [NSURL fileURLWithPath:newURLString];
	} else {
		return url;
	}
}

- (id)init {
	id _self = [super init];
	if(_self) {
		storage = [[NSMutableArray alloc] init];
	}

	return _self;
}

- (void)shutdown {
	for(SandboxEntry *obj in storage) {
		if([obj secureUrl]) {
			[[obj secureUrl] stopAccessingSecurityScopedResource];
		}
	}
}

+ (BOOL)isPath:(NSURL *)path aSubdirectoryOf:(NSURL *)directory {
	NSArray *pathComponents = [path pathComponents];
	NSArray *directoryComponents = [directory pathComponents];

	if([pathComponents count] < [directoryComponents count])
		return NO;

	for(size_t i = 0; i < [directoryComponents count]; ++i) {
		if(![pathComponents[i] isEqualToString:directoryComponents[i]])
			return NO;
	}

	return YES;
}

- (SandboxEntry *)recursivePathTest:(NSURL *)url {
	__block SandboxEntry *ret = nil;

	NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];
	
	NSPredicate *folderPredicate = [NSPredicate predicateWithFormat:@"folder == NO"];
	NSPredicate *filePredicate = [NSPredicate predicateWithFormat:@"path == %@", [url path]];

	[pc.viewContext performBlockAndWait:^{
		NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[folderPredicate, filePredicate]];

		NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
		request.predicate = predicate;

		NSError *error = nil;
		NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
		if(results && [results count] > 0) {
			ret = [[SandboxEntry alloc] initWithToken:results[0]];
		}

		if(!ret) {
			predicate = [NSPredicate predicateWithFormat:@"folder == YES"];

			NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"path.length" ascending:NO];

			request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
			request.sortDescriptors = @[sortDescriptor];
			request.predicate = predicate;

			error = nil;
			results = [pc.viewContext executeFetchRequest:request error:&error];

			if(results && [results count] > 0) {
				for(SandboxToken *token in results) {
					if(token.path && [SandboxBroker isPath:url aSubdirectoryOf:[NSURL fileURLWithPath:token.path]]) {
						SandboxEntry *entry = [[SandboxEntry alloc] initWithToken:token];

						ret = entry;
						break;
					}
				}
			}
		}
	}];

	if(ret) {
		BOOL isStale;
		NSError *err = nil;
		NSURL *secureUrl = [NSURL URLByResolvingBookmarkData:ret.token.bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&err];
		if(!secureUrl && err) {
			ALog(@"Failed to access bookmark for URL: %@, error: %@", ret.token.path, [err localizedDescription]);
			return nil;
		}

		[pc.viewContext performBlockAndWait:^{
			ret.secureUrl = secureUrl;
		}];

		return ret;
	}

	return nil;
}

static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_block_t block) {
	if(dispatch_queue_get_label(queue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) {
		block();
	} else {
		dispatch_sync(queue, block);
	}
}

static inline void dispatch_async_reentrant(dispatch_queue_t queue, dispatch_block_t block) {
	if(dispatch_queue_get_label(queue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) {
		block();
	} else {
		dispatch_async(queue, block);
	}
}

- (void)addFolderIfMissing:(NSURL *)folderUrl {
	if(![folderUrl isFileURL]) return;

	dispatch_async_reentrant(dispatch_get_main_queue(), ^{
		SandboxEntry *_entry = nil;

		for(SandboxEntry *entry in self->storage) {
			if(entry.path && entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) {
				_entry = entry;
				break;
			}
		}

		if(!_entry) {
			_entry = [self recursivePathTest:folderUrl];
		}

		if(!_entry) {
			NSError *err = nil;
			NSData *bookmark = [folderUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
			if(!bookmark && err) {
				ALog(@"Failed to add bookmark for URL: %@, with error: %@", folderUrl, [err localizedDescription]);
				return;
			}

			NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];

			[pc.viewContext performBlockAndWait:^{
				SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];

				if(token) {
					token.path = [folderUrl path];
					token.bookmark = bookmark;
					[SandboxBroker cleanupFolderAccess];
				}
			}];
		}
	});
}

- (void)addFileIfMissing:(NSURL *)fileUrl {
	if(![fileUrl isFileURL]) return;

	NSURL *url = [SandboxBroker urlWithoutFragment:fileUrl];

	dispatch_async_reentrant(dispatch_get_main_queue(), ^{
		SandboxEntry *_entry = nil;

		for(SandboxEntry *entry in self->storage) {
			if(entry.path) {
				if((entry.isFolder && [SandboxBroker isPath:url aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) ||
				   (!entry.isFolder && [url isEqualTo:[NSURL fileURLWithPath:entry.path]])) {
					_entry = entry;
					break;
				}
			}
		}

		if(!_entry) {
			_entry = [self recursivePathTest:url];
		}

		if(!_entry) {
			NSError *err = nil;
			NSData *bookmark = [fileUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
			if(!bookmark && err) {
				ALog(@"Failed to add bookmark for URL: %@, with error: %@", url, [err localizedDescription]);
				return;
			}

			NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];

			[pc.viewContext performBlockAndWait:^{
				SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];

				if(token) {
					token.path = [url path];
					token.bookmark = bookmark;
					token.folder = NO;
				}
			}];
		}
	});
}

- (void)requestFolderForFile:(NSURL *)fileUrl {
	if(![fileUrl isFileURL]) return;
	NSURL *folderUrl = [fileUrl URLByDeletingLastPathComponent];

	dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
		SandboxEntry *_entry = nil;

		for(SandboxEntry *entry in self->storage) {
			if(entry.path && entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) {
				_entry = entry;
				break;
			}
		}

		if(!_entry) {
			_entry = [self recursivePathTest:folderUrl];
		}

		if(!_entry) {
			static BOOL warnedYet = NO;

			if(!warnedYet) {
				NSAlert *alert = [[NSAlert alloc] init];
				[alert setMessageText:NSLocalizedString(@"GrantPathTitle", @"Title of file dialog for granting folder access")];
				[alert setInformativeText:NSLocalizedString(@"GrantPathMessage", @"Message to new users regarding file permissions")];
				[alert addButtonWithTitle:NSLocalizedString(@"GrantPathOK", @"OK button text")];
				[alert addButtonWithTitle:NSLocalizedString(@"GrantPathStopWarning", @"Button to stop warnings for session")];
				
				if([alert runModal] == NSAlertSecondButtonReturn) {
					warnedYet = YES;
				}
			}

			NSOpenPanel *panel = [NSOpenPanel openPanel];
			[panel setAllowsMultipleSelection:NO];
			[panel setCanChooseDirectories:YES];
			[panel setCanChooseFiles:NO];
			[panel setFloatingPanel:YES];
			[panel setDirectoryURL:folderUrl];
			[panel setTitle:NSLocalizedString(@"GrantPathTitle", @"Title of file dialog for granting folder access")];
			NSInteger result = [panel runModal];
			if(result == NSModalResponseOK) {
				NSURL *folderUrl = [panel URL];
				NSError *err = nil;
				NSData *bookmark = [folderUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
				if(!bookmark && err) {
					ALog(@"Failed to add bookmark for URL: %@, with error: %@", folderUrl, [err localizedDescription]);
					return;
				}

				NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];

				SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];

				if(token) {
					token.path = [folderUrl path];
					token.bookmark = bookmark;
					[SandboxBroker cleanupFolderAccess];
				}
			}
		}
	});
}

+ (void)cleanupFolderAccess {
	NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];

	NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"path.length" ascending:YES];
	NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
	request.sortDescriptors = @[sortDescriptor];

	[pc.viewContext performBlock:^{
		NSError *error = nil;
		NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
		NSMutableArray *resultsCopy = nil;
		if(results) {
			resultsCopy = [results mutableCopy];
		}

		BOOL isUpdated = NO;

		if(resultsCopy && [resultsCopy count]) {
			for(NSUInteger i = 0; i < [resultsCopy count] - 1; ++i) {
				SandboxToken *token = resultsCopy[i];
				NSURL *url = [NSURL fileURLWithPath:token.path];
				for(NSUInteger j = i + 1; j < [resultsCopy count];) {
					SandboxToken *compareToken = resultsCopy[j];
					if([SandboxBroker isPath:[NSURL fileURLWithPath:compareToken.path] aSubdirectoryOf:url]) {
						[pc.viewContext deleteObject:compareToken];
						isUpdated = YES;
						[resultsCopy removeObjectAtIndex:j];
					} else {
						++j;
					}
				}
			}
		}

		if(isUpdated) {
			NSError *error;
			[pc.viewContext save:&error];
			if(error) {
				ALog(@"Error saving data: %@", [error localizedDescription]);
			}
		}
	}];
}

- (const void *)beginFolderAccess:(NSURL *)fileUrl {
	NSURL *folderUrl = [SandboxBroker urlWithoutFragment:fileUrl];
	if(![folderUrl isFileURL]) return NULL;

	__block SandboxEntry *_entry = nil;

	NSString *sandboxPath = [folderUrl path];

	dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
		for(SandboxEntry *entry in self->storage) {
			if(entry.path) {
				if((entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) ||
				   (!entry.isFolder && [entry.path isEqualToString:sandboxPath])) {
					entry.refCount += 1;
					_entry = entry;
					break;
				}
			}
		}

		if(!_entry) {
			_entry = [self recursivePathTest:folderUrl];
			if(_entry)
				[self->storage addObject:_entry];
		}

		if(_entry && _entry.secureUrl) {
			[_entry.secureUrl startAccessingSecurityScopedResource];
		}
	});

	if(_entry)
		return CFBridgingRetain(_entry);
	else
		return NULL;
}

- (void)endFolderAccess:(const void *)handle {
	if(!handle) return;
	SandboxEntry *entry = CFBridgingRelease(handle);
	if(!entry) return;

	dispatch_async_reentrant(dispatch_get_main_queue(), ^{
		if(entry.refCount > 1) {
			entry.refCount -= 1;
		} else {
			if(entry.secureUrl) {
				[entry.secureUrl stopAccessingSecurityScopedResource];
				entry.secureUrl = nil;
			}
			entry.refCount = 0;

			[self->storage removeObject:entry];
		}
	});
}

- (BOOL)areAllPathsSafe:(NSArray *)urls {
	for(NSURL *url in urls) {
		if(![url isFileURL]) continue;
		if(![self recursivePathTest:url]) {
			return NO;
		}
	}
	return YES;
}

@end
