#import "FSOutlineViewDelegate.h"
#import "io/FSDataSource.h"
#import "io/FSItem.h"
#import "ui/OFBPathInfoCell.h"
#import <OakFoundation/NSString Additions.h>
#import <OakAppKit/OakAppKit.h>
#import <ns/ns.h>
#import <io/path.h>
#import <text/utf8.h>
#import <oak/oak.h>

@interface NSCell (FSItemCell)
- (void)setImage:(NSImage*)anImage;
- (void)setLabelIndex:(NSInteger)anInteger;
@end

static NSArray* ConvertURLSetToArray (NSSet* aSet)
{
	NSMutableArray* res = [NSMutableArray array];
	for(NSURL* url in aSet)
		[res addObject:[url absoluteString]];
	[res sortUsingSelector:@selector(compare:)];
	return res;
}

static NSMutableSet* ConvertURLArrayToSet (NSArray* anArray)
{
	NSMutableSet* res = [NSMutableSet set];
	for(NSString* urlString in anArray)
		[res addObject:[NSURL URLWithString:urlString]];
	return res;
}

// ================================
// = OutlineView Helper Functions =
// ================================

static NSSet* VisibleURLs (NSOutlineView* outlineView, FSItem* root, NSMutableSet* res = [NSMutableSet set])
{
	for(FSItem* item in root.children)
	{
		[res addObject:item.url];
		if(!item.leaf && [outlineView isItemExpanded:item])
			VisibleURLs(outlineView, item, res);
	}
	return res;
}

static NSSet* ExpandedURLs (NSOutlineView* outlineView, FSItem* root, NSMutableSet* res = [NSMutableSet set])
{
	for(FSItem* item in root.children)
	{
		if(!item.leaf && [outlineView isItemExpanded:item])
		{
			[res addObject:item.url];
			ExpandedURLs(outlineView, item, res);
		}
	}
	return res;
}

static NSSet* SelectedURLs (NSOutlineView* outlineView, FSItem* root)
{
	NSMutableSet* selectedURLs = [NSMutableSet set];
	NSIndexSet* indexSet = [outlineView selectedRowIndexes];
	for(NSUInteger index = [indexSet firstIndex]; index != NSNotFound; index = [indexSet indexGreaterThanIndex:index])
		[selectedURLs addObject:[[outlineView itemAtRow:index] url]];
	[selectedURLs intersectSet:VisibleURLs(outlineView, root)];

	return selectedURLs;
}

static void Snapshot (NSOutlineView* outlineView, FSItem* item, NSMutableSet* expandedURLs, NSMutableSet* selectedURLs)
{
	[expandedURLs unionSet:ExpandedURLs(outlineView, item)];
	[selectedURLs minusSet:VisibleURLs(outlineView, item)];
	[selectedURLs unionSet:SelectedURLs(outlineView, item)];
}

static NSSet* VisibleItems (NSOutlineView* outlineView, FSItem* root, NSMutableSet* res = [NSMutableSet set])
{
	for(FSItem* item in root.children)
	{
		[res addObject:item];
		if(!item.leaf && [outlineView isItemExpanded:item])
			VisibleItems(outlineView, item, res);
	}
	return res;
}

// ================================

@interface FSOutlineViewDelegate ()
- (void)applicationWillTerminate:(NSNotification*)aNotification;
@property (nonatomic, retain) NSSet* pendingSelectURLs;
@property (nonatomic, retain) NSURL* pendingEditURL;
@property (nonatomic, retain) NSURL* pendingMakeVisibleURL;
@property (nonatomic, assign) CGFloat pendingScrollOffset;
@end

@implementation FSOutlineViewDelegate
@synthesize outlineView, dataSource, openURLs, modifiedURLs, pendingSelectURLs, pendingEditURL, pendingMakeVisibleURL, pendingScrollOffset;

- (id)init
{
	if((self = [super init]))
	{
		recursiveExpandPaths = [NSMutableSet new];

		expandedURLs = [ConvertURLArrayToSet([[NSUserDefaults standardUserDefaults] arrayForKey:@"ExpandedURLs"]) retain];
		selectedURLs = [ConvertURLArrayToSet([[NSUserDefaults standardUserDefaults] arrayForKey:@"SelectedURLs"]) retain];

		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp];
	}
	return self;
}

- (void)dealloc
{
	[self applicationWillTerminate:nil];

	self.pendingSelectURLs = nil;
	self.pendingEditURL = nil;
	self.pendingMakeVisibleURL = nil;
	self.dataSource = nil;
	self.outlineView = nil;

	[[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillTerminateNotification object:NSApp];
	[recursiveExpandPaths release];
	[expandedURLs release];
	[selectedURLs release];
	[super dealloc];
}

- (void)applicationWillTerminate:(NSNotification*)aNotification
{
	Snapshot(outlineView, dataSource.rootItem, expandedURLs, selectedURLs);
	[[NSUserDefaults standardUserDefaults] setObject:ConvertURLSetToArray(expandedURLs) forKey:@"ExpandedURLs"];
	[[NSUserDefaults standardUserDefaults] setObject:ConvertURLSetToArray(selectedURLs) forKey:@"SelectedURLs"];
}

- (void)setOutlineView:(NSOutlineView*)anOutlineView
{
	if(outlineView != anOutlineView)
	{
		[outlineView setDelegate:nil];
		[outlineView release];
		outlineView = [anOutlineView retain];
		[outlineView setDelegate:self];
	}
}

- (void)expandAndSelectChildren:(FSItem*)anItem expandAll:(BOOL)flag
{
	for(FSItem* child in anItem.children)
	{
		if(!child.leaf && (flag || [expandedURLs containsObject:child.url]))
		{
			if(flag)
				[recursiveExpandPaths addObject:child.url];

			if(![outlineView isItemExpanded:child])
				[outlineView expandItem:child];
		}

		if([selectedURLs containsObject:child.url])
			[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:child]] byExtendingSelection:YES];

		if(!child.leaf && [outlineView isItemExpanded:child])
			[self expandAndSelectChildren:child expandAll:flag];
	}
}

- (void)setDataSource:(FSDataSource*)aDataSource
{
	self.pendingSelectURLs = nil;
	self.pendingEditURL = nil;
	self.pendingMakeVisibleURL = nil;
	self.pendingScrollOffset = 0;

	if(dataSource)
	{
		Snapshot(outlineView, dataSource.rootItem, expandedURLs, selectedURLs);

		[outlineView deselectAll:self];
		[outlineView setDataSource:nil];
		[[NSNotificationCenter defaultCenter] removeObserver:self name:FSItemDidReloadNotification object:dataSource];
		[dataSource release];
	}

	itemsReloading = 0;

	if(dataSource = [aDataSource retain])
	{
		[outlineView setDataSource:dataSource];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(itemDidReload:) name:FSItemDidReloadNotification object:dataSource];
		[self expandAndSelectChildren:dataSource.rootItem expandAll:NO];
	}
}

- (void)checkPendingSelectAndEditURLs
{
	if(pendingScrollOffset != 0 && pendingScrollOffset <= NSHeight([outlineView frame]) - NSHeight([outlineView visibleRect]))
	{
		[outlineView scrollPoint:NSMakePoint(0, pendingScrollOffset)];
		self.pendingScrollOffset = 0;
	}

	NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
	for(NSInteger i = 0; i < [outlineView numberOfRows] && pendingSelectURLs; ++i)
	{
		if([pendingSelectURLs containsObject:[[outlineView itemAtRow:i] url]])
			[indexSet addIndex:i];
	}

	if([indexSet count])
	{
		[outlineView selectRowIndexes:indexSet byExtendingSelection:NO];
		self.pendingSelectURLs = nil;
	}

	if(itemsReloading)
		return;

	for(NSInteger i = 0; i < [outlineView numberOfRows] && pendingEditURL; ++i)
	{
		if(![pendingEditURL isEqual:[[outlineView itemAtRow:i] url]])
			continue;

		[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO];
		[outlineView editColumn:0 row:i withEvent:nil select:YES];
		self.pendingEditURL = nil;
	}

	for(NSInteger i = 0; i < [outlineView numberOfRows] && pendingMakeVisibleURL; ++i)
	{
		if(![pendingMakeVisibleURL isEqual:[[outlineView itemAtRow:i] url]])
			continue;

		NSRect rowRect     = [outlineView rectOfRow:i];
		NSRect visibleRect = [outlineView visibleRect];
		if(NSMinY(rowRect) < NSMinY(visibleRect) || NSMaxY(rowRect) > NSMaxY(visibleRect))
			[outlineView scrollPoint:NSMakePoint(NSMinX(rowRect), round(NSMidY(rowRect) - NSHeight(visibleRect)/2))];

		self.pendingMakeVisibleURL = nil;
	}
}

- (void)selectURLs:(NSArray*)someURLs
{
	self.pendingSelectURLs = [NSSet setWithArray:someURLs];
	if([someURLs count] == 1)
		self.pendingMakeVisibleURL = [someURLs lastObject];
	[self checkPendingSelectAndEditURLs];
}

- (void)editURL:(NSURL*)anURL
{
	self.pendingEditURL = anURL;
	[self checkPendingSelectAndEditURLs];
}

- (void)scrollToOffset:(CGFloat)anOffset
{
	self.pendingScrollOffset = anOffset;
	[self checkPendingSelectAndEditURLs];
}

- (void)itemDidReload:(NSNotification*)aNotification
{
	FSDataSource* aDataSource = [aNotification object];
	if(aDataSource != dataSource)
		return;

	FSItem* item      = [[aNotification userInfo] objectForKey:@"item"];
	NSArray* children = [[aNotification userInfo] objectForKey:@"children"];
	BOOL requested    = [[[aNotification userInfo] objectForKey:@"requested"] boolValue];
	BOOL recursive    = [[[aNotification userInfo] objectForKey:@"recursive"] boolValue];

	if(requested)
		--itemsReloading;

	if([outlineView editedRow] != -1)
		return; // FIXME Queue reload while editing a cell

	if(recursive)
	{
		Snapshot(outlineView, item, expandedURLs, selectedURLs);
		for(FSItem* child in VisibleItems(outlineView, item))
			[outlineView deselectRow:[outlineView rowForItem:child]];
		item.children = children;
	}

	suppressAutoExpansion = YES;
	[outlineView reloadItem:(item == dataSource.rootItem ? nil : item) reloadChildren:recursive];
	suppressAutoExpansion = NO;

	if(!recursive)
		return [self checkPendingSelectAndEditURLs];

	BOOL recursiveExpand = [recursiveExpandPaths containsObject:item.url];
	[recursiveExpandPaths removeObject:item.url];

	[self expandAndSelectChildren:item expandAll:recursiveExpand];
	[self checkPendingSelectAndEditURLs];
}

// =================================
// = Outline view delegate methods =
// =================================

- (void)outlineView:(NSOutlineView*)anOutlineView willDisplayCell:(NSCell*)cell forTableColumn:(NSTableColumn*)tableColumn item:(FSItem*)item
{
	if([cell respondsToSelector:@selector(setImage:)])
	{
		NSImage* icon = item.icon;
		if([modifiedURLs containsObject:item.url])
		{
			NSImage* tmp = [[[NSImage alloc] initWithSize:[icon size]] autorelease];
			[tmp lockFocus];
			[icon drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:0.4];
			[tmp unlockFocus];
			icon = tmp;
		}
		[cell setImage:icon];
	}
	cell.stringValue       = item.name;
	// cell.textColor         = lstat([[item.url path] fileSystemRepresentation], &(struct stat){ 0 }) == 0 ? [NSColor textColor] : [NSColor redColor];
	// cell.target            = delegate;
	// cell.representedObject = item;
	if([cell respondsToSelector:@selector(setLabelIndex:)])
		[cell setLabelIndex:item.labelIndex];
	if([cell respondsToSelector:@selector(setIsOpen:)])
		((OFBPathInfoCell*)cell).isOpen = [openURLs containsObject:item.url];
	// cell.isLoading         = item.isLoading;
}

- (BOOL)outlineView:(NSOutlineView*)anOutlineView shouldSelectItem:(id)item
{
	NSInteger col = [anOutlineView clickedColumn];
	NSInteger row = [anOutlineView clickedRow];
	if(col != -1 && row != -1)
	{
		OFBPathInfoCell* cell = (OFBPathInfoCell*)[anOutlineView preparedCellAtColumn:col row:row];
		NSInteger hit = [cell hitTestForEvent:[NSApp currentEvent] inRect:[anOutlineView frameOfCellAtColumn:col row:row] ofView:anOutlineView];
		if(hit & OakImageAndTextCellHitImage)
			return NO;
		else if(hit & NSCellHitTrackableArea)
			return NO;
	}
	return YES;
}

- (BOOL)outlineView:(NSOutlineView*)anOutlineView shouldTrackCell:(NSCell*)cell forTableColumn:(NSTableColumn*)tableColumn item:(id)item
{
	return YES;
}

- (BOOL)outlineView:(NSOutlineView*)anOutlineView isGroupItem:(FSItem*)item
{
	return [item respondsToSelector:@selector(group)] ? item.group : NO;
}

- (NSString*)outlineView:(NSOutlineView*)outlineView toolTipForCell:(NSCell*)cell rect:(NSRectPointer)rect tableColumn:(NSTableColumn*)tc item:(FSItem*)item mouseLocation:(NSPoint)mouseLocation
{
	return [item respondsToSelector:@selector(toolTip)] ? item.toolTip : nil;
}

// ===========================
// = Expand Delegate Methods =
// ===========================

static BOOL MyEvent (NSEvent* anEvent, NSView* aView)
{
	if([anEvent window] == [aView window])
	{
		static std::set<std::string> const ArrowLeftRight = { "~" + utf8::to_s(NSLeftArrowFunctionKey), "~" + utf8::to_s(NSRightArrowFunctionKey) };
		if([anEvent type] == NSLeftMouseUp)
			return NSMouseInRect([aView convertPoint:[anEvent locationInWindow] fromView:nil], [aView frame], [aView isFlipped]);
		else if([anEvent type] == NSKeyDown && [[aView window] firstResponder] == aView)
			return ArrowLeftRight.find(to_s(anEvent)) != ArrowLeftRight.end();
	}
	return NO;
}

- (BOOL)outlineView:(NSOutlineView*)anOutlineView shouldExpandItem:(FSItem*)item
{
	if(suppressAutoExpansion && ![expandedURLs containsObject:item.url])
		return NO;

	if(MyEvent([NSApp currentEvent], anOutlineView) && OakIsAlternateKeyOrMouseEvent())
		[recursiveExpandPaths addObject:item.url];

	if([dataSource reloadItem:item])
		++itemsReloading;
	return YES;
}

- (void)outlineViewItemDidExpand:(NSNotification*)aNotification
{
	if(suppressAutoExpansion)
		return; // we issued a reload so don’t tinker with selection/expansion

	FSItem* item = [[aNotification userInfo] objectForKey:@"NSObject"];
	if([item isKindOfClass:[FSItem class]])
	   [self expandAndSelectChildren:item expandAll:NO];
}

// =============================
// = Collapse Delegate Methods =
// =============================

- (BOOL)outlineView:(NSOutlineView*)anOutlineView shouldCollapseItem:(FSItem*)item
{
	return suppressCollapsing == 0;
}

- (void)outlineViewItemWillCollapse:(NSNotification*)aNotification
{
	if(++suppressCollapsing != 1)
		return;

	FSItem* item = [[aNotification userInfo] objectForKey:@"NSObject"];
	Snapshot(outlineView, item, expandedURLs, selectedURLs);

	if(MyEvent([NSApp currentEvent], outlineView) && OakIsAlternateKeyOrMouseEvent())
		[expandedURLs minusSet:ExpandedURLs(outlineView, item)];
	[expandedURLs removeObject:item.url];
}

- (void)outlineViewItemDidCollapse:(NSNotification*)aNotification
{
	if(--suppressCollapsing == 0)
	{
		FSItem* item = [[aNotification userInfo] objectForKey:@"NSObject"];
		if([dataSource unloadItem:item])
			[outlineView reloadItem:item reloadChildren:YES];
	}
}
@end
