//****************************************************************************************
//
//	datasource.js
//
//	Object to support the construction and use of AJAX based data interaction
//
//	Depends On:
//		http_manager.js
//				
//
//	Copyright: 	David Horne, Tuross Technologies Australia P/L
//				dkhorne@bigpond.net.au
//
//****************************************************************************************

//****************************************************************************************
//
//	DataRecord Object used to communicate with server
//
//****************************************************************************************
	var dataStatus = { 'New': 'New', 'Update': 'Update', 'Delete': 'Delete', 'OK': 'OK', 'Error': 'Error', 'None': 'None' };

	function DataRecord(internalID, status) {
		
	//	Properties
		this._dataTag = {}								//	tag for this data record
		this._dataTag.internalID = internalID;			//	Internal ID for this data object.
		this._dataTag.action = "";						//	Action performed on object
		this._dataTag.status = status;					//	Status of Object - dataStatus
		this._dataTag.message = "";						//	Message returned from Server
		this._dataTag.isProcessed = false;				//	Processed flag
		this._dataTag.selected = false;					//	Selected or Current flag
		
	//	Methods
		this.get = DataRecord_Get;						//	Returns the value of a given field
		this.set = DataRecord_Set;						//	Sets the value of a given field
		this.merge = DataRecord_Merge;					//	Merges an object with this record
		this.toString = DataRecord_ToString;			//	Returns the JSON representation of this record
	}
	
//****************************************************************************************
//	Returns the value of a given field		
//****************************************************************************************
	function DataRecord_Get(fieldName) {
		switch (fieldName.toLowerCase()) {
			case "internalid":
				return this._dataTag.internalID;
				
			case "action":
				return this._dataTag.action;
				
			case "status":
				return this._dataTag.status;
				
			case "message":
				return this._dataTag.message;
				
			case "isprocessed":
				return this._dataTag.isProcessed;
				
			case "selected":
				return this._dataTag.selected;
				
			default:
				return this[fieldName];
		}
	}

//****************************************************************************************
//	Sets the value of a given field
//****************************************************************************************
	function DataRecord_Set(fieldName, value) {
		switch (fieldName.toLowerCase()) {
			case "internalid":
				this._dataTag.internalID = value;
				break;
				
			case "action":
				this._dataTag.action = value;
				break;
				
			case "status":
				this._dataTag.status = value;
				break;
				
			case "message":
				this._dataTag.message = value;
				break;
				
			case "isprocessed":
				this._dataTag.isProcessed = value;
				break;
				
			case "selected":
				this._dataTag.selected = value;
				break;
				
			default:
				this[fieldName] = value;
				break;
		}
	}

//****************************************************************************************
//	Merges an object with this record
//****************************************************************************************
	function DataRecord_Merge(source) {
		if (source != null) {
			for (var item in source)
				if ((source.hasOwnProperty(item)) && (item != "_dataTag"))
					this[item] = source[item];
					
			if (source["_dataTag"] != null) {
				source["_dataTag"].internalID = (source["_dataTag"].internalID != "" ? source["_dataTag"].internalID : this["_dataTag"].internalID);
				this["_dataTag"] = source["_dataTag"];
			}
		}
	}


//****************************************************************************************
//	Returns the JSON representation of this record
//****************************************************************************************
	function DataRecord_ToString() {
		var contents = "{_dataTag: {"
		contents += "internalID: " + this._dataTag.internalID + ", ";
		contents += "action: '" + this._dataTag.action + "', ";
		contents += "status: '" + this._dataTag.status + "', ";
		contents += "message: '" + this._dataTag.message + "', ";
		contents += "isProcessed: " + this._dataTag.isProcessed + "} ";
		
		for (var Item in this.Data)
			if (this.Data.hasOwnProperty(Item))
				contents += ", " + Item + ", '" + this.Data[Item] + "'";

		contents += "}";

		return contents;
	}


//****************************************************************************************
//
//	Index Management object for indexing data
//
//****************************************************************************************


//****************************************************************************************
//	Indexer for single field
//		NOTE: May need to keep track of deleted elements in the Values arrays and fill them 
//		with each new piece of data to make sure the array indexes do not get too big.
//****************************************************************************************
	function Indexer(name, field) {
	//	Properties
		this.name = name;							//	Name of Index
		this.field = field;							//	Field that is used to index
		
		this.values = new Object();					//	Array of Objects indexed by Value 
		this.loc = new Object();					//	Pointers to where an Object is located in Index (internal use)

	//	Methods
		this.clear = Indexer_Clear;					//	Clears the index of all data
		this.remove = Indexer_Remove;				//	Removes an object from the index
		this.index = Indexer_Index;					//	Indexes and reindexes an object
		this.find = Indexer_Find;					//	Returns the array of objects based on passed Value
	}

//****************************************************************************************
//	Clears the index of all data
//****************************************************************************************
	function Indexer_Clear() {
		this.values = new Object();
		this.loc = new Object();
	}

//****************************************************************************************
//	Removes an object from the index
//****************************************************************************************
	function Indexer_Remove(data) {
		var id = data.get("InternalID");
		if (this.loc[id] != null) {
			delete this.values[this.loc[id].value][this.loc[id].position];
			delete this.loc[id];
		}
	}

//****************************************************************************************
//	Indexes and reindexes an object
//****************************************************************************************
	function Indexer_Index(data) {
		var id = data.get("InternalID");
		var value = data.get(this.field);
		if (value == null)
			return;

		var pos = (((this.loc[id] != null) && (this.loc[id].value == value)) ? this.loc[id].position : -1);
		if (pos < 0) {
			this.remove(data);

			if (this.values[value] == null)
				this.values[value] = [];
				
			pos = (this.values[value].push(data) - 1);
		} else {
			this.values[value][pos] = data;
		}								

		this.loc[id] = { 'value': value, 'position': pos };
	}
	
//****************************************************************************************
//	Returns the array of objects based on passed Value
//****************************************************************************************
	function Indexer_Find(value) {
		var result = new Array();
		if (this.values[value] != null)
			for (var item in this.values[value])
				if (this.values[value].hasOwnProperty(item))
					result.push(this.values[value][item]);
				
		return result;
	}
	

//****************************************************************************************
//	Index Manager for multiple indexes
//****************************************************************************************
	var indexSortDesc = false;
	function IndexManager() {
	//	Properties
		this.indexes = new Array();					//	Array of Indexes being managed
		this.names = new Object();					//	Points to where an index is
		
		
	//	Methods
		this.clear = IndexManager_Clear;			//	Clears the DataSource of all data;
		this.add = IndexManager_Add;				//	Add an index to the Manager
		this.remove = IndexManager_Remove;			//	Removes an object from the managed indexes
		this.index = IndexManager_Index;			//	Index an Object
		this.find = IndexManager_Find;				//	Find Objects based on passed criteria
	}


//****************************************************************************************
//	Clears the DataSource of all data;
//****************************************************************************************
	function IndexManager_Clear() {
		for (var i = 0; i < this.indexes.length; i++)
			this.indexes[i].clear();
	}

//****************************************************************************************
//	Add an index to the Manager
//****************************************************************************************
	function IndexManager_Add(name, field) {
		var index = new Indexer(name, field);
		this.names[name] = (this.indexes.push(index) - 1);	
	};
					
//****************************************************************************************
//	Removes an object from the managed indexes
//****************************************************************************************
	function IndexManager_Remove(data) {
		for (var i = 0; i < this.indexes.length; i++)
			this.indexes[i].remove(data);
	};
						
//****************************************************************************************
//	Index an Object
//****************************************************************************************
	function IndexManager_Index(data) {
		for (var i = 0; i < this.indexes.length; i++)
			this.indexes[i].index(data);
	};

//****************************************************************************************
//	Find Objects based on passed criteria
//****************************************************************************************
	function IndexManager_Find(criteria, sortField, sortDesc) {
		var results = new Array();
		var tmpResults;

		var keys = parseCriteria(criteria);
		for (var i = 0; i < keys.length; i++) {
			var iIndex = this.names[keys[i].key];
			if (iIndex != null) {
				tmpResults = this.indexes[iIndex].find(keys[i].value);
				switch (keys[i].op.toLowerCase()) {
					case "start":
						results = tmpResults;
						break;
						
					case "and":
						results = intersect(results, tmpResults);
						break;

					case "or":
						results = union(results, tmpResults);
						break;
				}
			}
		}

		if ((results.length > 0) && (sortField != null)) {
			indexSortDesc = (sortDesc != null ? sortDesc : indexSortDesc);
			var sortFunction = new Function("Item1", "Item2", "var Value1 = Item1." + sortField + "; var Value2 = Item2." + sortField + "; if (Value1 == null) return (IndexSortDesc ? 1 : -1); if (Value2 == null) return (IndexSortDesc ? -1 : 1); if (Value1 > Value2) return (IndexSortDesc ? -1 : 1); if (Value1 == Value2) return 0; if (Value1 < Value2) return (IndexSortDesc ? 1 : -1); return 0");
			results = results.sort(sortFunction);
		}

		return results;
	};
							

	//	Parses the Find Criteria
	function parseCriteria(criteria) {
		var elements = new Array();
		var match;
		var regOption = /(start|and|or)+\s*(\w*)\s*=\s*(['"!]?)(.*?)\3/g;

		if (this != "") {
			var strElements = "start" + criteria.replace(/\\'/g, "~");
		
			while ((match = regOption.exec(strElements)) != null) {
				if (match.length >= 4)
					elements.push({'op': match[1], 'key': match[2], 'value': match[4].replace(/~/g, "'")});
			}
		}
		return elements;
	}

	//	Returns the Union between two arrays
	function union(arr1, arr2) {
		var processed = {};  
		var result = new Array();
		
		for (var item in arr1) {
			if (arr1.hasOwnProperty(item) && !processed[arr1[item].get("InternalID")]) {
				processed[arr1[item].get("InternalID")] = true;
				result.push(arr1[item]);
			}
		}
			
		for (var item in arr2) {
			if (arr2.hasOwnProperty(item) && !processed[arr2[item].get("InternalID")])
				result.push(arr2[item]);
		}
		return result;
	}

	//	Returns the Intersection between two arrays
	function intersect(arr1, arr2) {
		var processed = {};  
		var result = new Array();
		
		for (var item in arr1) {
			if (arr1.hasOwnProperty(item) && !processed[arr1[item].get("InternalID")]) 
				processed[arr1[item].get("InternalID")] = true;
		}
			
		for (var item in arr2) {
			if (arr2.hasOwnProperty(item) && processed[arr1[item].get("InternalID")]) 
				result.push(arr2[item]);
		}
		
		return result;
	}
	

//****************************************************************************************
//
//	Basic DataSource Object
//		The DataSource client/backend system communicate using an array of DataRecord 
//		objects. These objects have the following structure:
//
//			InternalID (string):	the internal ID allocated by the DataSource (clientside) 
//									to identify this data object.
//			Action (string):		unknown.
//			Status (string):		Status of the data object. 
//									Possible values: New, Update, Delete, Select, Error, None. 
//									The value determines what the backend does with the 
//									data when applied.
//			Message (string):		Any message returned from server - usually an error.
//			IsProcessed (bool):		Indicates if the data has been processed.
//			Data (object):			The Data.
//
//****************************************************************************************
	
	function DataSource(name, target, processPage) {
	
//*********************************
//	Properties
		this.name = name;							//	Name of object
		this.target = target;						//	Name of Datasource on Server
		this.id = 0;								//	Internal identity counter;
		this.dataFormat = "JSON";					//	Format of DataCommunications;
		this.clearAll = true;						//	Specifies if the existing data is cleared before new data is fetched.
		this.processPage = processPage;				//	Page that will perform the processing

		this.indexManager = new IndexManager();		//	Data Index Manager
		this.indexManager.add("Status", "Status");
		
		this.lastAction = "";						//	Keeps track of the last action
		this.ok = false;							//	Indicates if the latest action completed OK.
		
		this.data = new Array();					//	DataSource Data. Array of DataRecord
		this.loc = new Object();					//	Points to where the Object is based on Internal ID
		this.count = 0;								//	The number of data elements in the DataSet
		
		
//*********************************
//	Methods

//	Setup Methods
		this.addIndex =	DataSource_AddIndex;		//	Adds a new Data index
		this.clear = DataSource_Clear;				//	Clears the DataSource of all data;
		
//	Data Retrieval Methods
		this.get = DataSource_Get;					//	Returns a single element, specified by iData, from the data array once sorted by SortField
		this.find =	DataSource_Find;				//	Returns an array of Data Objects based on the Criteria and sortedby SortField
		this.all = DataSource_All;					//	Returns all Data Objects sorted by SortField

//	Data Management Methods
		this.create = DataSource_Create;			//	Returns a an empty data object for this DataSource with ID and status set
		this.update = DataSource_Update;			//	Updates the internal storage and indexes with the passed Data Object
		this.remove = DataSource_Remove;			//	Removes the indexes entries for Data and the internal entry if Permanent is set

//	Server Methods
		this.apply = DataSource_Apply;				//	Sends the data to the server
		this.fetch = DataSource_Fetch;				//	Fetches the data from the server
		this.execute = DataSource_Execute;			//	Executes the command on the server

		return this;
	}
	
//*********************************
//	Methods used by all Datasources

//	Setup Methods
	//	Adds a new Data index
	function DataSource_AddIndex(name, field) {
		this.lastAction = "AddIndex";
		this.ok = true;
		this.indexManager.add(name, field);
	}
		
	//	Clears the DataSource of all data;
	function DataSource_Clear() {
		this.id = 0;
		this.indexManager.clear();
		this.data = new Array();
		this.loc = new Object();
		this.count = 0;
		this.lastAction = "Clear";
		this.ok = true;
	}
		
//	Data Retrieval Methods
	//	Returns a single element, specified by iData, from the data array once sorted by SortField
	function DataSource_Get(iData, sortField, sortDesc) {
		iData = (iData != null ? iData : 0);
		var result = this.all(sortField, sortDesc);
		this.lastAction = "Find";
		this.ok = (result.length > iData);
		if (this.ok)
			return result[iData];
		else
			return {};
	}
	
	//	Returns an array of Data Objects based on the Criteria and sortedby SortField
	function DataSource_Find(criteria, sortField, sortDesc) {
		var result = this.indexManager.find(criteria, sortField, sortDesc);
		this.lastAction = "Find";
		this.ok = (result.length > 0);
		return result;
	}
		
	//	Returns all Data Objects sorted by SortField
	function DataSource_All(sortField, sortDesc) {
		var criteria = "Status = '" + dataStatus.OK + "'";
		criteria += " or Status = '" + dataStatus.New + "'";
		criteria += " or Status = '" + dataStatus.Update + "'";
		criteria += " or Status = '" + dataStatus.None + "'";
		
		var result = this.indexManager.find(criteria, sortField, sortDesc);
		this.lastAction = "All";
		this.ok = (result.length > 0);
		return result;
	}

//	Data Management Methods
	//	Returns a an empty data object for this DataSource with ID and status set
	function DataSource_Create(data) {
		this.lastAction = "New";
		this.OK = true;
		var newData, result;
		if (global.typeOf(data) == "array") {
			var result = [];
			for (var i = 0; i < data.length; i++) {
				if (data[i] instanceof DataRecord) {
					result.push(data[i]);
				} else {
					newData = new DataRecord(this.name + this.id++, dataStatus.None);
					newData.merge(data[i]);
					result.push(newData);
				}
			}
		} else {
			result = new DataRecord(this.name + this.id++, dataStatus.None);
			result.merge(data);
		}
		return result;
	}
							
	//	Updates the internal storage and indexes with the passed Data Object
	function DataSource_Update(data, statusOverRide) {
		this.lastAction = "Update";
		if (data == null) {
			this.ok = false;
			throw "DataSource Error: Cannot update null Data object.";
		}
			
		if (!(data instanceof DataRecord) || (data.get("InternalID") == null))
			data = this.create(data);

		var iData = this.loc[data.get("InternalID")];
		if (iData == null) {
			data.set("Status", (statusOverRide != null ? statusOverRide : dataStatus.New));
			iData = (this.data.push(data) - 1);
			this.loc[data.get("InternalID")] = iData;
		} else {
			data.set("Status", (statusOverRide != null ? statusOverRide : dataStatus.Update));
			this.data[iData] = data;
		}

		this.indexManager.index(data);
		this.ok = true;
	}

	//	Removes the indexes entries for Data and the internal entry if Permanent is set
	function DataSource_Remove(data, permanent) {
		this.lastAction = "Remove";
		if ((data == null) || !(data instanceof DataRecord) || (data.get("InternalID") == null)) {
			this.ok = false;
			throw "DataSource Error: Cannot remove null Data object.";
		}

		this.indexManager.remove(data);
			
		if ((permanent) && (this.loc[data.get("InternalID")] != null))
		{
			delete this.data[this.loc[data.get("InternalID")]];
			delete this.loc[data.get("InternalID")];
		}
		this.ok = true;
	}

//*********************************
//	Server Methods
	//	Sends the data to the server
	function DataSource_Apply(async, postProcess) {
		async = (async != null ? async : false);
		this.postProcess = postProcess;

		var isDirty = this.indexManager.find("Status = '" + dataStatus.New + "' or Status = '" + dataStatus.Update + "'");
		var json = JSON.toJSON(isDirty);

		var apply = HTTPManager.add("Apply", this.processPage, DataSourceManager.callback(this.name));
		apply.method = "Post";
		apply.execute("target=" + this.target, "JSON", json, async);

		this.lastAction = "Apply";
	}
		
	//	Fetches the data from the server
	function DataSource_Fetch(fetchData, async, postProcess) {
		async = (async != null ? async : false);
		this.postProcess = postProcess;
		
		fetchData = DataSourceManager.criteriaToObject(fetchData);

		var fetchDataRecord = new DataRecord("Input", "None");
		fetchDataRecord.merge(fetchData);
		var json = JSON.toJSON(fetchDataRecord);

		var fetch = HTTPManager.add("Fetch", this.processPage, DataSourceManager.callback(this.name));
		fetch.method = "Post";
		fetch.execute("target=" + this.target, "JSON", json, async);
	}
		
	//	Executes the command on the server
	function DataSource_Execute(command, async, postProcess, data) {
		async = (async != null ? async : false);
		this.postProcess = postProcess;
		var json = JSON.toJSON((data != null ? data : ""));

		var execute = HTTPManager.add("Execute", this.processPage, DataSourceManager.callback(this.name));
		apply.method = "Post";
		apply.execute("target=" + this.target, "JSON", json, async);
	}

//****************************************************************************************
//	DataSourceManager
//		Object that manages the various datasources
//****************************************************************************************

	var DataSourceManager = function() {
	
		return {

			create:				function(name, target, processPage) {
									var dataSource = new DataSource(name, target, processPage);
									this[name] = dataSource;
									return dataSource;
								},
							
			criteriaToObject:	function(criteria) {
									var objCriteria = {};

									switch (typeof(criteria)) {
										case "object":
											objCriteria = criteria;
											break;
										
										case "string":
											if (criteria != "") {
												var arrCriteria = parseCriteria(criteria);
												for (var i = 0; i < arrCriteria.length; i++)
													objCriteria[arrCriteria[i].key] = arrCriteria[i].value;
											}
											break;
									}
									return objCriteria;
								},
							
			lookup:				function(processPage, query, data) {
									return DataSourceManager.request(processPage, "Lookup", query, data);
								},
	
			request:			function(processPage, action, query, data, async) {
									var dataSource = new DataRecord("generic", "OK");
									dataSource.merge(data);
									var json = JSON.toJSON(dataSource);

									var request = HTTPManager.add(action, processPage);
									request.method = "Post";
									result = request.execute(query, "JSON", json, async);
									if (!async && result.ok)
										result.data = JSON.fromJSON(result.data);
										
									return result;
								},
							
			//	Callback processing function from AJAX call
			callback:			function(dataSource) {
									return	function(result) {
												DataSourceManager.process(dataSource, result);
											};
								}, 
								
			process:			function(dataSource, result) {
									var dataSourceObj = DataSourceManager[dataSource];
									if (dataSourceObj == null) {
										message.raise("DataSource", "Error", "Unable to process HTTP result - target (" + dataSource + ") unknown.", "", false);
										return;
									}
										
									try {
										if (!result.ok) {
											if (result.error != "")
												message.raise("DataSource", "Error", result.error, "", false);
											return;
										}
										
										if (result.warning != "")
											message.raise("DataSource", "Warning", result.warning, "", false);
											
										if (result.info != "")
											message.raise("DataSource", "Info", result.info, "", false);
											
										var resultData = eval("(" + result.data + ")");
												
										switch (result.action.toLowerCase()) {
											case "fetch":
												if (dataSourceObj.clearAll)
													dataSourceObj.clear();
												break;
													
											case "apply":
												break;
	
											case "execute":
												break;
										}

										if (global.typeOf(resultData) != "array")
											resultData = [resultData];
											
										for (var i = 0; i < resultData.length; i++)
											dataSourceObj.update(resultData[i], dataStatus.OK);
												
										dataSourceObj.count = parseInt(result.count);
									}
									catch (e) {
										message.raise("DataSource", "Error", e.message, "", false);
										return;
									}
									
									if (this.postProcess != null) {
										this.postProcess(result);
										this.postProcess = null;
									}
								}
	
	
		}
	}();
	var DSM = DataSourceManager;
