AUI.add('aui-nested-list', function(A) {
/**
 * The NestedList Utility - Full documentation coming soon.
 *
 * @module aui-nested-list
 */

var L = A.Lang,
	isString = L.isString,
	isFunction = L.isFunction,

	BLOCK = 'block',
	BODY = 'body',
	DD = 'dd',
	DISPLAY = 'display',
	DOWN = 'down',
	DRAG_NODE = 'dragNode',
	DROP_CONDITION = 'dropCondition',
	DROP_CONTAINER = 'dropContainer',
	DROP_ON = 'dropOn',
	FLOAT = 'float',
	HEIGHT = 'height',
	HELPER = 'helper',
	HIDDEN = 'hidden',
	LEFT = 'left',
	NESTED_LIST = 'nested-list',
	NODE = 'node',
	NODES = 'nodes',
	NONE = 'none',
	OFFSET_HEIGHT = 'offsetHeight',
	PLACEHOLDER = 'placeholder',
	PROXY = 'proxy',
	PX = 'px',
	RIGHT = 'right',
	SORT_CONDITION = 'sortCondition',
	UP = 'up',
	VISIBILITY = 'visibility',
	VISIBLE = 'visible',

	DDM = A.DD.DDM,

	isNodeList = function(v) {
		return (v instanceof A.NodeList);
	};

/**
 * A base class for NestedList, providing:
 * <ul>
 *    <li>Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)</li>
 *    <li>Nested sortable utility</li>
 * </ul>
 *
 * Quick Example:<br/>
 *
 * <pre><code>var nl = new A.NestedList({
 *  	nodes: '#demo li',
 *  	dropContainer: 'ul.droppable'
 *  });
 * </code></pre>
 *
 * Check the list of <a href="NestedList.html#configattributes">Configuration Attributes</a> available for
 * NestedList.
 *
 * @param config {Object} Object literal specifying widget configuration properties.
 *
 * @class NestedList
 * @constructor
 * @extends Base
 */
var NestedList = A.Component.create(
	{
		/**
		 * Static property provides a string to identify the class.
		 *
		 * @property NestedList.NAME
		 * @type String
		 * @static
		 */
		NAME: NESTED_LIST,

		/**
		 * Static property used to define the default attribute
		 * configuration for the NestedList.
		 *
		 * @property NestedList.ATTRS
		 * @type Object
		 * @static
		 */
		ATTRS: {
			dd: {
				value: null
			},

			dropCondition: {
				value: function() {
					return true;
				},
				setter: function(v) {
					return A.bind(v, this);
				},
				validator: isFunction
			},

			dropContainer: {
				value: function(event) {
					var instance = this;
					var drop = event.drop;
					var dropNode = drop.get(NODE);
					var dropOn = instance.get(DROP_ON);

					return dropNode.one(dropOn);
				},
				validator: isFunction
			},

			dropOn: {
				validator: isString
			},

			helper: {
				value: null
			},

			nodes: {
				setter: function(v) {
					return this._setNodes(v);
				}
			},

			placeholder: {
				value: null
			},

			proxy: {
				value: null,
				setter: function(val) {
					return A.merge(
						{
							moveOnEnd: false,
							positionProxy: false
						},
						val || {}
					);
				}
			},

			sortCondition: {
				value: function() {
					return true;
				},
				setter: function(v) {
					return A.bind(v, this);
				},
				validator: isFunction
			}
		},

		EXTENDS: A.Base,

		prototype: {
			/**
			 * Construction logic executed during NestedList instantiation. Lifecycle.
			 *
			 * @method initializer
			 * @protected
			 */
			initializer: function() {
				var instance = this;
				var nodes = instance.get(NODES);

				// drag & drop listeners
				instance.on('drag:align', instance._onDragAlign);
				instance.on('drag:end', instance._onDragEnd);
				instance.on('drag:exit', instance._onDragExit);
				instance.on('drag:mouseDown', instance._onDragMouseDown);
				instance.on('drag:over', instance._onDragOver);
				instance.on('drag:start', instance._onDragStart);

				instance._createHelper();

				if (nodes) {
					instance.addAll(nodes);
				}
			},

			/*
			* Methods
			*/
			add: function(node) {
				var instance = this;

				instance._createDrag(node);
			},

			addAll: function(nodes) {
				var instance = this;

				nodes.each(function(node) {
					instance.add(node);
				});
			},

			_createDrag: function(node) {
				var instance = this;
				var helper = instance.get(HELPER);

				if (!DDM.getDrag(node)) {
					var dragOptions = {
						bubbleTargets: instance,
						node: node,
						target: true
					};

					var proxyOptions = instance.get(PROXY);

					if (helper) {
						proxyOptions.borderStyle = null;
					}

					// creating delayed drag instance
					var dd = new A.DD.Drag(
						A.mix(dragOptions, instance.get(DD))
					)
					.plug(A.Plugin.DDProxy, proxyOptions);
				}
			},

			_createHelper: function() {
				var instance = this;
				var helper = instance.get(HELPER);

				if (helper) {
					// append helper to the body
					A.one(BODY).append( helper.hide() );

					instance.set(HELPER, helper);
				}
			},

			_updatePlaceholder: function(event, cancelAppend) {
				var instance = this;
				var drag = event.target;
				var drop = event.drop;
				var dragNode = drag.get(NODE);
				var dropNode = drop.get(NODE);
				var dropOn = instance.get(DROP_ON);

				var dropContainer = instance.get(DROP_CONTAINER);

				if (dropContainer) {
					var container = dropContainer.apply(instance, arguments);
				}

				var floating = false;
				var xDirection = instance.XDirection;
				var yDirection = instance.YDirection;

				if (dropNode.getStyle(FLOAT) != NONE) {
					floating = true;
				}

				var placeholder = instance.get(PLACEHOLDER);

				if (!placeholder) {
					// if no placeholder use the dragNode instead
					placeholder = dragNode;
				}

				if (!placeholder.contains(dropNode)) {
					// check for the user dropCondition
					var dropCondition = instance.get(DROP_CONDITION);

					// if there is a container waiting for nodes to be appended it's priority
					if (container && !cancelAppend && dropCondition(event)) {
						// this checking avoid the parent bubbling drag:over
						if (!container.contains(placeholder) &&
							!placeholder.contains(container)) {
								// append placeholder on the found container
								container.append(placeholder);
						}
					}
					// otherwise, check if it's floating and the xDirection
					// or if it's not floating and the yDirection
					else {
						if (floating && (xDirection == LEFT) || !floating && (yDirection == UP)) {
							// LEFT or UP directions means to place the placeholder before
							dropNode.placeBefore(placeholder);
						}
						else {
							// RIGHT or DOWN directions means to place the placeholder after
							dropNode.placeAfter(placeholder);
						}
					}
				}
			},

			/*
			* Listeners
			*/
			_onDragAlign: function(event) {
				var instance = this;
				var lastX = instance.lastX;
				var lastY = instance.lastY;
				var xy = event.target.lastXY;

				var x = xy[0];
				var y = xy[1];

				// if the y change
				if (y != lastY) {
					// set the drag vertical direction
					instance.YDirection = (y < lastY) ? UP : DOWN;
				}

				// if the x change
				if (x != lastX) {
					// set the drag horizontal direction
					instance.XDirection = (x < lastX) ? LEFT : RIGHT;
				}

				instance.lastX = x;
				instance.lastY = y;
			},

			_onDragEnd: function(event) {
				var instance = this;
				var drag = event.target;
				var dragNode = drag.get(NODE);
				var placeholder = instance.get(PLACEHOLDER);

				if (placeholder) {
					dragNode.show();
					placeholder.hide();

					if (!dragNode.contains(placeholder)) {
						// position dragNode after the placeholder
						placeholder.placeAfter(dragNode);
					}
				}
			},

			_onDragExit: function(event) {
				var instance = this;
				var sortCondition = instance.get(SORT_CONDITION);

				if (sortCondition(event)) {
					instance._updatePlaceholder(event, true);
				}
			},

			_onDragMouseDown: function(event) {
				var instance = this;
				var drag = event.target;
				var helper = instance.get(HELPER);

				if (helper) {
					// update the DRAG_NODE with the new helper
					drag.set(DRAG_NODE, helper);
				}
			},

			_onDragStart: function(event) {
		 		var instance = this;
				var drag = event.target;
				var node = drag.get(NODE);
				var helper = instance.get(HELPER);
				var placeholder = instance.get(PLACEHOLDER);

				if (placeholder) {
					// update placeholder height
					placeholder.setStyle(
						HEIGHT,
						node.get(OFFSET_HEIGHT) + PX
					);

					node.hide();
					placeholder.show();
					// position placeholder after the node
					node.placeAfter(placeholder);

					if (helper) {
						// show helper, we need display block here, yui dd hide it with display none
						helper.setStyles({
							display: BLOCK,
							visibility: VISIBLE
						}).show();
					}
				}
		 	},

			_onDragOver: function(event) {
				var instance = this;
				var sortCondition = instance.get(SORT_CONDITION);

				if (sortCondition(event)) {
					instance._updatePlaceholder(event);
				}
			},

			/*
			* Setters
			*/
			_setNodes: function(v) {
				var instance = this;

				if (isNodeList(v)) {
					return v;
				}
				else if (isString(v)) {
					return A.all(v);
				}

				return new A.NodeList([v]);
			}
		}
	}
);

A.NestedList = NestedList;

}, '1.0.1' ,{skinnable:false, requires:['aui-base','dd-drag','dd-drop','dd-proxy']});
