/**
 * Orignilly Hacked to enable Sortable/TinyMce compatibility.
 * Call back onPreUpdate is now present to retrieve data from tinyMCE editor component.
 * 
 * @author Michaël THOMAS
 * created_at 2010/09/13
 * updated_at 2010/09/13
 */
if (Object.isUndefined(Essilab))
	var Essilab = {};
Essilab.Scriptaculous = {};
Essilab.Scriptaculous.SortableObserver = Class.create({
	initialize: function(element, onStart, onStop) {
	this.element   = $(element);
	this.onStart  = onStart;
	this.onStop  = onStop;
	this.lastValue = Essilab.Scriptaculous.Sortable.serialize(this.element);
},

onStart: function(action, draggable, event) {
	if(this.lastValue != Essilab.Scriptaculous.Sortable.serialize(this.element))
		this.onStart(this.element, draggable, event);
},

onEnd: function(action, draggable, event) {

	Sortable.unmark();
	if(this.lastValue != Essilab.Scriptaculous.Sortable.serialize(this.element))
		this.onStop(this.element, draggable, event);
}
});


Essilab.Scriptaculous.Sortable = {
		SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

		sortables: { },

		_findRootElement: function(element) {
			while (element.tagName.toUpperCase() != "BODY") {  
				if(element.id && Essilab.Scriptaculous.Sortable.sortables[element.id]) return element;
				element = element.parentNode;
			}
		},

		options: function(element) {
			element = Essilab.Scriptaculous.Sortable._findRootElement($(element));
			if(!element) return;
			return Essilab.Scriptaculous.Sortable.sortables[element.id];
		},

		destroy: function(element){
			var s = Essilab.Scriptaculous.Sortable.options(element);

			if(s) {
				Draggables.removeObserver(s.element);
				s.droppables.each(function(d){ Droppables.remove(d) });
				s.draggables.invoke('destroy');

				delete Essilab.Scriptaculous.Sortable.sortables[s.element.id];
			}
		},

		create: function(element) {

			element = $(element);
			var options = Object.extend({ 
				element:     element,
				tag:         'li',       // assumes li children, override with tag: 'tagname'
				dropOnEmpty: false,
				tree:        false,
				treeTag:     'ul',
				overlap:     'vertical', // one of 'vertical', 'horizontal'
				constraint:  'vertical', // one of 'vertical', 'horizontal', false
				containment: element,    // also takes array of elements (or id's); or false
				handle:      false,      // or a CSS class
				only:        false,
				delay:       0,
				hoverclass:  null,
				ghosting:    false,
				quiet:       false, 
				scroll:      false,
				scrollSensitivity: 20,
				scrollSpeed: 15,
				format:      this.SERIALIZE_RULE,

				// these take arrays of elements or ids and can be 
				// used for better initialization performance
				elements:    false,
				handles:     false,

				onChange:    Prototype.emptyFunction,
				onUpdate:    Prototype.emptyFunction,
				onPreUpdate:    Prototype.emptyFunction

			}, arguments[1] || { });

			// clear any old sortable with same element
			this.destroy(element);

			// build options for the draggables
			var options_for_draggable = {
					revert:      true,
					quiet:       options.quiet,
					scroll:      options.scroll,
					scrollSpeed: options.scrollSpeed,
					scrollSensitivity: options.scrollSensitivity,
					delay:       options.delay,
					ghosting:    options.ghosting,
					constraint:  options.constraint,
					handle:      options.handle };

			if(options.starteffect)
				options_for_draggable.starteffect = options.starteffect;

			if(options.reverteffect)
				options_for_draggable.reverteffect = options.reverteffect;
			else
				if(options.ghosting) options_for_draggable.reverteffect = function(element) {
					element.style.top  = 0;
					element.style.left = 0;
				};

				if(options.endeffect)
					options_for_draggable.endeffect = options.endeffect;

				if(options.zindex)
					options_for_draggable.zindex = options.zindex;

				// build options for the droppables  
				var options_for_droppable = {
						overlap:     options.overlap,
						containment: options.containment,
						tree:        options.tree,
						hoverclass:  options.hoverclass,
						onHover:     Essilab.Scriptaculous.Sortable.onHover
				}

				var options_for_tree = {
						onHover:      Essilab.Scriptaculous.Sortable.onEmptyHover,
						overlap:      options.overlap,
						containment:  options.containment,
						hoverclass:   options.hoverclass
				}

				// fix for gecko engine
				Element.cleanWhitespace(element); 

				options.draggables = [];
				options.droppables = [];

				// drop on empty handling
				if(options.dropOnEmpty || options.tree) {
					Droppables.add(element, options_for_tree);
					options.droppables.push(element);
				}

				(options.elements || this.findElements(element, options) || []).each( function(e,i) {
					var handle = options.handles ? $(options.handles[i]) :
						(options.handle ? $(e).select('.' + options.handle)[0] : e); 
					options.draggables.push(
							new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
					Droppables.add(e, options_for_droppable);
					if(options.tree) e.treeNode = element;
					options.droppables.push(e);      
				});

				if(options.tree) {
					(Essilab.Scriptaculous.Sortable.findTreeElements(element, options) || []).each( function(e) {
						Droppables.add(e, options_for_tree);
						e.treeNode = element;
						options.droppables.push(e);
					});
				}

				// keep reference
				this.sortables[element.id] = options;

				// for onupdate
				Draggables.addObserver(new Essilab.Scriptaculous.SortableObserver(element, options.onPreUpdate, options.onUpdate));

		},

//		return all suitable-for-sortable elements in a guaranteed order
		findElements: function(element, options) {
			return Element.findChildren(
					element, options.only, options.tree ? true : false, options.tag);
		},

		findTreeElements: function(element, options) {
			return Element.findChildren(
					element, options.only, options.tree ? true : false, options.treeTag);
		},

		onHover: function(element, dropon, overlap) {
			if(Element.isParent(dropon, element)) return;

			if(overlap > .33 && overlap < .66 && Essilab.Scriptaculous.Sortable.options(dropon).tree) {
				return;
			} else if(overlap>0.5) {
				Essilab.Scriptaculous.Sortable.mark(dropon, 'before');
				if(dropon.previousSibling != element) {
					var oldParentNode = element.parentNode;
					element.style.visibility = "hidden"; // fix gecko rendering
					dropon.parentNode.insertBefore(element, dropon);
					if(dropon.parentNode!=oldParentNode) 
						Essilab.Scriptaculous.Sortable.options(oldParentNode).onChange(element);
					Essilab.Scriptaculous.Sortable.options(dropon.parentNode).onChange(element);
				}
			} else {
				Essilab.Scriptaculous.Sortable.mark(dropon, 'after');
				var nextElement = dropon.nextSibling || null;
				if(nextElement != element) {
					var oldParentNode = element.parentNode;
					element.style.visibility = "hidden"; // fix gecko rendering
					dropon.parentNode.insertBefore(element, nextElement);
					if(dropon.parentNode!=oldParentNode) 
						Essilab.Scriptaculous.Sortable.options(oldParentNode).onChange(element);
					Essilab.Scriptaculous.Sortable.options(dropon.parentNode).onChange(element);
				}
			}
		},

		onEmptyHover: function(element, dropon, overlap) {
			var oldParentNode = element.parentNode;
			var droponOptions = Essilab.Scriptaculous.Sortable.options(dropon);

			if(!Element.isParent(dropon, element)) {
				var index;

				var children = Essilab.Scriptaculous.Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
				var child = null;

				if(children) {
					var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

					for (index = 0; index < children.length; index += 1) {
						if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
							offset -= Element.offsetSize (children[index], droponOptions.overlap);
						} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
							child = index + 1 < children.length ? children[index + 1] : null;
							break;
						} else {
							child = children[index];
							break;
						}
					}
				}

				dropon.insertBefore(element, child);

				Essilab.Scriptaculous.Essilab.Scriptaculous.Sortable.options(oldParentNode).onChange(element);
				droponOptions.onChange(element);
			}
		},

		unmark: function() {
			if(Essilab.Scriptaculous.Sortable._marker) Essilab.Scriptaculous.Sortable._marker.hide();
		},

		mark: function(dropon, position) {
			// mark on ghosting only
			var sortable = Essilab.Scriptaculous.Sortable.options(dropon.parentNode);
			if(sortable && !sortable.ghosting) return; 

			if(!Essilab.Scriptaculous.Sortable._marker) {
				Essilab.Scriptaculous.Sortable._marker = 
					($('dropmarker') || Element.extend(document.createElement('DIV'))).
					hide().addClassName('dropmarker').setStyle({position:'absolute'});
				document.getElementsByTagName("body").item(0).appendChild(Essilab.Scriptaculous.Sortable._marker);
			}    
			var offsets = Position.cumulativeOffset(dropon);
			Essilab.Scriptaculous.Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

			if(position=='after')
				if(sortable.overlap == 'horizontal') 
					Essilab.Scriptaculous.Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
				else
					Essilab.Scriptaculous.Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

			Essilab.Scriptaculous.Sortable._marker.show();
		},

		_tree: function(element, options, parent) {
			var children = Essilab.Scriptaculous.Sortable.findElements(element, options) || [];

			for (var i = 0; i < children.length; ++i) {
				var match = children[i].id.match(options.format);

				if (!match) continue;

				var child = {
						id: encodeURIComponent(match ? match[1] : null),
						element: element,
						parent: parent,
						children: [],
						position: parent.children.length,
						container: $(children[i]).down(options.treeTag)
				}

				/* Get the element containing the children and recurse over it */
				if (child.container)
					this._tree(child.container, options, child)

					parent.children.push (child);
			}

			return parent; 
		},

		tree: function(element) {
			element = $(element);
			var sortableOptions = this.options(element);
			var options = Object.extend({
				tag: sortableOptions.tag,
				treeTag: sortableOptions.treeTag,
				only: sortableOptions.only,
				name: element.id,
				format: sortableOptions.format
			}, arguments[1] || { });

			var root = {
					id: null,
					parent: null,
					children: [],
					container: element,
					position: 0
			}

			return Essilab.Scriptaculous.Sortable._tree(element, options, root);
		},

		/* Construct a [i] index for a particular node */
		_constructIndex: function(node) {
			var index = '';
			do {
				if (node.id) index = '[' + node.position + ']' + index;
			} while ((node = node.parent) != null);
			return index;
		},

		sequence: function(element) {
			element = $(element);
			var options = Object.extend(this.options(element), arguments[1] || { });

			return $(this.findElements(element, options) || []).map( function(item) {
				return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
			});
		},

		setSequence: function(element, new_sequence) {
			element = $(element);
			var options = Object.extend(this.options(element), arguments[2] || { });

			var nodeMap = { };
			this.findElements(element, options).each( function(n) {
				if (n.id.match(options.format))
					nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
				n.parentNode.removeChild(n);
			});

			new_sequence.each(function(ident) {
				var n = nodeMap[ident];
				if (n) {
					n[1].appendChild(n[0]);
					delete nodeMap[ident];
				}
			});
		},

		serialize: function(element) {
			element = $(element);
			var options = Object.extend(Essilab.Scriptaculous.Sortable.options(element), arguments[1] || { });
			var name = encodeURIComponent(
					(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

			if (options.tree) {
				return Essilab.Scriptaculous.Sortable.tree(element, arguments[1]).children.map( function (item) {
					return [name + Essilab.Scriptaculous.Sortable._constructIndex(item) + "[id]=" + 
					        encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
				}).flatten().join('&');
			} else {
				return Essilab.Scriptaculous.Sortable.sequence(element, arguments[1]).map( function(item) {
					return name + "[]=" + encodeURIComponent(item);
				}).join('&');
			}
		}
}

//Returns true if child is contained within element
Element.isParent = function(child, element) {
	if (!child.parentNode || child == element) return false;
	if (child.parentNode == element) return true;
	return Element.isParent(child.parentNode, element);
}

Element.findChildren = function(element, only, recursive, tagName) {   
	if(!element.hasChildNodes()) return null;
	tagName = tagName.toUpperCase();
	if(only) only = [only].flatten();
	var elements = [];
	$A(element.childNodes).each( function(e) {
		if(e.tagName && e.tagName.toUpperCase()==tagName &&
				(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
			elements.push(e);
		if(recursive) {
			var grandchildren = Element.findChildren(e, only, recursive, tagName);
			if(grandchildren) elements.push(grandchildren);
		}
	});

	return (elements.length>0 ? elements.flatten() : []);
}

Element.offsetSize = function (element, type) {
	return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
}
