/**
 * List.js
 * Creates a list of items for use in a form, and includes +/- controls for manipulation
 * 
 * @author Sean McCann
 * @version 0.1
 */

/**
 * @constructor
 * @param {object} params An associative array (i.e. JSON object) of parameters to pass to the constructor.  Acceptable parameters are listed in the List's "valid_params" member variable
 */
var List = function ( params ) {
	this.data = [];
	this.id = null;
	this.element = null;
	this.acClassName = 'add-control';
	this.rcClassName = 'remove-control';
	
	this.valid_callback_names = ['onAdd', 'onRemove', 'onOnlyOne'];
	this.valid_params = ['data','id', 'itemHTML', 'addControlHTML', 'removeControlHTML', 'removeControlDisabledHTML'];

	// Loop through the parameters passed to this constructor and
	// set the appropriate members
	var i, p;
	for (i in this.valid_params) {
		p = this.valid_params[i];
		if (typeof(params[p]) !== 'undefined'){
			this[p] = params[p];
		}
	}
	
	// Retain an internal reference to the element holding the list
	this.element = document.getElementById(this.id);
	if (this.element === null) {
		// Code 8 == NOT_FOUND_ERR
		// http://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/DOMException
		throw 'Could not find an element with id "' + this.id + '"';
	}
	
	// extend each data object so that it may be used easily from within
	// a template used by the List.
	for (i = 0; i < this.data.length; i++) {
		this.data[i].t = this.t;
	}
}


List.prototype = {
	/**
	 * HTML template for the button that adds a new items to the list
	 * @return {string} HTML representation of the "Add a new item" button
	 */
	addControlHTML: function () {
		return '<a href="javascript:// Add a new Item">Add</a>';
	},
	
	/**
	 * Adds a new item to the list.  This will be run as an event handler for
	 * the element representing the "Add a new item" button created by addControlHTML()
	 *
	 * @param {int} index Optional. The index in the list where the new item should be inserted.
	 * @param {object} item Optional. A JSON object that can be used to initialize the form template for the new item
	 */
	addItem: function (index, item) {
		var list_wrapper;
		
		if (typeof(item) === 'undefined'){
			item = { };
			item.t = this._list.t;
		}
		if (typeof(index) === 'undefined') {
			index = this._list.data.length;
		}
		
		this._list.data.splice(index, 0, item);
		this._list.element.insertBefore(this._list.itemContainer(parseInt(this.parentNode._list_index) + 1, item), this.parentNode.nextSibling);
		this._list.normalizeIndexes(parseInt(this.parentNode._list_index) + 1);
		
		// If we just added a second element to our list,
		// display our previously disabled "remove" button
		if (this._list.length() == 2) {
			var oldControl = this.parentNode._removeControl;
			this.parentNode._removeControl = this._list.listControl(
				this._list.removeControlHTML(),
				this._list.rcClassName,
				this._list.removeItem
			);
			this.parentNode.replaceChild( this.parentNode._removeControl, oldControl );
		}
	},
	
	/**
	 * HTML template that wraps each item in the list.  This wrapper includes the
	 * add and remove controls that enable the list to function
	 *
	 * @param {int} item_num
	 * @param {object} item The data object used to initialize the item
	 */
	itemContainer: function (item_num, item) {
		var id, element;

		element = document.createElement('div');
		element.id = this.itemId(item_num);
		element.innerHTML = this.itemHTML(item_num, item);
		element._list_index = item_num;
		if (element.childNodes.length == 0) {
			throw 'itemHTML() must return a valid HTML string';
		}
		
		this._setValues(element, item);
		
		// The "Add a new item" button
		element._addControl = this.listControl( this.addControlHTML(), this.acClassName, this.addItem ); 
		element.appendChild(element._addControl);
		
		// @todo make this optional
		element.appendChild(document.createTextNode(' '));
		
		// The "Remove this item" button
		element._removeControl = this.listControl( 
			(this.data.length > 1) ? this.removeControlHTML() : this.removeControlDisabledHTML(),
			 this.rcClassName + ((this.data.length > 1) ? '' : ' disabled'),
			(this.data.length > 1) ? this.removeItem : null );
		element.appendChild(element._removeControl);

		return element;
	},

	/**
	 * Creates the HTML for items going into our list.  People using the List
	 * class are expected to overwrite this method to generate the HTML that
	 * suits their particular needs.  Keep in mind that the HTML returned by
	 * this method should not contain markup for the add and remove buttons;
	 * these controls will be created by addControlHTML, removeControlHTML,
	 * and itemContainer.
	 *
	 * @param {int} index The index of this item within the List.  Used to uniquely identify each name, id, etc.
	 * @param {object} item An object representing an item whose members may be used to fill in values in the HTML
	 * @return {string} Returns valid HTML representing an item in the list
	 */
	itemHTML: function (index, item) {
		return '<input name="employee[' + index + ']" value="' + item.t('employee') + '" />';
	},
	
	/**
	 * Returns an ID for an item container given a position
	 * in the List.
	 *
	 * @param {int} index The index of the item container
	 */
	itemId: function (index) {
		return (typeof(index) === 'undefined') ? null : this.id + '-item-' + index;
	},
	
	/**
	 * Returns the number of items in the List
	 * @return {int} number of items in the list
	 */
	length: function () {
		return this.element.childNodes.length;
	},
	
	/**
	 * Returns a DOM node containing a control to add or remove
	 * items from our list.  Each control is extended so that they have
	 * the proper hooks for the List and accompanying event handler.
	 *
	 * NOTE: we assume that our template methods (addControlHTML(), 
	 * removeControlHTML(), and removeControlDisabledHTML() ) each
	 * return HTML for an element that has no siblings (children are fine)
	 *
	 * @param {string} html The HTML representing the control being created
	 * @param {string} classname The name of the CSS class to be added to the control element
	 * @param {function} handler The onclick observer to be added to the element
	 * @return {node} DOM node reference to the control
	 */
	listControl: function ( html, classname, handler ) {
		var tmp = document.createElement('div');
		tmp.innerHTML = html;
		if (tmp.childNodes.length !== 1) {
			throw 'The template for your "' + classname + '" must return a single HTML element.';
		}
		tmp.childNodes[0].className += ((tmp.childNodes[0].className) ? ' ' : '') + classname;
		tmp.childNodes[0].onclick = handler;
		tmp.childNodes[0]._list = this;
		return tmp.childNodes[0];
	},
	
	/**
	 * Loops through the list and makes sure that all
	 * indexes match the item's position in the list
	 */
	normalizeIndexes: function ( start ) {
		var i;
		
		if (typeof(start) === 'undefined'){ start = 0; }

		for (i = start; i < this.element.childNodes.length; i++) {
			this.element.childNodes[i]._list_index = i;
			this.element.childNodes[i].id = this.itemId(i);
			this._normalizeIndexes( this.element.childNodes[i], i );
		}
	},
	
	/**
	 * A recursive helper method for normalizeIndexes that loops through
	 * each list item and adjusts indices used in input/textarea/select
	 * names and ids.
	 */
	_normalizeIndexes: function ( element, index ) {
		var i;
		var attr = ['id', 'name', 'htmlFor'];
		
		// If this isn't an HTML element, stop working
		// For a full list of nodeType values, see
		// http://developer.mozilla.org/en/DOM/element.nodeType
		if (element.nodeType != 1) return;

		// If any of our attribute values use indices to
		// uniquely identify themselves (e.g. <input name="email[3]" />),
		// update each index so that it corresponds with this item's
		// position in the List.
		for (i in attr) {
			if (typeof(element[attr[i]]) !== 'undefined') {
				element[attr[i]] = element[attr[i]].replace(/\[\d+\]/, '[' + index + ']' );
			}
		}
		
		// Do the same for all children of this element.
		for (i = 0; i < element.childNodes.length; i++) {
			this._normalizeIndexes( element.childNodes[i], index );
		}
	},
	
	_setValues: function ( element, item ) {
		var i, selected, property;
		
		// If this isn't an HTML element, stop working
		// For a full list of nodeType values, see
		// http://developer.mozilla.org/en/DOM/element.nodeType
		if (!element || element.nodeType != 1) return;
		
		if (typeof(element.name) !== 'undefined') {
			property = element.name.replace(/^[\w-]+\[\d+\]\[([\w-]+)\]/,"$1");
			if (typeof(item[property]) !== 'undefined') {
				switch(element.nodeName) {
					case 'SELECT':
						for (i = 0; i < element.options.length && !element.selectedIndex; i++){
							element.selectedIndex = (element.options[i].value == item.t(property)) ? i : false; 
						}
						break;
					case 'INPUT':
						if (element.type == 'checkbox') {
							element.checked = true;
						}
						else if (element.type == 'radio') {
							if (element.value == item.t(property)) {
								element.checked = true;
							}
						}
						else {
							element.value = item.t(property);
						}
						break;
					case 'TEXTAREA':
						element.innerHTML = item.t(property);
				}
			}
		}
		
		// Do the same for all children of this element.
		for (i = 0; i < element.childNodes.length; i++) {
			this._setValues( element.childNodes[i], item );
		}
	},

	/**
	 * Removes an item from the List.  This method will be invoked
	 * as an onclick handler of the HTML element created by removeControlHTML()
	 *
	 * @param {int} index The position of the item to be removed
	 */
	removeItem: function () {
		var list_wrapper;

		if (this._list.length() == 1) {
			return false;
		}

		this._list.data.splice(this.parentNode._list_index, 1);
		this._list.element.removeChild(this.parentNode);
		this._list.normalizeIndexes();
		
		// If removing an item from our list only results in
		// a single element remaining, 
		if (this._list.length() == 1) {
			var oldControl = this._list.element.childNodes[0]._removeControl;
			this._list.element.childNodes[0]._removeControl = this._list.listControl(
				this._list.removeControlDisabledHTML(),
				this._list.rcClassName + ' disabled',
				null
			);
			this._list.element.childNodes[0].replaceChild(
				this._list.element.childNodes[0]._removeControl,
				oldControl
			);
		}
	},
	
	/**
	 * HTML template for the button that removes items from the list
	 * @return {string} HTML representation of the 'Remove this item' button
	 */
	removeControlHTML: function () {
		return '<a href="javascript:// Remove this Item">Remove</a>';
	},

	/**
	 * HTML template for the button that removes items from the list
	 * @return {string} HTML representation of the 'Remove this item' button
	 */
	removeControlDisabledHTML: function () {
		return '<span></span>';
	},


	/**
	 * Performs the initial printing of the list into the HTML element identified
	 * by the 'id' paramter passed to the List constructor.  You must call render()
	 * before the list will appear in your document.
	 */
	render: function () {
		var i, html = '';
		
		// Rendering a completely empty list is useless
		// If we don't have any data to render, insert a
		// blank item into the list so that someone has
		// a place to keep their new data
		if (this.data.length === 0) {
			this.data[0] = {};
			this.data[0].t = this.t;
		}
		
		for (i = 0; i < this.data.length; i++) {
			this.element.appendChild(this.itemContainer(i, this.data[i] ));
		}
	},
	
	/**
	 * Creates strings suitable for use the HTML templates used by
	 * the List class.  Basically, this just returns an empty string
	 * if a property is undefined.  All data objects used by the list
	 * will be extended with this method.
	 *
	 * @param {mixed} property An object property that you want to print
	 * @return empty string if the property is undefined.  Otherwise, returns the string representation of that property
	 */
	t: function (property) {
		return ((typeof(this[property]) === 'undefined') ? '' : this[property]);
	}
}
