/* ===================================================
 *  jquery-sortable.js v0.9.13
 *  http://johnny.github.com/jquery-sortable/
 * ===================================================
 *  Copyright (c) 2012 Jonas von Andrian
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 *  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 *  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ========================================================== */

!function ( $, window, pluginName, undefined){
	var containerDefaults = {
			// If true, items can be dragged from this container
			drag: true,
			// If true, items can be droped onto this container
			drop: true,
			// Exclude items from being draggable, if the
			// selector matches the item
			exclude: "",
			// If true, search for nested containers within an item.If you nest containers,
			// either the original selector with which you call the plugin must only match the top containers,
			// or you need to specify a group (see the bootstrap nav example)
			nested: true,
			// If true, the items are assumed to be arranged vertically
			vertical: true
		}, // end container defaults
		groupDefaults = {
			// This is executed after the placeholder has been moved.
			// $closestItemOrContainer contains the closest item, the placeholder
			// has been put at or the closest empty Container, the placeholder has
			// been appended to.
			afterMove: function ($placeholder, container, $closestItemOrContainer) {
			},
			// The exact css path between the container and its items, e.g. "> tbody"
			containerPath: "",
			// The css selector of the containers
			containerSelector: "ol, ul",
			// Distance the mouse has to travel to start dragging
			distance: 0,
			// Time in milliseconds after mousedown until dragging should start.
			// This option can be used to prevent unwanted drags when clicking on an element.
			delay: 0,
			// The css selector of the drag handle
			handle: "",
			// The exact css path between the item and its subcontainers.
			// It should only match the immediate items of a container.
			// No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
			itemPath: "",
			// The css selector of the items
			itemSelector: "li",
			// The class given to "body" while an item is being dragged
			bodyClass: "dragging",
			// The class giving to an item while being dragged
			draggedClass: "dragged",
			// Check if the dragged item may be inside the container.
			// Use with care, since the search for a valid container entails a depth first search
			// and may be quite expensive.
			isValidTarget: function ($item, container) {
				return true
			},
			// Executed before onDrop if placeholder is detached.
			// This happens if pullPlaceholder is set to false and the drop occurs outside a container.
			onCancel: function ($item, container, _super, event) {
			},
			// Executed at the beginning of a mouse move event.
			// The Placeholder has not been moved yet.
			onDrag: function ($item, position, _super, event) {
				$item.css(position)
			},
			// Called after the drag has been started,
			// that is the mouse button is being held down and
			// the mouse is moving.
			// The container is the closest initialized container.
			// Therefore it might not be the container, that actually contains the item.
			onDragStart: function ($item, container, _super, event) {
				$item.css({
					height: $item.outerHeight(),
					width: $item.outerWidth()
				})
				$item.addClass(container.group.options.draggedClass)
				$("body").addClass(container.group.options.bodyClass)
			},
			// Called when the mouse button is being released
			onDrop: function ($item, container, _super, event) {
				$item.removeClass(container.group.options.draggedClass).removeAttr("style")
				$("body").removeClass(container.group.options.bodyClass)
			},
			// Called on mousedown. If falsy value is returned, the dragging will not start.
			// Ignore if element clicked is input, select or textarea
			onMousedown: function ($item, _super, event) {
				if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
					event.preventDefault()
					return true
				}
			},
			// The class of the placeholder (must match placeholder option markup)
			placeholderClass: "placeholder",
			// Template for the placeholder. Can be any valid jQuery input
			// e.g. a string, a DOM element.
			// The placeholder must have the class "placeholder"
			placeholder: '<li class="placeholder"></li>',
			// If true, the position of the placeholder is calculated on every mousemove.
			// If false, it is only calculated when the mouse is above a container.
			pullPlaceholder: true,
			// Specifies serialization of the container group.
			// The pair $parent/$children is either container/items or item/subcontainers.
			serialize: function ($parent, $children, parentIsContainer) {
				var result = $.extend({}, $parent.data())

				if(parentIsContainer)
					return [$children]
				else if ($children[0]){
					result.children = $children
				}

				delete result.subContainers
				delete result.sortable

				return result
			},
			// Set tolerance while dragging. Positive values decrease sensitivity,
			// negative values increase it.
			tolerance: 0
		}, // end group defaults
		containerGroups = {},
		groupCounter = 0,
		emptyBox = {
			left: 0,
			top: 0,
			bottom: 0,
			right:0
		},
		eventNames = {
			start: "touchstart.sortable mousedown.sortable",
			drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
			drag: "touchmove.sortable mousemove.sortable",
			scroll: "scroll.sortable"
		},
		subContainerKey = "subContainers"

	/*
	 * a is Array [left, right, top, bottom]
	 * b is array [left, top]
	 */
	function d(a,b) {
		var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
			y = Math.max(0, a[2] - b[1], b[1] - a[3])
		return x+y;
	}

	function setDimensions(array, dimensions, tolerance, useOffset) {
		var i = array.length,
			offsetMethod = useOffset ? "offset" : "position"
		tolerance = tolerance || 0

		while(i--){
			var el = array[i].el ? array[i].el : $(array[i]),
				// use fitting method
				pos = el[offsetMethod]()
			pos.left += parseInt(el.css('margin-left'), 10)
			pos.top += parseInt(el.css('margin-top'),10)
			dimensions[i] = [
				pos.left - tolerance,
				pos.left + el.outerWidth() + tolerance,
				pos.top - tolerance,
				pos.top + el.outerHeight() + tolerance
			]
		}
	}

	function getRelativePosition(pointer, element) {
		var offset = element.offset()
		return {
			left: pointer.left - offset.left,
			top: pointer.top - offset.top
		}
	}

	function sortByDistanceDesc(dimensions, pointer, lastPointer) {
		pointer = [pointer.left, pointer.top]
		lastPointer = lastPointer && [lastPointer.left, lastPointer.top]

		var dim,
			i = dimensions.length,
			distances = []

		while(i--){
			dim = dimensions[i]
			distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
		}
		distances = distances.sort(function  (a,b) {
			return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
		})

		// last entry is the closest
		return distances
	}

	function ContainerGroup(options) {
		this.options = $.extend({}, groupDefaults, options)
		this.containers = []

		if(!this.options.rootGroup){
			this.scrollProxy = $.proxy(this.scroll, this)
			this.dragProxy = $.proxy(this.drag, this)
			this.dropProxy = $.proxy(this.drop, this)
			this.placeholder = $(this.options.placeholder)

			if(!options.isValidTarget)
				this.options.isValidTarget = undefined
		}
	}

	ContainerGroup.get = function  (options) {
		if(!containerGroups[options.group]) {
			if(options.group === undefined)
				options.group = groupCounter ++

			containerGroups[options.group] = new ContainerGroup(options)
		}

		return containerGroups[options.group]
	}

	ContainerGroup.prototype = {
		dragInit: function  (e, itemContainer) {
			this.$document = $(itemContainer.el[0].ownerDocument)

			// get item to drag
			var closestItem = $(e.target).closest(this.options.itemSelector);
			// using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
			// this may also be helpful in instantiating multidrag.
			if (closestItem.length) {
				this.item = closestItem;
				this.itemContainer = itemContainer;
				if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
					return;
				}
				this.setPointer(e);
				this.toggleListeners('on');
				this.setupDelayTimer();
				this.dragInitDone = true;
			}
		},
		drag: function  (e) {
			if(!this.dragging){
				if(!this.distanceMet(e) || !this.delayMet)
					return

				this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
				this.item.before(this.placeholder)
				this.dragging = true
			}

			this.setPointer(e)
			// place item under the cursor
			this.options.onDrag(this.item,
				getRelativePosition(this.pointer, this.item.offsetParent()),
				groupDefaults.onDrag,
				e)

			var p = this.getPointer(e),
				box = this.sameResultBox,
				t = this.options.tolerance

			if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
				if(!this.searchValidTarget()){
					this.placeholder.detach()
					this.lastAppendedItem = undefined
				}
		},
		drop: function  (e) {
			this.toggleListeners('off')

			this.dragInitDone = false

			if(this.dragging){
				// processing Drop, check if placeholder is detached
				if(this.placeholder.closest("html")[0]){
					this.placeholder.before(this.item).detach()
				} else {
					this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
				}
				this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)

				// cleanup
				this.clearDimensions()
				this.clearOffsetParent()
				this.lastAppendedItem = this.sameResultBox = undefined
				this.dragging = false
			}
		},
		searchValidTarget: function  (pointer, lastPointer) {
			if(!pointer){
				pointer = this.relativePointer || this.pointer
				lastPointer = this.lastRelativePointer || this.lastPointer
			}

			var distances = sortByDistanceDesc(this.getContainerDimensions(),
					pointer,
					lastPointer),
				i = distances.length

			while(i--){
				var index = distances[i][0],
					distance = distances[i][1]

				if(!distance || this.options.pullPlaceholder){
					var container = this.containers[index]
					if(!container.disabled){
						if(!this.$getOffsetParent()){
							var offsetParent = container.getItemOffsetParent()
							pointer = getRelativePosition(pointer, offsetParent)
							lastPointer = getRelativePosition(lastPointer, offsetParent)
						}
						if(container.searchValidTarget(pointer, lastPointer))
							return true
					}
				}
			}
			if(this.sameResultBox)
				this.sameResultBox = undefined
		},
		movePlaceholder: function  (container, item, method, sameResultBox) {
			var lastAppendedItem = this.lastAppendedItem
			if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
				return;

			item[method](this.placeholder)
			this.lastAppendedItem = item
			this.sameResultBox = sameResultBox
			this.options.afterMove(this.placeholder, container, item)
		},
		getContainerDimensions: function  () {
			if(!this.containerDimensions)
				setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
			return this.containerDimensions
		},
		getContainer: function  (element) {
			return element.closest(this.options.containerSelector).data(pluginName)
		},
		$getOffsetParent: function  () {
			if(this.offsetParent === undefined){
				var i = this.containers.length - 1,
					offsetParent = this.containers[i].getItemOffsetParent()

				if(!this.options.rootGroup){
					while(i--){
						if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
							// If every container has the same offset parent,
							// use position() which is relative to this parent,
							// otherwise use offset()
							// compare #setDimensions
							offsetParent = false
							break;
						}
					}
				}

				this.offsetParent = offsetParent
			}
			return this.offsetParent
		},
		setPointer: function (e) {
			var pointer = this.getPointer(e)

			if(this.$getOffsetParent()){
				var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
				this.lastRelativePointer = this.relativePointer
				this.relativePointer = relativePointer
			}

			this.lastPointer = this.pointer
			this.pointer = pointer
		},
		distanceMet: function (e) {
			var currentPointer = this.getPointer(e)
			return (Math.max(
				Math.abs(this.pointer.left - currentPointer.left),
				Math.abs(this.pointer.top - currentPointer.top)
			) >= this.options.distance)
		},
		getPointer: function(e) {
			var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
			return {
				left: e.pageX || o.pageX,
				top: e.pageY || o.pageY
			}
		},
		setupDelayTimer: function () {
			var that = this
			this.delayMet = !this.options.delay

			// init delay timer if needed
			if (!this.delayMet) {
				clearTimeout(this._mouseDelayTimer);
				this._mouseDelayTimer = setTimeout(function() {
					that.delayMet = true
				}, this.options.delay)
			}
		},
		scroll: function  (e) {
			this.clearDimensions()
			this.clearOffsetParent() // TODO is this needed?
		},
		toggleListeners: function (method) {
			var that = this,
				events = ['drag','drop','scroll']

			$.each(events,function  (i,event) {
				that.$document[method](eventNames[event], that[event + 'Proxy'])
			})
		},
		clearOffsetParent: function () {
			this.offsetParent = undefined
		},
		// Recursively clear container and item dimensions
		clearDimensions: function  () {
			this.traverse(function(object){
				object._clearDimensions()
			})
		},
		traverse: function(callback) {
			callback(this)
			var i = this.containers.length
			while(i--){
				this.containers[i].traverse(callback)
			}
		},
		_clearDimensions: function(){
			this.containerDimensions = undefined
		},
		_destroy: function () {
			containerGroups[this.options.group] = undefined
		}
	}

	function Container(element, options) {
		this.el = element
		this.options = $.extend( {}, containerDefaults, options)

		this.group = ContainerGroup.get(this.options)
		this.rootGroup = this.options.rootGroup || this.group
		this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector

		var itemPath = this.rootGroup.options.itemPath
		this.target = itemPath ? this.el.find(itemPath) : this.el

		this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))

		if(this.options.drop)
			this.group.containers.push(this)
	}

	Container.prototype = {
		dragInit: function  (e) {
			var rootGroup = this.rootGroup

			if( !this.disabled &&
				!rootGroup.dragInitDone &&
				this.options.drag &&
				this.isValidDrag(e)) {
				rootGroup.dragInit(e, this)
			}
		},
		isValidDrag: function(e) {
			return e.which == 1 ||
				e.type == "touchstart" && e.originalEvent.touches.length == 1
		},
		searchValidTarget: function  (pointer, lastPointer) {
			var distances = sortByDistanceDesc(this.getItemDimensions(),
					pointer,
					lastPointer),
				i = distances.length,
				rootGroup = this.rootGroup,
				validTarget = !rootGroup.options.isValidTarget ||
					rootGroup.options.isValidTarget(rootGroup.item, this)

			if(!i && validTarget){
				rootGroup.movePlaceholder(this, this.target, "append")
				return true
			} else
				while(i--){
					var index = distances[i][0],
						distance = distances[i][1]
					if(!distance && this.hasChildGroup(index)){
						var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
						if(found)
							return true
					}
					else if(validTarget){
						this.movePlaceholder(index, pointer)
						return true
					}
				}
		},
		movePlaceholder: function  (index, pointer) {
			var item = $(this.items[index]),
				dim = this.itemDimensions[index],
				method = "after",
				width = item.outerWidth(),
				height = item.outerHeight(),
				offset = item.offset(),
				sameResultBox = {
					left: offset.left,
					right: offset.left + width,
					top: offset.top,
					bottom: offset.top + height
				}
			if(this.options.vertical){
				var yCenter = (dim[2] + dim[3]) / 2,
					inUpperHalf = pointer.top <= yCenter
				if(inUpperHalf){
					method = "before"
					sameResultBox.bottom -= height / 2
				} else
					sameResultBox.top += height / 2
			} else {
				var xCenter = (dim[0] + dim[1]) / 2,
					inLeftHalf = pointer.left <= xCenter
				if(inLeftHalf){
					method = "before"
					sameResultBox.right -= width / 2
				} else
					sameResultBox.left += width / 2
			}
			if(this.hasChildGroup(index))
				sameResultBox = emptyBox
			this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
		},
		getItemDimensions: function  () {
			if(!this.itemDimensions){
				this.items = this.$getChildren(this.el, "item").filter(
					":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
				).get()
				setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
			}
			return this.itemDimensions
		},
		getItemOffsetParent: function  () {
			var offsetParent,
				el = this.el
			// Since el might be empty we have to check el itself and
			// can not do something like el.children().first().offsetParent()
			if(el.css("position") === "relative" || el.css("position") === "absolute"  || el.css("position") === "fixed")
				offsetParent = el
			else
				offsetParent = el.offsetParent()
			return offsetParent
		},
		hasChildGroup: function (index) {
			return this.options.nested && this.getContainerGroup(index)
		},
		getContainerGroup: function  (index) {
			var childGroup = $.data(this.items[index], subContainerKey)
			if( childGroup === undefined){
				var childContainers = this.$getChildren(this.items[index], "container")
				childGroup = false

				if(childContainers[0]){
					var options = $.extend({}, this.options, {
						rootGroup: this.rootGroup,
						group: groupCounter ++
					})
					childGroup = childContainers[pluginName](options).data(pluginName).group
				}
				$.data(this.items[index], subContainerKey, childGroup)
			}
			return childGroup
		},
		$getChildren: function (parent, type) {
			var options = this.rootGroup.options,
				path = options[type + "Path"],
				selector = options[type + "Selector"]

			parent = $(parent)
			if(path)
				parent = parent.find(path)

			return parent.children(selector)
		},
		_serialize: function (parent, isContainer) {
			var that = this,
				childType = isContainer ? "item" : "container",

				children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
					return that._serialize($(this), !isContainer)
				}).get()

			return this.rootGroup.options.serialize(parent, children, isContainer)
		},
		traverse: function(callback) {
			$.each(this.items || [], function(item){
				var group = $.data(this, subContainerKey)
				if(group)
					group.traverse(callback)
			});

			callback(this)
		},
		_clearDimensions: function  () {
			this.itemDimensions = undefined
		},
		_destroy: function() {
			var that = this;

			this.target.off(eventNames.start, this.handle);
			this.el.removeData(pluginName)

			if(this.options.drop)
				this.group.containers = $.grep(this.group.containers, function(val){
					return val != that
				})

			$.each(this.items || [], function(){
				$.removeData(this, subContainerKey)
			})
		}
	}

	var API = {
		enable: function() {
			this.traverse(function(object){
				object.disabled = false
			})
		},
		disable: function (){
			this.traverse(function(object){
				object.disabled = true
			})
		},
		serialize: function () {
			return this._serialize(this.el, true)
		},
		refresh: function() {
			this.traverse(function(object){
				object._clearDimensions()
			})
		},
		destroy: function () {
			this.traverse(function(object){
				object._destroy();
			})
		}
	}

	$.extend(Container.prototype, API)

	/**
	 * jQuery API
	 *
	 * Parameters are
	 *   either options on init
	 *   or a method name followed by arguments to pass to the method
	 */
	$.fn[pluginName] = function(methodOrOptions) {
		var args = Array.prototype.slice.call(arguments, 1)

		return this.map(function(){
			var $t = $(this),
				object = $t.data(pluginName)

			if(object && API[methodOrOptions])
				return API[methodOrOptions].apply(object, args) || this
			else if(!object && (methodOrOptions === undefined ||
				typeof methodOrOptions === "object"))
				$t.data(pluginName, new Container($t, methodOrOptions))

			return this
		});
	};

}(jQuery, window, 'sortable');
