/**
 * @fileoverview Defines the KitDragDropList class.
 */

YAHOO.util.DDM.mode = YAHOO.util.DDM.INTERSECT;

/**
 * Construct a drag-and-drop reorderable list.
 * 
 * @class Represents a drag-and-drop reorderable list.
 *
 * <h4>Running this script</h4>
 *
 * <ul>
 *   <li>
 *     You need to load the dragdrop.js {@link http://developer.yahoo.com/yui YUI}
 *     script before loading this, and by extension, the following YUI scripts as well:
 *
 *     <ul>
 *       <li>yahoo.js</li>
 *       <li>dom.js</li>
 *       <li>event.js</li>
 *     </ul>
 *   </li>
 *
 *   <li>
 *     This script requires that the YAHOO Drag and Drop mode is
 *     {@link http://developer.yahoo.com/yui/dragdrop/#intersect INTERSECT};
 *     indeed it sets it to this when you load the script.
 *   </li>
 * </ul>
 *
 * <h4 id="listItemTypes">Types of list item</h4>
 *
 * <p>A list item can be an HTML element or anything with a <code>render(element)</code>
 * method which renders the item inside <code>element</code> (such as a
 * {@link http://developer.yahoo.com/yui/container/module/index.html YUI Module}).</p>
 * 
 * <h4 id="listManipulationNote">Important note on list manipulation</h4>
 * 
 * <p>Some item insertion/removal/reordering methods and callbacks may take
 * one or both of the following parameters:</p>
 * 
 * <dl>
 *   <dt><code>to</code></dt><dd>index of item to be moved</dd>
 *   <dt><code>from</code></dt><dd>index of drop target in the space where the item should go</dd>
 * </dl>
 * 
 * <p>The items and drop targets' indices are arranged like this:</p>
 * 
 * <table>
 *   <tr><th style="text-align: left; padding-right: 1em;">Items</th>
 *   <td> </td> <td>0</td> <td> </td> <td>1</td> <td> </td> <td>2</td> <td> </td> <td>3</td> <td> </td></tr>
 * <tr><th style="text-align: left; padding-right: 1em;">Targets</th>
 *   <td>0</td> <td> </td> <td>1</td> <td> </td> <td>2</td> <td> </td> <td>3</td> <td> </td> <td>4</td></tr>
 * </table>
 * 
 * <p>So e.g. to move item 0 to between items 2 and 3, do <code>moveItem(0, 3)</code>.</p>
 * 
 * <p>This means that if <code>from == to</code> or <code>from == to+1</code> then nothing will happen.</p>
 * 
 * @constructor
 * @param targetList <code>ul</code> or <code>ol</code> element (or id thereof) of list to render to
 * @param opt        Hash of options. The possible options are:
 *   <dl>
 *     <dt><code>items</code></dt>
 *       <dd>
 *         array of items to instantiate the list with (defaults to <code>[]</code>,
 *         see {@link #listItemTypes note on item types})
 *       </dd>
 *     <dt><code>noReorder</code></dt>
 *       <dd>do not allow the list to be reordered, or for items to be dragged into it (defaults to <code>false</code>)</dd>
 *     <dt><code>active</code></dt>
 *       <dd>start in the active state (defaults to <code>false</code>)</dd>
 *   </dl>
 */
function KitDragDropList(targetList, opt) {
	/**
	 * @private
	 */
	this.targetList = YAHOO.util.Dom.get(targetList);
		
	this.defaults = {
		items:     [],
		noReorder: false,
		active:    false
	};
	
	for (var i in this.defaults)
		this[i] = this.defaults[i];
	
	if (opt)
		for (var i in this.defaults)
			if (typeof(opt[i]) != 'undefined')
				this[i] = opt[i];
	
	this.dds       = [];
	this.ddTargets = [];	
	
	this._redraw();
}

/**
 * Enable drag and drop on the list
 */
KitDragDropList.prototype.activate = function() {
	this.active = true;
	this._redraw();
}

/**
 * Disable drag and drop on the list
 */
KitDragDropList.prototype.deactivate = function() {
	this.active = false;
	this._redraw();
}

/**
 * Insert an item into the list
 * @param item Item to insert (see {@link #listItemTypes note on item types})
 * @param to Index to insert at (see {@link #listManipulationNote note on list manipulation})
 * @returns true if item was inserted, false otherwise
 */
KitDragDropList.prototype.insertItem = function(item, to) {
	if (!this.onInsertItem(item, to))
		return false;
	
	this.items.splice(to, 0, item);
	this._redraw();
	
	return true;
}

/**
 * Remove an item from the list
 * @param from Index of item to remove
 * @returns Item removed (see {@link #listItemTypes note on item types})
 */
KitDragDropList.prototype.removeItem = function(from) {
	if (!this.onRemoveItem(from))
		return null;
	
	var item = this.items.splice(from, 1)[0];
	this._redraw();
	
	return item;
}

/**
 * Move an item from one position in a list to another
 * @param from Index of item to move
 * @param to Index to insert at (see {@link #listManipulationNote note on list manipulation})
 * @return true if item was moved (even if that didn't result in a change to the list, see note), false otherwise
 */
KitDragDropList.prototype.moveItem = function(from, to) {
	if (!this.onMoveItem(to, from))
		return false;
	
	if (to < from) {
		this.items = this.items.slice(0, to).concat(
		             [this.items[from]],
		             this.items.slice(to, from),
		             this.items.slice(from+1));
		this._redraw();
	} else if (to > from+1) {
		this.items = this.items.slice(0, from).concat(
		             this.items.slice(from+1, to),
		             [this.items[from]],
		             this.items.slice(to));
		this._redraw();
	}
	
	return true;
}

/**
 * Callback which fires when an item is inserted into the list, just before anything happens
 * @return Return true to continue, false to abort.
 */
KitDragDropList.prototype.onInsertItem = function(item, to) { return true; }

/**
 * Callback which fires when an item is removed the list, just before anything happens
 * @return Return true to continue, false to abort.
 */
KitDragDropList.prototype.onRemoveItem = function(from)     { return true; }

/**
 * Callback which fires when an item is moved within the list, just before anything happens
 * @return Return true to continue, false to abort.
 */
KitDragDropList.prototype.onMoveItem   = function(from, to) { return true; }

/**
 * @private
 */
KitDragDropList.prototype._redraw = function() {
	this._clear();
	this._build();
}

/**
 * @private
 */
KitDragDropList.prototype._clear = function() {
	while (dd = this.dds.shift())
		dd.unreg();
	
	while (ddTarget = this.ddTargets.shift())
		ddTarget.unreg();
	
	var list = this.targetList;
	
	while (list.childNodes.length)
		list.removeChild(list.childNodes[0]);
}

/**
 * @private
 */
KitDragDropList.prototype._build = function() {
	if (this.active) this._addDropTarget(0);
	
	for (var i=0; i<this.items.length; i++) {
		var item = this.items[i];
		var li   = this._addItem(item, i);

		if (this.active) this._addDropTarget(i+1);

		this._drawItem(item, li);
	}
}

/**
 * @private
 */
KitDragDropList.prototype._addDropTarget = function(index) {
	var e = this._addListItem('kit-drop-target-li-' + this.targetList.id + '-' + index);
	e.className = 'drop-target';
	e.appendChild(document.createTextNode(' '));
	
	this.ddTargets.push(new KitDragDropTarget(e.id, index, this));
	
	return e;
}

/**
 * @private
 */
KitDragDropList.prototype._addItem = function(item, index) {
	var e = this._addListItem('kit-item-li-' + this.targetList.id + '-' + index);
	
	if (this.active) {
		e.className = 'draggable';
		this.dds.push(new KitDragDropItem(item, index, this));
	}
	
	return e;
}

/**
 * @private
 */
KitDragDropList.prototype._addListItem = function(id) {
	var e = document.createElement('li');
	e.id = id;
	
	this.targetList.appendChild(e);
	
	return e;
}

/**
 * @private
 */
KitDragDropList.prototype._drawItem = function(item, tgt) {
	if (typeof(item.render) == 'function')
		item.render(tgt);
	else
		tgt.appendChild(item);
}

/*
 * KitDragDropItem
 *
 * Represents a drag-and-drop list item
 */

/**
 * @private
 */
function KitDragDropItem(item, position, master) {
	var element = item.id; //item.element || item;
	
	this.init(element);
	
	this.item     = item;
	this.position = position;
	this.master   = master;
	
	this.hoverTargets = {};
	this.isTarget = false;
	
	this.addInvalidHandleType('button');
	
	if (item.onDraw)
		item.onDraw();
}

KitDragDropItem.prototype = new YAHOO.util.DD();

// YAHOO.util.DragDrop callbacks

KitDragDropItem.prototype.onMouseDown = function(e) {
	YAHOO.util.Dom.addClass(this.getEl(), 'drag');
}

KitDragDropItem.prototype.onMouseUp = function(e) {
	var el = this.getEl();
	
	this.master._clear();

	el.style.top = 0;
	el.style.left = 0;

	YAHOO.util.Dom.removeClass(el, 'drag');

	this.master._build();
}

KitDragDropItem.prototype.onDragEnter = function(e, ids) {
	this.addHoverTargets(ids);
	this.refreshHoverHighlight();
};

KitDragDropItem.prototype.onDragOut = function(e, ids) {
	this.removeHoverTargets(ids);
	this.refreshHoverHighlight();
};

KitDragDropItem.prototype.onDragDrop = function(e, ids) {
	this.addHoverTargets(ids);
	var target = this.getHoverTarget();
	
	if (!target) return;
	
	var from = this.position;
	var to = target.position;
	
	/*console.log(
		'moving %s[%s] to %s[%s]',
		this.master.targetList.id, from,
		target.master.targetList.id, to
	);*/

	if (this.master != target.master) {
		var item = this.master.removeItem(from);
		
		if (item)
			target.master.insertItem(item, to);
	} else {
		this.master.moveItem(from, to);
	}
};

// Add any new hover-targets from ids to this.hoverTargets

KitDragDropItem.prototype.addHoverTargets = function(ids) {
	for (var i=0; i<ids.length; i++) {
		var id = ids[i].getEl().id;
		
		if (!this.hoverTargets[id])
			this.hoverTargets[id] = ids[i];
	}
	
	//this.debugHoverTargets();
}

// Remove all hover-targets in ids from this.hoverTargets

KitDragDropItem.prototype.removeHoverTargets = function(ids) {
	for (var i=0; i<ids.length; i++) {
		var id = ids[i].getEl().id;
		
		if (this.hoverTargets[id]) {
			YAHOO.util.Dom.removeClass(id, 'highlight');
			delete this.hoverTargets[id];
		}
	}
	
	//this.debugHoverTargets();
}

// Returns a single hover target for dropping on.
// Picks whatever target is closest to the dragged item.

KitDragDropItem.prototype.getHoverTarget = function() {
	var ids = [];
	
	var from = this.position;
	
	for (var id in this.hoverTargets) {
		var target = this.hoverTargets[id];
		var to = target.position;

		if (to < from || to > from + 1 || this.master != target.master)
			ids.push(id);
	}
	
	var e = this.getEl();
	
	ids.sort(function(a,b) {
		var dist = function(a, b) {
			rtn = Math.pow(YAHOO.util.Dom.getX(a)-YAHOO.util.Dom.getX(b), 2) +
			      Math.pow(YAHOO.util.Dom.getY(a)-YAHOO.util.Dom.getY(b), 2);
			
			/*console.log('(%d, %d), (%d, %d) => %d',
				YAHOO.util.Dom.getX(a),
				YAHOO.util.Dom.getY(a),
				YAHOO.util.Dom.getX(b),
				YAHOO.util.Dom.getY(b),
				rtn
			);*/
			return rtn;
		}
		
		return dist(a, e) - dist(b, e);
	});
	
	return this.hoverTargets[ids[0]];
}

// Refreshes classes of hover targets, so that only the designated
// hover target has the 'highlight' class.

KitDragDropItem.prototype.refreshHoverHighlight = function() {
	for (var id in this.hoverTargets)
		YAHOO.util.Dom.removeClass(id, 'highlight');
	
	var target = this.getHoverTarget();
	
	if (target)
		YAHOO.util.Dom.addClass(target.getEl(), 'highlight');
}

// Some debug.

KitDragDropItem.prototype.debugHoverTargets = function() {
	var ids = [];
	
	for (var id in this.hoverTargets)
		ids.push(id);
			
	ids.sort();
	
	console.log(ids);
}

/*
 * KitDragDropTarget
 *
 * Represents a drop target.
 */

/**
 * @private
 */
function KitDragDropTarget(id, position, master) {
	this.init(id);
	
	this.position = position;
	this.master   = master;
}

KitDragDropTarget.prototype = new YAHOO.util.DDTarget();