﻿
// Wx.Data.JS
// (c) Webfuel

// FINQ: TODO: Can we make Wx independent of any specific query language?
;

var FINQ = {};

// Static members on the FINQ object
$.extend(FINQ, {

	// Enumerations (must remain in sync with the C# equivalents!)
	Op: {
		None: 0,
		Equals: 1,
		NotEqual: 2,
		GreaterThanOrEqual: 3,
		LessThanOrEqual: 4,
		GreaterThan: 5,
		LessThan: 6
	},

	ConditionToFQL: function (field, op, value) {
		return 'WHERE ' + field + ' ' + FINQ.OpToFQL(op) + ' ' + FINQ.ValueToFQL(value);
	},

	OpToFQL: function (op) {
		if ($.isString(op)) {
			return op;
		} else {
			switch (op) {
				case 1: return '=';
				case 2: return '!=';
				case 3: return '>=';
				case 4: return '<=';
				case 5: return '>';
				case 6: return '<';
			}
		}
		$.error('FINQ.OpToFQL(): Illegal op code.');
	},

	ValueToFQL: function (value) {
		if (value == null)
			return "NULL";
		if ($.isString(value))
			return "'" + value + "'";
		if ($.isDate(value))
			return "# " + $.formatDate(value) + " #";
		return '' + value;
	}
});

; (function ($) {

	// Ajax Wrapper

	var ajaxCounter = 0;
	var ajaxRetries = 0;

	// Modified version of JSON2 (http://www.json.org/json2.js) by Crockford.  Modification just 
	// fixes up dates passed back from the server as strings (either in ISO or MSAJAX format)
	var Json = function() {

		function f(n) {
			// Format integers to have at least two digits.
			return n < 10 ? '0' + n : n;
		}

		Date.prototype.toJSON = function(key) {
			return this.getUTCFullYear() + '-' +
				 f(this.getUTCMonth() + 1) + '-' +
				 f(this.getUTCDate()) + 'T' +
				 f(this.getUTCHours()) + ':' +
				 f(this.getUTCMinutes()) + ':' +
				 f(this.getUTCSeconds()) + 'Z';
		};

		Number.prototype.toJSON =
		Boolean.prototype.toJSON = function(key) {
			return this.valueOf();
		};

		String.prototype.toJSON = function(key) {
			return this.valueOf();
		};

		var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
		escapeable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
		gap,
		indent,
		meta = {    // table of character substitutions
			'\b': '\\b',
			'\t': '\\t',
			'\n': '\\n',
			'\f': '\\f',
			'\r': '\\r',
			'"': '\\"',
			'\\': '\\\\'
		},
		rep;

		function quote(string) {

			escapeable.lastIndex = 0;
			return escapeable.test(string) ?
		'"' + string.replace(escapeable, function(a) {
			var c = meta[a];
			if (typeof c === 'string') {
				return c;
			}
			return '\\u' + ('0000' +
					(+(a.charCodeAt(0))).toString(16)).slice(-4);
		}) + '"' :
		'"' + string + '"';
		}

		function str(key, holder) {
			var i,          // The loop counter.
			k,          // The member key.
			v,          // The member value.
			length,
			mind = gap,
			partial,
			value = holder[key];

			if (value && typeof value === 'object' &&
			typeof value.toJSON === 'function') {
				value = value.toJSON(key);
			}

			if (typeof rep === 'function') {
				value = rep.call(holder, key, value);
			}

			switch (typeof value) {
				case 'string':
					return quote(value);

				case 'number':
					return isFinite(value) ? String(value) : 'null';

				case 'boolean':
				case 'null':
					return String(value);

				case 'object':
					if (!value) {
						return 'null';
					}

					gap += indent;
					partial = [];

					if (typeof value.length === 'number' &&
						!(value.propertyIsEnumerable('length'))) {

						length = value.length;
						for (i = 0; i < length; i += 1) {
							partial[i] = str(i, value) || 'null';
						}

						v = partial.length === 0 ? '[]' :
							gap ? '[\n' + gap +
							partial.join(',\n' + gap) + '\n' +
							mind + ']' :
							'[' + partial.join(',') + ']';
						gap = mind;
						return v;
					}

					if (rep && typeof rep === 'object') {
						length = rep.length;
						for (i = 0; i < length; i += 1) {
							k = rep[i];
							if (typeof k === 'string') {
								v = str(k, value);
								if (v) {
									partial.push(quote(k) + (gap ? ': ' : ':') + v);
								}
							}
						}
					} else {
						for (k in value) {
							if (Object.hasOwnProperty.call(value, k)) {
								v = str(k, value);
								if (v) {
									partial.push(quote(k) + (gap ? ': ' : ':') + v);
								}
							}
						}
					}
					v = partial.length === 0 ? '{}' :
						gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
						mind + '}' : '{' + partial.join(',') + '}';
					gap = mind;
					return v;
			}
		}

		return {
			stringify: function(value, replacer, space) {
				var i;
				gap = '';
				indent = '';

				if (typeof space === 'number') {
					for (i = 0; i < space; i += 1) {
						indent += ' ';
					}
				} else if (typeof space === 'string') {
					indent = space;
				}

				rep = replacer;
				if (replacer && typeof replacer !== 'function' &&
				(typeof replacer !== 'object' ||
				 typeof replacer.length !== 'number')) {
					throw new Error('JSON.stringify');
				}
				return str('', { '': value });
			},

			parse: function(text, reviver) {
				var j;

				function walk(holder, key) {
					var k, v, value = holder[key];
					if (value && typeof value === 'object') {
						for (k in value) {
							if (Object.hasOwnProperty.call(value, k)) {
								v = walk(value, k);
								if (v !== undefined) {
									value[k] = v;
								} else {
									delete value[k];
								}
							}
						}
					}
					return reviver.call(holder, key, value);
				}

				cx.lastIndex = 0;
				if (cx.test(text)) {
					text = text.replace(cx, function(a) {
						return '\\u' + ('0000' +
						(+(a.charCodeAt(0))).toString(16)).slice(-4);
					});
				}

				if (/^[\],:{}\s]*$/.
					test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
					replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
					replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

					j = eval('(' + text + ')');

					return typeof reviver === 'function' ? walk({ '': j }, '') : j;
				}
				throw new SyntaxError('JSON.parse');
			},

			parseWithReviver: function(text) {
				return Json.parse(text, function(key, value) {
					if (value && typeof value === "string") {
						// Revive MSAJAX dates
						var a = /^\/Date\((d|-|.*)\)[\/|\\]$/.exec(value);
						if(a){
                        	var b = a[1].split(/[-+,.]/);
                        	return new Date(b[0] ? +b[0] : 0 - +b[1]);
						}
						// Revive ISO dates
						a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
						if(a)
                        	return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));
					}
					return value;
				});
			}
		};
	} ();

	// Extensions to jQuery's wonderful $.ajax method
	// Store the original version of the method
	$._ajax = $.ajax;

	$.extend($, {

		toJson: function(value) {
			return Json.stringify(value);
		},

		fromJson: function(value) {
			return Json.parseWithReviver(value);
		},

		ajax: function (s) {
			s.ajaxCounter = ajaxCounter++;
			if(!s._retry) {
				s._error = s.error;
				s._success = s.success;
				s.type = s.type || 'POST';
				s.contentType = s.contentType || "application/json; charset=utf-8";
				s.beforeSend = function (xhr) { xhr.setRequestHeader("Content-type", s.contentType); };
				s.dataType = s.dataType || "text"; // Not Json - we will parse
				s.data = s.contentType == "application/json; charset=utf-8" ? $.toJson(s.data || {}) : s.data;
				s.success = function (result) {
					if(!s._success) return;
					if(s.contentType == "application/json; charset=utf-8" && s.dataType == "text")
						result = $.fromJson(result); // Using our Json convertor fixes the issue with server side dates
					s._success(result.d);
				};
				s.error = function (xhr, errorType, e) {
					// If this request has retries left the retry it
					if(s.retries > 0 && s.retries <= 3) {
						ajaxRetries++;
						s._retry = true;
						s.retries--;
						$.ajax(s);
						return;
					}
					// Force the unlock of any current modal locking
					$.modalUnlock(true);
					// Analyse the xhr to see if we can extract a useful error message, otherwise set a default error message
					var err = { Message: "Communication link with the server has been lost.  Please try again or refresh the page." };
					if (xhr.responseText && xhr.responseText.length > 0 && xhr.responseText.charAt(0) == '{') {
						err = $.fromJson(xhr.responseText);
					}
					if(err.Message) err.message = err.Message;
					// TODO: Should we catch certain types of errors here (e.h. Http Error codes)?
					// If we were given an error handling method then call it,  otherwise raise the error ourselves
					if(s._error) {
						s._error(err);
					} else {
						$.error(err.Message);
					}
				};
			}
			$._ajax(s);
		},

      invoke: function(service, method, data, success, options) {
            options = options || {};

            options.retries = options.retries || 0;
            options.TrustLevel = options.TrustLevel || 'Low';
            options.data = { service: service, method: method, parameters: data };
            options.success = success;
            options.url = "/WebServices/" + options.TrustLevel + "Trust/FusionWebService.asmx/Invoke";

            $.ajax(options);
        }
	});

	// TODO: Modify this to a more generic datasource and implement AdminDataSource in script
	Wx.DataSource = Wx.Widget.extend({
		_init: function(p) {
			Wx.Widget.prototype._init.call(this, p);
			// Set properties
			this._url = p.url;
			this._selectUrl = p.selectUrl || p.url;
			this._insertUrl = p.insertUrl || p.url;
			this._updateUrl = p.updateUrl || p.url;
			this._deleteUrl = p.deleteUrl || p.url;

			this._service = p.service;
			this._selectService = p.selectService || p.service;
			this._insertService = p.insertService || p.service;
			this._updateService = p.updateService || p.service;
			this._deleteService = p.deleteService || p.service;

			this._selectMethod = p.selectMethod || 'Select';
			this._insertMethod = p.insertMethod || 'Insert';
			this._updateMethod = p.updateMethod || 'Update';
			this._deleteMethod = p.deleteMethod || 'Delete';

			this._disableCache = p.disableCache === true;
			this._sourceQuery = p.sourceQuery || '';
			this._cache = {};
			this._signature = 'invoke';
		},
		destroy: function() {
			Wx.Widget.prototype.destroy.call(this);
		},
		flushData: function() {
			this._cache = {};
			this.trigger('flushedData');
		},
		selectData: function(parameters, callback, options) {
			// If we provided a filter query as parameters then build the parameters object
			if(!$.isObject(parameters)){
				var query = this._sourceQuery;
				if($.isString(parameters))
					query += ' ' + parameters;
				parameters = { fql: query };
			}
			// Check to see if we have this result cached
			if((this._disableCache !== true) && this._cache[query]) {
				if(callback)
					callback(this._cache[query]);
				return;
			}
			options = $.extend({ cssClass: 'loading', interval: 0, eventType: 'selectedData', retries: 3, flush: false }, options || {});
			var _callback = callback;
			var self = this;
			callback = function(result) {
				self._cache[query] = result;
				if(_callback)
					_callback(result);
			}
			this._invoke(this._selectUrl, this._selectService, this._selectMethod, parameters, callback, options);
		},
		insertData: function(parameters, callback, options) {
			options = $.extend({ cssClass: 'saving', interval: 0, eventType: 'insertedData' }, options || {});
			this._invoke(this._insertUrl, this._insertService, this._insertMethod, parameters, callback, options);
		},
		updateData: function(parameters, callback, options) {
			options = $.extend({ cssClass: 'saving', interval: 1500, eventType: 'updatedData' }, options || {});
			this._invoke(this._updateUrl, this._updateService, this._updateMethod, parameters, callback, options);
		},
		deleteData: function(parameters, callback, options) {
			options = $.extend({ cssClass: 'processing', interval: 0, eventType: 'deletedData' }, options || {});
			this._invoke(this._deleteUrl, this._deleteService, this._deleteMethod, parameters, callback, options);
		},
		invoke: function(method, parameters, callback, options) {
			this._invoke(this._url, this._service, method, parameters, callback, options);
		},
		_invoke: function(url, service, method, parameters, callback, options) {
			options = options || {};
			if(options.flush !== false)
				this.flushData();
			if(options.modalLock !== false)
				$.modalLock(options);
			var self = this;
			var s = { 
				url: url,
				retries: options.retries || 0, 
                error: options.error,
				data: {
					service: service,
					method: method,
					parameters: parameters
				},
				success: function(result) {
					if(callback)
						callback(result);
					if(options.eventType)
						self.trigger({ type: options.eventType, dataItem: result });
					if(options.modalLock !== false)
						$.modalUnlock();
				}
			};
			$.ajax(s);
		}
	});

})(jQuery);
