/** SnappySQL - web-based SQL editor - bfults@gmail.com - 2006-01-27        **
 ** Code licensed under Creative Commons Attribution-ShareAlike License     **
 ** http://creativecommons.org/licenses/by-sa/2.5/                          **/

/* constructor */
function SnappySQL()
{
	this.count = SnappySQL.count++;
	this.id = "ss-"+ this.count;
	this.cols = [];
	this.perms = [];
	this.bRequired = [];
	this.data = [];
	this.cmdMeta = '';
	this.countRows = 0;
	this.URLPost = '';
	return this;
}

SnappySQL.prototype = {

	/* public methods */
	setCols: function(aCols)
	{
		this.cols = aCols;
	},

	setPerms: function(aPerms)
	{
		this.perms = aPerms;
	},

	setRequired: function(aReq)
	{
		this.bRequired = aReq;
	},

	setHandler: function(sURL)
	{
		this.URLPost = sURL;
	},

	setData: function(aData)
	{
		this.data = aData;
	},

	setCommandMeta: function(sMeta)
	{
		this.cmdMeta = sMeta + '&';
	},

	generateTableHTML: function()
	{
		var html = '<div id="'+ this.id +'-container" class="snappysql">';

		// if we have valid data and columns
		var cc = this.cols.length;
		var cd = (this.data && this.data.length) ? this.data[0].length : cc;
		var cp = (this.perms) ? this.perms.length : cd;

		// if we have a consistent structure
		if (cc == cd && cd == cp)
		{
			this.countRows = this.data.length;

			var i = 0, j = 0, idRow;
			html += '<table id="'+ this.id +'-table">';

			// header
			html += '<thead><tr>';
			for (i=0; i < this.cols.length; i++)
			{
				html += '<th>'+ this.cols[i] +'</th>';
			}
			html += '<th><button id="'+ this.id +
				'rH-b-add" class="add">Add Row</button></th>';
			html += '</tr></thead>';

			// body
			html += '<tbody>';
			for (i=-1; i < this.data.length; i++)
			{
				html += this.generateRowHTML(i);
			}
			html += '</tbody></table>';
		}
		else
		{
			html += '<p class="error">Columns, permissions and data '+
				'structure is inconsistent.</p>';
		}

		html += '</div>';

		return html;
	},

	/* private methods */
	generateRowHTML: function(i)
	{
		var idRow = this.id +'-r'+ ((i >= 0) ? i : '#');
		html = '<tr id="'+ idRow + '"' + ((i < 0) ? ' class="dummy"' : '') +'>';

		// output an input element in each cell
		for (var j=0; j < this.cols.length; j++)
		{
			// data row
			if (i >= 0)
			{
				html += '<td><input type="text" class="readonly" name="r'+ i +
					'-c'+ j +'" value="'+ this.data[i][j] +
					'" readonly="readonly" /></td>';
			}
			// dummy row
			else
			{
				html += '<td><input type="text" name="r#-c'+ j +
					'" value="" /></td>';
			}
		}

		// output edit/remove buttons at the end of the row
		html += '<td>';
		html += '<button id="'+ idRow +'-b-edit" class="edit">Edit</button>';
		html += '<button id="'+ idRow +'-b-remove" class="remove">Remove</button>';
		html += '</td>';

		html += '</tr>';

		return html;
	},

	initialize: function()
	{
		var o = this;
		Events.addHandler(this.id+"-container", "click", function(evt)
		{
			o.handleClick(evt);
		});
	},

	handleClick: function(evt)
	{
		var elTarget = Events.getTargetElement(evt);

		if (elTarget && elTarget.id)
		{
			var aParts = elTarget.id.match(/(?:r(\w+))?-b-(\w+)$/);
			if (aParts && aParts.length == 3)
			{
				var iRow = (aParts[1] * 1) || 0;
				var idRow = this.id + "-r" + iRow;
				var sType = aParts[2];
				switch (sType)
				{
					case 'add':
					this.addRow();
					break;

					case 'edit':
					this.editRow(idRow);
					break;

					case 'remove':
					this.removeRow(idRow);
					break;

					case 'save':
					this.saveRow(idRow);
					break;

					case 'cancel':
					this.cancelRowEdit(idRow);
					break;

					default:
					//
				}
			}
		}
	},

	addRow: function()
	{
		var iRow = ++this.countRows;
		var elTbody = $tag("tbody", $(this.id +"-table"))[0];
		var elRowDummy = elTbody.rows[0];
		var elRowNew = elRowDummy.cloneNode(true);
		elRowNew.isNew = true;

		// un-dummify the new row
		elRowNew.id = elRowNew.id.replace(/#/g, iRow);
		var nlTDs = $tag("td", elRowNew);
		for (var i=0; i < nlTDs.length; i++)
		{
			nlTDs[i].innerHTML = nlTDs[i].innerHTML.replace(/#/g, iRow);
		}
		elRowNew.className = '';

		elTbody.insertBefore(elRowNew, elTbody.rows[1]);
		this.editRow(elRowNew.id);

		return true;
	},

	editRow: function(idRow)
	{
		var elRow = $(idRow);
		CSS.addClass(elRow, "editable");
		var lsInputs = $tag("input", elRow);
		var bFocus = false;
		for (var i=0; i < lsInputs.length; i++)
		{
			// if it should be editable, make it editable
			if (this.perms[i] || elRow.isNew)
			{
				lsInputs[i].readOnly = false;
				CSS.remClass(lsInputs[i], "readonly");
				lsInputs[i].oldValue = lsInputs[i].value;
				if (!bFocus)
				{
					lsInputs[i].focus();
					bFocus = true;
				}
			}
		}

		// change the buttons to Save / Cancel
		this.changeButtons($tag("button", elRow),
			["edit", "save", "remove", "cancel"]);
		return true;
	},

	removeRow: function(idRow)
	{
		var elRow = $(idRow);
		this.countRows--;
		CSS.addClass(elRow, "removed");
		window.setTimeout(function()
		{
			elRow.parentNode.removeChild(elRow);
		}, 750);

		// send the remove request to the server
		var o = this;
		Net.post(this.URLPost, "action=delete&" + this.cmdMeta +
			this.serializeRow(idRow), function(){});
	},

	saveRow: function(idRow)
	{
		var elRow = $(idRow);

		// first validate the data we're trying to save
		if (this.validateInputs(idRow))
		{
			// figure out the action and send the request to the server
			var sAction = "action=update&";
			if (elRow.isNew)
			{
				sAction = "action=insert&";
				elRow.isNew = false;
			}
			var o = this;
			Net.post(this.URLPost, sAction + this.cmdMeta +
				this.serializeRow(idRow), function(){});
		}
		else
		{
			alert("Please fill out all required fields.");
			return false;
		}

		CSS.remClass(elRow, "editable");
		var lsInputs = $tag("input", elRow);
		for (var i=0; i < lsInputs.length; i++)
		{
			CSS.addClass(lsInputs[i], "readonly");
			lsInputs[i].readOnly = true;
		}

		// change the buttons to Edit / Remove
		this.changeButtons($tag("button", elRow),
			["save", "edit", "cancel", "remove"]);
		return true;
	},

	cancelRowEdit: function(idRow)
	{
		var elRow = $(idRow);

		// if it was new row, just throw it away
		if (elRow.isNew)
		{
			elRow.parentNode.removeChild(elRow);
			this.countRows--;
			return true;
		}

		CSS.remClass(elRow, "editable");
		var lsInputs = $tag("input", elRow);
		for (var i=0; i < lsInputs.length; i++)
		{
			if (lsInputs[i].oldValue) lsInputs[i].value = lsInputs[i].oldValue;
			CSS.addClass(lsInputs[i], "readonly");
			lsInputs[i].readOnly = true;
		}

		// change the buttons to Edit / Remove
		this.changeButtons($tag("button", elRow),
			["save", "edit", "cancel", "remove"]);
		return true;
	},

	serializeRow: function(idRow)
	{
		var elRow = $(idRow);
		var nlInputs = $tag("input", elRow);
		var sVals = '';

		for (var i=0; i < nlInputs.length; i++)
		{
			sVals += ((i>0)?'&':'')+ encodeURIComponent(nlInputs[i].name) +'='+
				encodeURIComponent(nlInputs[i].value);
		}
		return sVals;
	},

	changeButtons: function(nlB, aLabels)
	{
		nlB[0].id = nlB[0].id.replace(new RegExp(aLabels[0]+'$'), aLabels[1]);
		nlB[1].id = nlB[1].id.replace(new RegExp(aLabels[2]+'$'), aLabels[3]);
		CSS.remClass(nlB[0], aLabels[0]);
		CSS.addClass(nlB[0], aLabels[1]);
		CSS.remClass(nlB[1], aLabels[2]);
		CSS.addClass(nlB[1], aLabels[3]);
		nlB[0].firstChild.nodeValue = aLabels[1][0].toUpperCase()+aLabels[1].slice(1);
		nlB[1].firstChild.nodeValue = aLabels[3][0].toUpperCase()+aLabels[3].slice(1);
		return true;
	},

	validateInputs: function(idRow)
	{
		var elRow = $(idRow);
		if (elRow)
		{
			var nlInputs = $tag("input", elRow);
			var ctBad = 0;
			var bFocus = false;
			for (var i=0; i < nlInputs.length; i++)
			{
				if (this.bRequired[i] && !/\S/.test(nlInputs[i].value))
				{
					SnappySQL.setErrorOnField(nlInputs[i]);
					if (!bFocus)
					{
						nlInputs[i].focus();
						bFocus = true;
					}
					ctBad++;
				}
			}
			if (ctBad == 0) return true;
		}
		return false;
	}
};

/* static members */
SnappySQL.count = 0;
SnappySQL.stack = [];

/* static functions */
SnappySQL.load = function()
{
	for (var i=0; i < SnappySQL.stack.length; i++)
	{
		SnappySQL.stack[i].initialize();
	}
	return true;
}
SnappySQL.create = function()
{
	SnappySQL.stack.push(new SnappySQL());
	return SnappySQL.stack[SnappySQL.stack.length - 1];
}
SnappySQL.setErrorOnField = function(el)
{
	CSS.addClass(el, "form-error");
	Events.addHandler(el, "change", function()
	{
		if (this.value && /\S/.test(this.value))
		{
			CSS.remClass(this, "form-error");
		}
		return true;
	});
}

/* external attachment */
Events.addHandler(window, "load", SnappySQL.load);


/* Example Usage *

 * Requires: Events, CSS, DOM, Net

var mySnap = SnappySQL.create();
mySnap.setCommandMeta("table_id=354");
mySnap.setCols(["Make", "Model", "Trim", "MSRP"]);
mySnap.setPerms([0, 0, 1, 1]);
mySnap.setRequired([1, 1, 1, 0]);
mySnap.setHandler("foo.php");
mySnap.setData([["Toyota", "Camry", "CE", "14995"],
                ["Toyota", "Corolla", "Type-S", "19995"],
                ["Volkswagen", "GTI", "336", "21995"],
                ["Volkswagen", "Jetta", "GLI", "24995"]]);
// ...

document.write(mySnap.generateTableHTML());

*/
