/*******************************************************************************
 Copyright (c) 2005-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.
*******************************************************************************/

//
//  DockItemInfo.m
//  Dockyard
//

#import "DockItemInfo.h"

static NSString * const kDashboardPath = @"/Applications/Dashboard.app";

/*!
 * Stores essential information about a dock item, specifically its
 * path, icon, and label.
 */
@implementation DockItemInfo
/*!
 * Makes a guess about an item's type based on its path or URL.
 * First, checks to see if the path is the location of Dashboard.app.
 * Next, checks the extension is @"wdgt" or @"dock" (widgets or docklings).
 * Then, checks the first character to see if it is a <code>/</code> or a
 * <code>~</code> (beginnings of paths).
 * Finally, tries to create an <code>NSURL</code> to check if the path
 * is a valid URL.
 * If nothing matches, returns <code>DockyardUnknownType</code>.
 *
 * @see #defaultIconForPath:type:
 * @see #defaultLabelForPath:type:
 */
+ (enum DockyardItemType)itemTypeForPath:(NSString *)givenPath
{
	if ([kDashboardPath isEqualToString:givenPath])
		return DockyardDashboardType;
		
	NSString *extension = [givenPath pathExtension];
	if ([extension isEqualToString:@"wdgt"])
		return DockyardWidgetType;
	
	else if ([extension isEqualToString:@"dock"])
		return DockyardDocklingType;
	
	unichar firstChar = [givenPath characterAtIndex:0];
	if (firstChar == '/' || firstChar == '~')
		return DockyardFileType;
	
	NSURL *url = [[NSURL alloc] initWithString:givenPath];
/*	if ([path hasPrefix:@"http:"]	|| [path hasPrefix:@"file:"]	||
		[path hasPrefix:@"ftp:"]	|| [path hasPrefix:@"feed:"]	||
		[path hasPrefix:@"about:"]	|| [path hasPrefix:@"mailto:"]	||
		[path hasPrefix:@"https:"]) */
	if (url != nil) { // path is valid
		[url release];
		return DockyardURLType;
	}
	
	return DockyardUnknownType;
}

static NSWorkspace *workspace = nil;
static NSFileManager *manager = nil;
/*!
 * Tries to locate an icon in five 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: Checks to see if the path is of a widget, and retrieves the widget's <code>Icon.png</code> file.
 * Fourth: Assumes the path is a local file, and looks for its icon on the filesystem.
 * Fifth: Gives up and assigns a default unknown icon.
 *
 * @see #itemTypeForPath:
 * @see #defaultLabelForPath:type:
 */
+ (NSImage *)defaultIconForPath:(NSString *)givenPath type:(enum DockyardItemType)itemType
{
	if (manager == nil)
		manager = [NSFileManager defaultManager];
	if (workspace == nil) 
		workspace = [NSWorkspace sharedWorkspace];
		
	NSImage *icon;

	if (givenPath == nil || [givenPath isEqualToString:@""]) {
		// Not found...
		icon = [NSImage imageNamed:@"unknown"];
	
	} else {
		switch (itemType) {
		case DockyardURLType:
			icon = [NSImage imageNamed:@"url"];
			break;
		case DockyardDashboardType:
			if ([manager fileExistsAtPath:kDashboardPath]) {
				// Use system icon
				icon = [workspace iconForFile:kDashboardPath];

			} else {
				// Use built-in icon
				icon = [NSImage imageNamed:@"dashboard"];
			}
			break;
		case DockyardWidgetType:
			icon = [[[NSImage alloc] initWithContentsOfFile:[givenPath stringByAppendingPathComponent:@"Icon.png"]] autorelease];
			if (icon != nil)
				break;
		default: // Files + docklings + anything else
			if ([manager fileExistsAtPath:givenPath]) {
				// Just a normal file
				icon = [workspace iconForFile:givenPath];
				
			} else {
				// Not found...
				icon = [NSImage imageNamed:@"unknown"];
			}
		}
	}
	
	[icon setScalesWhenResized:YES];
	if (itemType != DockyardWidgetType) {
		static NSSize iconSize = {128,128};
		[icon setSize:iconSize];
	}
	return icon;
}

/*!
 * Tries to create a label for <code>path</code> in four ways:
 * First, checks to see if the item is a URL. If so, just returns the URL itself. Short of fetching
 * the file, there's no way to check the page's real title. Dock data stores the label seperately,
 * so this is usually not a problem.
 * Second, checks to see if the item is the Dashboard icon. If so, retrieves a localized name from
 * the Dock's application bundle.
 * Third, checks to see if the item is a widget. If so, retrieves a localized name from its bundle.
 * Fourth, assumes the item is a local file, and retrieves its display name using LaunchServices
 *
 * @see #itemTypeForPath:
 * @see #defaultIconForPath:type:
 */
+ (NSString *)defaultLabelForPath:(NSString *)path type:(enum DockyardItemType)itemType
{
	static NSString *dashboardLabel = nil;

	NSString *label;
	switch (itemType) {
	case DockyardURLType:
		label = [[path copy] autorelease]; // Bad solution, but there's no real answer, short of fetching the file
		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;
	case DockyardWidgetType:
		{
			NSBundle *widgetBundle = [[NSBundle alloc] initWithPath:path];
			label = [widgetBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
			[widgetBundle release];
		}
		if (label != nil)
			break;
	default:
		if (manager == nil)
			manager = [NSFileManager defaultManager];
		label = [manager displayNameAtPath:path];
	}
	
	return label;
}

#pragma mark -

/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath
{
	enum DockyardItemType givenType = [[self class] itemTypeForPath:givenPath];
	
	if (self = [self initWithPath:givenPath
							label:[[self class] defaultLabelForPath:givenPath type:givenType]
							 icon:[[self class] defaultIconForPath:givenPath type:givenType]
							 type:givenType]) {
		flags.hasLabel	= NO;
		flags.hasIcon	= NO;
		flags.hasType	= NO;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path and label.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath label:(NSString *)givenLabel
{
	enum DockyardItemType givenType = [[self class] itemTypeForPath:givenPath];
	
	if (self = [self initWithPath:givenPath
							label:givenLabel
							 icon:[[self class] defaultIconForPath:givenPath type:givenType]
							 type:givenType]) {
		flags.hasLabel	= YES;
		flags.hasIcon	= NO;
		flags.hasType	= NO;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path and icon.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath icon:(NSImage *)givenIcon
{
	enum DockyardItemType givenType = [[self class] itemTypeForPath:givenPath];
	
	if (self = [self initWithPath:givenPath
							label:[[self class] defaultLabelForPath:givenPath type:givenType]
							 icon:givenIcon
							 type:givenType]) {
		flags.hasLabel	= NO;
		flags.hasIcon	= YES;
		flags.hasType	= NO;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path and item type.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath type:(enum DockyardItemType)givenType
{	
	if (self = [self initWithPath:givenPath
							label:[[self class] defaultLabelForPath:givenPath type:givenType]
							 icon:[[self class] defaultIconForPath:givenPath type:givenType]
							 type:givenType]) {
		flags.hasLabel	= NO;
		flags.hasIcon	= NO;
		flags.hasType	= YES;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path, label, and item type.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath label:(NSString *)givenLabel type:(enum DockyardItemType)givenType
{	
	if (self = [self initWithPath:givenPath
							label:givenLabel
							 icon:[[self class] defaultIconForPath:givenPath type:givenType]
							 type:givenType]) {
		flags.hasLabel	= YES;
		flags.hasIcon	= NO;
		flags.hasType	= YES;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path, icon, and item type.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath icon:(NSImage *)givenIcon type:(enum DockyardItemType)givenType
{	
	if (self = [self initWithPath:givenPath
							label:[[self class] defaultLabelForPath:givenPath type:givenType]
							 icon:givenIcon
							 type:givenType]) {
		flags.hasLabel	= NO;
		flags.hasIcon	= YES;
		flags.hasType	= YES;
	}
	
	return self;
}
/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path, label, and icon.
 *
 * @see #initWithPath:label:icon:type:
 */
- (id)initWithPath:(NSString *)givenPath label:(NSString *)givenLabel icon:(NSImage *)givenIcon
{	
	if (self = [self initWithPath:givenPath
							label:label
							 icon:icon
							 type:[[self class] itemTypeForPath:givenPath]]) {
		flags.hasLabel	= YES;
		flags.hasIcon	= YES;
		flags.hasType	= NO;
	}
	
	return self;
}

/*!
 * Initializes and returns an instance of <code>DockItemInfo</code> with the specified path, label, icon, and item type.
 * This is the designated initializer for DockItemInfo.
 */
- (id)initWithPath:(NSString *)givenPath label:(NSString *)givenLabel icon:(NSImage *)givenIcon type:(enum DockyardItemType)givenType
{
	if (self = [super init]) {
		flags.hasLabel	= YES;
		flags.hasIcon	= YES;
		flags.hasType	= YES;
		
		path	= [givenPath copy];
		label	= [givenLabel copy];
		icon	= [givenIcon copy];
		itemType = givenType;
		
		if (manager == nil)
			manager = [NSFileManager defaultManager];
		if (givenType == DockyardURLType || givenType == DockyardDashboardType)
			flags.exists = YES;
		else
			flags.exists = [manager fileExistsAtPath:path];
		pathURL	= nil;
//		flags.urlType = NoType;
	}
	return self;
}

- (void)dealloc
{
	[path release];
	[label release];
	[icon release];
	[pathURL release];
	[representedData release];
	[super dealloc];
}

- (id)copyWithZone:(NSZone *)zone
{
	DockItemInfo *copy = NSCopyObject(self, 0, zone);
	copy->path	= [path copy];
	copy->label	= [label copy];
	copy->icon	= [icon copy];
	copy->pathURL = [pathURL copy];
	copy->representedData = [representedData retain]; // NOT COPIED!
	return copy;
}

#pragma mark -

/*!
 * Sets the item's path. If the label, icon, or type was found using the class functions
 * <code>itemTypeForPath:</code>, <code>defaultIconForPath:type:</code>, or
 * <code>defaultLabelForPath:</code>, it will be regenerated.
 *
 * @see #path
 * @see #pathURL
 */
- (void)setPath:(NSString *)newPath
{
	if (newPath != path) {
		[path release];
		path = [newPath copy];
		
		if (!flags.hasType) {
			[self willChangeValueForKey:@"itemType"];
			itemType = [[self class] itemTypeForPath:path];
			[self didChangeValueForKey:@"itemType"];
		}
		
		if (!flags.hasLabel) {
			[self willChangeValueForKey:@"label"];
			[label release];
			label = [[[self class] defaultLabelForPath:path type:itemType] retain];
			[self didChangeValueForKey:@"label"];
		}
		
		if (!flags.hasIcon) {
			[self willChangeValueForKey:@"icon"];
			[icon release];
			icon = [[[self class] defaultIconForPath:path type:itemType] retain];
			[self didChangeValueForKey:@"icon"];
		}
		
		if (pathURL != nil) {
			[self willChangeValueForKey:@"pathURL"];
			[pathURL release];
			pathURL = nil; // initialization handled lazily
			[self didChangeValueForKey:@"pathURL"];
		}
		
		[self willChangeValueForKey:@"exists"];
		if (itemType == DockyardURLType || itemType == DockyardDashboardType)
			flags.exists = YES;
		else
			flags.exists = [manager fileExistsAtPath:path];
		[self didChangeValueForKey:@"exists"];
	}
}
/*!
 * Returns the item's path.
 *
 * @see #setPath:
 * @see #pathURL
 */
- (NSString *)path
{
	return path;
}

/*!
 * Sets the item's label. If <code>newLabel</code> is <code>nil</code>, restores
 * the cell's default label, generated with <code>defaultLabelForPath:type:</code>.
 *
 * @see #label
 * @see #hasLabel
 */
- (void)setLabel:(NSString *)newLabel
{
	if (newLabel == nil && flags.hasLabel) {
		[label release];
		flags.hasLabel = NO;
		label = [DockItemInfo defaultLabelForPath:path type:itemType];
	
	} else if (newLabel != label) {
		[label release];
		flags.hasLabel = YES;
		label = [newLabel copy];
	}
}
/*!
 * Returns the item's label.
 *
 * @see #setLabel:
 * @see #hasLabel
 */
- (NSString *)label
{
	return label;
}

/*!
 * Sets the item's label. If <code>newIcon</code> is <code>nil</code>, restores
 * the cell's default icon, generated with <code>defaultIconForPath:type:</code>.
 *
 * @see #icon
 * @see #hasIcon
 */
- (void)setIcon:(NSImage *)newIcon
{
	if (icon == nil && flags.hasIcon) {
		[icon release];
		flags.hasIcon = NO;
		icon = [DockItemInfo defaultIconForPath:path type:itemType];
	
	} else if (newIcon != icon) {
		[icon release];
		flags.hasIcon = YES;
		icon = [newIcon copy];
	}
}
/*!
 * Returns the item's icon.
 *
 * @see #setIcon:
 * @see #hasIcon
 */
- (NSImage *)icon
{
	return icon;
}

/*!
 * Sets the item's type. If the label or icon was generated with
 * <code>defaultLabelForPath:type:</code> or <code>defaultIconForPath:type:</code>,
 * they will be regenerated.
 *
 * @see #itemType
 * @see #hasType
 */
- (void)setItemType:(enum DockyardItemType)newType
{
	if (itemType != newType) {
		itemType = newType;
		
		if (!flags.hasLabel) {
			[self willChangeValueForKey:@"label"];
			[label release];
			label = [[[self class] defaultLabelForPath:path type:itemType] retain];
			[self didChangeValueForKey:@"label"];
		}
		
		if (!flags.hasIcon) {
			[self willChangeValueForKey:@"icon"];
			[icon release];
			icon = [[[self class] defaultIconForPath:path type:itemType] retain];
			[self didChangeValueForKey:@"icon"];
		}
	}
}
/*!
 * Returns the item's type.
 *
 * @see #setItemType:
 * @see #hasType
 */
- (enum DockyardItemType)itemType
{
	return itemType;
}

/*!
 * Returns <code>YES</code> if the item's label was not automatically generated, <code>NO</code> otherwise.
 *
 * @see #label
 * @see #hasIcon
 * @see #hasType
 */
- (BOOL)hasLabel
{
	return flags.hasLabel;
}
/*!
 * Returns <code>YES</code> if the item's icon was not automatically generated, <code>NO</code> otherwise.
 *
 * @see #icon
 * @see #hasLabel
 * @see #hasType
 */
- (BOOL)hasIcon
{
	return flags.hasIcon;
}
/*!
 * Returns <code>YES</code> if the item's type was not automatically generated, <code>NO</code> otherwise.
 *
 * @see #itemType
 * @see #hasLabel
 * @see #hasIcon
 */
- (BOOL)hasType
{
	return flags.hasType;
}
/*!
 * Returns <code>YES</code> if the item exists, <code>NO</code> otherwise.
 * Always returns <code>YES</code> for the Dashboard icon and URLs.
 */
- (BOOL)exists
{
	return flags.exists;
}

#pragma mark -
/*!
 * Returns the Finder kind string for this item, unless the item is a URL. In this case, 
 * returns a string of the form @"URL (<i>scheme</i>)", where <i>scheme</i> is the URL
 * scheme (such as HTTP or FTP). Uses LaunchServices to find filesystem kind strings.
 */
- (NSString *)kindString
{
	NSString *kindString;
	if (!flags.exists) {
		LSCopyKindStringForTypeInfo(kLSUnknownType, kLSUnknownCreator, NULL, (CFStringRef *)&kindString);
	
	} else if (itemType == DockyardURLType) {
/*		if ((enum URLType) flags.urlType == NoType) {
			NSString *urlScheme = [[self pathURL] scheme];
			if ([urlScheme isEqualToString:@"http"])
				flags.urlType = HTTPType;
			else if ([urlScheme isEqualToString:@"https"])
				flags.urlType = HTTPSType;
			else if ([urlScheme isEqualToString:@"ftp"])
				flags.urlType = FTPType;
			else if ([urlScheme isEqualToString:@"file"])
				flags.urlType = FileType;
			else if ([urlScheme isEqualToString:@"about"])
				flags.urlType = AboutType;
			else if ([urlScheme isEqualToString:@"mailto"])
				flags.urlType = MailtoType;
			else if ([urlScheme isEqualToString:@"feed"])
				flags.urlType = FeedType;
			else
				flags.urlType = UnknownType;
		}
		return urlTypes[flags.urlType]; */
		kindString = [[NSString alloc] initWithFormat:@"URL (%@)", [[self pathURL] scheme]];
	
	} else if (itemType == DockyardDashboardType) {
		LSCopyKindStringForTypeInfo(kLSUnknownType, kLSUnknownCreator, (CFStringRef)@"app", (CFStringRef *)&kindString);
		
	} else {
		LSCopyKindStringForURL((CFURLRef)[self pathURL], (CFStringRef *)&kindString);
	}
	
	return [kindString autorelease];
}

/*!
 * Returns the URL representation of this item's path. This method uses lazy initialization,
 * so if the URL is never asked for, it will never take up space. However, <code>kindString</code>
 * also requires a URL.
 *
 * @see #path
 * @see #setPath:
 */
- (NSURL *)pathURL
{
	if (pathURL == nil) {
		if (itemType == DockyardURLType)
			pathURL = [[NSURL alloc] initWithString:path];
		else if (itemType == DockyardDashboardType)
			pathURL = [[NSURL alloc] initWithPath:kDashboardPath];
		else
			pathURL = [[NSURL alloc] initFileURLWithPath:path];
	}
	return pathURL;
}

/*!
 * Sets the data represented by this item. This is usually the raw dock data, but you can really
 * store anything in here. This is not used by this class at all -- it is for your personal use.
 *
 * @see #representedData
 */
- (void)setRepresentedData:(id)newData
{
	if (representedData != newData) {
		[representedData release];
		representedData = [newData retain];
	}
}

/*!
 * Returns the data associated with this item, usually the raw dock data.
 *
 * @see #setRepresentedData:
 */
- (id)representedData
{
	return representedData;
}

@end
