/*
	Copyright (C) 2012 - Juan Ferrer Toribio

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this program; if not, write to the Free
	Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
	02111-1307 USA.
*/

//+++++++++++++++++++++++++++++++++++++++++++++++++++ DbRequest

var DbRequest = new Class
({
	initialize: function (obj, func, data, handle)
	{
		this.obj = obj;
		this.func = func;
		this.data = data;
		this.handle = handle;
		this.str = null;
	}

	,addValue: function (vname, value)
	{
		if (value)
		{
			if (typeof value == 'boolean')
				value = new Number (value);
	
			if (this.str == null)
				this.str = vname + '=' + value;
			else
				this.str += '&' + vname + '=' + value;
		}
	}
});

var DB_ERROR_SESSION_EXPIRED	= 1;
var DB_ERROR_USER_DISCONNECTED	= 2011;

var DbError = new Class
({
	initialize: function (str, no)
	{
		this.str = str;
		this.no = no;
	}

	,show: function ()
	{
		alert (this.str);
	}
});

//+++++++++++++++++++++++++++++++++++++++++++++++++++ DbConnection

var DbConnection = new Class
({
	Extends: SqlObject

	,connected: false
	,request: null
	,queue: new Array ()
	,ready: true
	,handler: null

	,initialize: function ()
	{
		this.parent ();
		this.rollback ();
	}

	,connect: function (user, pass, remember)
	{
		var req;
		var obj = this;
		
		if (this.connected)
			return;

		req = new DbRequest (this, this.opened);
		req.addValue ('mod', 'connect');
		
		if (user)
		{
			req.addValue ('user', user);
			req.addValue ('pass', pass);
			req.addValue ('remember', remember);
		}
		
		this.addRequest (req);
		this.handler = function () { obj.close (); };
		window.addEventListener ('unload', this.handler, false);
	}

	,opened: function (json, error)
	{
		if (json)
		{
			this.connected = true;
			this.signalEmit ('opened');
		}
	}

	,close: function ()
	{
		if (this.connected)
		{	
			var req = new DbRequest (this, this.closed);
			req.addValue ('mod', 'close');	
			this.addRequest (req);
		}
	}

	,closed: function ()
	{
		var obj = this;
		this.connected = false;
		window.removeEventListener ('unload', this.handler, false);
		this.signalEmit ('closed');
	}

	,addRequest: function (rData)
	{
		this.queue.push (rData);
		
		if (this.ready)
			this.next ();
	}

	,next: function ()
	{
		if (this.queue.length > 0)
		{
			var req;
			var obj = this;
			var rData = this.queue[0];
		
			this.ready = false;

			req = new XMLHttpRequest ();
			req.open ('post', 'module.php', true);
			req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded');
			req.onreadystatechange = function () { obj.requestDone (req) };
			req.send (rData.str);
		}
	}

	,nextData: function ()
	{
		return this.queue.shift ();
	}

	,requestDone: function (req)
	{
		if (req.readyState == 4)
		{
			var error;
			var json = null;
			var rData = this.nextData ();

			if (req.status == 200)
			{
				try {
					var reply;
					var err;
	
					reply = eval (req.responseText);
					err = reply.error;
					json = reply.data;
					
					if (reply.newVersion)
					{
						var str =
							TEXT_NewVersionAvailable + 
							'\n\n' + TEXT_ChangeLog + ': ' + reply.changelog;
	
						if (confirm (str))
							location.reload ();
						else
							Cookie.del ('hedera_version');
					}

					error = (err) ? new DbError (err.str, err.no) : null;
				}
				catch (e)
				{
					error = new DbError (TEXT_BadServerReply, 1);
				}
			}
			else
				error = new DbError (TEXT_ConnError, req.status);

			this.callHandler (rData, json, error);
			this.ready = true;
			
			if (error)
			{				
				switch (error.no)
				{
					case DB_ERROR_USER_DISCONNECTED:
					case DB_ERROR_SESSION_EXPIRED:
						this.closed ();
						break;
				}
			}
			else
				this.next ();
		}
	}

	,callHandler: function (rData, json, error)
	{	
		if (rData.obj != undefined)
		{
			try {
				rData.func.call (rData.obj, json, error, rData.data);
			}
			catch (e)
			{
				alert (TEXT_InternalError);
			}
		}
		
		if (error != null && !rData.handle)
			error.show ();
	}

	,startTransaction: function ()
	{
		this.transaction++;
		
		if (this.transaction == 1)
			this.queryCache = new Array ();
	}

	,rollback: function ()
	{
		this.transaction = 0;
		this.queryCache = null;
	}

	,commitSuccess: function (json, error, rData)
	{
		var len = (json) ? json.length - 1 : 0;
	
		for (var n = 0; n < rData.length; n++)
		{
			if (n < len)
				this.callHandler (rData[n], json[n], null);
			else if (n == len)
				this.callHandler (rData[n], json[n], error);
			else
				this.callHandler (rData[n], null, null);
		}
	}

	,commit: function ()
	{
		if (!this.transaction)
			return;
	
		this.transaction--;
		
		if (!this.transaction)
		{
			var n;
			var sql = '';
			var count = this.queryCache.length;
	
			for (n = 0; n < count; n++)
				sql += this.queryCache[n].sql + '; ';

			if (count > 0)
				this.multiQuery (sql, this, this.commitSuccess, this.queryCache, true);

			this.rollback ();
		}
	}

	,realQuery: function (sql, multi, obj, func, data, handle)
	{
		var rData = new DbRequest (obj, func, data, handle);
		rData.addValue ('mod', 'query');
		rData.addValue ('sql', sql);
		rData.addValue ('multi', multi);
		this.addRequest (rData);
	}

	,query: function (sql, obj, func, data, handle)
	{
		if (!this.transaction)
			this.realQuery (sql, false, obj, func, data, handle);
		else
		{	
			var qData = new Object ();
			qData.sql = sql;
			qData.obj = obj;
			qData.func = func;
			qData.data = data;
			qData.handle = handle;
			this.queryCache.push (qData);
		}
	}

	,multiQuery: function (sql, obj, func, data, handle)
	{
		this.realQuery (sql, true, obj, func, data, handle);
	}

	,stmt: function (stmt, obj, func, data, handle)
	{
		this.query (stmt.render (), obj, func, data, handle);
	}

	,multiStmt: function (stmt, obj, func, data, handle)
	{
		this.multiQuery (stmt.render (), obj, func, data, handle);
	}
});

//+++++++++++++++++++++++++++++++++++++++++++++++++++ DbFormParam

var DbFormParam = new Class
({
	Extends: SqlObject,
	Implements: SqlParam

	,initialize: function (form, col)
	{
		this.parent ();
		this.form = form;
		this.col = col;
		
		form.addSignal ('iter-changed', this.formIterChanged, this);
	}

	,changed: function ()
	{
		this.signalEmit ('changed');
	}

	,formIterChanged: function (form)
	{
		this.changed ();
	}

	,setRealValue: function (value)
	{
		this.form.setValue (this.col, value);
	}

	,getValue: function ()
	{
		return this.form.getValue (this.col);
	}
});

//+++++++++++++++++++++++++++++++++++++++++++++++++++ DbForm

var DbForm = new Class
({
	Extends: SqlObject

	,initialize: function (model)
	{
		this.parent ();
		this.model = model;
		this.row = -1;
		this.param = new Array ();
		
		model.addSignal ('status-changed', this.modelStatusRefresh, this);
		model.addSignal ('row-updated', this.modelRowUpdated, this);
	}

	,modelStatusRefresh: function (model, status)
	{
		switch (status)
		{
			case DB_MODEL_STATUS_READY:
			case DB_MODEL_STATUS_ERROR:
				this.iterChanged ();
		}
	}

	,modelRowUpdated: function (model, row, col)
	{
		if (row != this.row)
			return;

		for (var n = 0; n < this.param.length; n++)
		{
			if (this.param[n].col == col)
			{
				this.param[n].changed ();
				break;	
			}
		}
	}

	,iterChanged: function ()
	{
		this.signalEmit ('iter-changed');
	}

	,createParam: function (col)
	{
		var param = this.param;
	
		for (var n = 0; n < param.length; n++)
		{
			if (param[n].col == col)
				return param[n];
		}

		param = new DbFormParam (this, col);
		this.param.push (param);

		return param;
	}

	,getParam: function (col)
	{
		return this.createParam (col);
	}

	,bindParam: function (param, col, simplex)
	{
		var fParam = this.createParam (col);
		fParam.bindParam (param, simplex);
	}

	,setRow: function (row)
	{
		this.row = row;
		this.iterChanged ();
	}

	,delRow: function ()
	{
		this.model.delRow (this.row);
	}

	,getValue: function (col)
	{
		return this.model.getValue (this.row, col);
	}

	,setValue: function (col, value, obj, func, data)
	{
		return this.model.setValue (this.row, col, value, obj, func, data);
	}
});

//+++++++++++++++++++++++++++++++++++++++++++++++++++ DbModel

var DB_MODEL_FLAG_PRI_KEY	= 2;
var DB_MODEL_FLAG_AI		= 512 | DB_MODEL_FLAG_PRI_KEY;
var DB_MODEL_FLAG_NOT_NULL	= 1;

var DB_MODEL_TYPE_TIMESTAMP	= 7;
var DB_MODEL_TYPE_DATE		= 10;
var DB_MODEL_TYPE_DATE_TIME	= 12;

n = -1;
var DB_MODEL_STATUS_CLEAN	= ++n;
var DB_MODEL_STATUS_LOADING	= ++n;
var DB_MODEL_STATUS_READY	= ++n;
var DB_MODEL_STATUS_ERROR	= ++n;

var DbModel = new Class
({
	Extends: SqlObject

	,db: null
	,stmt: null
	,target: null
	,orderCol: -1
	,data: null
	,field: null
	,status: DB_MODEL_STATUS_CLEAN
	,preStmt: new Array ()
	,postStmt: new Array ()
	,foreach: new Array ()
	,lostFlag: new Array ()	// Trash
	,lostView: new Array ()	// Trash

	,initialize: function (db)
	{
		this.parent ();
		this.db = db;
		this.cleanData (false);
	}

	,setStmt: function (stmt)
	{
		this.stmt = stmt;
		this.refresh ();
		stmt.addSignal ('changed', this.stmtChanged, this);
	}

	,setSql: function (sql)
	{
		this.setStmt (new SqlString (sql));
	}

	,addPreStmt: function (stmt)
	{
		this.preStmt.push (stmt);
		stmt.addSignal ('changed', this.stmtChanged, this);
	}

	,addPreSql: function (sql)
	{
		this.addPreStmt (new SqlString (sql));
	}

	,addPostStmt: function (stmt)
	{
		this.postStmt.push (stmt);
		stmt.addSignal ('changed', this.stmtChanged, this);
	}

	,addPostSql: function (sql)
	{
		this.addPostStmt (new SqlString (sql));
	}

	,refresh: function ()
	{
		var n;
		var ready;

		if ((ready = this.stmt.isReady ()))
		{
			var pStmt;

			for (var m = 0; m < 2; m++)
			{
				pStmt = (m == 0) ? this.preStmt : this.postStmt;

				for (n = 0; n < pStmt.length; n++)
					if (!(ready = pStmt[n].isReady ()))
						break;
			}
		}

		if (ready)
		{
			this.setStatus (DB_MODEL_STATUS_LOADING);
			this.db.startTransaction ();
	
			pStmt = this.preStmt;

			for (n = 0; n < pStmt.length; n++)
				this.db.stmt (pStmt[n]);
	
			this.db.stmt (this.stmt, this, this.selected);
	
			pStmt = this.postStmt;

			for (n = 0; n < pStmt.length; n++)
				this.db.stmt (pStmt[n]);

			this.db.commit ();
		}
		else
			this.cleanData (false);
	}
	
	,cleanData: function (error)
	{
		this.data = new Array ();
		this.field = new Array ();

		for (var n = 0; n < this.foreach.length; n++)
			this.foreach[n].init ();

		this.foreachDone ();

		if (error)
			this.setStatus (DB_MODEL_STATUS_ERROR);
		else
			this.setStatus (DB_MODEL_STATUS_CLEAN);
	}

	,setUpdatable: function (tname)
	{
		var insert;
		var sqldel;
		var col = new Array ();
		var field = this.field;
		var target = new SqlTable (tname);

		for (var n = 0; n < field.length; n++)
		{
			if (field[n].table == this.talias)
				col.push (n);
		}

		insert = new SqlInsert ();
		insert.addTarget (target);
	
		sqldel = new SqlDelete ();
		sqldel.addTarget (target);

		this.col = col;
		this.sqldel = sqldel;
		this.insert = insert;
		this.target = target;
		this.left = new Array ();
		this.def = new Array ();
	}

	,addLeft: function (fname, index)
	{
		var left = new Object ();
		left.fname = fname;
		left.index = index;
		this.left.push (left);
	}

	,linkParam: function (fname, value)
	{
		var def = new Object ();
		def.fname = fname;
		def.value = value;
		this.def.push (def);
	}

	,addForeach: function (obj)
	{
		this.foreach.push (obj);
	}

	,checkColExists: function (col)
	{
		if (col >= 0 && col < this.field.length)
			return true;
		else
			alert (TEXT_ColNotExists + ' (' + col + ')');

		return false;
	}

	,checkRowExists: function (row)
	{
		if (this.status == DB_MODEL_STATUS_READY)
		{
			if (row >= 0 && row < this.data.length)
				return true;

//			alert (TEXT_RowNotExists + ' (' + row + ')');
		}

		return false;
	}

	,checkUpdatable: function ()
	{
		if (this.target)
			return true;
		alert (TEXT_ModelNotUpdatable);
		return false;
	}

	,checkRowUpdatable: function (row)
	{
		if (this.checkUpdatable () && this.checkRowExists (row))
			return true;
		return false;
	}

	,getValue: function (row, col)
	{
		if (this.checkRowExists (row) && this.checkColExists (col))
			return this.data[row][col];
		else
			return undefined;
	}

	,setValue: function (row, col, value, obj, func, udata)
	{
		if (this.checkRowUpdatable (row) && this.checkColExists (col))
		{
			var stmt;
			var select;
			var table;
			var where;
			var data;
			var field = this.field[col];
			
			data = new Object ();
			data.row = row;
			data.col = col;
			data.value = value;
			data.obj = obj;
			data.func = func;
			data.data = udata;

			table = new SqlTable (field.table, field.schema);
			where = this.getWhere (table, row);
			
			select = new SqlSelect ();
			select.addField (field.name);

			if (where)
			{
				var update;
			
				stmt = new SqlMultiStmt ();
			
				update = new SqlUpdate ();
				update.addTarget (table);
				update.addSet (field.name, value);
				update.where = where;
				stmt.addStmt (update);
			
				select.addTarget (table);
				select.where = where;
				stmt.addStmt (select);
			}
			else
			{
				var f = this.field;
				var left = this.left;
				var insert = this.insert;
				var n, equal, field, func;

				stmt = this.createInsert ();
				insert.addSet (field.name, value);
			
				for (n = 0; n < left.length; n++)
					insert.addSet (left[n].fname, this.data[row][left[n].index]);

				for (n = 0; n < f.length; n++)
				{
					if (f[n].flags & DB_MODEL_FLAG_AI && f[n].table == this.target.tname)
					{
						equal = new SqlOperation (SQL_OPERATION_EQUAL);

						field = new SqlField (f[n].name, f[n].table);
						equal.addExpr (field);
				
						func = new SqlFunction ('LAST_INSERT_ID');
						equal.addExpr (func);
						
						break;
					}
				}
				
				select.addTarget (this.target);
				select.where = equal;
				stmt.addStmt (select);

				stmt.addStmt (new SqlString ('SELECT LAST_INSERT_ID()'));
			}

			this.db.multiStmt (stmt, this, this.updated, data);
		}
	}

	,delRow: function (row)
	{
		if (this.checkRowUpdatable (row))
		{
			var where = this.getWhere (this.target, row);

			if (where)
			{
				this.sqldel.where = where;
				this.db.stmt (this.sqldel, this, this.deleted, row);
			}
		}
	}

	,insertRow: function ()
	{
		if (this.checkUpdatable ())
		{
			var insert = this.createInsert ();
			this.db.multiStmt (insert, this, this.inserted);
		}
	}

	,orderBy: function (col)
	{
		var data = this.data;

		this.setStatus (DB_MODEL_STATUS_LOADING);

		if (col != this.orderCol)
		{		
			var m, n;
			var len = data.length;
		
			for (m = 1; m < len; m++)
			{
				for (n = m; n > 0 && data[m][col] < data[n - 1][col]; n--);
				
				if (n != m)
				{
					data.splice (n, 0, data[m]);
					data.splice (m + 1, 1);		
				}
			}

			this.orderCol = col;
		}
		else
			data.reverse ();

		this.setStatus (DB_MODEL_STATUS_READY);
	}

	,searchValue: function (col, value)
	{
		if (this.status == DB_MODEL_STATUS_READY && this.checkColExists (col))
		{
			var data = this.data;
		
			switch (this.field[col].type)
			{
				case DB_MODEL_TYPE_TIMESTAMP:
				case DB_MODEL_TYPE_DATE_TIME:
				case DB_MODEL_TYPE_DATE:

					value = value.toString ();
					
					for (var n = 0; n < data.length; n++)
					{
						if (value === data[n][col].toString ());
							return n;
					}
							
					break;

				default:

					for (var n = 0; n < data.length; n++)
					{
						if (value === data[n][col])
							return n;
					}
			}
		}
		
		return -1;
	},
	
// private:
	
	stmtChanged: function (stmt)
	{
		this.refresh ();
	}

	,setStatus: function (status)
	{
		this.status = status;
		this.signalEmit ('status-changed', status);
	}

	,pForeach: function (time, row)
	{
		var n;
		var foreach = this.foreach;

		if (time == 0)
			for (n = 0; n < foreach.length; n++)
				foreach[n].pre (row);
		else
			for (n = 0; n < foreach.length; n++)
				foreach[n].post (row);
	}

	,foreachDone: function ()
	{
		for (var n = 0; n < this.foreach.length; n++)
			this.foreach[n].done ();
	}

	,getWhere: function (t, index)
	{
		var pk = false;
		var f = this.field;
		var row, and, equal, field, value;

		row = this.data[index];
		and = new SqlOperation (SQL_OPERATION_AND);

		for (var n = 0; n < f.length; n++)
		{
			if (f[n].flags & DB_MODEL_FLAG_PRI_KEY && f[n].table == t.tname && row[n] != null)
			{
				equal = new SqlOperation (SQL_OPERATION_EQUAL);
				and.addExpr (equal);
				
				field = new SqlField (f[n].name, f[n].table);
				equal.addExpr (field);

				value = new SqlValue ();
				value.setValue (row[n]);
				equal.addExpr (value);
				
				pk = true;
			}
		}
		
		return (pk) ? and : null;
	}

	,createInsert: function ()
	{
		var n;
		var stmt;
		var insert;
		var select;
		var def = this.def;
	
		stmt = new SqlMultiStmt ();

		insert = this.insert;
		insert.delSet ();
		stmt.addStmt (insert);

		for (n = 0; n < def.length; n++)
			insert.addSet (def[n].fname, def[n].value);
		
		return stmt;
	}

	,getInsertRowValues: function (json, row)
	{
		var m, n;
		var lastid;
		var def = this.def;
		var field = this.field;

		lastid = json[2].data[0][0];
		
		for (n = 0; n < field.length; n++)
		{
			if (field[n].table == this.target.tname)
			{
				if (field[n].flags & DB_MODEL_FLAG_AI)
					row[n] = lastid;
				else
				{
					for (m = 0; m < def.length && def[m].fname != field[n].name; m++);

					if (m != def.length)
						row[n] = def[m].value;
					else if (field[n].flags & DB_MODEL_FLAG_NOT_NULL)
						row[n] = field[n].def;
					else
						row[n] = null;
				}
			}
		}
	}

	,getTypeCastFunc: function (n)
	{
		switch (this.field[n].type) {
			case DB_MODEL_TYPE_DATE:
			case DB_MODEL_TYPE_DATE_TIME:
			case DB_MODEL_TYPE_TIMESTAMP:
				return this.toDate;
		}

		return null;
	}

	,toDate: function (val)
	{
		return (val != null) ? new Date (val * 1000) : null;
	}

	,selected: function (json, error)
	{	
		if (json)
		{
			var m, n;
			var row;
			var func;
			var data = json.data;
			var field = json.field;
	
			this.repairField (field);	// Trash
			this.data = data;
			this.field = field;

			for (n = 0; n < this.foreach.length; n++)
				this.foreach[n].init ();

			for (n = 0; n < field.length; n++)
			{
				func = this.getTypeCastFunc (n);
			
				if (func != null)
				{
					field[n].def = func (field[n].def);

					for (m = 0; m < data.length; m++)
						data[m][n] = func (data[m][n]);
				}				
			}
		
			for (m = 0; m < data.length; m++)
				this.pForeach (1, data[m]);

			this.foreachDone ();	
			this.orderCol = -1;
			this.setStatus (DB_MODEL_STATUS_READY);
		}
		else
			this.cleanData (true);
	}

	,updated: function (json, error, data)
	{
		if (json && json[0])
		{
			var col;
			var func;
			var row = this.data[data.row];

			this.pForeach (0, row);

			switch (json.length)
			{
				case 2:
					col = new Array ();
					break;
				case 3:
					this.getInsertRowValues (json, row);
					col = this.col;
					break;
			}
			
			func = this.getTypeCastFunc (data.col);

			if (func != null)
				row[data.col] = func (json[1].data[0][0]);
			else
				row[data.col] = json[1].data[0][0];
			
			col.push (data.col);
			this.pForeach (1, row);
			this.signalEmit ('row-updated', data.row, col);
			this.foreachDone ();
		}

		if (data.func != undefined)
			data.func.call (data.obj, json, data.data);
	}

	,deleted: function (json, error, index)
	{
		if (json)
		{
			var row = this.data[index];
			
			this.pForeach (0, row);
		
			if (this.left.length == 0)
			{
				this.data.splice (index, 1);
				this.signalEmit ('row-deleted', index);
			}
			else
			{
				var col = new Array ();
	
				for (var n = 0; n < this.field.length; n++)
				{
					if (this.field[n].table == this.target.tname)
					{
						row[n] = null;
						col.push (n);
					}
				}

				this.pForeach (1, row);
				this.signalEmit ('row-updated', index, col);
			}

			this.foreachDone ();
		}
	}

	,inserted: function (json, error)
	{
		if (json && json.length == 2)
		{
			var index;
			var row = new Array (this.field.length);
		
			this.getInsertRowValues (json, row);	
			index = this.data.push (row);
			this.pForeach (1, row);
			this.foreachDone ();
			this.signalEmit ('row-inserted', index);
		}
	},

// Delete when MySQL FLAG and view orgname "bugs" are repaired:
	
	setView: function (alias, vname, schema)
	{
		var obj = new Object ();
		obj.alias = alias;
		obj.vname = vname;
		obj.schema = schema;
		this.lostView.push (obj);
	}

	,setFlag: function (flag, field)
	{	
		var obj = new Object ();
		obj.field = field;
		obj.flag = flag;
		this.lostFlag.push (obj);
	}

	,repairField: function (field)
	{
		var m, n;
		var flag = this.lostFlag;
		var view = this.lostView;

		for (m = 0; m < field.length; m++)
		{
			for (n = 0; n < flag.length; n++)
			{
				if (field[m].name == flag[n].field)
				{
					field[m].flags |= flag[n].flag;
					break;
				}
			}
			
			for (n = 0; n < view.length; n++)
			{
				if (field[m].table == view[n].alias)
				{
					field[m].table = view[n].vname;
					field[m].schema = view[n].schema;
					break;
				}
			}
		}
	}
});

/**
 * Interface for handle foreach operations on the model.
 **/
var DbModelForeach = new Class
({
	/**
	 * Called before each update or delete row operation.
	 * You don't need to define it if model isn't updatable.
	 *
	 * @param row: array with the current row content.
	 **/
	pre: function (row) {}

	/**
	 * Called after each update or insert row operation.
	 *
	 * @param row: array with the current row content.
	 **/
	,post: function (row) {}

	/**
	 * Called before each model refresh.
	 **/
	,init: function () {}

	/**
	 * Called when an operation in the model is complete.
	 **/
	,done: function () {}
});


