/*=============================================================================
 *  CAFileBrowser.mm
 *  PlayPen
 *-----------------------------------------------------------------------------
 *
 * $Log: CAFileBrowser.mm,v $
 * Revision 1.10  2004/12/19 20:27:18  luke
 * add 'presetName:ExistsAsChildOf:' method
 *
 * Revision 1.9  2004/12/19 18:11:45  luke
 * only expand 1st level user/local/network root items
 *
 * Revision 1.8  2004/11/16 17:36:17  luke
 * last part of fix for [3747338]
 *
 * Revision 1.7  2004/11/13 17:51:12  luke
 * fix warning
 *
 * Revision 1.6  2004/11/13 17:46:17  luke
 * removed NSAsserts & better handling errors
 *
 * Revision 1.5  2004/10/29 01:17:18  luke
 * fix warnings in Panther
 *
 * Revision 1.4  2004/09/20 17:10:23  luke
 * conditionalize some NSTableView API usage based on OSX version (due to AppKit API deprecation)
 *
 * Revision 1.3  2004/08/03 18:34:16  luke
 * tweaks
 *
 * Revision 1.2  2004/08/03 18:31:17  luke
 * tighten up setName code
 *
 * Revision 1.1  2004/07/29 00:21:15  luke
 * factored out from PlayPen
 *
 * Revision 1.17  2004/07/29 00:08:51  luke
 * tweaks
 *
 * Revision 1.16  2004/07/28 23:57:20  luke
 * pass 3: final
 *
 * Revision 1.15  2004/07/28 21:43:26  luke
 * 2nd pass: CAFileBrowser API breakup
 *
 * Revision 1.14  2004/07/28 20:50:15  luke
 * first pass: transition into approved API
 *
 * Revision 1.13  2004/07/22 19:44:35  luke
 * Panther fix...
 *
 * Revision 1.12  2004/07/21 23:54:42  luke
 * fix warnings on Tiger
 *
 * Revision 1.11  2004/07/07 00:24:16  luke
 * [3482090] make AUInspector localizable
 *
 * Revision 1.10  2004/07/02 01:02:31  mhopkins
 * Fixed hang renaming files
 *
 * Revision 1.9  2004/06/24 23:21:58  mhopkins
 * added methods to return name and data key strings
 *
 * Revision 1.8  2004/05/27 22:55:50  luke
 * fix last network-access dangler.  allow preset renaming to be blocked by subclasses
 *
 * Revision 1.7  2004/05/26 21:54:27  luke
 * remove NSLog()
 *
 * Revision 1.6  2004/05/26 21:09:54  luke
 * add hasPresets method
 *
 * Revision 1.5  2004/05/26 19:41:23  luke
 * fix font clipping
 *
 * Revision 1.4  2004/05/26 18:20:38  luke
 * [3665153] fix MIDIThru preset dictionary embedding
 *
 * Revision 1.3  2004/05/26 17:13:01  luke
 * [3656248] only search Network for presets when preferences say to do so
 *
 * Revision 1.2  2004/05/25 22:25:19  luke
 * allow preset renaming (per [3616417])
 *
 * Revision 1.1  2004/04/17 20:05:20  luke
 * source reorg.
 *
 * Revision 1.15  2003/12/18 19:48:29  luke
 * push directory-creation code into CAFileBrowser superclass
 *
 * Revision 1.14  2003/11/06 19:28:23  luke
 * pull 'savePresetWithName:asLocal: into superclass
 *
 * Revision 1.13  2003/10/28 19:02:37  luke
 * tweaks
 *
 * Revision 1.12  2003/10/27 23:57:29  luke
 * treat enter/return press as a double-click
 *
 * Revision 1.11  2003/10/22 22:17:32  luke
 * add path accessor
 *
 * Revision 1.10  2003/10/22 00:08:24  mhopkins
 * Added initWithCoder method so browser can be instantiated via a nib file
 *
 * Revision 1.9  2003/10/21 23:28:26  luke
 * added features that future subclasses may need
 *
 * Revision 1.8  2003/10/21 22:03:31  luke
 * inherit from NSOutlineView & change some behavior per discussion with Bill
 *
 * Revision 1.7  2003/10/21 20:27:30  luke
 * fix edge null-deref error
 *
 * Revision 1.6  2003/10/20 18:29:14  luke
 * improve preset mgr. behavior (still not correct on scrolling though)
 *
 * Revision 1.5  2003/10/16 22:38:39  luke
 * improved behavior
 *
 * Revision 1.4  2003/10/15 16:56:43  luke
 * tighten up code.
 *
 * Revision 1.3  2003/10/15 00:46:03  luke
 * tighten up code
 *
 * Revision 1.2  2003/10/15 00:26:28  luke
 * label preset groups
 *
 * Revision 1.1  2003/10/15 00:12:30  luke
 * new file
 *
 *
 *-----------------------------------------------------------------------------
 *  Created by Luke Bellandi on Tue Oct 14 2003.
 *  Copyright (c) 2003 Apple Computer Inc. All rights reserved.
 *=============================================================================*/

#import "CAFileHandling.h"

#import "CAFileBrowser.h"
#import "CAFileBrowser_Protected.h"

NSString *CARootUserDirectory		= @"CARootUserDirectory";
NSString *CARootLocalDirectory		= @"CARootLocalDirectory";
NSString *CARootNetworkDirectory	= @"CARootNetworkDirectory";

@interface CAFileBrowser(Internal)
- (float)fontHeight;
- (void)dispatchedObjectSetup;
- (CFTreeRef)privGetTreeFromItem:(id)item;
- (void)handleBrowserClick:(id)inSender;
- (void)handleBrowserClickOffloaded:(NSValue *)inWrappedMouseLocation;
- (void)handleBrowserDoubleClick:(id)inSender;
- (void)itemWasActivatedDispatcher:(id)sender;
@end

#pragma mark -

@implementation CAFileBrowser
#pragma mark ____ INIT / DEALLOC ____
- (id)init {
    [self release];
    return nil;
}

- (id)initWithFrame:(NSRect)frame {
	return [self initWithFrame:frame shouldScanNetworkForFiles:NO];
}

- (id)initWithFrame:(NSRect)frame shouldScanNetworkForFiles:(BOOL)shouldScanNetworkForFiles {
    self = [super initWithFrame:frame];
    
	mScanNetworkForFiles = shouldScanNetworkForFiles;
    [self dispatchedObjectSetup];
    
    return self;
}

- (id)initWithCoder:(NSCoder *)decoder {
	self = [super initWithCoder:decoder];
    
	// N.B.: Nothing currently encodes the 'kScanNetworkForFilesKey'
	mScanNetworkForFiles = [decoder decodeBoolForKey:@"kScanNetworkForFilesKey"];
	
    [self dispatchedObjectSetup];

	return self;
}

- (void)dealloc {
    [mOutlineViewDataRetentionPool release];
    [mBrowserTreeList release];
    
    [mCell release];
	[mCachedRenameValue release];
	
	[super dealloc];
}

#pragma mark ____ NEW PUBLIC FUNCTIONS ____
- (BOOL)scansNetworkForFiles {
	return mScanNetworkForFiles;
}

- (NSURL *)URLForItem:(id)item {
    if ([item isKindOfClass:[NSValue class]]) {
        CFTreeRef tree = (CFTreeRef)[(NSValue *)item pointerValue];
        if (mFileHandler->IsItem(tree)) {
            tree = CFTreeGetParent(tree);
        }
        CFURLRef url = NULL;
        mFileHandler->GetPathCopy(tree, url);
        return [(NSURL *)url autorelease];
    }
    
    return nil;
}

- (CFPropertyListRef)readPropertyListForItem:(id)item {
    if ([item isKindOfClass:[NSValue class]]) {
        CFTreeRef tree = (CFTreeRef)[(NSValue *)item pointerValue];
        CFPropertyListRef retVal = NULL;
         if (mFileHandler->ReadFromTreeLeaf(tree, retVal) == noErr) {
			return retVal;
		 }
	}
	
	return NULL;
}

- (BOOL)presetName:(NSString *)inPresetName existsAsChildOf:(id)item {
	if (![item isKindOfClass:[NSValue class]]) return NO;
	
	NSURL *baseURL = [self URLForItem:item];
	NSString *potentialPath = [NSString stringWithFormat:@"%@/%@.%@", [baseURL path], inPresetName, [self fileExtension]];
	
	return [[NSFileManager defaultManager] fileExistsAtPath:potentialPath];
}

- (BOOL)savePresetWithName:(NSString *)inPresetName asChildOf:(id)item {
    // subclasses should implement this method to save appropriate
	// data at the location specified by the client.
	return NO;
}

- (BOOL)createDirectory:(NSString *)inName asChildOfItem:(id)parent {
	if (!inName) return NO;
	
	CFTreeRef parentTree = [self privGetTreeFromItem:parent];
	
	CFURLRef url = NULL;
	mFileHandler->GetPathCopy(parentTree, url);
	NSURL *baseURL = [(NSURL *)url autorelease];
	
	NSString *path = [[baseURL path] stringByAppendingPathComponent:inName];
	
	BOOL retVal = [[NSFileManager defaultManager] createDirectoryAtPath:path attributes:nil];
	
	[self rescanFiles];
	
	return retVal;
}

- (void)rescanFiles {
    // subclasses should implement this to re-create the CAFileHandlingObject & reset it
    // (to be called when directory structure changes.)
}

#pragma mark ____ NSOutlineView.DataSource implementation ____
- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
    // TOP LEVEL
    if (item == nil) {
        return [mBrowserTreeList objectAtIndex:index];
    }
    
    // SUB-LEVELS
    void *ptr = [(NSValue *)item pointerValue];
    
    // CFTreeRef
    if (CFGetTypeID(ptr) == CFTreeGetTypeID()) {
        NSValue *newValue = [NSValue valueWithPointer:CFTreeGetChildAtIndex ((CFTreeRef)ptr, index)];
        [mOutlineViewDataRetentionPool addObject:newValue];
        return newValue;
    }
    
    return nil;
}

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    // TOP LEVEL
    if (item == nil)
        return [mBrowserTreeList count];
    
    // SUB-LEVELS
    if ([item isKindOfClass:[NSValue class]]) {
        // CFTreeRef
        CFTreeRef tree = (CFTreeRef)[(NSValue *)item pointerValue];
        return CFTreeGetChildCount(tree);
    }
    
    return 0;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    // TOP LEVEL
    if (item == nil) return NO;
    
    // SUB-LEVELS
    if ([item isKindOfClass:[NSValue class]]) {
        if ( mFileHandler->IsDirectory((CFTreeRef)[(NSValue *)item pointerValue]) )
            return YES;
    }
    
    return NO;
}

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    if (item == nil) return nil;
	
    CFTreeRef tree = (CFTreeRef)[(NSValue *)item pointerValue];
    NSString *pName;
    
    // special case naming for tree roots
    if (tree == mFileHandler->GetUserTree()) {
        pName = NSLocalizedStringFromTable(@"User", @"AUInspector", @"Preset file browser group heading for 'User'");
    } else if (tree == mFileHandler->GetLocalTree()) {
        pName = NSLocalizedStringFromTable(@"Local", @"AUInspector", @"Preset file browser group heading for 'Local'");
    } else if (tree == mFileHandler->GetNetworkTree()) {
        pName = NSLocalizedStringFromTable(@"Network", @"AUInspector", @"Preset file browser group heading for 'Network'");
    } else {
        CFStringRef string = nil;
        mFileHandler->GetNameCopy(tree, string);
        pName = [(NSString *)string autorelease];
    }
    
    if ([self outlineView:outlineView isItemExpandable:item]) {
        [mCell setFont:[NSFont boldSystemFontOfSize:[self fontHeight]]];
    } else {
        [mCell setFont:[NSFont systemFontOfSize:[self fontHeight]]];
    }
	
    if (pName == nil) {
		if (!mFileIsBeingRenamed) {
			// if we get here, the file was probably erased... we should rescan our model.
			[self rescanFiles];
			[outlineView reloadData];
        }
		
        pName = @"";
    }
    
    [mCell setTitle:pName];
    
    return mCell;
}

- (void)outlineView:(NSOutlineView *)inOutlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)inTableColumn byItem:(id)item
{
	[inTableColumn setDataCell:mCell];
	
	if (mCachedRenameValue == nil) return;
	
	NSString *oldName = [(NSTextFieldCell *)[self outlineView:self objectValueForTableColumn:inTableColumn byItem:item] stringValue];
	
	[self selectRow:[self selectedRow] byExtendingSelection:NO];
	id theObject = [self itemAtRow:[self selectedRow]];
	
	if ([oldName isEqual:mCachedRenameValue]) return;
	if (![theObject isKindOfClass:[NSValue class]]) return;
	
	CFTreeRef tree = (CFTreeRef)[(NSValue *)theObject pointerValue];
	
	CFURLRef myRef = nil;
	mFileHandler->GetPathCopy (tree, myRef);
	NSURL *url = [(NSURL *)myRef autorelease];

	NSString *sourcePath = [url path];
	NSString *fileExtension = [self fileExtension];
	NSString *destPath = [NSString stringWithFormat:@"%@/%@.%@", [sourcePath stringByDeletingLastPathComponent], mCachedRenameValue, fileExtension];
	
	// rename preset in file
	NSDictionary *fileData = [NSDictionary dictionaryWithContentsOfFile:sourcePath];
	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	if (fileData != nil)
	{
		NSMutableDictionary *mutableFileData = [NSMutableDictionary dictionaryWithDictionary:fileData];
		[mutableFileData setObject:mCachedRenameValue forKey: [self nameKeyString]];
		
		[mutableFileData writeToFile:sourcePath atomically:YES];
		
		// rename file
		[fileMgr movePath:sourcePath toPath:destPath handler:nil];
	}
	
	// give file system a second to catch up with changes
	// wait until file manager's done its work
	mFileIsBeingRenamed = YES;
		int timeOutCount = 50;
		int count = 0;
		while (count < timeOutCount) {
			  if ([fileMgr fileExistsAtPath:destPath]) break;

			  CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, false);
			  ++count;
		};
	mFileIsBeingRenamed = NO;
	
	[mCachedRenameValue release];
	mCachedRenameValue = nil;
	
	[self rescanFiles];
}

#pragma mark ____ NSOutlineView delegates ____
- (BOOL)outlineView:(NSOutlineView *)inOutlineView shouldEditTableColumn:(NSTableColumn *)inTableColumn item:(id)inItem
{
	return NO;
}

- (void)textDidEndEditing:(NSNotification *)notification {
    if([[[notification userInfo] valueForKey:@"NSTextMovement"] intValue] == NSReturnTextMovement)
	{
		mCachedRenameValue = [[mCell stringValue] retain];
		NSMutableDictionary *newUserInfo = [[NSMutableDictionary alloc] initWithDictionary:[notification userInfo]];
		[newUserInfo setObject:[NSNumber numberWithInt:NSIllegalTextMovement] forKey:@"NSTextMovement"];
		notification = [NSNotification	notificationWithName:[notification name]
										object:[notification object]
										userInfo:newUserInfo];
		[super textDidEndEditing:notification];
		[newUserInfo release];
		[[self window] makeFirstResponder:self];
	} else {
		[super textDidEndEditing:notification];
    }
}

#pragma mark ____ NSResponder/NSView overrides ____
- (void)keyDown:(NSEvent *)inEvent {
    BOOL eventWasHandled = NO;
    
    UInt32		i, length;
    NSString *	keyInput = [inEvent charactersIgnoringModifiers];
    length = [keyInput length];
    for (i = 0; i < length; i++) {
        if ( 	([keyInput characterAtIndex:i] == NSCarriageReturnCharacter) ||
                ([keyInput characterAtIndex:i] == NSEnterCharacter) )	{
            [self handleBrowserDoubleClick:self];
            eventWasHandled = YES;
        }
    }
    
    if (!eventWasHandled)
        [super keyDown:inEvent];
}

- (BOOL)acceptsFirstMouse:(NSEvent *)inEvent {
    return YES;
}
@end

#pragma mark -

@implementation CAFileBrowser(Protected)
- (BOOL)writePropertyList:(CFPropertyListRef)inPropertyList withName:(NSString *)inName asChildOfItem:(id)parent {
	
	CFTreeRef tree = [self privGetTreeFromItem:parent];
	
	// if this tree is a leaf, use its parent
	if (mFileHandler->IsItem(tree)) {
		tree = CFTreeGetParent(tree);
	}
	
    return (mFileHandler->SaveInDirectory (tree, CFStringRef(inName), inPropertyList) == noErr);
}

- (void)setCAFileHandlingObject:(CAFileHandling *)inCAFileHandlingObject {
    mFileHandler = inCAFileHandlingObject;
	
    [mOutlineViewDataRetentionPool removeAllObjects];
    [mBrowserTreeList removeAllObjects];
	
	CFTreeRef networkTree = NULL;
    // Network (only include if user pref says to do so)
	if (mScanNetworkForFiles) {
		networkTree = mFileHandler->GetNetworkTree();
		if (networkTree)
			[mBrowserTreeList addObject:[NSValue valueWithPointer:networkTree]];
	}
    // Local
    CFTreeRef localTree = mFileHandler->GetLocalTree();
    if (localTree)
        [mBrowserTreeList addObject:[NSValue valueWithPointer:localTree]];
    // User
    CFTreeRef userTree = mFileHandler->GetUserTree();
    if (userTree)
        [mBrowserTreeList addObject:[NSValue valueWithPointer:userTree]];
    
    [self reloadData];
    
    // open first-level items for Network, User, Local only
    int count = [self outlineView:self numberOfChildrenOfItem:nil];
    int i;
    id currentItem;
	CFTreeRef currentTree;
    
    for (i = 0; i < count; ++i) {
        currentItem = [self outlineView:self child:i ofItem:nil];
		if (![currentItem isKindOfClass:[NSValue class]]) continue;
		
		currentTree = CFTreeRef([(NSValue *)currentItem pointerValue]);
        if (	(currentTree == networkTree) ||
				(currentTree == localTree) ||
				(currentTree == userTree)	) {
			[self expandItem:currentItem expandChildren:NO];
		}
    }
}

- (CAFileHandling *)CAFileHandlingObject {
	return mFileHandler;
}

- (void)setTitle:(NSString *)inTitle {
    NSString *title = inTitle;
    if (inTitle == nil) title = @"";
    
    [(NSTableHeaderCell *)[[self outlineTableColumn] headerCell] setStringValue:title];
}

- (NSString *)title {
	return [(NSTableHeaderCell *)[[self outlineTableColumn] headerCell] stringValue];
}

- (BOOL)itemWasActivated:(id)object {
    // subclasses can override this to respond to outline view item selection
	// returns YES if action was successfully handled, NO on error condition
	return YES;
}

- (NSString *)fileExtension {
	// subclasses should override this to return the file extension (without the period)
	return nil;
}

- (NSString *)nameKeyString {
	// subclasses should override this to return the name key string
	return nil;	
}

- (BOOL)shouldAllowItemRenaming:(id)item {
	// default to allow all presets to be renamed.  Subclasses can override to disallow
	return YES;
}
@end

#pragma mark -

@implementation CAFileBrowser(Internal)
- (float)fontHeight {
	return [NSFont smallSystemFontSize] - 1.5;
}

- (void)dispatchedObjectSetup {
	mOutlineViewDataRetentionPool = [[NSMutableArray alloc] init];
    mBrowserTreeList = [[NSMutableArray alloc] init];
	
    [self setTarget:self];
	[self setAction:@selector(handleBrowserClick:)];
    [self setDoubleAction:@selector(handleBrowserDoubleClick:)];
    
    // setup cell prototype for outline view
    mCell = [[NSTextFieldCell alloc] init];
    [mCell setBordered:NO];
    [mCell setBezeled:NO];
	[mCell setEditable:YES];
	[mCell setFont:[NSFont systemFontOfSize:[self fontHeight]]];
    [mCell setAlignment:NSLeftTextAlignment];
	[self setCell:mCell];
	
    // setup tableColumn in outline view
    NSTableColumn *tc = [[[NSTableColumn alloc] init] autorelease];
    [tc setEditable:YES];
    [self setRowHeight:[NSFont smallSystemFontSize]];
    [self addTableColumn:tc];
    [self setOutlineTableColumn:tc];
	
    [self setAutoresizesOutlineColumn:YES];
    [self sizeLastColumnToFit];
    [self sizeToFit];
    
    // set data source after view is setup
    [self setDataSource:self];
    [self setDelegate:self];
}

- (CFTreeRef)privGetTreeFromItem:(id)item {
    CFTreeRef	parentTree = NULL;
    
	// Case 1: base directory
	if ([item isKindOfClass:[NSString class]]) {
		NSString *baseDir = (NSString *)item;
		
		// [1a] user
		if ([baseDir isEqualToString:CARootUserDirectory]) {
			parentTree = mFileHandler->GetUserTree();
			if (parentTree == NULL) {
				mFileHandler->CreateUserDirectories();
				int timeOutCount = 20;
				int count = 0;
				// allow some time for the directory to be created
				while ((parentTree == NULL) && (count < timeOutCount)) {
					CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, false);
					++count;
					parentTree = mFileHandler->GetUserTree();
				}
			}
		}
		// [1b] local
		if ([baseDir isEqualToString:CARootLocalDirectory]) {
			parentTree = mFileHandler->GetLocalTree();
			if (parentTree == NULL) {
				mFileHandler->CreateLocalDirectories();
				int timeOutCount = 20;
				int count = 0;
				// allow some time for the directory to be created
				while ((parentTree == NULL) && (count < timeOutCount)) {
					CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, false);
					++count;
					parentTree = mFileHandler->GetLocalTree();
				}
			}
		}
		// [1c] network
		if ([baseDir isEqualToString:CARootNetworkDirectory]) {
			parentTree = mFileHandler->GetNetworkTree();
			if (parentTree == NULL) {
				mFileHandler->CreateNetworkDirectories();
				int timeOutCount = 20;
				int count = 0;
				// allow some time for the directory to be created
				while ((parentTree == NULL) && (count < timeOutCount)) {
					CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, false);
					++count;
					parentTree = mFileHandler->GetNetworkTree();
				}
			}
		}
    }
	
	// Case 2: child directory
	if ([item isKindOfClass:[NSValue class]]) {
		parentTree = (CFTreeRef)[(NSValue *)item pointerValue];
	}
	
	return parentTree;
}

- (void)handleBrowserClick:(id)inSender {
	NSValue *wrappedMouseLocation = [NSValue valueWithPoint:[NSEvent mouseLocation]];
	[self performSelector:@selector(handleBrowserClickOffloaded:) withObject:wrappedMouseLocation afterDelay:0.5];
}

- (void)handleBrowserClickOffloaded:(NSValue *)inWrappedMouseLocation {
	// UI: mouse must not have ben moved since first click, and must not have been double-clicked
	if (	(!mBrowserWasDoubleClicked) &&
			(NSEqualPoints([inWrappedMouseLocation pointValue], [NSEvent mouseLocation]))   )
	{
		int selectedRow = [self selectedRow];
		// must have valid row selected
		if (selectedRow > 0)
		{
			id itemAtRow = [self itemAtRow:selectedRow];
			// must be a leaf node (preset file, not directory)
			if (![self outlineView:self isItemExpandable:itemAtRow]) {
				// one last browser-specific check
				if ([self shouldAllowItemRenaming:itemAtRow])
					[self editColumn:0 row:[self selectedRow] withEvent:nil select:YES];
			}
		}
	}
	
	mBrowserWasDoubleClicked = NO;
}

- (void)handleBrowserDoubleClick:(id)inSender {
	mBrowserWasDoubleClicked = YES;
	
    [self itemWasActivatedDispatcher:inSender];
}

- (void)itemWasActivatedDispatcher:(id)sender {
	id object = [self itemAtRow:[self selectedRow]];
    if (object == nil) return;
    
    // expand/collapse preset groups
    if ([self outlineView:self isItemExpandable:object]) {
        if ([self isItemExpanded:object])
            [self collapseItem:object];
        else
            [self expandItem:object];
    }
    
    [self itemWasActivated:object];
}

@end
