dojo.provide("dojox.mobile.app.List");
dojo.experimental("dojox.mobile.app.List");

dojo.require("dojo.string");
dojo.require("dijit._WidgetBase");

(function(){

	var templateCache = {};

	dojo.declare("dojox.mobile.app.List", dijit._WidgetBase, {
		// summary:
		//		A templated list widget. Given a simple array of data objects
		//		and a HTML template, it renders a list of elements, with
		//		support for a swipe delete action.  An optional template
		//		can be provided for when the list is empty.

		// items: Array
		//		The array of data items that will be rendered.
		items: null,

		// itemTemplate: String
		//		The URL to the HTML file containing the markup for each individual
		//		data item.
		itemTemplate: "",

		// emptyTemplate: String
		//		The URL to the HTML file containing the HTML to display if there
		//		are no data items. This is optional.
		emptyTemplate: "",

		// dividerTemplate: String
		//		The URL to the HTML file containing the markup for the dividers
		//		between groups of list items
		dividerTemplate: "",

		// dividerFunction: Function
		//		Function to create divider elements. This should return a divider
		//		value for each item in the list
		dividerFunction: null,

		// labelDelete: String
		//		The label to display for the Delete button
		labelDelete: "Delete",

		// labelCancel: String
		//		The label to display for the Cancel button
		labelCancel: "Cancel",

		// controller: Object
		controller: null,

		// autoDelete: Boolean
		autoDelete: true,

		// enableDelete: Boolean
		enableDelete: true,

		// enableHold: Boolean
		enableHold: true,

		// formatters: Object
		//		A name/value map of functions used to format data for display
		formatters: null,

		// _templateLoadCount: Number
		//		The number of templates remaining to load before the list renders.
		_templateLoadCount: 0,

		// _mouseDownPos: Object
		//		The coordinates of where a mouseDown event was detected
		_mouseDownPos: null,

		baseClass: "list",

		constructor: function(){
			this._checkLoadComplete = dojo.hitch(this, this._checkLoadComplete);
			this._replaceToken = dojo.hitch(this, this._replaceToken);
			this._postDeleteAnim = dojo.hitch(this, this._postDeleteAnim);
		},

		postCreate: function(){

			var _this = this;

			if(this.emptyTemplate){
				this._templateLoadCount++;
			}
			if(this.itemTemplate){
				this._templateLoadCount++;
			}
			if(this.dividerTemplate){
				this._templateLoadCount++;
			}

			this.connect(this.domNode, "onmousedown", function(event){
				var touch = event;
				if(event.targetTouches && event.targetTouches.length > 0){
					touch = event.targetTouches[0];
				}

				// Find the node that was tapped/clicked
				var rowNode = _this._getRowNode(event.target);

				if(rowNode){
					// Add the rows data to the event so it can be picked up
					// by any listeners
					_this._setDataInfo(rowNode, event);

					// Select and highlight the row
					_this._selectRow(rowNode);

					// Record the position that was tapped
					_this._mouseDownPos = {
						x: touch.pageX,
						y: touch.pageY
					};
					_this._dragThreshold = null;
				}
			});

			this.connect(this.domNode, "onmouseup", function(event){
				// When the mouse/finger comes off the list,
				// call the onSelect function and deselect the row.
				if(event.targetTouches && event.targetTouches.length > 0){
					event = event.targetTouches[0];
				}
				var rowNode = _this._getRowNode(event.target);

				if(rowNode){

					_this._setDataInfo(rowNode, event);

					if(_this._selectedRow){
						_this.onSelect(rowNode._data, rowNode._idx, rowNode);
					}

					this._deselectRow();
				}
			});

			// If swipe-to-delete is enabled, listen for the mouse moving
			if(this.enableDelete){
				this.connect(this.domNode, "mousemove", function(event){
					dojo.stopEvent(event);
					if(!_this._selectedRow){
						return;
					}
					var rowNode = _this._getRowNode(event.target);

					// Still check for enableDelete in case it's changed after
					// this listener is added.
					if(_this.enableDelete && rowNode && !_this._deleting){
						_this.handleDrag(event);
					}
				});
			}

			// Put the data and index onto each onclick event.
			this.connect(this.domNode, "onclick", function(event){
				if(event.touches && event.touches.length > 0){
					event = event.touches[0];
				}
				var rowNode = _this._getRowNode(event.target, true);

				if(rowNode){
					_this._setDataInfo(rowNode, event);
				}
			});

			// If the mouse or finger moves off the selected row,
			// deselect it.
			this.connect(this.domNode, "mouseout", function(event){
				if(event.touches && event.touches.length > 0){
					event = event.touches[0];
				}
				if(event.target == _this._selectedRow){
					_this._deselectRow();
				}
			});

			// If no item template has been provided, it is an error.
			if(!this.itemTemplate){
				throw Error("An item template must be provided to " + this.declaredClass);
			}

			// Load the item template
			this._loadTemplate(this.itemTemplate, "itemTemplate", this._checkLoadComplete);

			if(this.emptyTemplate){
				// If the optional empty template has been provided, load it.
				this._loadTemplate(this.emptyTemplate, "emptyTemplate", this._checkLoadComplete);
			}

			if(this.dividerTemplate){
				this._loadTemplate(this.dividerTemplate, "dividerTemplate", this._checkLoadComplete);
			}
		},

		handleDrag: function(event){
			// summary:
			//		Handles rows being swiped for deletion.
			var touch = event;
			if(event.targetTouches && event.targetTouches.length > 0){
				touch = event.targetTouches[0];
			}

			// Get the distance that the mouse or finger has moved since
			// beginning the swipe action.
			var diff = touch.pageX - this._mouseDownPos.x;

			var absDiff = Math.abs(diff);
			if(absDiff > 10 && !this._dragThreshold){
				// Make the user drag the row 60% of the width to remove it
				this._dragThreshold = dojo.marginBox(this._selectedRow).w * 0.6;
				if(!this.autoDelete){
					this.createDeleteButtons(this._selectedRow);
				}
			}

			this._selectedRow.style.left = (absDiff > 10 ? diff : 0) + "px";

			// If the user has dragged the row more than the threshold, slide
			// it off the screen in preparation for deletion.
			if(this._dragThreshold && this._dragThreshold < absDiff){
				this.preDelete(diff);
			}
		},

		handleDragCancel: function(){
			// summary:
			//		Handle a drag action being cancelled, for whatever reason.
			//		Reset handles, remove CSS classes etc.
			if(this._deleting){
				return;
			}
			dojo.removeClass(this._selectedRow, "hold");
			this._selectedRow.style.left = 0;
			this._mouseDownPos = null;
			this._dragThreshold = null;

			this._deleteBtns && dojo.style(this._deleteBtns, "display", "none");
		},

		preDelete: function(currentLeftPos){
			// summary:
			//		Slides the row offscreen before it is deleted

			// TODO: do this with CSS3!
			var self = this;

			this._deleting = true;

			dojo.animateProperty({
				node: this._selectedRow,
				duration: 400,
				properties: {
					left: {
					end: currentLeftPos +
						((currentLeftPos > 0 ? 1 : -1) * this._dragThreshold * 0.8)
					}
				},
				onEnd: dojo.hitch(this, function(){
					if(this.autoDelete){
						this.deleteRow(this._selectedRow);
					}
				})
			}).play();
		},

		deleteRow: function(row){

			// First make the row invisible
			// Put it back where it came from
			dojo.style(row, {
				visibility: "hidden",
				minHeight: "0px"
			});
			dojo.removeClass(row, "hold");

			this._deleteAnimConn =
				this.connect(row, "webkitAnimationEnd", this._postDeleteAnim);

			dojo.addClass(row, "collapsed");
		},

		_postDeleteAnim: function(event){
			// summary:
			//		Completes the deletion of a row.

			if(this._deleteAnimConn){
				this.disconnect(this._deleteAnimConn);
				this._deleteAnimConn = null;
			}

			var row = this._selectedRow;
			var sibling = row.nextSibling;
			var prevSibling = row.previousSibling;

			// If the previous node is a divider and either this is
			// the last element in the list, or the next node is
			// also a divider, remove the divider for the deleted section.
			if(prevSibling && prevSibling._isDivider){
				if(!sibling || sibling._isDivider){
					prevSibling.parentNode.removeChild(prevSibling);
				}
			}

			row.parentNode.removeChild(row);
			this.onDelete(row._data, row._idx, this.items);

			// Decrement the index of each following row
			while(sibling){
				if(sibling._idx){
					sibling._idx--;
				}
				sibling = sibling.nextSibling;
			}

			dojo.destroy(row);

			// Fix up the 'first' and 'last' CSS classes on the rows
			dojo.query("> *:not(.buttons)", this.domNode).forEach(this.applyClass);

			this._deleting = false;
			this._deselectRow();
		},

		createDeleteButtons: function(aroundNode){
			// summary:
			//		Creates the two buttons displayed when confirmation is
			//		required before deletion of a row.
			// aroundNode:
			//		The DOM node of the row about to be deleted.
			var mb = dojo.marginBox(aroundNode);
			var pos = dojo._abs(aroundNode, true);

			if(!this._deleteBtns){
			// Create the delete buttons.
				this._deleteBtns = dojo.create("div",{
					"class": "buttons"
				}, this.domNode);

				this.buttons = [];

				this.buttons.push(new dojox.mobile.Button({
					btnClass: "mblRedButton",
					label: this.labelDelete
				}));
				this.buttons.push(new dojox.mobile.Button({
					btnClass: "mblBlueButton",
					label: this.labelCancel
				}));

				dojo.place(this.buttons[0].domNode, this._deleteBtns);
				dojo.place(this.buttons[1].domNode, this._deleteBtns);

				dojo.addClass(this.buttons[0].domNode, "deleteBtn");
				dojo.addClass(this.buttons[1].domNode, "cancelBtn");

				this._handleButtonClick = dojo.hitch(this._handleButtonClick);
				this.connect(this._deleteBtns, "onclick", this._handleButtonClick);
			}
			dojo.removeClass(this._deleteBtns, "fade out fast");
			dojo.style(this._deleteBtns, {
				display: "",
				width: mb.w + "px",
				height: mb.h + "px",
				top: (aroundNode.offsetTop) + "px",
				left: "0px"
			});
		},

		onDelete: function(data, index, array){
			// summary:
			//		Called when a row is deleted
			// data:
			//		The data related to the row being deleted
			// index:
			//		The index of the data in the total array
			// array:
			//		The array of data used.

			array.splice(index, 1);

			// If the data is empty, rerender in case an emptyTemplate has
			// been provided
			if(array.length < 1){
				this.render();
			}
		},

		cancelDelete: function(){
			// summary:
			//		Cancels the deletion of a row.
			this._deleting = false;
			this.handleDragCancel();
		},

		_handleButtonClick: function(event){
			// summary:
			//		Handles the click of one of the deletion buttons, either to
			//		delete the row or to cancel the deletion.
			if(event.touches && event.touches.length > 0){
				event = event.touches[0];
			}
			var node = event.target;
			if(dojo.hasClass(node, "deleteBtn")){
				this.deleteRow(this._selectedRow);
			}else if(dojo.hasClass(node, "cancelBtn")){
				this.cancelDelete();
			}else{
				return;
			}
			dojo.addClass(this._deleteBtns, "fade out");
		},

		applyClass: function(node, idx, array){
			// summary:
			//		Applies the 'first' and 'last' CSS classes to the relevant
			//		rows.

			dojo.removeClass(node, "first last");
			if(idx == 0){
				dojo.addClass(node, "first");
			}
			if(idx == array.length - 1){
				dojo.addClass(node, "last");
			}
		},

		_setDataInfo: function(rowNode, event){
			// summary:
			//		Attaches the data item and index for each row to any event
			//		that occurs on that row.
			event.item = rowNode._data;
			event.index = rowNode._idx;
		},

		onSelect: function(data, index, rowNode){
			// summary:
			//		Dummy function that is called when a row is tapped
		},

		_selectRow: function(row){
			// summary:
			//		Selects a row, applies the relevant CSS classes.
			if(this._deleting && this._selectedRow && row != this._selectedRow){
				this.cancelDelete();
			}

			if(!dojo.hasClass(row, "row")){
				return;
			}
			if(this.enableHold || this.enableDelete){
				dojo.addClass(row, "hold");
			}
			this._selectedRow = row;
		},

		_deselectRow: function(){
			// summary:
			//		Deselects a row, and cancels any drag actions that were
			//		occurring.
			if(!this._selectedRow || this._deleting){
				return;
			}
			this.handleDragCancel();
			dojo.removeClass(this._selectedRow, "hold");
			this._selectedRow = null;
		},

		_getRowNode: function(fromNode, ignoreNoClick){
			// summary:
			//		Gets the DOM node of the row that is equal to or the parent
			//		of the node passed to this function.
			while(fromNode && !fromNode._data && fromNode != this.domNode){
				if(!ignoreNoClick && dojo.hasClass(fromNode, "noclick")){
					return null;
				}
				fromNode = fromNode.parentNode;
			}
			return fromNode == this.domNode ? null : fromNode;
		},

		applyTemplate: function(template, data){
			return dojo._toDom(dojo.string.substitute(
					template, data, this._replaceToken, this.formatters || this));
		},

		render: function(){
			// summary:
			//		Renders the list.

			// Delete all existing nodes, except the deletion buttons.
			dojo.query("> *:not(.buttons)", this.domNode).forEach(dojo.destroy);

			// If there is no data, and an empty template has been provided,
			// render it.
			if(this.items.length < 1 && this.emptyTemplate){
				dojo.place(dojo._toDom(this.emptyTemplate), this.domNode, "first");
			}else{
				this.domNode.appendChild(this._renderRange(0, this.items.length));
			}
			if(dojo.hasClass(this.domNode.parentNode, "mblRoundRect")){
				dojo.addClass(this.domNode.parentNode, "mblRoundRectList")
			}

			var divs = dojo.query("> .row", this.domNode);
			if(divs.length > 0){
				dojo.addClass(divs[0], "first");
				dojo.addClass(divs[divs.length - 1], "last");
			}
		},

		_renderRange: function(startIdx, endIdx){

			var rows = [];
			var row, i;
			var frag = document.createDocumentFragment();
			startIdx = Math.max(0, startIdx);
			endIdx = Math.min(endIdx, this.items.length);

			for(i = startIdx; i < endIdx; i++){
				// Create a document fragment containing the templated row
				row = this.applyTemplate(this.itemTemplate, this.items[i]);
				dojo.addClass(row, 'row');
				row._data = this.items[i];
				row._idx = i;
				rows.push(row);
			}
			if(!this.dividerFunction || !this.dividerTemplate){
				for(i = startIdx; i < endIdx; i++){
					rows[i]._data = this.items[i];
					rows[i]._idx = i;
					frag.appendChild(rows[i]);
				}
			}else{
				var prevDividerValue = null;
				var dividerValue;
				var divider;
				for(i = startIdx; i < endIdx; i++){
					rows[i]._data = this.items[i];
					rows[i]._idx = i;

					dividerValue = this.dividerFunction(this.items[i]);
					if(dividerValue && dividerValue != prevDividerValue){
						divider = this.applyTemplate(this.dividerTemplate, {
							label: dividerValue,
							item: this.items[i]
						});
						divider._isDivider = true;
						frag.appendChild(divider);
						prevDividerValue = dividerValue;
					}
					frag.appendChild(rows[i]);
				}
			}
			return frag;
		},

		_replaceToken: function(value, key){
			if(key.charAt(0) == '!'){ value = dojo.getObject(key.substr(1), false, _this); }
			if(typeof value == "undefined"){ return ""; } // a debugging aide
			if(value == null){ return ""; }

			// Substitution keys beginning with ! will skip the transform step,
			// in case a user wishes to insert unescaped markup, e.g. ${!foo}
			return key.charAt(0) == "!" ? value :
				// Safer substitution, see heading "Attribute values" in
				// http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
				value.toString().replace(/"/g,"&quot;"); //TODO: add &amp? use encodeXML method?

		},

		_checkLoadComplete: function(){
			// summary:
			//		Checks if all templates have loaded
			this._templateLoadCount--;

			if(this._templateLoadCount < 1 && this.get("items")){
				this.render();
			}
		},

		_loadTemplate: function(url, thisAttr, callback){
			// summary:
			//		Loads a template
			if(!url){
				callback();
				return;
			}

			if(templateCache[url]){
				this.set(thisAttr, templateCache[url]);
				callback();
			}else{
				var _this = this;

				dojo.xhrGet({
					url: url,
					sync: false,
					handleAs: "text",
					load: function(text){
						templateCache[url] = dojo.trim(text);
						_this.set(thisAttr, templateCache[url]);
						callback();
					}
				});
			}
		},


		_setFormattersAttr: function(formatters){
			// summary:
			//		Sets the data items, and causes a rerender of the list
			this.formatters = formatters;
		},

		_setItemsAttr: function(items){
			// summary:
			//		Sets the data items, and causes a rerender of the list

			this.items = items || [];

			if(this._templateLoadCount < 1 && items){
				this.render();
			}
		},

		destroy: function(){
			if(this.buttons){
				dojo.forEach(this.buttons, function(button){
					button.destroy();
				});
				this.buttons = null;
			}

			this.inherited(arguments);
		}

	});

})();