//
//  PWOutlineView.m
//  PWTreeControllerDemo
//
//  Created by Frank Illenberger on 04.03.06.
//  Copyright 2006 ProjectWizards, Melle, Germany. 
//

#import "PWOutlineView.h"
#import "PWTreeController.h"
#import "PWTreeControllerNode.h"
#import "NSOutlineView-PWExtensions.h"
//#import <OmniAppKit/NSOutlineView-OAExtensions.h>

#define PWOutlineAttributesContext (void *)@"PWOutlineAttributesContext"

typedef struct {
	float red1, green1, blue1, alpha1;
	float red2, green2, blue2, alpha2;
} _twoColorsType;
extern void _linearColorBlendFunction(void *info, const float *in, float *out);
extern void _linearColorReleaseInfoFunction(void *info);

static const CGFunctionCallbacks linearFunctionCallbacks = {0, &_linearColorBlendFunction, &_linearColorReleaseInfoFunction};

@implementation PWOutlineView

- (void) showActionable: (id) sender
{
	[_delegate showActionable: sender];
}
- (void) showToDo: (id) sender
{
	[_delegate showToDo: sender];
}
- (void) showDone: (id) sender
{
	[_delegate showDone: sender];
}

- (void) group: (id) sender
{
	[_delegate group: sender];
}

- (void) ungroup: (id) sender
{
	[_delegate ungroup: sender];
}

- (void) delete: (id) sender
{
	[_delegate delete: sender];
}

- (void) addChild: (id) sender
{
	[_delegate addChild: sender];
}

- (void) addSibling: (id) sender
{
	[_delegate addSibling: sender];
}


- (void) textDidEndEditing: (NSNotification *) notification
{
    NSDictionary *userInfo;
    userInfo = [notification userInfo];
	
    NSNumber *textMovement;
    textMovement = [userInfo objectForKey: @"NSTextMovement"];
	
    int movementCode;
    movementCode = [textMovement intValue];
	
    // see if this a 'pressed-return' instance
	
    if (movementCode == NSReturnTextMovement) {
        // hijack the notification and pass a different textMovement
        // value
		
        textMovement = [NSNumber numberWithInt: NSIllegalTextMovement];
		
        NSDictionary *newUserInfo;
        newUserInfo = [NSDictionary dictionaryWithObject: textMovement
												  forKey: @"NSTextMovement"];
		
        notification = [NSNotification notificationWithName:
			[notification name]
													 object: [notification object]
												   userInfo: newUserInfo];
    }
	
    [super textDidEndEditing: notification];
	
} // textDidEndEditing

- (void)setIsExpandedKeyPath:(NSString *)keyPath
{
	[keyPath retain];
	[isExpandedKeyPath release];
	isExpandedKeyPath = keyPath;
}

- (NSString *)isExpandedKeyPath
{
	return isExpandedKeyPath;
}

- (void)setIsExpandedFilteredKeyPath:(NSString *)keyPath
{
	[keyPath retain];
	[isExpandedFilteredKeyPath release];
	isExpandedFilteredKeyPath = keyPath;
}

- (NSString *)isExpandedFilteredKeyPath
{
    return isExpandedFilteredKeyPath;
}

- (void)expandItem:(id)item expandChildren:(BOOL)expandChildren
{
	[super expandItem:item expandChildren:expandChildren];
	if(!dontRegisterCollapsing && isExpandedKeyPath)
	{
		id source = [self dataSource];
		id originalItem = [item representedObject];
		NSString *keyPath = [source respondsToSelector:@selector(filterPredicate)] && [source filterPredicate] ? isExpandedFilteredKeyPath : isExpandedKeyPath;
		if(keyPath && ![[originalItem valueForKey:keyPath] boolValue])
		{
			[originalItem setValue:[NSNumber numberWithBool:YES] forKey:keyPath];
			[self restoreCollapsedItems];
		}
		if([[self delegate] respondsToSelector:@selector(itemDidExpand:)])
			[[self delegate] performSelector:@selector(itemDidExpand:) withObject:originalItem];
	}
}

- (void)collapseItem:(id)item collapseChildren:(BOOL)collapseChildren
{
	[super collapseItem:item collapseChildren:collapseChildren];
	if(!dontRegisterCollapsing && isExpandedKeyPath)
	{
		id source = [self dataSource];
		id originalItem = [item representedObject];
		NSString *keyPath = [source respondsToSelector:@selector(filterPredicate)] && [source filterPredicate] ? isExpandedFilteredKeyPath : isExpandedKeyPath;
		if(keyPath)
			[originalItem setValue:[NSNumber numberWithBool:NO] forKey:keyPath];
		if([[self delegate] respondsToSelector:@selector(itemDidCollapse:)])
			[[self delegate] performSelector:@selector(itemDidCollapse:) withObject:originalItem];
	}
}

- (void)restoreCollapsedStateOfItem:(id)item
{
	@try 
	{
		id originalItem = [item representedObject];
		BOOL collapsed;
		if([[self dataSource] filterPredicate])
			collapsed = isExpandedFilteredKeyPath ? ![[originalItem valueForKey:isExpandedFilteredKeyPath] boolValue] : NO;
		else
			collapsed = ![[originalItem valueForKey:isExpandedKeyPath] boolValue];
		if(collapsed && [self isItemExpanded:item])
			[self collapseItem:item collapseChildren:NO];
		else if(!collapsed && ![self isItemExpanded:item] )
			[self expandItem:item expandChildren:NO];
	}
	@catch(NSException *e)
	{
	}
}

- (void)restoreCollapsedItems
{
	dontRegisterCollapsing = YES;
	int rows = [self numberOfRows];
	int row=0;
	while(row<rows)
	{
		id item = [self itemAtRow:row++];
		if(item && [self isExpandable:item])
		{
			[self restoreCollapsedStateOfItem:item];
			rows = [self numberOfRows];
		}
	}
	dontRegisterCollapsing = NO;
}

- (id)originalItemAtRow:(int)row
{
	return [[self itemAtRow:row] representedObject];
}

- (int)rowForOriginalItem:(id)originalItem
{
	id source = [self dataSource];
	if(![source isKindOfClass:[PWTreeController class]])
		return [super rowForOriginalItem:originalItem];
	id item = [source nodeForObservedObject:originalItem];
	if(item)
		return [self rowForItem:item];
	else
		return -1;
}

- (id)itemForOriginalItem:(id)originalItem
{
	id source = [self dataSource];
	if(![source isKindOfClass:[PWTreeController class]])
		return [super itemForOriginalItem:originalItem];
	return [[self dataSource] nodeForObservedObject:originalItem];
}


- (void)dealloc
{
	[isExpandedKeyPath release];
	[isExpandedFilteredKeyPath release];
	[savedSelectedOriginalItems release];
	[editItem release];
	[[NSNotificationCenter defaultCenter] removeObserver:self];
	[super dealloc];
}

- (void)restoreSelection
{
	isRestoringSelection = YES;
	[(PWTreeController *)[self dataSource] setSelectedObjects:savedSelectedOriginalItems];
	isRestoringSelection = NO;
}

- (void)saveState
{
	PWTreeController *dataSource = [self dataSource];
	if(!savedSelectedOriginalItems && [dataSource isKindOfClass:[PWTreeController class]] && [self editedRow]>=0)
	{
		[editItem release];
		editItem =  [[self originalItemAtRow:[self editedRow]] retain];
		editColumn = [self editedColumn];
		savedSelectedOriginalItems = [[NSArray alloc] initWithArray:[dataSource selectedObjects]];
	}
}

- (void)restoreState
{
	if(isExpandedKeyPath)
		[self restoreCollapsedItems];
	
	if(editItem)
	{
		// Restore row being edited
		int row = [self rowForOriginalItem:editItem];
		if(editColumn!=-1 && row!=-1 && [self editedRow]==-1)
		{
			[self selectRow:row byExtendingSelection:NO];
			[self editColumn:editColumn row:row withEvent:nil select:YES];
		}
		[editItem release];
		editItem = nil;
	}
	if([savedSelectedOriginalItems count])
	{
		[self restoreSelection];
	}	
	[savedSelectedOriginalItems release];
	savedSelectedOriginalItems = nil;
}

- (void)reloadData
{
	isReloading = YES;
	[self saveState];
	[super reloadData];
	[self restoreState];
	isReloading = NO;
}

- (void)reloadItem:(id)item reloadChildren:(BOOL)reloadChildren
{
	if(!isReloading)
	{
		[self saveState];
		[super reloadItem:item reloadChildren:reloadChildren];
		[self restoreState];
	}
}

- (BOOL)isRestoringSelection
{
	return isRestoringSelection;
}

- (void)setDontRegisterCollapsing:(BOOL)dont
{
	dontRegisterCollapsing = dont;
}

- (void)uncoverItem:(PWTreeControllerNode *)item
{
	NSMutableArray *path = [NSMutableArray array];
	while(item)
	{
		[path insertObject:item atIndex:0];
		item = [item parentNode];
	}
	
	int count = [path count];
	for(int index=0; index<count; index++)
	{
		item = [path objectAtIndex:index];
		[self expandItem:item];
	}
}

- (void)uncoverOriginalItems:(NSArray *)items
{
	NSEnumerator *e = [items objectEnumerator];
    id originalItem = nil;
	while(originalItem = [e nextObject]) 
		[self uncoverItem:[self itemForOriginalItem:originalItem]];
}

- (NSArray *)attributeKeyPaths
{
	NSMutableSet *paths = [NSMutableSet set];
	
	NSArray *columns = [self tableColumns];
	int count = [columns count];
	for(int index=0; index<count; index++)
	{
		NSTableColumn *col = [columns objectAtIndex:index];
		NSString *keyPath = [col valueForKey:@"keyPath"];
		if(keyPath)
			[paths addObject:keyPath];
		NSSet *additionalKeyPaths = [col additionalObservingKeyPaths];
		if(additionalKeyPaths)
			[paths unionSet:additionalKeyPaths];
	}
	return [paths allObjects];
}

- (void)createAttributeObservers
{
	if(!dontCreateAttributeObservers)
	{
		NSArray *paths = [self attributeKeyPaths];
		int count = [paths count];
		PWTreeController *controller = [self dataSource];
		for(int index=0; index<count; index++)
		{
			NSString *path = [@"arrangedObjects." stringByAppendingString:[paths objectAtIndex:index]];
			[controller addObserver:self forKeyPath:path options:0 context:PWOutlineAttributesContext];
		}
	}
}

- (void)removeAttributeObservers
{
	if(!dontCreateAttributeObservers)
	{
		NSArray *paths = [self attributeKeyPaths];
		int count = [paths count];
		PWTreeController *controller = [self dataSource];
		for(int index=0; index<count; index++)
		{
			NSString *path = [@"arrangedObjects." stringByAppendingString:[paths objectAtIndex:index]];
			[controller removeObserver:self forKeyPath:path];
		}
	}
}

- (void)addTableColumn:(NSTableColumn *)aColumn
{
	[self removeAttributeObservers];
	[super addTableColumn:aColumn];
	[self createAttributeObservers];
}

- (void)removeTableColumn:(NSTableColumn *)aColumn
{
	if(!dontCreateAttributeObservers)
		[self removeAttributeObservers];
	[super removeTableColumn:aColumn];
	if(!dontCreateAttributeObservers)
		[self createAttributeObservers];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	if(context == PWOutlineAttributesContext)
	{
		id item = [self itemForOriginalItem:object];
		if(item)
			[self reloadItem:item];
	}
}

- (void)dispose
{
	[self removeAttributeObservers];
}

- (void)setDontCreateAttributeObservers:(BOOL)dont
{
	dontCreateAttributeObservers = dont;
}

- (BOOL)dontCreateAttributeObservers
{
	return dontCreateAttributeObservers;
}
// NSView

- (void)viewWillMoveToWindow:(NSWindow *)newWindow;
{
	[[NSNotificationCenter defaultCenter] removeObserver:self
													name:NSWindowDidResignKeyNotification object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(_windowDidChangeKeyNotification:)
												 name:NSWindowDidResignKeyNotification object:newWindow];
	[[NSNotificationCenter defaultCenter] removeObserver:self
													name:NSWindowDidBecomeKeyNotification object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(_windowDidChangeKeyNotification:)
												 name:NSWindowDidBecomeKeyNotification object:newWindow];
}


// NSTableView

- (void)highlightSelectionInClipRect:(NSRect)rect;
{
	// Take the color apart
	NSColor *alternateSelectedControlColor = [NSColor alternateSelectedControlColor];
	float hue, saturation, brightness, alpha;
	[[alternateSelectedControlColor colorUsingColorSpaceName:NSDeviceRGBColorSpace] getHue:&hue
											saturation:&saturation brightness:&brightness alpha:&alpha];
	
	// Create synthetic darker and lighter versions
	NSColor *lighterColor = [NSColor colorWithDeviceHue:hue
											 saturation:MAX(0.0, saturation-.12) 
											 brightness:MIN(1.0,brightness+0.30) 
												  alpha:alpha];
	NSColor *darkerColor = [NSColor colorWithDeviceHue:hue
											saturation:MIN(1.0, (saturation > .04) ? saturation+0.12 : 0.0) 
											brightness:MAX(0.0, brightness-0.045) 
												 alpha:alpha];
	
	/* If this view isn't key, use the gray version of the dark color.
	Note that this varies from the standard gray version that NSCell
		returns as its highlightColorWithFrame: when the cell is not in a
		key view, in that this is a lot darker. Mike and I think this is
		justified for this kind of view -- if you're using the dark
			selection color to show the selected status, it makes sense to
			leave it dark. */
	NSResponder *firstResponder = [[self window] firstResponder];
	if (![firstResponder isKindOfClass:[NSView class]] ||
		![(NSView *)firstResponder isDescendantOf:self] || 
		![[self window] isKeyWindow]) 
	{
		alternateSelectedControlColor = [[alternateSelectedControlColor colorUsingColorSpaceName:NSDeviceWhiteColorSpace] colorUsingColorSpaceName:NSDeviceRGBColorSpace];
		lighterColor = [[lighterColor colorUsingColorSpaceName:NSDeviceWhiteColorSpace] colorUsingColorSpaceName:NSDeviceRGBColorSpace];
		darkerColor = [[darkerColor colorUsingColorSpaceName:NSDeviceWhiteColorSpace] colorUsingColorSpaceName:NSDeviceRGBColorSpace];
	}
	
	// Set up the helper function for drawing washes
	CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
	_twoColorsType *twoColors = malloc(sizeof(_twoColorsType)); /* We
	malloc() the helper data because we may draw this wash during
		printing, in which case it won't necessarily be evaluated
			immediately. We need for all the data the shading function needs
				to draw to potentially outlive us.*/
			[lighterColor getRed:&twoColors->red1 green:&twoColors->green1
								blue:&twoColors->blue1 alpha:&twoColors->alpha1];
			[darkerColor getRed:&twoColors->red2 green:&twoColors->green2
						   blue:&twoColors->blue2 alpha:&twoColors->alpha2];
			static const float domainAndRange[8] = {0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0};
			CGFunctionRef linearBlendFunctionRef = CGFunctionCreate(twoColors, 1,
																	domainAndRange, 4, domainAndRange, &linearFunctionCallbacks);
			
			NSIndexSet *selectedRowIndexes = [self selectedRowIndexes];
			unsigned int rowIndex = [selectedRowIndexes indexGreaterThanOrEqualToIndex:0];
			
			while (rowIndex != NSNotFound) {
				unsigned int endOfCurrentRunRowIndex, newRowIndex = rowIndex;
				do {
					endOfCurrentRunRowIndex = newRowIndex;
					newRowIndex = [selectedRowIndexes indexGreaterThanIndex:endOfCurrentRunRowIndex];
				} while (newRowIndex == endOfCurrentRunRowIndex + 1);
				
				NSRect rowRect = NSUnionRect([self rectOfRow:rowIndex],
											 [self rectOfRow:endOfCurrentRunRowIndex]);
				
				NSRect topBar, washRect;
				NSDivideRect(rowRect, &topBar, &washRect, 1.0, NSMinYEdge);
				
				// Draw the top line of pixels of the selected row in the alternateSelectedControlColor
					[alternateSelectedControlColor set];
				NSRectFill(topBar);
				
				// Draw a soft wash underneath it
				CGContextRef context = [[NSGraphicsContext currentContext]
					graphicsPort];
				CGContextSaveGState(context); {
					CGContextClipToRect(context, (CGRect){{NSMinX(washRect),
						NSMinY(washRect)}, {NSWidth(washRect),
							NSHeight(washRect)}});
					CGShadingRef cgShading = CGShadingCreateAxial(colorSpace,
																  CGPointMake(0, NSMinY(washRect)), CGPointMake(0,
																												NSMaxY(washRect)), linearBlendFunctionRef, NO, NO);
					CGContextDrawShading(context, cgShading);
					CGShadingRelease(cgShading);
				} CGContextRestoreGState(context);
				
				rowIndex = newRowIndex;
			}
				
				
			CGFunctionRelease(linearBlendFunctionRef);
			CGColorSpaceRelease(colorSpace);
}

- (void)selectRow:(int)row byExtendingSelection:(BOOL)extend;
{
	[super selectRow:row byExtendingSelection:extend];
	[self setNeedsDisplay:YES]; // we display extra because we draw multiple contiguous selected rows differently, so changing one row's selection can change how others draw.
}

- (void)deselectRow:(int)row;
{
	[super deselectRow:row];
	[self setNeedsDisplay:YES]; // we display extra because we draw multiple contiguous selected rows differently, so changing one row's selection can change how others draw.
}

// NSTableView (Private)

- (id)_highlightColorForCell:(NSCell *)cell;
{
	return nil;
}
- (void)_windowDidChangeKeyNotification:(NSNotification
										 *)notification;
{
	[self setNeedsDisplay:YES];
}



// Actions

- (IBAction)expandSelection:(id)sender;
{
    NSArray *selectedItems;
    NSEnumerator *itemEnumerator;
    id item;
	
    selectedItems = [self selectedItems];
    itemEnumerator = [selectedItems objectEnumerator];
    while ((item = [itemEnumerator nextObject]))
        [self expandItemAndChildren:item];
    
    [self setSelectedItems:selectedItems];
}

- (IBAction)contractSelection:(id)sender;
{
    NSArray *selectedItems;
    NSEnumerator *itemEnumerator;
    id item;
    
    selectedItems = [self selectedItems];
    itemEnumerator = [selectedItems reverseObjectEnumerator];
    while ((item = [itemEnumerator nextObject]))
        [self collapseItemAndChildren:item];
    
    [self setSelectedItems:selectedItems];
}

- (void)keyDown:(NSEvent *)theEvent;
{
    NSString *characters;
    unichar firstCharacter;
    unsigned int modifierFlags;
	
    characters = [theEvent characters];
    modifierFlags = [theEvent modifierFlags];
    firstCharacter = [characters characterAtIndex:0];
	    
    switch (firstCharacter) {
		case 13:
			if (modifierFlags & NSAlternateKeyMask) 
				[self addChild:nil];
			else
				[self addSibling:nil];
			return;
		case NSDeleteFunctionKey:
		case 127:
			[self delete: nil];
			return;
			
		case 'a':
			[self showActionable: nil];
			return;
		case 't':
			[self showToDo: nil];
			return;
		case 'd':
			[self showDone: nil];
			return;
		
        case 'e': { // not reached if type-ahead selection is turned on
            int columnIndex, rowIndex;
			
            columnIndex = [[self tableColumns] indexOfObject:[self outlineTableColumn]];
            rowIndex = [self selectedRow];
            [self editColumn:columnIndex row:rowIndex withEvent:nil select:YES];
            return;
        } 
        case 'g': // not reached if type-ahead selection is turned on
            [self group:nil];
            return;
        case 'u': // not reached if type-ahead selection is turned on
            [self ungroup:nil];
            return;
        case '<': {
            NSArray *selectedItems;
			
            selectedItems = [self selectedItems];
            [self contractAll:nil];
            [self setSelectedItems:selectedItems];
            return;
        }
        case '>': {
            NSArray *selectedItems;
			
            selectedItems = [self selectedItems];
            [self expandAll:nil];
            [self setSelectedItems:selectedItems];
            return;
        }
        case NSLeftArrowFunctionKey:
            if (modifierFlags & NSAlternateKeyMask) {
                [self contractSelection:nil];
                return;
            }
            break;
        case NSRightArrowFunctionKey:
            if (modifierFlags & NSAlternateKeyMask) {
                [self expandSelection:nil];
                return;
            }
            break;
        default:
            break;
    }
    [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
}
@end


@implementation NSTableColumn (PWAdditions)
- (NSString *)keyPath
{
	return [self identifier];
}

- (NSString *)bindingKeyPath
{
	return [self keyPath];
}

- (NSSet *)additionalObservingKeyPaths
{
	return nil;
}
@end
