/*
	Copyright (c) 2010 Peter MacWhinnie

	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.
 */

//MARK: Utilities

/*!
 @function	bind
 @abstract	Bind the 'this' context of a specified function.
 */
function bind(/*Object*/self, /*Function*/target)
{
	return function() {
		target.apply(self, arguments);
	}
}

//MARK: -
//MARK: ListView

/*!
 @class		ListView
 @abstract	The List class is wraps an ordered-list element and provides a delegate driven API for it.
 */
function ListView(/*jQuery(<ol>)*/listElement)
{
	this.listElement = listElement;
	this.selectedElement = null;
	
	return this;
}

/*MARK: -*/
/*MARK: Delegate*/

/*!
 @abstract	The data-delegate of the List class.
 */
ListView.prototype.delegate = {
	/*!
	 @method	numberOfItemsInList
	 @abstract	Returns the number of items to display in a specified ListView.
	 @param		list	The List object invoking this delegate method.
	 @result	int
	 */
	numberOfItemsInList: function(/*List*/list) {
		return 0;
	},
	
	/*!
	 @method		listWillDisplayItem
	 @abstract		Called when a List is about to display a list item.
	 @param			list		The List object invoking this delegate method.
	 @param			itemIndex	The index of the item that is about to be displayed.
	 @param			element		A jQuery object wrapping a list element (<li>). This should be stylized/populated as appropriate for the data the List is displaying.
	 @result		void
	 @discussion	This method should be used to populate a list item with appropriate contents. 
	 List makes no assumptions about the contents of the items it is displaying.
	 */
	listWillDisplayItem: function(/*List*/list, /*int*/itemIndex, /*jQuery(<li>)*/element) {
		
	},
	
	
	/*!
	 @method	selectionDidChangeInList
	 @abstract	Called when the selection of a List object changes.
	 @param		list		The List object invoking this delegate method.
	 @param		itemIndex	The index of the item now selected in the List object.
	 @result	void
	 */
	selectionDidChangeInList: function(/*List*/list, /*int*/itemIndex) {
		
	},
	
	/*!
	 @method	itemWasDoubleClickedInList
	 @abstract	Called when an item in a List object is double clicked.
	 @param		list		The List object invoking this delegate method.
	 @param		itemIndex	The index of the item in the List object that was double clicked.
	 @result	void
	 */
	itemWasDoubleClickedInList: function(/*List*/list, /*int*/itemIndex) {
		
	},
};

ListView.prototype.invokeDelegateMethod = function(methodName, arguments) {
	var method = this.delegate[methodName];
	if(method)
	{
		return method.apply(this.delegate, arguments);
	}
	
	return undefined;
};

/*MARK: -*/
/*MARK: Selection*/

/*!
 @method	_elementWasClicked
 @abstract	Respond to an element being clicked by the user.
 @param		element	A jQuery object wrapping a list item (<li>).
 */
ListView.prototype._elementWasClicked = function(/*jQuery(<li>)*/element) {
	var itemIndex = element.data("list_index");
	this.setSelectedRow(itemIndex);
};

/*!
 @method		_elementWasDoubleClicked
 @abstract		Respond to an element being double clicked by the user.
 @param			element	A jQuery object wrapping a list item (<li>).
 @discussion	This method invokes the itemWasDoubleClickedInList delegate method.
 */
ListView.prototype._elementWasDoubleClicked = function(element) {
	this.invokeDelegateMethod('itemWasDoubleClickedInList', [this, element.attr("list_index")]);
};

/*MARK: -*/

/*!
 @method		setSelectedRow
 @abstract		Update the selected row of the ListView.
 @param			rowIndex	If rowIndex is within (0, this.numberOfItems()) then the list item at that index will be selected; otherwise the selection will be cleared.
 @result		List
 @discussion	This method invokes the selectionDidChangeInList delegate method.
 */
ListView.prototype.setSelectedRow = function(rowIndex) {
	if(this.selectedElement)
	{
		this.selectedElement.removeClass("selected");
		this.selectedElement = null;
	}
	
	var childListItems = this.listElement.children();
	if(rowIndex >= 0 && rowIndex < childListItems.length)
	{
		if((this.selectedElement = $(childListItems.get(rowIndex))) != null)
		{
			this.selectedElement.addClass("selected");
		}
	}
	
	this.invokeDelegateMethod('selectionDidChangeInList', [this, rowIndex]);
};

/*!
 @method	selectedRow
 @abstract	Returns the selected row in the ListView.
 */
ListView.prototype.selectedRow = function() {
	if(this.selectedElement)
	{
		return this.selectedElement.data("list_index");
	}
	
	return -1;
};

/*!
 @method	deselectAll
 @abstract	Clears all selection in the ListView.
 */
ListView.prototype.deselectAll = function() {
	this.setSelectedRow(-1);
};

/*MARK: -*/
/*MARK: Population*/

/*!
 @method	numberOfItems
 @abstract	Ask the List's delegate for the number of items to display.
 */
ListView.prototype.numberOfItems = function() {
	return this.invokeDelegateMethod('numberOfItemsInList', [this]);
};

/*!
 @method	_createListItem
 @abstract	Create a list item for a specified row.
 @param		rowIndex	The row of the item.
 @result	A jQuery(<li>) object.
 */
ListView.prototype._createListItem = function(rowIndex) {
	var self = this;
	return $(document.createElement('li'))
			.data("list_index", rowIndex)
			.click(function(e) { self._elementWasClicked($(e.currentTarget)); })
			.dblclick(function(e) { self._elementWasDoubleClicked($(e.currentTarget)); });
};

//MARK: -

/*!
 @method	reloadData
 @abstract	Reload all of the data of the ListView.
 */
ListView.prototype.reloadData = function() {
	this.listElement.empty();
	this.selectedElement = null;
	
	var numberOfItems = this.numberOfItems();
	for (var index = 0; index < numberOfItems; index++)
	{
		var element = this._createListItem(index);
		this.invokeDelegateMethod('listWillDisplayItem', [this, index, element]);
		this.listElement.append(element);
	}
};

//MARK: -
//MARK: Menu

/*!
 @class		Menu
 @abstract	The Menu class creates a menu that can be popped anywhere in a container view.
 @param		container	A jQuery object wrapping a div who's filling the entire contents of the page's <body>.
 */
function Menu(/*jQuery(<div>)*/container)
{
	this.mItems = [];
	this.mContainer = container;
	
	this.mPoppedUp = false;
	this.mListElement = null;
	
	return this;
}

//MARK: -
//MARK: Menu Items

/*!
 @method	addItem
 @abstract	Add a specified item to the menu.
 @result	this
 */
Menu.prototype.addItem = function(menuItem) {
	this.mItems.push(menuItem);
	return this;
};

/*!
 @method	addItems
 @abstract	Add a specified array of items to the menu.
 @result	this
 */
Menu.prototype.addItems = function(menuItems) {
	menuItems.forEach(this.addItem, this);
	return this;
};

/*!
 @method	removeItem
 @abstract	Remove a specified item from the menu.
 @result	this
 */
Menu.prototype.removeItem = function(menuItem) {
	for (var index = 0, length = this.mItems.length; index < length; index++)
	{
		if(this.mItems[index] == menuItem)
		{
			this.mItems.splice(index, 1);
			return this;
		}
	}
	
	throw ("Menu Item " + menuItem + "isn't in menu " + this);
};

/*!
 @method	removeItems
 @abstract	Remove a specified array of items from the menu.
 @result	this
 */
Menu.prototype.removeItems = function(menuItems) {
	menuItems.forEach(this.removeItem, this);
	return this;
};

/*!
 @method	removeAllItems
 @abstract	Remove all of the items from the Menu.
 @result	this
 */
Menu.prototype.removeAllItems = function() {
	this.mItems.splice(0, this.mItems.length);
	return this;
};

//MARK: -

/*!
 @method	items
 @abstract	Returns the items of the menu.
 */
Menu.prototype.items = function() {
	return mItems;
};

//MARK: -
//MARK: Popping Up

/*!
 @method	close
 @abstract	Close the menu if it is currently popped up.
 @param		clickedItem	The menu item that was clicked, causing the menu to close. May be null.
 */
Menu.prototype.close = function(clickedItem) {
	if(!this.mPoppedUp)
	{
		return;
	}
	
	//Send the clicked item's action, if it has one.
	if(clickedItem && clickedItem.action())
	{
		clickedItem.action()(clickedItem);
	}
	
	//Hide our list element
	this.mListElement.fadeOut('fast', bind(this, function() {
		//Hide the container and remove our mouseup listener
		this.mContainer.css('display', 'none');
		this.mContainer.unbind('mouseup');
		
		//Destroy our list element, we don't need it anymore
		if(this.mListElement)
		{
			this.mListElement.remove();
			this.mListElement = null;
		}
		
		this.mPoppedUp = false;
	}));
};

/*!
 @method	popUpAt
 @abstract	Pop up the menu at a specified point.
 @param		offset	An object with top and left properties.
 */
Menu.prototype.popUpAt = function(offset) {
	if(!this.mPoppedUp)
	{
		//Our menu is represented as an ordered list populated with items
		this.mListElement = $(document.createElement('ol')).addClass('menu');
		
		//Enumerate each menu item and add a list item
		this.mItems.forEach(function(menuItem, index) {
			var item = $(document.createElement('li'));
			if(menuItem.isSeparator())
			{
				item.addClass('separator');
			}
			else
			{
				item.text(menuItem.title());
				item.mouseup(bind(this, function(event) {
					this.close(menuItem);
				}));
				
				item.css(menuItem.style());
			}
			
			this.mListElement.append(item);
		}, this);
		
		this.mContainer.append(this.mListElement);
		
		//We listen for mouse ups in the container element so the menu closes when clicked.
		this.mContainer.mouseup(bind(this, function(event) {
			this.close(null);
		}));
		
		this.mPoppedUp = true;
	}
	
	//Update the offset of the menu list element
	this.mListElement.css('top', offset.top).css('left', offset.left);
	
	//Make the container visible.
	this.mContainer.css('display', 'block');
};

/*!
 @class		Menu.Item
 @abstract	The Menu.Item class represents items in the Menu class.
 */
Menu.Item = function(title, action)
{
	this.mTitle = title;
	this.mAction = action;
	this.mRepresentedObject = null;
	this.mTag = 0;
	this.mIsSeparator = false;
	this.mStyle = {};
	
	return this;
}

/*!
 @method	isSeparator
 @abstract	Returns whether or not the item is a separator.
 */
Menu.Item.prototype.isSeparator = function() {
	return this.mIsSeparator;
};


/*!
 @method	setTitle
 @abstract	Set the title of the item.
 @param		title	The string to use as the item's title.
 @result	this
 */
Menu.Item.prototype.setTitle = function(/*String*/title) {
	this.mTitle = title;
	return this;
};

/*!
 @method	title
 @abstract	Returns the title of the item
 */
Menu.Item.prototype.title = function() {
	return this.mTitle;
};


/*!
 @method	setStyle
 @abstract	Set the style of the item.
 @param		style	The hash to use as the item's style.
 @result	this
 */
Menu.Item.prototype.setStyle = function(/*hash*/style) {
	this.mStyle = style;
	return this;
};

/*!
 @method	style
 @abstract	Returns the style of the item
 */
Menu.Item.prototype.style = function() {
	return this.mStyle;
};


/*!
 @method	setAction
 @abstract	Set the actino of the item.
 @param		action	A function expecting a single paramater, a Menu.Item, to be used as the action of the item.
 @result	this
 */
Menu.Item.prototype.setAction = function(/*(Integer) -> void*/action) {
	this.mAction = action;
	return this;
};

/*!
 @method	action
 @abstract	Returns the action of the item.
 */
Menu.Item.prototype.action = function() {
	return this.mAction;
};


/*!
 @method	setTag
 @abstract	Set the tag of the item.
 @param		tag	An integer to use as the tag of the item.
 @result	this
 */
Menu.Item.prototype.setTag = function(/*Number*/tag) {
	this.mTag = tag;
	return this;
};

/*!
 @method	tag
 @abstract	Returns the tag of the item.
 */
Menu.Item.prototype.tag = function() {
	return this.mTag;
};


/*!
 @method	setRepresentedObject
 @abstract	Set the represented object of the item.
 @param		representedObject	An object to use as the represented object of the item.
 @result	this
 */
Menu.Item.prototype.setRepresentedObject = function(/*Object*/representedObject) {
	this.mRepresentedObject = representedObject;
	return this;
};

/*!
 @method	representedObject
 @abstract	Returns the items represented object.
 */
Menu.Item.prototype.representedObject = function() {
	return this.mRepresentedObject;
};

/*!
 @method	separatorItem
 @abstract	Returns a new separator item suitable for use with a Menu.
 */
Menu.Item.separatorItem = function() {
	var item = new Menu.Item("-", null);
	item.mIsSeparator = true;
	return item;
};

//MARK: -
//MARK: Defaults

/*!
 @abstract	The Defaults object provides a persistent storage place for simple attributes.
 */
var Defaults = {
	/*!
	 @abstract	The storage for the key-value pairs to be persisted.
	 */
	values: {},
	
	/*!
	 @method	set
	 @abstract	Associate a specified key with a specified value in the Defaults storage.
	 @param		key		A string to use as the key in the storage.
	 @param		value	The value to associate with the key.
	 @result	this
	 */
	set: function(key, value) {
		this.values[key] = value;
		return this;
	}, 
	
	/*!
	 @method	get
	 @abstact	Get the value for a specified key, substituting a default value if one cannot be found.
	 @param		key				The	key of the value to lookup.
	 @param		defaultValue	The object to substitute if no value is available.
	 @result	Object
	 */
	get: function(key, defaultValue) {
		var value = this.values[key];
		if(value !== undefined)
		 	return value;
		
		return defaultValue;
	}, 
	
	/*!
	 @method	remove
	 @abstract	Remove the value for a specified key from the persistent storage.
	 @param		key		The key of the value to remove.
	 @result	this
	 */
	remove: function(key) {
		delete this.values[key];
		return this;
	},
	
	/*!
	 @method	load
	 @abstract	Load the Defaults-values from persistent storage.
	 @result	this
	 */
	load: function() {
		var jsonData = unescape(document.cookie);
		try
		{
			this.values = $.parseJSON(jsonData) || {};
		}
		catch(e)
		{
			this.values = {};
			document.cookie = '';
		}
		return this;
	},
	
	/*!
	 @method	synchronize
	 @abstract	Write the Defaults-values to persistent storage.
	 @result	this
	 */
	synchronize: function() {
		document.cookie = escape($.toJSON(this.values));
		return this;
	},
};

