/**
 * Base class from Ext.ux.TabReorderer.
 */
Ext.define('Ext.ux.BoxReorderer', {

	mixins: {
		observable: 'Ext.util.Observable'
	},

	alias: "plugin.boxreorder",

	/**
	 * @cfg {String} itemSelector
	 * A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child
	 * Components which participate in reordering.
	 */
	itemSelector: '.x-box-item',

	/**
	 * @cfg {Mixed} animate
	 * If truthy, child reordering is animated so that moved boxes slide smoothly into position.
	 * If this option is numeric, it is used as the animation duration in milliseconds.
	 */
	animate: 100,

	/**
	 * @event StartDrag
	 * Fires when dragging of a child Component begins.
	 * @param {Ext.ux.BoxReorderer} this
	 * @param {Ext.container.Container} container The owning Container
	 * @param {Ext.Component} dragCmp The Component being dragged
	 * @param {Number} idx The start index of the Component being dragged.
	 */

	/**
	 * @event Drag
	 * Fires during dragging of a child Component.
	 * @param {Ext.ux.BoxReorderer} this
	 * @param {Ext.container.Container} container The owning Container
	 * @param {Ext.Component} dragCmp The Component being dragged
	 * @param {Number} startIdx The index position from which the Component was initially dragged.
	 * @param {Number} idx The current closest index to which the Component would drop.
	 */

	/**
	 * @event ChangeIndex
	 * Fires when dragging of a child Component causes its drop index to change.
	 * @param {Ext.ux.BoxReorderer} this
	 * @param {Ext.container.Container} container The owning Container
	 * @param {Ext.Component} dragCmp The Component being dragged
	 * @param {Number} startIdx The index position from which the Component was initially dragged.
	 * @param {Number} idx The current closest index to which the Component would drop.
	 */

	/**
	 * @event Drop
	 * Fires when a child Component is dropped at a new index position.
	 * @param {Ext.ux.BoxReorderer} this
	 * @param {Ext.container.Container} container The owning Container
	 * @param {Ext.Component} dragCmp The Component being dropped
	 * @param {Number} startIdx The index position from which the Component was initially dragged.
	 * @param {Number} idx The index at which the Component is being dropped.
	 */

	constructor: function() {
		this.mixins.observable.constructor.apply(this, arguments);
	},

	init: function(container) {
		var me = this;

		me.container = container;

		// Set our animatePolicy to animate the start position (ie x for HBox, y for VBox)
		me.animatePolicy = {};
		me.animatePolicy[container.getLayout().names.x] = true;



		// Initialize the DD on first layout, when the innerCt has been created.
		me.container.on({
			scope: me,
			boxready: me.onBoxReady,
			beforedestroy: me.onContainerDestroy
		});
	},

	/**
	 * @private Clear up on Container destroy
	 */
	onContainerDestroy: function() {
		var dd = this.dd;
		if (dd) {
			dd.unreg();
			this.dd = null;
		}
	},

	onBoxReady: function() {
		var me = this,
			layout = me.container.getLayout(),
			names = layout.names,
			dd;

		// Create a DD instance. Poke the handlers in.
		// TODO: Ext5's DD classes should apply config to themselves.
		// TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
		// TODO: Ext5's DD classes should be Observable.
		// TODO: When all the above are trus, this plugin should extend the DD class.
		dd = me.dd = new Ext.dd.DD(layout.innerCt, me.container.id + '-reorderer');
		Ext.apply(dd, {
			animate: me.animate,
			reorderer: me,
			container: me.container,
			getDragCmp: me.getDragCmp,
			clickValidator: Ext.Function.createInterceptor(dd.clickValidator, me.clickValidator, me, false),
			onMouseDown: me.onMouseDown,
			startDrag: me.startDrag,
			onDrag: me.onDrag,
			endDrag: me.endDrag,
			getNewIndex: me.getNewIndex,
			doSwap: me.doSwap,
			findReorderable: me.findReorderable
		});

		// Decide which dimension we are measuring, and which measurement metric defines
		// the *start* of the box depending upon orientation.
		dd.dim = names.width;
		dd.startAttr = names.beforeX;
		dd.endAttr = names.afterX;
	},

	getDragCmp: function(e) {
		return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
	},

	// check if the clicked component is reorderable
	clickValidator: function(e) {
		var cmp = this.getDragCmp(e);

		// If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
		return !!(cmp && cmp.reorderable !== false);
	},

	onMouseDown: function(e) {
		var me = this,
			container = me.container,
			containerBox,
			cmpEl,
			cmpBox;

		// Ascertain which child Component is being mousedowned
		me.dragCmp = me.getDragCmp(e);
		if (me.dragCmp) {
			cmpEl = me.dragCmp.getEl();
			me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);

			// Start position of dragged Component
			cmpBox = cmpEl.getBox();

			// Last tracked start position
			me.lastPos = cmpBox[me.startAttr];

			// Calculate constraints depending upon orientation
			// Calculate offset from mouse to dragEl position
			containerBox = container.el.getBox();
			if (me.dim === 'width') {
				me.minX = containerBox.left;
				me.maxX = containerBox.right - cmpBox.width;
				me.minY = me.maxY = cmpBox.top;
				me.deltaX = e.getX() - cmpBox.left;
			} else {
				me.minY = containerBox.top;
				me.maxY = containerBox.bottom - cmpBox.height;
				me.minX = me.maxX = cmpBox.left;
				me.deltaY = e.getY() - cmpBox.top;
			}
			me.constrainY = me.constrainX = true;
		}
	},

	startDrag: function() {
		var me = this,
			dragCmp = me.dragCmp;

		if (dragCmp) {
			// For the entire duration of dragging the *Element*, defeat any positioning and animation of the dragged *Component*
			dragCmp.setPosition = Ext.emptyFn;
			dragCmp.animate = false;

			// Animate the BoxLayout just for the duration of the drag operation.
			if (me.animate) {
				me.container.getLayout().animatePolicy = me.reorderer.animatePolicy;
			}
			// We drag the Component element
			me.dragElId = dragCmp.getEl().id;
			me.reorderer.fireEvent('StartDrag', me, me.container, dragCmp, me.curIndex);
			// Suspend events, and set the disabled flag so that the mousedown and mouseup events
			// that are going to take place do not cause any other UI interaction.
			dragCmp.suspendEvents();
			dragCmp.disabled = true;
			dragCmp.el.setStyle('zIndex', 100);
		} else {
			me.dragElId = null;
		}
	},

	/**
	 * @private
	 * Find next or previous reorderable component index.
	 * @param {Number} newIndex The initial drop index.
	 * @return {Number} The index of the reorderable component.
	 */
	findReorderable: function(newIndex) {
		var me = this,
			items = me.container.items,
			newItem;

		if (items.getAt(newIndex).reorderable === false) {
			newItem = items.getAt(newIndex);
			if (newIndex > me.startIndex) {
				 while(newItem && newItem.reorderable === false) {
					newIndex++;
					newItem = items.getAt(newIndex);
				}
			} else {
				while(newItem && newItem.reorderable === false) {
					newIndex--;
					newItem = items.getAt(newIndex);
				}
			}
		}

		newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);

		if (items.getAt(newIndex).reorderable === false) {
			return -1;
		}
		return newIndex;
	},

	/**
	 * @private
	 * Swap 2 components.
	 * @param {Number} newIndex The initial drop index.
	 */
	doSwap: function(newIndex) {
		var me = this,
			items = me.container.items,
			container = me.container,
			wasRoot = me.container._isLayoutRoot,
			orig, dest, tmpIndex;

		newIndex = me.findReorderable(newIndex);

		if (newIndex === -1) {
			return;
		}

		me.reorderer.fireEvent('ChangeIndex', me, container, me.dragCmp, me.startIndex, newIndex);
		orig = items.getAt(me.curIndex);
		dest = items.getAt(newIndex);
		items.remove(orig);
		tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
		items.insert(tmpIndex, orig);
		items.remove(dest);
		items.insert(me.curIndex, dest);

		// Make the Box Container the topmost layout participant during the layout.
		container._isLayoutRoot = true;
		container.updateLayout();
		container._isLayoutRoot = wasRoot;
		me.curIndex = newIndex;
	},

	onDrag: function(e) {
		var me = this,
			newIndex;

		newIndex = me.getNewIndex(e.getPoint());
		if ((newIndex !== undefined)) {
			me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
			me.doSwap(newIndex);
		}

	},

	endDrag: function(e) {
		if (e) {
			e.stopEvent();
		}
		var me = this,
			layout = me.container.getLayout(),
			temp;

		if (me.dragCmp && !me.reorderer.removeOnDrop(e.target.offsetParent, me.dragCmp)) {
			delete me.dragElId;

			// Reinstate the Component's positioning method after mouseup, and allow the layout system to animate it.
			delete me.dragCmp.setPosition;
			me.dragCmp.animate = true;

			// Ensure the lastBox is correct for the animation system to restore to when it creates the "from" animation frame
			me.dragCmp.lastBox[layout.names.x] = me.dragCmp.getPosition(true)[layout.names.widthIndex];

			// Make the Box Container the topmost layout participant during the layout.
			me.container._isLayoutRoot = true;
			me.container.updateLayout();
			me.container._isLayoutRoot = undefined;

			// Attempt to hook into the afteranimate event of the drag Component to call the cleanup
			temp = Ext.fx.Manager.getFxQueue(me.dragCmp.el.id)[0];
			if (temp) {
				temp.on({
					afteranimate: me.reorderer.afterBoxReflow,
					scope: me
				});
			}
			// If not animated, clean up after the mouseup has happened so that we don't click the thing being dragged
			else {
				Ext.Function.defer(me.reorderer.afterBoxReflow, 1, me);
			}

			if (me.animate) {
				delete layout.animatePolicy;
			}
			me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
		}
	},

	// THIS IS FOR HANDLING DROP IF WE SHOULD REMOVE THEM
	removeOnDrop: function() {
		return false;
	},

	/**
	 * @private
	 * Called after the boxes have been reflowed after the drop.
	 * Re-enabled the dragged Component.
	 */
	afterBoxReflow: function() {
		var me = this;
		me.dragCmp.el.setStyle('zIndex', '');
		me.dragCmp.disabled = false;
		me.dragCmp.resumeEvents();
	},

	/**
	 * @private
	 * Calculate drop index based upon the dragEl's position.
	 */
	getNewIndex: function(pointerPos) {
		var me = this,
			dragEl = me.getDragEl(),
			dragBox = Ext.fly(dragEl).getBox(),
			targetEl,
			targetBox,
			targetMidpoint,
			i = 0,
			it = me.container.items.items,
			ln = it.length,
			lastPos = me.lastPos;

		me.lastPos = dragBox[me.startAttr];

		for (; i < ln; i++) {
			targetEl = it[i].getEl();

			// Only look for a drop point if this found item is an item according to our selector
			if (targetEl.is(me.reorderer.itemSelector)) {
				targetBox = targetEl.getBox();
				targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
				if (i < me.curIndex) {
					if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
						return i;
					}
				} else if (i > me.curIndex) {
					if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {
						return i;
					}
				}
			}
		}
	}
});
