/*******************************************************************************
 Copyright (c) 2004-2006 Jordy Rose
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
 Except as contained in this notice, the name(s) of the above copyright holders
 shall not be used in advertising or otherwise to promote the sale, use or other
 dealings in this Software without prior authorization.
*******************************************************************************/

#import "DockyardMenuExtra.h"

#define MENU_INDEX_OF_FIRST_DOCK 6

/*!
 * Central class of the Dockyard menu extra. This class handles the menu and commands,
 * as well as functioning as a Controller in the MVC scheme (although Dockyard usually
 * implements two levels of Controllers), between the Dockyard library (Model) and the
 * menus presented to the user (View). Actions are handled by their respective controllers,
 * but most presentation configuration is done here.
 *
 * @version 1.6
 * @author Belkadan Software
 */
@implementation DockyardMenuExtra

/*!
 * Creates basically the entire menu extra. First, the backend (<code>DockyardPreferences</code>)
 * is set up, and checked to see whether the black-and-white icon should be used. Next, the menu
 * extra's view is set up and the Dockyard icon is set. After that, the menu commands are added,
 * followed by a separator. Finally, the docks themselves are added, all of which trigger a
 * switch when chosen.
 */
- (id)initWithBundle:(NSBundle *)bundle
{	
	NSMenuItem *temp;
	NSString *key;
	int i;
	
	self = [super initWithBundle:bundle];
	if (self == nil) return nil;
			
	// Set up back-end
	preferences = [[DockyardPreferences alloc] initWithIdentifier:@"com.belkadan.Dockyard.MenuExtra"];
	[preferences setDelegate:self];
	
	NSNumber *numberValue;
	[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldUseBWIconChanged:) name:DockyardDefaultsChangedNotification object:@"MenuExtra:shouldUseBWIcon"];
	numberValue = [preferences defaultsValueForKey:@"MenuExtra:shouldUseBWIcon"];
	if (numberValue == nil) {
		shouldUseBWIcon = NO;
		[preferences setDefaultsValue:[NSNumber numberWithBool:NO] forKey:@"MenuExtra:shouldUseBWIcon"];
	} else shouldUseBWIcon = [numberValue boolValue];
	
	// we will create and set the MenuExtraView
	NSMenuExtraView *theView = [[NSMenuExtraView alloc] initWithFrame:[[self view] frame] menuExtra:self];
	[self setView:theView];
	if (shouldUseBWIcon) {
		blackWhiteIcon = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"iconbw"]];
		[theView setImage:blackWhiteIcon];
		
		blackWhiteAltIcon = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"iconbwclicked"]];
		[theView setAlternateImage:blackWhiteAltIcon];
		
	} else {
		colorIcon = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"icon"]];
		[theView setImage:colorIcon];
	}
		
	// prepare menu
	NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Dockyard"];
	[menu setAutoenablesItems: NO];
	
	temp = (NSMenuItem *) [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"About Dockyard...\t",		nil, bundle, @"Trailing tab to avoid dock name collisions.")
		action:@selector(showAbout:)	keyEquivalent:@""];
	[temp setTarget:self];
	temp = (NSMenuItem *) [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Create New Dock...\t",	nil, bundle, @"Trailing tab to avoid dock name collisions.")
		action:@selector(showNew:)		keyEquivalent:@""];
	[temp setTarget:self];
	temp = (NSMenuItem *) [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Delete Dock...\t",		nil, bundle, @"Trailing tab to avoid dock name collisions.")
		action:@selector(showDelete:)	keyEquivalent:@""];
	[temp setTarget:self];
	temp = (NSMenuItem *) [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Rename Dock...\t",		nil, bundle, @"Trailing tab to avoid dock name collisions.")
		action:@selector(showRename:)	keyEquivalent:@""];
	[temp setTarget:self];
	temp = (NSMenuItem *) [menu addItemWithTitle:NSLocalizedStringFromTableInBundle(@"Reorder Docks...\t",		nil, bundle, @"Trailing tab to avoid dock name collisions.")
		action:@selector(showReorder:)	keyEquivalent:@""];
	[temp setTarget:self];
	
	[menu addItem:[NSMenuItem separatorItem]];
	
	NSArray *names = [preferences dockNames];
	NSString *currentDock = [preferences currentDock];
	// get names of all the docks
	for (i = 0; i < [names count]; i++) {
		key = [names objectAtIndex:i];
		temp = (NSMenuItem *) [menu addItemWithTitle:key action:@selector(changeDockCommand:) keyEquivalent:@""];
		[temp setTarget:self];
		if ([key isEqualToString:currentDock]) [temp setState:NSOnState];

		// Added in Dockyard 1.5
		[temp setRepresentedObject:key];
		[temp setSubmenu:[self menuForDockNamed:key]];
	}
	[self setMenu:menu];
	[menu release];

	return self;
}

/**
 * Releases the menu, view, lists of docks, and strings
 * that were allocated. Then calls [super dealloc].
 */
- (void)dealloc
{
	[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
	
	[colorIcon release];
	[blackWhiteIcon release];
	[blackWhiteAltIcon release];
	
	[aboutController release];
	[newController release];
	[deleteController release];
	[renameController release];
	[reorderController release];

	[preferences release];
	[super dealloc];
}


#pragma mark -

/*!
 * Returns the instance of <code>DockyardPreferences</code> shared throughout the menu extra.
 * Mainly used for KVC purposes and access by the other controllers.
 */
- (DockyardPreferences *)preferences
{
	return preferences;
}

/*!
 * Response to a DockyardDefaultsChangedNotification when the key is <code>@"MenuExtra:shouldUseBWIcon"</code>.
 * This method should never be directly invoked.
 */
- (void)shouldUseBWIconChanged:(NSNotification *)note
{
	NSMenuExtraView *view = (NSMenuExtraView *)[self view];
	
	shouldUseBWIcon = [[[note userInfo] objectForKey:@"value"] boolValue];
	
	if (shouldUseBWIcon) {
		if (blackWhiteIcon == nil) {
			NSBundle *bundle = [self bundle];
			blackWhiteIcon = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"iconbw"]];
			blackWhiteAltIcon = [[NSImage alloc] initWithContentsOfFile:[bundle pathForImageResource:@"iconbwclicked"]];
		}
		[view setImage:blackWhiteIcon];
		[view setAlternateImage:blackWhiteAltIcon];
	} else {
		if (colorIcon == nil) {
			colorIcon = [[NSImage alloc] initWithContentsOfFile:[[self bundle] pathForImageResource:@"icon"]];
		}
		[view setImage:colorIcon];
		[view setAlternateImage:nil];
	}
}

#pragma mark -

/*!
 * Displays Dockyard's About panel, loading the required nib file if necessary.
 * The about panel also contains check boxes that toggle Dashboard switching
 * and the black-and-white icon on and off.
 */
- (void)showAbout:(NSMenuItem *)menuItem
{
	if (aboutController == nil) {
		aboutController = [[ComBelkadanDockyard_AboutController alloc] initWithPreferences:preferences windowNibName:@"AboutBox"];
	}
	
	[aboutController showWindow:menuItem];
}

/*!
 * Displays the panel used to create new docks, loading the required nib file if necessary.
 */
- (void)showNew:(NSMenuItem *)menuItem
{
	if (newController == nil) {
		newController = [[ComBelkadanDockyard_NewDockController alloc] initWithPreferences:preferences windowNibName:@"NewBox"]; 
	}
			
	[newController showWindow:menuItem];
}

/*!
 * Displays the panel used to delete docks, loading the required nib file if necessary.
 * If there is only one dock, it beeps instead.
 */
- (void)showDelete:(NSMenuItem *)menuItem
{
	if ([preferences numberOfDocks] < 2) {
		NSBeep();
		return;
	}
	
	if (deleteController == nil) {
		deleteController = [[ComBelkadanDockyard_DeleteDockController alloc] initWithPreferences:preferences windowNibName:@"DeleteBox"]; 
	}

	[deleteController showWindow:menuItem];
}

/*!
 * Displays the panel used to reorder docks, loading the required nib file if necessary.
 * If there is only one dock, it beeps instead.
 */
- (void)showReorder:(NSMenuItem *)menuItem
{
	if ([preferences numberOfDocks] < 2) {
		NSBeep();
		return;
	}
	
	if (reorderController == nil) {
		reorderController = [[ComBelkadanDockyard_RearrangeController alloc] initWithPreferences:preferences windowNibName:@"RearrangeBox"];
	}

	[reorderController showWindow:menuItem];
}

/*!
 * Displays the panel used to rename a dock, loading the required nib file if necessary.
 */
- (void)showRename:(NSMenuItem *)menuItem
{
	if (renameController == nil) {
		renameController = [[ComBelkadanDockyard_RenameDockController alloc] initWithPreferences:preferences windowNibName:@"RenameBox"];
	}
			
	[renameController showWindow:menuItem];
}

/*!
 * Switches to the dock with the same name as the represented object
 * of menuItem (stored on creation). This function just calls changeDock:
 * on <code>DockyardPreferences</code>
 *
 * @see DockyardPreferences#changeDock:
 */
- (void)changeDockCommand:(NSMenuItem *)menuItem
{
	NSString *dockName = [menuItem representedObject];
	if (dockName == nil)
		dockName = [menuItem title];
	if ([preferences changeDock:dockName])
		NSBeep();
}

/*!
 * Opens the file with the path specified by the menu item.
 * The path is stored in the menu item's <code>representedObject</code>
 */
- (void)openFile:(NSMenuItem *)sender
{
	static NSWorkspace *workspace;
	if (workspace == nil) workspace = [NSWorkspace sharedWorkspace];
	
	NSString *path = [sender representedObject];
	if ([path hasPrefix:@"/"]) {
		[workspace openFile:path]; // It's a file
	} else {
		[workspace openURL:[NSURL URLWithString:path]]; // It's a URL
	}
}

#pragma mark -

- (void)dockyardDockChanged:(NSNotification *)note
{
	NSDictionary *info = [note userInfo];
	NSString *newDock = [[info objectForKey:@"newDock"] retain];
	NSString *oldDock = [[info objectForKey:@"oldDock"] retain];
	NSMenuItem *oldDockItem, *newDockItem;
	NSMenu *menu = [self menu];
	
	oldDockItem = (NSMenuItem *) [menu itemWithTitle:oldDock];
	if (oldDockItem != nil) [oldDockItem setState:NSOffState];
		
	newDockItem = (NSMenuItem *) [menu itemWithTitle:newDock];
	[newDockItem setState:NSOnState];

	[oldDockItem setSubmenu:[self menuForDockNamed:oldDock]]; // Update in case the old dock changed while in use
	
	[newDock release];
	[oldDock release];
}

- (void)dockyardDockCreated:(NSNotification *)note
{
	NSString *name = [[note userInfo] objectForKey:@"name"];

	NSMenuItem *newItem = (NSMenuItem *) [[self menu] addItemWithTitle:name action:@selector(changeDockCommand:) keyEquivalent:@""];
	[newItem setTarget:self];
	// Added in Dockyard 1.5
	[newItem setRepresentedObject:name];
	[newItem setSubmenu:[self menuForDockNamed:name]];
}

- (void)dockyardDockRenamed:(NSNotification *)note
{
	NSDictionary *info = [note userInfo];
	NSString *oldName = [info objectForKey:@"oldName"];
	NSString *newName = [info objectForKey:@"newName"];
	
	NSMenuItem *dockItem = [[self menu] itemWithTitle:oldName];
	
	[dockItem setTitle:newName];
	[dockItem setRepresentedObject:newName];
	
	dockItem = (NSMenuItem *) [[dockItem submenu] itemAtIndex:0];
	[dockItem setTitle:[NSString stringWithFormat:
		NSLocalizedStringFromTableInBundle(@"Switch To %@", nil, [self bundle], @"Menu item title representing a dock switch action."),
		newName]];
	[dockItem setRepresentedObject:newName];
}

- (void)dockyardDocksReordered:(NSNotification *)note
{
	NSDictionary *info = [note userInfo];
	NSArray *names = [info objectForKey:@"dockNames"];
	NSMenu *menu = [self menu];
	
	int i, count;
	NSMenuItem *item;
	
	count = [names count];
	
	for (i = 0; i < count; i++) {
		item = [[menu itemWithTitle:[names objectAtIndex:i]] retain];
		
		[menu removeItem:item];
		
		if (i == count - 1) [menu addItem:item];
		else [menu insertItem:item atIndex:i + MENU_INDEX_OF_FIRST_DOCK];
		
		[item release];
	}
}

- (void)dockyardDockDeleted:(NSNotification *)note
{
	NSDictionary *info = [note userInfo];
	NSString *name = [info objectForKey:@"name"];
	NSMenu *menu = [self menu];
	
	[menu removeItem:[menu itemWithTitle:name]];
}

- (void)dockyardDocksDeleted:(NSNotification *)note
{
	NSDictionary *info = [note userInfo];
	NSIndexSet *indexSet = [NSKeyedUnarchiver unarchiveObjectWithData:[info objectForKey:@"indexSet"]];
	NSIndexSetEnumerator *indexEnum = [[NSIndexSetEnumerator alloc] initWithIndexSet:indexSet];
    unsigned index, count = 0;
    while ([indexEnum nextIndex:&index])
    {
		[[self menu] removeItemAtIndex:(index + MENU_INDEX_OF_FIRST_DOCK - count)];
		count++;
    }
	[indexEnum release];
}

- (void)dockyardDockItemsChanged:(NSNotification *)note
{
	NSString *whichDock = [[note userInfo] objectForKey:@"dockName"];
	[[[self menu] itemWithTitle:whichDock] setSubmenu:[self menuForDockNamed:whichDock]];
}

#pragma mark -
/*!
 * Creates a menu based on the name of a dock.
 * This is accomplished by searching the data for that particular dock
 * for information about the items contained within it.
 * The resulting menu has the following form, including images for both
 * applications and documents:<pre>
 * DOCK NAME | Switch to DOCK NAME
 *           | -------------------
 *           | [_] First App
 *           | [_] Second App
 *           | -------------------
 *           | [-] First Document	</pre>
 */
- (NSMenu *)menuForDockNamed:(NSString *)dockName
{
	NSMenu *dockMenu		= [[NSMenu alloc] initWithTitle:dockName];
	
	NSMenuItem *menuItem;
	menuItem = (NSMenuItem *) [dockMenu addItemWithTitle:
		[NSLocalizedStringFromTableInBundle(@"Switch To ", nil, [self bundle], @"Menu item title representing a dock switch action. Notice the space...change to whatever is appropriate.")
			stringByAppendingString:dockName]
		action:@selector(changeDockCommand:) keyEquivalent:@""];
	[menuItem setRepresentedObject:dockName];
	[menuItem setTarget:self];
	
	NSDictionary *itemData, *rawData;
	NSEnumerator *itemEnum;
	enum DockyardItemType itemType;
	NSArray *itemsArray;
	
	itemsArray = [preferences itemsInSet:DockyardDockAppSet ofDock:dockName];
	if (itemsArray != nil && [itemsArray count] > 0) {
		[dockMenu addItem:[NSMenuItem separatorItem]];
			
		itemEnum = [itemsArray objectEnumerator];
		
		while ((rawData = [itemEnum nextObject]) != nil) {
			itemData = [rawData objectForKey:@"tile-data"];
			itemType = [self typeForDockItemData:itemData rawType:[rawData objectForKey:@"tile-type"]];
			menuItem = [self menuItemWithDockItemData:itemData type:itemType];
			[dockMenu addItem:menuItem];
		}
	}
	
	itemsArray = [preferences itemsInSet:DockyardDockItemSet ofDock:dockName];
	if (itemsArray != nil && [itemsArray count] > 0) {	
		[dockMenu addItem:[NSMenuItem separatorItem]];
		
		itemEnum = [itemsArray objectEnumerator];
		
		while ((rawData = [itemEnum nextObject]) != nil) {
			itemData = [rawData objectForKey:@"tile-data"];
			itemType = [self typeForDockItemData:itemData rawType:[rawData objectForKey:@"tile-type"]];
			menuItem = [self menuItemWithDockItemData:itemData type:itemType];
			[dockMenu addItem:menuItem];
		}
	}
	
	return [dockMenu autorelease];
}

/*!
 * Returns the item type for the given data, where <code>rawType</code> is in the
 * form of a @"tile-type" string from the defaults domain com.apple.dock.
 */
- (enum DockyardItemType)typeForDockItemData:(NSDictionary *)itemData rawType:(NSString *)rawType
{
	if (rawType == nil) rawType = [itemData objectForKey:@"tile-type"]; // Weird quirk in Apple's default Dock...
	if (rawType != nil) {
		if ([rawType isEqualToString:@"file-tile"])
			return DockyardFileType;
		else if ([rawType isEqualToString:@"url-tile"])
			return DockyardURLType;
		else if ([rawType isEqualToString:@"dashboard-tile"])
			return DockyardDashboardType;
		else if ([rawType isEqualToString:@"dockling-tile"])
			return DockyardDocklingType;
	}
	// Assume no "tile-data" info was found, or that it denotes an unknown type.
	if ([itemData objectForKey:@"DockExtraType"] != nil) return DockyardDocklingType;
	else if ([itemData objectForKey:@"file-data"] != nil) return DockyardFileType;
	else if ([itemData objectForKey:@"url"] != nil) return DockyardURLType;
	else return DockyardUnknownType;
}
 
/*!
 * This method takes the data of an item in a dock and creates a menu from it.
 * The two attributes focused on are the label and icon of the item, as well as
 * the path of the item (so it can be opened later) but these are taken from
 * labelForDockItemData:, iconForDockItemPath:, and pathForDockItemData:.
 */
- (NSMenuItem *)menuItemWithDockItemData:(NSDictionary *)itemData type:(enum DockyardItemType)itemType
{
	NSString *tempStr		= [self labelForDockItemData:itemData type:itemType]; // Name of item
	NSMenuItem *menuItem	= [[NSMenuItem alloc] initWithTitle:tempStr action:@selector(openFile:) keyEquivalent:@""];
		
	[menuItem setTarget:self];
	
	tempStr = [self pathForDockItemData:itemData type:itemType]; // Path of item
	
	NSImage *icon = [self iconForDockItemPath:tempStr type:itemType];
	[menuItem setImage:icon];
	[menuItem setRepresentedObject:tempStr]; // Store item path inside menu item
	
	return [menuItem autorelease];
}

/*!
 * Finds a label for the specified data by searching the item data for the value of "file-label"
 * or "label". If neither is found, the name of the program or file is returned.
 */
- (NSString *)labelForDockItemData:(NSDictionary *)itemData type:(enum DockyardItemType)itemType
{		
	static NSString *dashboardLabel = nil;

	NSString *label;
	switch (itemType) {
	case DockyardFileType:
	case DockyardDocklingType:
		label = [itemData objectForKey:@"file-label"];
		break;
	case DockyardURLType:
		label = [itemData objectForKey:@"label"];
		break;
	case DockyardDashboardType:
		if (dashboardLabel == nil) {
			NSBundle *dockBundle = [[NSBundle alloc] initWithPath:@"/System/Library/CoreServices/Dock.app"];
			dashboardLabel = [[dockBundle objectForInfoDictionaryKey:@"DashboardName"] retain];
			[dockBundle release];
		}
		label = dashboardLabel;
		break;
	default:
		label = [itemData objectForKey:@"file-label"];
		if (label == nil)
			label = [itemData objectForKey:@"label"];
	}
	
	if (label == nil) { // if (label STILL equals nil)
		label = [[self pathForDockItemData:itemData type:itemType] lastPathComponent];
		if ([label hasSuffix:@".app"]) label = [label stringByDeletingPathExtension];
	}
	
	return label;
}

/*!
 * Finds the path of the given data by first looking for a file path, then URL data.
 */
- (NSString *)pathForDockItemData:(NSDictionary *)itemData type:(enum DockyardItemType)itemType
{
	static NSString *dashboardPath = @"/Applications/Dashboard.app";

	NSString *path;
	switch (itemType) {
	case DockyardURLType:
		path = [[itemData objectForKey:@"url"] objectForKey:@"_CFURLString"];
		break;
	case DockyardFileType:
	case DockyardDocklingType:
		path = [[itemData objectForKey:@"file-data"] objectForKey:@"_CFURLString"];
		break;
	case DockyardDashboardType:
		path = dashboardPath;
		break;
	default:
		path = [[itemData objectForKey:@"file-data"] objectForKey:@"_CFURLString"];
		if (path == nil)
			path = [[itemData objectForKey:@"url"] objectForKey:@"_CFURLString"];
	}

	if (path == nil) path = @"";
	else if ([path hasSuffix:@"/"]) path = [path substringToIndex:([path length] - 1)];
	
	return path;
}

/*!
 * Tries to locate an icon in four ways:
 * First: Looks to see if there is a path, and assigns a default unknown icon if there isn't.
 * Second: Checks the type of the path, and assigns a default icon for URLs.
 * Third: Assumes the path is a local file, and looks for its icon on the filesystem.
 * Fourth: Gives up and assigns a default unknown icon.
 */
- (NSImage *)iconForDockItemPath:(NSString *)path type:(enum DockyardItemType)itemType
{
	static NSWorkspace *workspace;
	static NSFileManager *fileManager;
	static const NSSize iconSize = {16, 16};

	if (workspace == nil)	workspace	= [NSWorkspace sharedWorkspace];
	if (fileManager == nil)	fileManager	= [NSFileManager defaultManager];
	
	NSString *iconPath;
	NSImage *icon;

	if (path == nil || [path isEqualToString:@""]) {
		// Not found...
		iconPath = [[self bundle] pathForImageResource:@"unknown"];
		icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
//		[icon setScalesWhenResized:YES];
		
	} else {
		switch (itemType) {
		case DockyardURLType:
			iconPath = [[self bundle] pathForImageResource:@"url"];
			icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
			break;
		case DockyardDashboardType:
			if ([fileManager fileExistsAtPath:path]) {
				// Use system icon
				icon = [[workspace iconForFile:path] retain];

			} else {
				// Use built-in icon
				iconPath = [[self bundle] pathForImageResource:@"dashboard"];
				icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
			}
			break;
		default: // Files + docklings + anything else
			if ([fileManager fileExistsAtPath:path]) {
				// Just a normal file
				icon = [[workspace iconForFile:path] retain];
				
			} else {
				// Not found...
				iconPath = [[self bundle] pathForImageResource:@"unknown"];
				icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
			}
		}
	}
		
	[icon setScalesWhenResized:YES];
	[icon setSize:iconSize];
	return [icon autorelease];
}
	
@end
