if(!window.Droppables)
{
	var Droppables = {};
}

Object.extend(Droppables, 
{
	add: function(inDroppable)
	{
		if(inDroppable)
		{
			if(!this.droppables)
			{
				this.droppables = [];
			}
		
			this.droppables.push(inDroppable);
		}
	},
	
	findElements: function(inElement)
	{
		var results = [];
		
		for(var i = 0; i < this.droppables.length; i++)
		{
			if(this.droppables[i].canDrop(inElement))
			{
				var elements = this.droppables[i].getDroppable();
		
				for(var j = 0; j < elements.length; j++)
				{
					if(elements[j] != inElement && elements[j].identify() != 'draggable_ghost')
					{			
						var bounds = new Bounds(elements[j]);
			
						bounds.options = this.droppables[i].options;

						results.push(bounds);
					}
				}
			}
		}
		
		return results;
	}
});

// -----------

var Draggable = Class.create(
{
	initialize: function(inElement, inOptions)
	{
		this.element = $(inElement);
		
		if(!this.element)
		{
			return;
		}

		this.options = {
			select: 'draggable',
			constrain: null,
			dragClass: 'dragging',
			hoverClass: 'hover',
			scroll: null,
			bounds: null,
			ghosting: false,
			ghostImage: null,
			onStart: null,
			onDrag: null,
			onEnd: null,
			revert: true
		};
	
		Object.extend(this.options, inOptions);
		
		var draggable = this.getDraggable();
		
		for(var i = 0; i < draggable.length; i++)
		{
			draggable[i].handle = draggable[i].select('.handle').first();
		
			if(draggable[i].handle)
			{
				draggable[i].handle.observe('mousedown', this.mousedown.bindAsEventListener(this));
			}
			else
			{
				draggable[i].observe('mousedown', this.mousedown.bindAsEventListener(this));
			}
		}

		this.element.observe('behavior:drag', this.updateBoundaries.bindAsEventListener(this));
		
		document.observe('behavior:scroll', this.updateBoundaries.bindAsEventListener(this));

		this.getDraggable();
	},

	getDraggable: function()
	{
		if(!this.draggable)
		{
			this.draggable = this.element.select('.' + this.options.select);
	
			if(this.draggable.length == 0)
			{
				this.draggable[0] = this.element;
			}
		}
		
		return this.draggable;
	},
	
	getSelected: function(inTrim)
	{
		if(this.selected)
		{
			if(inTrim)
			{
				var name = this.element.identify();
				var results = new Array(length);
				var length = this.selected.length;
				
				while(length--)
				{
					results[length] = this.selected[length]
					
					if(results[length].startsWith(name))
					{
						results[length] = results[length].substring(name.length + 1);
					}
				}
				
				return results;
			}
		
			return this.selected;
		}
		
		return [];
	},

	updateSource: function(inEvent)
	{
		if(!this.source)
		{
			return;
		}
		
		if(this.options.ghostImage)
		{
			this.source.ghost = this.source.element.insert({after: "<img src='" + this.options.ghostImage + "' style='position:fixed;'>"}).next();
			this.pointer.offsetX = -this.source.ghost.getWidth() / 2;
			this.pointer.offsetY = -this.source.ghost.getHeight() / 2;
			this.options.dragClass = null;
		}
		else
		{
			this.source.ghost = this.source.element.insert({after: this.source.element.cloneNode(true)}).next().insert({top: "<div class=\"mask\" style=\"position:absolute; top:0px; bottom:0px; right:0px; left:0px; z-index:-1;\"></div>"});
			this.source.ghost.setStyle({ position:"fixed", width: this.source.element.getWidth() + "px", height: this.source.element.getHeight() + "px" });
			
			if(this.options.dragClass)
			{
				this.source.ghost.addClassName(this.options.dragClass);
			}
		}

		this.source.ghost.id = "draggable_ghost";

		this.selected = [this.source.element.identify()];
		
		if(this.element.selected)
		{
			this.selected = this.element.selected();
		}
		
		for(var i = 0; i < this.selected.length; i++)
		{
			$(this.selected[i]).setStyle({ display: this.options.ghosting ? "block" : "none" });
		}

		this.source.ghost.bounds = new Bounds(this.source.ghost);
		
		if(this.selected.length > 1)
		{
			this.source.multiple = this.source.element.insert({after: "<div class='multiple' style='position:fixed'>" + this.selected.length + "</div>"}).next();
			this.source.multiple.offsetX = this.source.ghost.bounds.getWidth();
			this.source.multiple.offsetY = this.source.ghost.bounds.getHeight() - (this.source.multiple.getHeight() / 2);
		}
		
		if(this.options.onStart)
		{
			this.options.onStart(inEvent, this);
		}
	},
	
	updateBoundaries: function()
	{
		if(!this.source)
		{
			return;
		}

		this.viewportBounds.update();

		// ----------

		var bounds = $(this.options.bounds);

		if(!bounds)
		{
			bounds = document.viewport;
		}
	
		this.bounds = new Bounds(bounds);
		this.bounds.right -= this.source.ghost.getWidth();
		this.bounds.bottom -= this.source.ghost.getHeight();

		// --------

		this.boundaries = [];

		var scroll = $(this.options.scroll);
		
		if(scroll)
		{
			var scrollBounds = new Bounds(scroll);

			if(scroll.scrollTop > 0)
			{
				var boundary = scrollBounds.clone();
				
				boundary.bottom = boundary.top + 20;
				boundary.top = -1000000;
				boundary.scrollX = 0;
				boundary.scrollY = -10;
				boundary.element = scroll;
		
				this.boundaries.push(boundary);
			}
			
			if(scroll.scrollHeight - scroll.scrollTop > scroll.getHeight())
			{
				var boundary = scrollBounds.clone();
				
				boundary.top = boundary.bottom - 20;
				boundary.bottom = 1000000;
				boundary.scrollX = 0;
				boundary.scrollY = 10;
				boundary.element = scroll;
			
				this.boundaries.push(boundary);
			}
			
			if(scroll.scrollLeft > 0)
			{
				var boundary = scrollBounds.clone();
			
				boundary.right = boundary.left + 20;
				boundary.left = -1000000;
				boundary.scrollX = -10;
				boundary.scrollY = 0;
				boundary.element = scroll;

				this.boundaries.push(boundary);
			}
			
			if(scroll.scrollWidth - scroll.scrollLeft > scroll.getWidth())
			{
				var boundary = scrollBounds.clone();
			
				boundary.left = boundary.right - 20;
				boundary.right = 1000000;
				boundary.scrollX = 10;
				boundary.scrollY = 0;
				boundary.element = scroll;
				
				this.boundaries.push(boundary);
			}
		}

		// ---------

		if(this.viewportBounds.top > 0)
		{
			var boundary = this.viewportBounds.clone();
				
			boundary.bottom = boundary.top + 20;
			boundary.top = -1000000;
			boundary.scrollX = 0;
			boundary.scrollY = Math.max(-10, -this.viewportBounds.top);
			boundary.element = window;
		
			this.boundaries.push(boundary);
		}
	
		if(document.body.scrollHeight > this.viewportBounds.bottom)
		{
			var boundary = this.viewportBounds.clone();
			
			boundary.top = boundary.bottom - 20;
			boundary.bottom = 1000000;
			boundary.scrollX = 0;
			boundary.scrollY = Math.min(10, document.body.scrollHeight - this.viewportBounds.bottom);
			boundary.element = window;
		
			this.boundaries.push(boundary);
		}
			
		if(this.viewportBounds.left > 0)
		{
			var boundary = this.viewportBounds.clone();
		
			boundary.right = boundary.left + 20;
			boundary.left = -1000000;
			boundary.scrollX = Math.max(-10, this.viewportBounds.left);
			boundary.scrollY = 0;
			boundary.element = window;

			this.boundaries.push(boundary);
		}
			
		if(document.body.scrollWidth > this.viewportBounds.right)
		{
			var boundary = this.viewportBounds.clone();
		
			boundary.left = boundary.right - 20;
			boundary.right = 1000000;
			boundary.scrollX = Math.min(10, document.body.scrollWidth - this.viewportBounds.bottom);
			boundary.scrollY = 0;
			boundary.element = window;
				
			this.boundaries.push(boundary);
		}
		
		// ------------
		
		if(!this.droppables)
		{
			this.droppables = Droppables.findElements(this.source.element);
		}
		
		if(this.droppables)
		{
			for(var i = 0; i < this.droppables.length; i++)
			{
				this.droppables[i].update();
			}
		}
	},
	
	testBoundaries: function()
	{
		if(this.source && (!this.scrolling))
		{
			for(var i = 0; i < this.boundaries.length; i++)
			{
				if(this.boundaries[i].contains(this.pointer.x, this.pointer.y))
				{
					this.scrollBoundary(this.boundaries[i]);
					
					return;
				}
			}
			
			this.scrolling = false;
		}
	},
	
	scrollBoundary: function(inBoundary)
	{
		this.scrolling = true;	

		if(inBoundary.element == window && this.pointer)
		{
			this.pointer.x += inBoundary.scrollX;
			this.pointer.y += inBoundary.scrollY;

			window.scrollBy(inBoundary.scrollX, inBoundary.scrollY);
		}
		else
		{
			inBoundary.element.scrollLeft += inBoundary.scrollX;
			inBoundary.element.scrollTop += inBoundary.scrollY;
		}

		document.fire('behavior:scroll');

		this.timer = setTimeout(this.resetScrolling.bind(this), 50);
	},

	resetScrolling: function()
	{
		this.scrolling = false;

		this.test();
	},

	test: function()
	{
		if(!this.source)
		{
			return;
		}

		var deltaX = this.pointer.x - this.pointer.baseX;
		var deltaY = this.pointer.y - this.pointer.baseY;

		if(!this.dragStarted && (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2))
		{
			this.dragStarted = true;

			this.updateSource(this.pointer.event);

			this.element.fire('behavior:drag');
		}
		
		if(this.dragStarted)
		{
			if(this.options.constrain == 'vertical')
			{
				deltaX = 0;
			}
			else if(this.options.constrain == 'horizontal')
			{
				deltaY = 0;
			}
				
			var top = this.pointer.baseY + deltaY + this.pointer.offsetY;
			var left = this.pointer.baseX + deltaX + this.pointer.offsetX;

			if(this.bounds)
			{
				if(left < this.bounds.left)
				{
					left = this.bounds.left;
				}
					
				if(left > this.bounds.right)
				{
					left = this.bounds.right;
				}
					
				if(top < this.bounds.top)
				{
					top = this.bounds.top;
				}
					
				if(top > this.bounds.bottom)
				{
					top = this.bounds.bottom;
				}
			}
			
			this.source.ghost.bounds.moveTo(left, top);
			
			this.source.ghost.setStyle({left: (left - this.viewportBounds.left) + "px", top: (top - this.viewportBounds.top) + "px" });
			
			if(this.source.multiple)
			{
				this.source.multiple.setStyle({left: (left - this.viewportBounds.left + this.source.multiple.offsetX) + "px", top: (top - this.viewportBounds.top + this.source.multiple.offsetY) + "px" });
			}

			if(this.options.onDrag && this.source)
			{
				this.options.onDrag(this.pointer.event, this);
			}

			var droppableTarget = null;
			
			for(var i = 0; i < this.droppables.length; i++)
			{
				if(this.droppables[i].contains(this.pointer.x, this.pointer.y))
				{
					droppableTarget = this.droppables[i];
					
					break;
				}
			}

			if(droppableTarget != this.target)
			{
				if(this.target)
				{
					if(this.options.hoverClass)
					{
						this.source.ghost.removeClassName(this.options.hoverClass);
					}
				
					if(this.target.options.hoverClass)
					{
						this.target.element.removeClassName(this.target.options.hoverClass);
					}

					if(this.target.options.onLeave)
					{
						this.target.options.onLeave(this.pointer.event, this);
					}
				}
					
				this.target = droppableTarget;
					
				if(this.target)
				{
					if(this.options.hoverClass)
					{
						this.source.ghost.addClassName(this.options.hoverClass);
					}
					
					if(this.target.options.hoverClass)
					{
						this.target.element.addClassName(this.target.options.hoverClass);
					}

					if(this.target.options.onEnter)
					{
						this.target.options.onEnter(this.pointer.event, this);
					}
				}
			}
				
			if(this.target && this.target.options.onHover)
			{
				var xOffset = (2.0 * (this.pointer.x - this.target.left) - this.target.getWidth()) / (2.0 * this.source.ghost.getWidth()) + 0.5;
				var yOffset = (2.0 * (this.pointer.y - this.target.top) - this.target.getHeight()) / (2.0 * this.source.ghost.getHeight()) + 0.5;
				
				this.target.options.onHover(this.pointer.event, this, xOffset, yOffset);
			}

			this.testBoundaries();
		}
	},

	mousedown: function(inEvent)
	{
		var element = inEvent.element();
		
		if(element.hasClassName("input"))
		{
			return;
		}
			
		if(!element.hasClassName(this.options.select))
		{
			element = element.up('.' + this.options.select);
		}

		if(!element)
		{
			return;
		}
		
		this.dragStarted = false;
		
		this.viewportBounds = new Bounds(document.viewport);

		this.source = {element: element };
		this.source.bounds = new Bounds(element);

		this.pointer = { x: inEvent.pointerX(), y: inEvent.pointerY() };
		this.pointer.baseX = this.pointer.x;
		this.pointer.baseY = this.pointer.y;
		this.pointer.offsetX = this.source.bounds.left - this.pointer.x;
		this.pointer.offsetY = this.source.bounds.top - this.pointer.y;
		
		this.mousemoveFunction = this.mousemove.bindAsEventListener(this);
		this.mouseupFunction = this.mouseup.bindAsEventListener(this);

		document.observe('mousemove', this.mousemoveFunction);
		document.observe('mouseup', this.mouseupFunction);
	},

	mousemove: function(inEvent)
	{
		if(this.source)
		{
			this.pointer.x = inEvent.pointerX();
			this.pointer.y = inEvent.pointerY();
			this.pointer.event = inEvent;
			
			this.test();
		}
	},
		
	mouseup: function(inEvent)
	{
		if(this.source && this.dragStarted)
		{
			if(this.source.ghost)
			{
				this.source.ghost.remove();
			}
			
			if(this.source.multiple)
			{
				this.source.multiple.remove();
			}

			if(this.target && this.target.options.onDrop)
			{
				this.target.options.onDrop(inEvent, this);
			}

			if(this.options.onEnd)
			{
				this.options.onEnd(inEvent, this);
			}

			if(this.options.revert && this.selected)
			{
				for(var i = 0; i < this.selected.length; i++)
				{
					$(this.selected[i]).setStyle({display:""});
				}
			}
				
			document.stopObserving('mousemove', this.mousemoveFunction);
			document.stopObserving('mouseup', this.mouseupFunction);

			this.element.fire('behavior:update');
			
			if(this.target)
			{
				this.target.element.removeClassName(this.target.options.hoverClass);
			}
		}
		
		this.selected = null;
		this.source = null;
		this.target = null;
		this.droppables = null;
		this.pointer = null;
		this.dragStarted = false;
	}
});

var Droppable = Class.create(
{
	initialize: function(inElement, inOptions)
	{
		this.element = $(inElement);
		
		if(!this.element)
		{
			return;
		}
		
		this.options = {
			select: 'droppable',
			hoverClass: 'hover',
			accept: null,
			containment: null,
			onHover: null,
			onDrop: null,
			onEnter: null,
			onLeave: null
		};

		Object.extend(this.options, inOptions);
		
		// -------

		Droppables.add(this);

		this.getDroppable();
	},
	
	getDroppable: function()
	{
		if(!this.droppable)
		{
			this.droppable = this.element.select('.' + this.options.select);
			
			if(this.droppable.length == 0)
			{
				this.droppable[0] = this.element;
			}
		}
		
		return this.droppable;
	},
	
	canDrop: function(inElement)
	{
		if(this.options.containment)
		{
			if(!inElement.descendantOf(this.options.containment))
			{
				return false;
			}
		}
		
		if(this.options.accept)
		{
			if(!inElement.hasClassName(this.options.accept))
			{
				return false;
			}
		}
		
		return true;
	}
});

var Positionable = Class.create(
{
	initialize: function(inElement, inOptions)
	{
		this.element = $(inElement);
		
		if(!this.element)
		{
			return;
		}
		
		this.options = {
			select: 'positionable',
			tree: false,
			constrain: null,
			dragClass: 'dragging',
			hoverClass: null,
			inClass: 'in',
			beforeClass: 'before',
			afterClass: 'after',
			placeholderClass: null,
			scroll: null,
			bounds: null,
			onUpdate: null,
			onChange: null
		};
		
		Object.extend(this.options, inOptions);
		
		// -------
		
		var positionedID = "positioned_" + this.element.identify()
		
		this.positioned = $(positionedID);
		
		if(!this.positioned)
		{
			this.positioned = this.element.insert({before: "<input type='hidden' name='" + positionedID + "'>"}).previous();
		}
		
		// -------
		
		if(!this.options.placeholderClass)
		{
			this.options.placeholderClass = this.options.select;
		}
		
		new Draggable(inElement, {
			select: this.options.select,
			constrain: this.options.constrain,
			dragClass: this.options.dragClass,
			scroll: this.options.scroll,
			bounds: this.options.bounds,
			ghosting: this.options.tree,
			onStart: this.start.bind(this),
			onEnd: this.end.bind(this)
		});
		
		new Droppable(inElement, {
			select: this.options.select,
			hoverClass: this.options.hoverClass,
			accept: this.options.select,
			containment: this.element,
			onHover: this.hover.bind(this),
			onEnter: this.enter.bind(this),
			onLeave: this.leave.bind(this)
		});
	
		this.placeholder = null;
		
		this.target = null;
		
		this.position = null;
	},

	update: function(inDraggable)
	{
		if(this.options.tree)
		{
			if(this.target && this.position)
			{
				this.positioned.value = inDraggable.getSelected().join(",") + " " + this.position + " " + this.target.identify();
			}
		}
		else
		{
			var positionable = this.element.select('.' + this.options.select);
				
			var ids = [];
			
			for(var i = 0; i < positionable.length; i++)
			{
				ids.push(positionable[i].identify());
			}
			
			this.positioned.value = ids.join(",");
		}
	},
	
	start: function(inEvent, inDraggable)
	{
		var source = inDraggable.source.element;
	
		if(this.options.tree)
		{
			this.placeholder = null;
		}
		else
		{
			var dimensions = source.getDimensions();
		
			this.placeholder = source.insert({before: "<div></div>"}).previous();
			this.placeholder.setStyle({width: dimensions.width + "px", height: dimensions.height + "px"});
	
			if(this.options.placeholderClass)
			{
				this.placeholder.addClassName(this.options.placeholderClass);
			}
		}
	},

	enter: function(inEvent, inDraggable)
	{
		this.target = inDraggable.target.element;
		this.position = null;
	},

	leave: function(inEvent, inDraggable)
	{
		this.target = null;
		this.position = null;
		
		if(this.options.tree)
		{
			var target = inDraggable.target.element;
			
			target.removeClassName(this.options.inClass);
			target.removeClassName(this.options.afterClass);
			target.removeClassName(this.options.beforeClass);
		}
	},

	hover: function(inEvent, inDraggable, inOffsetX, inOffsetY)
	{
		var source = inDraggable.source.element;
		var target = inDraggable.target.element;
		
		if(this.options.tree)
		{
			var position = "in";

			if(inOffsetY < 0.25)
			{
				position = "before";
			}
			else if(inOffsetY > 0.75)
			{
				position = "after";
			}
	
			if(this.position != position)
			{
				this.position = position;
				
				target.removeClassName(this.options.inClass);
				target.removeClassName(this.options.afterClass);
				target.removeClassName(this.options.beforeClass);
				
				if(position == "before" && this.options.beforeClass)
				{
					target.addClassName(this.options.beforeClass);
				}
				else if(position == "after" && this.options.afterClass)
				{
					target.addClassName(this.options.afterClass);
				}
				else
				{
					target.addClassName(this.options.inClass);
				}
			}
		}
		else
		{
			if(inOffsetY < 0 || inOffsetY > 1.0)
			{
				return;
			}
			
			var position = "before";
	
			if(inOffsetY < 0.5)
			{
				position = "after";
			}
		
			if(this.position != position)
			{
				this.position = position;
				
				if(position == "before")
				{
					this.target.insert({before: this.placeholder});
				}
				else
				{
					this.target.insert({after: this.placeholder});
				}
	
				this.element.fire('behavior:drag');
	
				if(this.options.onChange)
				{
					this.options.onChange(source, this.target, this.position);
				}
			}
		}
	},
	
	end: function(inEvent, inDraggable)
	{
		if(this.placeholder)
		{
			var selected = inDraggable.getSelected(false);

			for(var i = selected.length - 1; i >= 0; i--)
			{
				this.placeholder.insert({after: $(selected[i])});
			}

			this.placeholder.remove();
		}

		this.update(inDraggable);
		
		this.placeholder = null;

		if(this.options.onUpdate)
		{
			this.options.onUpdate(inDraggable);
		}
	}
});

var Moveable = Class.create(
{
	initialize: function(inSource, inTarget, inOptions)
	{
		this.sourceElement = $(inSource);
		this.targetElement = $(inTarget);
		
		if(!this.sourceElement)
		{
			return;
		}
		
		if(!this.targetElement)
		{
			return;
		}
		
		
		this.options = {
			sourceSelect: 'row',
			targetSelect: 'row',
			dragClass: 'dragging',
			hoverClass: 'hover',
			ghostImage: null,
			onUpdate: null,
			onChange: null
		};
		
		Object.extend(this.options, inOptions);
		
		// -------

		var positionedID = "moved_" + this.sourceElement.identify();
		
		this.positioned = $(positionedID);
		
		if(!this.positioned)
		{
			this.positioned = this.sourceElement.insert({before: "<input type='hidden' name='" + positionedID + "'>"}).previous();
		}
		
		// -------
		
		new Draggable(this.sourceElement, {
			select: this.options.sourceSelect,
			dragClass: this.options.dragClass,
			hoverClass: this.options.hoverClass,
			ghosting: true,
			ghostImage: this.options.ghostImage,
			onEnd: this.end.bind(this),
			bounds: this.options.bounds,
			constrain: this.options.constrain,
			scroll: this.options.scroll
		});
		
		new Droppable(this.targetElement, {
			select: this.options.targetSelect,
			hoverClass: this.options.hoverClass,
			accept: this.options.sourceSelect,
			containment: this.sourceElement,
			onEnter: this.enter.bind(this),
			onLeave: this.leave.bind(this)
		});
		
		this.target = null;
	},

	update: function(inDraggable)
	{
		if(this.target)
		{
			this.positioned.value = inDraggable.getSelected().join(",") + " in " + this.target.identify();
		}
	},
	
	enter: function(inEvent, inDraggable)
	{
		this.target = inDraggable.target.element;
		
		if(this.options.onChange)
		{
			this.options.onChange(inDraggable.source.element, this.target);
		}
	},

	leave: function(inEvent, inDraggable)
	{
		this.target = null;
		
		if(this.options.onChange)
		{
			this.options.onChange(inDraggable.source.element, this.target);
		}
	},

	end: function(inEvent, inDraggable)
	{
		if(this.target)
		{
			this.update(inDraggable);
			
			if(this.options.onUpdate)
			{
				this.options.onUpdate(inDraggable);
			}
		}
	}
});

