// ===================================================================
// Author: Denis Howlett <feedback@isocra.com>
// WWW: http://www.isocra.com/
// ===================================================================

// Revised Oct 2008
// Now shows a floating copy of the dragged row(s)
// Also grabs child rows

/** Keep hold of the current table being dragged */
var currenttable = null;
var debug = null;

/** Capture the onmousemove so that we can see if a row from the current
 *  table if any is being dragged.
 * @param ev the event (for Firefox and Safari, otherwise we use window.event for IE)
 */
document.onmousemove = function(ev){
	if (currenttable && currenttable.dragObject) {
		ev   = ev || window.event;
		var mousePos = currenttable.mouseCoords(ev);
		var x = mousePos.x - currenttable.mouseOffset.x;
		var y = mousePos.y - currenttable.mouseOffset.y;

		if(debug) { debug.innerHTML = "x:" + x + "<br>y:" + y + "mousePos.x:" + mousePos.x + "<br>mousePos.y:" + mousePos.y; }


		var doX = false;
		if (y != currenttable.oldY) {
			currenttable.dragger.style.top = (y - 2) + "px";

			// work out if we're going up or down...
			var movingDown = y > currenttable.oldY;
			// update the old value
			currenttable.oldY = y;

			// If we're over a row then move the dragged row to there so that the user sees the
			// effect dynamically
			var currentRow = currenttable.findDropTargetRow(y);
			if (currentRow) {
				//parentdepth = currentRow.className.match(/depth([0-9])/)[1];
				var otherrow = currenttable.dragObject != currentRow;
				for(i = 0; i < currenttable.dragChildren.length && !otherrow; ++i) {
					if(currenttable.dragChildren[i] == currentRow) { otherrow = false; }
				}
				if (movingDown && otherrow) {
					var lastin = currentRow.nextSibling;
					for(i = currenttable.dragChildren.length - 1; i >= 0; i--) {
						currenttable.dragObject.parentNode.insertBefore(currenttable.dragChildren[i], lastin);
						lastin = currenttable.dragChildren[i];
					}
					currenttable.dragObject.parentNode.insertBefore(currenttable.dragObject, lastin);
					doX = true;
				} else if (! movingDown && otherrow) {
					var lastin = currentRow;
					for(i = currenttable.dragChildren.length - 1; i >= 0; --i) {
						currenttable.dragObject.parentNode.insertBefore(currenttable.dragChildren[i], lastin);
						lastin = currenttable.dragChildren[i];
					}
					currenttable.dragObject.parentNode.insertBefore(currenttable.dragObject, lastin);
					doX = true;
				}
			}
		}
		if(x != currenttable.oldX || doX) {
			currenttable.dragger.style.left = (x - 2) + "px";
			currenttable.oldX = x;

			//Indenting by changing the depth class on the row, 0-based
			depth = Math.min(Math.max(Math.floor((x - currenttable.tableOffset.x) / 25), 0), currenttable.limit);
			if(debug) { debug.innerHTML = "curdepth:" + currenttable.curdepth + "<br>depth:" + depth + "<br>cursize:" + currenttable.cursize + "<br>limit:" + currenttable.limit; }

			//if doX is true, snap to nearest allowable depth
			if(doX) {
				var newdepth = 0;
				while(!currenttable.isValidDepth(newdepth) && newdepth <= currenttable.limit) { newdepth++; };
				depth = newdepth;
			}

			if(depth != currenttable.curdepth && currenttable.isValidDepth(depth)) {
				oldclass = currenttable.dragObject.className;
				currenttable.dragObject.className = oldclass.replace(/depth[0-9]/, "depth" + depth);
				currenttable.setDepth(currenttable.dragObject, depth);
				for(i = 0; i < currenttable.dragChildren.length; ++i) {
					mydepth = currenttable.getDepth(currenttable.dragChildren[i]) - currenttable.curdepth + depth;
					currenttable.dragChildren[i].className = currenttable.dragChildren[i].className.replace(/depth[0-9]/, "depth" + mydepth);
					currenttable.setDepth(currenttable.dragChildren[i], mydepth);
				}
				currenttable.curdepth = depth;
			}
		}

		return false;
	}
}

// Similarly for the mouseup
document.onmouseup   = function(ev){
	if (currenttable && currenttable.dragObject) {
		var droppedRow = currenttable.dragObject;
		// If we have a dragObject, then we need to release it,
		// The row will already have been moved to the right place so we just reset stuff
		droppedRow.style.backgroundColor = 'transparent';
		droppedRow.style.opacity = 1;
		droppedRow.style.filter = 'alpha(opacity=100)';

		currenttable.dragObject   = null;
		currenttable.dragger.style.display = 'none';
		// And then call the onDrop method in case anyone wants to do any post processing
		currenttable.onDrop(currenttable.table, droppedRow);
		currenttable = null; // let go of the table too
	}
}


/** get the source element from an event in a way that works for IE and Firefox and Safari
 * @param evt the source event for Firefox (but not IE--IE uses window.event) */
function getEventSource(evt) {
	if (window.event) {
		evt = window.event; // For IE
		return evt.srcElement;
	} else {
		return evt.target; // For Firefox
	}
}

/**
 * Encapsulate table Drag and Drop in a class. We'll have this as a Singleton
 * so we don't get scoping problems.
 */
function TableDnD() {
	/** Keep hold of the current drag object if any */
	this.dragObject = null;
	this.dragChildren = null;

	/** The current mouse offset */
	this.mouseOffset = null;
	this.tableOffset = null;
	/** The current table */
	this.table = null;
	/** Remember the old value of Y so that we don't do too much processing */
	this.oldX = 0;
	this.oldY = 0;
	this.curdepth = 0;
	this.cursize = 0;

	this.dragger = null;


	/** Initialise the drag and drop by capturing mouse move events */
	this.init = function(table, limit) {
		this.table = table;
		this.limit = limit;
		var rows = table.tBodies[0].rows; //getElementsByTagName("tr")
		for (var i=0; i<rows.length; i++) {
			// John Tarr: added to ignore rows that I've added the NoDnD attribute to (Category and Header rows)
			var nodrag = rows[i].getAttribute("NoDrop")
			if (nodrag == null || nodrag == "undefined") { //There is no NoDnD attribute on rows I want to drag
				this.makeDraggable(rows[i]);
			}
		}
		this.tableOffset = this.getPosition(table);
	}

	/** This function is called when you drop a row, so redefine it in your code
		to do whatever you want, for example use Ajax to update the server */
	this.onDrop = function(table, droppedRow) {
		// Do nothing for now
	}

	this.setDepth = function(row, depth) {
		// Called when a row changes depth
	}

	this.getParentRow = function() {
		var parent = null;
		for(i = 0; i < this.table.rows.length; ++i) {
			if(this.table.rows[i] == this.dragObject) {
				parent = this.table.rows[i - 1];
			}
		}
		return parent;
	}

	this.getLastChild = function() {
		var obj = this.dragObject;
		if(this.dragChildren && this.dragChildren.length) {
			obj = this.dragChildren[this.dragChildren.length - 1];
		}
		return obj;
	}

	/** This function checks whether a depth value is valid based on the row's position */
	this.isValidDepth = function(depth) {
		var isValid = true;

		var parent = null;
		for(i = 0; i < this.table.rows.length; ++i) {
			if(this.table.rows[i] == this.dragObject) {
				if(i == 0) {
					parent = null;
				} else {
					parent = this.table.rows[i - 1];
				}
			}
		}
		var parentdepth = -1;
		if(parent) { parentdepth = this.getDepth(parent); }

		var child = null;
		for(i = 0; i < this.table.rows.length; ++i) {
			if(this.table.rows[i] == this.getLastChild()) {
				child = this.table.rows[i + 1];
			}
		}
		var childdepth = 0;
		if(child) { childdepth = this.getDepth(child); }

		if(parentdepth == childdepth) { // They're just 2 consecutive rows at the same depth
			isValid = (depth == parentdepth || depth == (parentdepth + 1));
		} else if(parentdepth < childdepth) { // Child is the first child of the parent
			isValid = (depth == childdepth);
		} else if(parentdepth > childdepth) { // Parent is the last child of a group and child is the next parent-level
			isValid = (depth == childdepth || depth == (childdepth + 1) || depth == parentdepth || depth == (parentdepth + 1));
		}

		//debug.innerHTML = "<br><br>parent:" + (parent ? parent.id : parent) + "," + parentdepth + "<br>child:" + (child ? child.id : child) + "," + childdepth;

		return isValid && (depth + currenttable.cursize) <= currenttable.limit; // Make sure we're not too deep
	}

	/** This function will read the depth classes and return a string that can be passed as a GET
	 */
	this.serialize = function() {
		//We need tableid[0][id]=rowid&tableid[0][depth]=depth, in order
		// Ideally we could put this in an array, but probably easier to do that in PHP
		var str = "";
		for(i = 0; i < this.table.rows.length; ++i) {
			depth = this.getDepth(this.table.rows[i]);
			str = str + this.table.id + "[" + i + "][id]=" + this.table.rows[i].id + "&"+ this.table.id + "[" + i + "][depth]=" + depth + "&";
		}
		return str;
	}
	/** This function will read the depth classes and return a string that can be passed as a POST
	 */
	this.serializep = function() {
		//We need tableid[0][id]=rowid&tableid[0][depth]=depth, in order
		// Creates additional form fields for each row
		var str = "";
		for(i = 0; i < this.table.rows.length; ++i) {
			depth = this.getDepth(this.table.rows[i]);
			str = str + "<input type='hidden' name='" + this.table.id + "[" + i + "][id]' value='" + this.table.rows[i].id + "' /><input type='hidden' name='"+ this.table.id + "[" + i + "][depth]' value='" + depth + "' />";
		}
		return str;
	}

	/** Get the position of an element by going up the DOM tree and adding up all the offsets */
	this.getPosition = function(e){
		var left = 0;
		var top  = 0;
		/** Safari fix -- thanks to Luis Chato for this! */
		if (e.offsetHeight == 0) {
			/** Safari 2 doesn't correctly grab the offsetTop of a table row
				this is detailed here:
				http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
				the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
				note that firefox will return a text node as a first child, so designing a more thorough
				solution may need to take that into account, for now this seems to work in firefox, safari, ie */
			e = e.firstChild; // a table cell
		}

		while (e.offsetParent){
			left += e.offsetLeft;
			top  += e.offsetTop;
			e     = e.offsetParent;
		}

		left += e.offsetLeft;
		top  += e.offsetTop;

		return {x:left, y:top};
	}

	/** Get the mouse coordinates from the event (allowing for browser differences) */
	this.mouseCoords = function(ev){
		if(ev.pageX || ev.pageY){
			return {x:ev.pageX, y:ev.pageY};
		}
		return {
			x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
			y:ev.clientY + document.body.scrollTop  - document.body.clientTop
		};
	}

	/** Given a target element and a mouse event, get the mouse offset from that element.
		To do this we need the element's position and the mouse position */
	this.getMouseOffset = function(target, ev){
		ev = ev || window.event;

		var docPos    = this.getPosition(target);
		var mousePos  = this.mouseCoords(ev);
		return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
	}

	this.getDepth = function(e) {
		if(e && e.className) {
			thisdep = e.className.match(/depth([0-9])/);
		}
		return (thisdep ? parseInt(thisdep[1]) : -1);
	}

	/** Take an item and add an onmousedown method so that we can make it draggable */
	this.makeDraggable = function(item) {
		if(!item) return;
		var self = this; // Keep the context of the TableDnd inside the function
		var thisrow = item;

		var obj = item; // See if there's a handle in this row
		var handles = getElementsByClassName("drag-handle", null, item);
		if(handles && handles.length) { obj = handles[0]; }

		obj.onmousedown = function(ev) {
			// Need to check to see if we are an input or not, if we are an input, then
			// return true to allow normal processing
			var target = getEventSource(ev);
			if (target.tagName == 'INPUT' || target.tagName == 'SELECT') return true;
			currenttable = self;
			// Create overlay div that will drag with the cursor
			if(self.dragger == null) {
				self.dragger = document.createElement('div');
				self.dragger.style.border = '1px dashed #CCC';
				self.dragger.style.backgroundColor = '#FFF';
				self.dragger.style.zIndex = '100000';
				self.dragger.style.position = 'absolute';
				self.dragger.style.textAlign = 'left';
				self.dragger.style.opacity = 0.8;
				self.dragger.style.filter = 'alpha(opacity=' + (0.8 * 100) + ')';
				document.body.appendChild(self.dragger);
			}

			self.dragObject  = thisrow;
			self.dragChildren = new Array();
			self.mouseOffset = self.getMouseOffset(this, ev);

			//Position the dragger
			var pos = self.getPosition(thisrow);
			self.dragger.style.top = (pos.y - 2) + "px";
			self.dragger.style.left = (pos.x - 2) + "px";
			//Expand it to the required size
			self.dragger.style.width = (thisrow.offsetWidth + 4) + "px";
			//dragger.style.height = this.clientHeight + 4;
			//Fill it with the row's content (hopefully)
			var html = "<table width='100%'";
			for(i = 0; i < currenttable.table.attributes.length; ++i) {
				a = currenttable.table.attributes[i];
				if(!a.isId && a.nodeValue) {
					html = html + " " + a.nodeName + '="' + a.nodeValue + '"';
				}
			}
			html = html + "><colgroup>";
			for(i = 0; i < thisrow.childNodes.length; ++i) {
				if(thisrow.childNodes[i].tagName == "TD") {
					html = html + "<col style='width:" + thisrow.childNodes[i].offsetWidth + "px;' />";
				}
			}
			html = html + "</colgroup><tr";
			for(i = 0; i < thisrow.attributes.length; ++i) {
				a = thisrow.attributes[i];
				if(!a.isId && a.nodeValue) {
					html = html + " " + a.nodeName + '="' + a.nodeValue + '"';
				}
			}
			html = html + ">" + thisrow.innerHTML + "</tr>";
			// Need to add child rows here
			parentdepth = -1;
			maxdepth = 0;
			for(i = 0; i < currenttable.table.rows.length; ++i) {
				if(parentdepth > -1) {
					thisdepth = self.getDepth(currenttable.table.rows[i]);
					if(thisdepth > parentdepth) {
						self.dragChildren.push(currenttable.table.rows[i]);
						thisrow.cursize = thisdepth - parentdepth;
						html = html + "<tr";
						for(g = 0; g < currenttable.table.rows[i].attributes.length; ++g) {
							a = currenttable.table.rows[i].attributes[g];
							if(!a.isId && a.nodeValue) {
								if(a.nodeName == 'class') {
									html = html + " " + a.nodeName + '="' + a.nodeValue + '"';
								} else {
									html = html + " " + a.nodeName + '="' + a.nodeValue + '"';
								}
							}
						}
						html = html + ">" + currenttable.table.rows[i].innerHTML + "</tr>";
					} else {
						break;
					}
				}
				if(currenttable.table.rows[i] == thisrow) {
					parentdepth = self.getDepth(thisrow);
				}
			}
			// Need to figure out which rows are child rows
			html = html + "</table>";
			self.dragger.innerHTML = html;
			//Display it over top of the table
			self.dragger.style.display = 'block';

			// update the style to show we're dragging
			thisrow.style.backgroundColor = "#eee";
			thisrow.style.opacity = 0.5;
			thisrow.style.filter = 'alpha(opacity=' + (0.5 * 100) + ')';

			//debug = document.getElementById('debug');
			return false;
		}
		obj.style.cursor = "move";
	}

	/** We're only worried about the y position really, because we can only move rows up and down */
	this.findDropTargetRow = function(y) {
		var rows = this.table.tBodies[0].rows;
		for (var i=0; i<rows.length; i++) {
			var row = rows[i];
			// John Tarr added to ignore rows that I've added the NoDnD attribute to (Header rows)
			var nodrop = row.getAttribute("NoDrop");
			var isChild = false;
			for(g=0; g < this.dragChildren.length; ++g) {
				if(this.dragChildren[g] == row) { isChild = true; }
			}
			if((nodrop == null || nodrop == "undefined") && !isChild) {  //There is no NoDnD attribute on rows I want to drag
				var rowY    = this.getPosition(row).y;
				var rowHeight = parseInt(row.offsetHeight)/2;
				if (row.offsetHeight == 0) {
					rowY = this.getPosition(row.firstChild).y;
					rowHeight = parseInt(row.firstChild.offsetHeight)/2;
				}
				// Because we always have to insert before, we need to offset the height a bit
				if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
					// that's the row we're over
					return row;
				}
			}
		}
		return null;
	}
}

