﻿
// Wx.Core.JS
// (c) Webfuel

// Array Prototype Core



; (function ($) {

	if (!Array.prototype.find) {
		Array.prototype.find = function (o) {
			var len = this.length >>> 0;
			for (var i = 0; i < len; i++) {
				if (this[i] === o)
					return i;
			}
			return -1;
		};
	}

	if (!Array.prototype.where) {
		Array.prototype.where = function (fn /*, scope */) {
			var len = this.length >>> 0;
			var res = [];
			var scope = arguments[1];
			for (var i = 0; i < len; i++) {
				if (fn.call(scope, this[i], i, this))
					res.push(this[i]);
			}
			return res;
		}
	}

	if (!Array.prototype.orderBy) {
		// fn = function(a, b) : returns < 0 if a < b, 0 if a == b, > 0 if a > b;
		Array.prototype.orderBy = function (fn /*, scope */) {
			var res = this.slice(0);
			var scope = arguments[1];
			res.sort(function (a, b) { return fn.call(scope, a, b); });
			return res;
		}
	}

	if (!Array.prototype.orderByDescending) {
		// fn = function(a, b) : returns < 0 if a < b, 0 if a == b, > 0 if a > b;
		Array.prototype.orderByDescending = function (fn /*, scope */) {
			var res = this.slice(0);
			var scope = arguments[1];
			res.sort(function (a, b) { return fn.call(scope, b, a); });
			return res;
		}
	}

	if (!Array.prototype.page) {
		Array.prototype.page = function (skip, take) {
			var len = this.length >>> 0;
			var res = [];
			for (var i = skip; (i < len) && ((i - skip) < take); i++) {
				if (i >= 0)
					res.push(this[i]);
			}
			return res;
		}
	}

	// select == map (using select to try to mirror C# LINQ syntax)
	if (!Array.prototype.select) {
		Array.prototype.select = function (fn /*, scope*/) {
			var len = this.length >>> 0;
			var res = new Array(len);
			var scope = arguments[1];
			for (var i = 0; i < len; i++) {
				res[i] = fn.call(scope, this[i], i, this);
			}
			return res;
		};
	}

	if (!Array.prototype.reduce) {
		Array.prototype.reduce = function (fn, init /*, scope */) {
			var len = this.length >>> 0;
			var scope = arguments[2];
			for (var i = 0; i < len; i++) {
				init = fn(init, this[i], i, this);
			}
			return init;
		};
	}

	if (!Array.prototype.foldl) {
		Array.prototype.foldl = Array.prototype.reduce;
	}

	if (!Array.prototype.foldr) {
		Array.prototype.foldr = function (fn, init /*, scope */) {
			var len = this.length >>> 0;
			var scope = arguments[2];
			for (var i = len - 1; i >= 0; i--) {
				init = fn(init, this[i], i, this);
			}
			return init;
		};
	}

	var _pre = 'uid', _uid = 10000; // GUID stamps

	var _modalElement = null;
	var _modalCounter = 1; // Start at 1, as the screen loads with a loading lock on

	var TOSTRING = Object.prototype.toString,

		ARRAY = 'array',
		BOOLEAN = 'boolean',
		DATE = 'date',
		ERROR = 'error',
		FUNCTION = 'function',
		NUMBER = 'number',
		NULL = 'null',
		OBJECT = 'object',
		REGEX = 'regex',
		STRING = 'string',
		UNDEFINED = 'undefined',

		TYPES = {
			'undefined': UNDEFINED,
			'number': NUMBER,
			'boolean': BOOLEAN,
			'string': STRING,
			'[object Function]': FUNCTION,
			'[object RegExp]': REGEX,
			'[object Array]': ARRAY,
			'[object Date]': DATE,
			'[object Error]': ERROR
		},

		EMPTYSTRING = '',

		DEFAULTDATEFORMAT = 'dd MMM yyyy',
		SHORTYEARCUTOFF = '+50', // Years < this are in the current century, >= this are in the previous century

		MONTHNAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
		MONTHNAMESSHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
		DAYNAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
		DAYNAMESSHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

	$.extend($, {
	
		// Generate a new unique identifier
		uid: function (pre) {
			var p = pre || _pre;
			return p + '_' + (_uid++)
		},

		// Returns the uid associated with an object.
		// If the object does not have a uid a new one is created unless readOnly is true
		stamp: function (o, readOnly) {
			if (!o) {
				return o;
			}
			var uid = (typeof o === 'string') ? o : o._uid;
			if (!uid) {
				uid = this.uid();
				if (!readOnly) {
					try {
						o._uid = uid;
					} catch (e) {
						uid = null;
					}
				}
			}
			return uid;
		},

		alert: function (message, callback) {
			//if (window.tinymce && tinyMCE.activeEditor) {
			//	tinyMCE.activeEditor.windowManager.alert(message, callback);
			//} else {
				alert(message);
				if (callback) callback();
			//}
		},

		confirm: function (message, callback) {
			//if (window.tinymce && tinyMCE.activeEditor)
			//	tinyMCE.activeEditor.windowManager.alert(message);
			//else {
				var result = confirm(message);
				if (callback) callback(result);
			//}
		},

		error: function (message) {
			$.alert(message);
			//throw new Error(message);
		},

		assert: function (test, message) {
			if (!test) $.error(message);
		},

		openWindow: function (url, options) {
			if (!options) options = {};
			var name = options.name || ('win' + $.uid());
			var params = '';
			if (options.width != undefined)
				params += 'width=' + options.width + ',';
			if (options.height != undefined)
				params += 'height=' + options.height + ',';
			window.open(url, name, params);
		},

		openDialog: function (s, p) {
			s = s || {}; p = p || {};
			if (s.dialog) {
				s.inline = true;
				s.file = '/wx/external/tinymce/plugins/' + s.dialog + '/dialog.htm';
				p.plugin_url = '/wx/external/tinymce/plugins/' + s.dialog;
			}
			if (window.tinymce && tinyMCE.activeEditor)
				return tinyMCE.activeEditor.windowManager.open(s, p);
		},

		// setTimeout wrapper to deal with environments where it is not supported
		timeout: function (fn, delay, context) {
			if (context)
				fn = $.proxy(fn, context);
			if (window.setTimeout)
				window.setTimeout(fn, delay);
			else
				fn();
		},

		// Firefox uses textContent rather than innerText
		innerText: function (element) {
			if (element.innerText)
				return element.innerText;
			return element.textContent;
		},

		redirectTo: function (url) {
			window.location = url;
		},

		// Returns the specified namespace (creates it if it does not exist)
		namespace: function () {
			var a = arguments, o = null, i, j, d;
			for (i = 0; i < a.length; i = i + 1) {
				d = ("" + a[i]).split(".");
				o = window;
				for (j = 0; j < d.length; j = j + 1) {
					o[d[j]] = o[d[j]] || {};
					o = o[d[j]];
				}
			}
			return o;
		},

		type: function (o) {
			return TYPES[typeof o] || TYPES[TOSTRING.call(o)] || (o ? OBJECT : NULL);
		},

		isArray: function (o) {
			return $.type(o) === ARRAY;
		},

		isBoolean: function (o) {
			return $.type(o) === BOOLEAN;
		},

		isFunction: function (o) {
			return $.type(o) === FUNCTION;
		},

		isDate: function (o) {
			return $.type(o) === DATE;
		},

		isNull: function (o) {
			return o === null;
		},

		isNullOrEmpty: function (o) {
			if (o === null)
				return true;
			if (($.isString(o) || $.isArray(o)) && o.length == 0)
				return true;
			return false;
		},

		isNumber: function (o) {
			return typeof (o) === NUMBER && isFinite(o);
		},

		isObject: function (o, failfn) {
			return (o && (typeof o === OBJECT || (!failfn && $.isFunction(o))) || false);
		},

		isString: function (o) {
			return typeof (o) === STRING;
		},

		isUndefined: function (o) {
			return typeof (o) === UNDEFINED;
		},

		isRegExp: function (o) {
			return $.type(o) === REGEX;
		},

		// Returns false for null/undefined/NaN,  true for other values (including false)
		isValue: function (o) {
			var t = $.type(o);
			switch (t) {
				case NUMBER:
					return isFinite(o);
				case NULL:
				case UNDEFINED:
					return false;
				default:
					return true;
			}
		},

		// Evaluates a client template using the given evaluator
		evaluateTemplate: function (template, evaluator) {
			var s = 0, e = 0, result = '';
			if (!template)
				return '';
			do {
				// Find the start of the next binding expression
				if ((s = template.indexOf('##', e)) == -1)
					break;
				// Append from previous end to start
				if (s > e)
					result += template.slice(e, s)
				// Find the end of the next binding expression
				if ((e = template.indexOf('##', s + 2)) == -1)
					break;
				// Extract the binding and evaluate it
				var binding = template.slice(s + 2, e);
				result += evaluator(binding);
				// Move end on past the previous binding marker
				e += 2;
			} while (true);
			if (e < template.length)
				result += template.slice(e);
			return result;
		},

		// TODO: Is this needed if we wrap all our event handlers in function wrappers?
		stopPropagation: function (event) {
			event = event || window.event || window.Event;
			if (!event) return;
			if (event.preventDefault) {
				event.preventDefault();
			}
			if (event.stopPropagation) {
				event.stopPropagation();
			}
			if ('cancelBubble' in event) {
				event.cancelBubble = true;
			}
			if ('returnValue' in event) {
				event.returnValue = false;
			}
		},

		// Formats a field name (in pascal/camel case) to put spaces between each word and remove Id from the end
		formatNameForDisplay: function (name) {
			if (!name)
				return '';
			var displayName = '';
			var upper = null;
			for (var i = 0; i < name.length; i++) {
				var c = name.substr(i, 1);
				if (!upper && (c === c.toUpperCase()) && displayName != "") displayName += " ";
				displayName += c;
				upper = (c === c.toUpperCase());
			}
			displayName = displayName.replace(" Id", "");
			return displayName;
		},

		// Format a date object into a string value.
		// Format specifiers:
		// d    - day of month (no leading zero)
		// dd   - day of month (two digit)
		// ddd -  abbreviated day of the week [TODO - need to handle this in the date parser]
		// dddd - full day of the week [TODO - need to handle this in the date parser]
		// M    - month of year (no leading zero)
		// MM   - month of year (two digit)
		// MMM  - month name short
		// MMMM - month name long
		// y    - year (0 to 99)
		// yy   - year (00 to 99)
		// yyyy - year (four digits)
		formatDate: function (date, format) {
			if (!$.isDate(date))
				return '';
			if (!format)
				format = DEFAULTDATEFORMAT;
			var dayNamesShort = DAYNAMESSHORT;
			var dayNames = DAYNAMES;
			var monthNamesShort = MONTHNAMESSHORT;
			var monthNames = MONTHNAMES;
			// match a specifier in the input stream (always test the longest match first)
			var match = function (specifier) {
				if (format.substring(iFormat, iFormat + specifier.length) != specifier)
					return false;
				iFormat += specifier.length;
				return true;
			};
			var output = '';
			for (var iFormat = 0; iFormat < format.length; ) {
				if (match('dd')) {
					output += $.formatNumber(date.getDate(), 0, 2);
				} else if (match('d')) {
					output += $.formatNumber(date.getDate(), 0, 1);
				} else if (match('MMMM')) {
					output += monthNames[date.getMonth()];
				} else if (match('MMM')) {
					output += monthNamesShort[date.getMonth()];
				} else if (match('MM')) {
					output += $.formatNumber(date.getMonth() + 1, 0, 2);
				} else if (match('M')) {
					output += $.formatNumber(date.getMonth() + 1, 0, 1);
				} else if (match('yyyy')) {
					output += $.formatNumber(date.getFullYear());
				} else if (match('yy')) {
					output += $.formatNumber(date.getFullYear()).substring(2, 4);
				} else if (match('y')) {
					output += $.formatNumber(parseInt($.formatNumber(date.getFullYear()).substring(2, 4), 10), 0, 1);
				} else {
					output += format.charAt(iFormat++);
				}
			}
			return output;
		},

		// Parse a string into a date object
		parseDate: function (value) {
			value = (typeof value == 'object' ? value.toString() : value + '');
			if (value == '')
				return null;
			// Parse 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]);
			}
			var shortYearCutoff = SHORTYEARCUTOFF;
			var dayNamesShort = DAYNAMESSHORT;
			var dayNames = DAYNAMES;
			var monthNamesShort = MONTHNAMESSHORT;
			var monthNames = MONTHNAMES;
			var year = -1, month = -1, day = -1;
			// Split the date string into parts
			var parts = value.split(/\s*[\s\-\\\/]\s*/);
			if (parts.length != 3) return null;
			// Parse the day part
			day = parseInt(parts[0], 10);
			// Parse the month part
			month = parseInt(parts[1], 10);
			if (!isFinite(month)) {
				var i;
				// Check for month short names
				for (i = 0; i < 12; i++) {
					if (monthNamesShort[i].toLowerCase() == parts[1].toLowerCase()) {
						month = i;
						break;
					}
				}
				if (i == 12) {
					// Chech for month long names
					for (i = 0; i < 12; i++) {
						if (monthNames[i].toLowerCase() == parts[1].toLowerCase()) {
							month = i;
							break;
						}
					}
				}
				if (i == 12)
					return null; // Couldn't parse the month
			} else {
				month--;
			}
			// Parse the year part
			year = parseInt(parts[2], 10);
			if (year < shortYearCutoff)
				year = year + 2000;
			else if (year < 100)
				year = year + 1900;
			return new Date(year, month, day);
		},

		formatNumber: function (value, precision, scale) {
			if (typeof (value) !== "number" || !isFinite(value)) return '';
			var s = value.toString();
			var parts = s.split('.');
			if (parts.length == 1) parts[1] = "";
			if ($.isNumber(precision)) {
				if (parts[1].length > precision)
					parts[1] = parts[1].substring(0, precision);
				else
					while (parts[1].length < precision)
						parts[1] += '0';
			}
			if ($.isNumber(scale)) {
				while (parts[0].length < scale)
					parts[0] = '0' + parts[0];
			}
			if (parts[1].length > 0)
				return parts[0] + "." + parts[1];
			return parts[0];
		},

        parseBoolean: function(value) {
            if(!$.isString(value)) return false;
            if(value == "1" || value == "true") return true;
            return false;
        },

		modalLock: function (options) {
			if (!_modalElement) {
				if (!(_modalElement = document.getElementById("_modalElement"))) {
					return;
				}
				_modalElement = $(_modalElement);
			}
			options = options || {};
			if (_modalCounter <= 0) {
				_modalElement.removeClass().addClass("wx-modal wx-modal-" + (options.cssClass || "lock")).fadeTo(options.speed === undefined ? 'slow' : options.speed, options.opacity === undefined ? 0.6 : options.opacity);
				_modalCounter = 0;
			}
			_modalCounter++;
			if (options.interval) {
				_modalCounter++;
				$.timeout($.modalUnlock, options.interval);
			}
		},

		modalUnlock: function (force) {
			if (!_modalElement) {
				if (!(_modalElement = document.getElementById("_modalElement"))) {
					return;
				}
				_modalElement = $(_modalElement);
			}
			_modalCounter--;
			if (_modalCounter <= 0 || force === true) {
				_modalElement.stop().fadeTo('Fast', 0.0).hide();
				_modalCounter = 0;
				// Force a relayout
				$(window).resize();
			}
		}
	});

	$.namespace('Wx'); // classes, constants etc
	$.namespace('wx'); // instances

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Base is an empty base class.  It does nothing, except it is the base class of all inheritance hierachies.

	Wx.Base = function () { }
	Wx.Base.prototype._init = function () { }
	Wx.Base.prototype.destroy = function () { }

	// To extend an existing constructor call <constructor>.extend(<prototype>, <static>), which returns a new constructor.
	Wx.Base.extend = function (_prototype, _static) {

		var baseConstructor = this;

		// Create a constructor for the new 'class'
		var constructor = function () {
			// When we are here,  this == the instance of the object being constructed.
			if (!initializing) {
				// Call our prototype _init method with the arguments passed to the constructor.
				this._init.apply(this, arguments);
			};
		}

		// Here is the actual inheritance. The prototype of the new constructor is set to an instance of the base class.
		initializing = true;
		constructor.prototype = new baseConstructor();
		initializing = false;

		// Need to be explicit about the constructor as the prototype contains a constructor itself (i.e. the constructor of the base class).
		constructor.prototype.constructor = constructor;

		// Extend the prototype of the constructor using the provided prototype.  i.e. bolt on all the publicly available methods and properties.
		$.extend(constructor.prototype, _prototype || {});

		// Extend the constructor itself (i.e. static methods)
		$.extend(constructor, _static || {});

		// Finally make this constructor extendable
		constructor.extend = arguments.callee; // arguments.callee == Wx.Base.extend

		return constructor;
	}

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Widget is the base class of all widgets

	// Auto-bound events
	var AUTOBOUNDEVENTS = [
	// DOM-events
		'blur',
		'change',
		'click',
		'contextmenu',
		'dblclick',
		'focus',
		'focusin',
		'focusout',
		'hover',
		'keydown',
		'keypress',
		'keyup',
		'mousedown',
		'mouseenter',
		'mouseleave',
		'mousemove',
		'mouseout',
		'mouseover',
		'mouseup',
		'resize',
		'scroll',
	// Non-DOM events
		'ready',
		'propertyChanged',
		'openPopup',
		'closePopup'
	];

	Wx.Widget = Wx.Base.extend({
		_init: function (p) {
			this.set_initialising(true);
			Wx.Base.prototype._init.call(this);
			// Set properties
			this._id = p.id;
			this._element = $(document.getElementById(p.id) || this).data('_wx_', this);
			this._eventHandlers = p.eventHandlers || []; // Declarative event handlers
			this._contextMenuId = p.contextMenuId;
			this._group = p.group;
			if (p.id) {
				$.assert(wx[p.id] === undefined, "Wx.Widget: Id '" + p.id + "' is not unique.");
				wx[p.id] = this;
			}
			var self = this;
			$.each(AUTOBOUNDEVENTS, function (i, e) { self._autobind(e); });
			$.each(this._eventHandlers, function(i, e) { e.bindHandler(self); });
		},
		destroy: function () {
			this.unbind();
			Wx.Base.prototype.destroy.call(this);
		},
		// Property accessors
		get_id: function () {
			return this._id;
		},
		get_element: function () {
			return this._element;
		},
		get_contextMenuId: function() {
			return this._contextMenuId;
		},
		get_group: function() {
			return this._group;
		},
		get_initialising: function() {
			return this._initialising;
		},
		set_initialising: function(value) {
			this._initialising = value;
		},	
		// Refresh UI
		refresh: function() {
			this.trigger({ type: 'refreshed', widget: this });
		},
		show: function() {
			this._element.show();
		},
		hide: function() { 
			this._element.hide();
		},
		// Events
		_autobind: function (eventType) {
			if (this['_' + eventType])
				this.bind(eventType, $.proxy(this, '_' + eventType));
		},
		bind: function (eventType, eventData /*optional*/, handler) {
			this._element.bind(eventType, eventData, handler);
		},
		trigger: function (eventType, extraParameters) {
			this._element.trigger(eventType, extraParameters);
		},
		unbind: function (eventType, handler) {
			this._element.unbind(eventType, handler);
		},
		// Event Handlers
		_ready: function(event) {
			this.refresh();
		},
		// Popup (TODO: Factor this out into a plugin?)
		openPopup: function(options){
			if(this.__popupOptions)
				return $.assert(false, 'Wx.Widget.openPopup(): Widget is already a popup');
			if(!options) 
				options = {};
			this.__popupOptions = options;
			this._element.addClass('popup');
			if(options.target instanceof Wx.Widget) {
				options.target = options.target.get_element();
			}
			if(options.target instanceof $){
				options.target.addClass('popupTarget');
			}
			this._element.css('position', 'fixed').css('left', options.left).css('top', options.top).show();
			$(document).bind('click.popup' + this._id, $.proxy(function(event) { this.closePopup(); }, this));
			this.trigger({ type: 'openPopup', widget: this });
		},
		closePopup: function() {
			if(!this.__popupOptions)
				return $.assert(false, 'Wx.Widget.closePopup(): Widget is not a popup');
			this._element.hide();
			$(document).unbind('click.popup' + this._id);
			this._element.removeClass('popup');
			if(this.__popupOptions.target && this.__popupOptions.target instanceof $)
				this.__popupOptions.target.removeClass('popupTarget');
			if(this.__popupOptions.callback)
				this.__popupOptions.callback(); // TODO: Parameters provided by the popup target?
			delete this.__popupOptions;
			this.trigger({ type: 'closePopup', widget: this });
		},
		isPopup: function() {
			return !(!this.__popupOptions);
		},
		// Context Menu
		_contextmenu: function(event) {
			var contextMenuId = this.get_contextMenuId();
			if(contextMenuId && wx[contextMenuId]) {
				wx[contextMenuId].openPopup({ target: this, left: event.pageX, top: event.pageY }); 
			}
			return false; // === stopPropagation + preventDefault
		}
	});

	// Globally trap all click events in popup and popupTarget elements - to prevent them propagating to the document element and closing the popup
	$('.popup, .popupTarget').live('click', function(event) { event.stopImmediatePropagation(); return false; });

	// Iterate over a jQuery object, returning just widgets
	$.fn.wx = function (fn/*(index, widget, element)*/) {
		var iw = 0;
		return this.each(function () {
			var w = $(this).data('_wx_');
			if (w instanceof Wx.Widget) {
				return fn(iw++, w, this);
			}
		});
	}

	// Call ready event on all widgets
	$(document).ready(function() {
		$.each(wx, function(k, w) { w.trigger('ready'); w.set_initialising(false); });
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.EventHandler is a declerative event handler

	Wx.EventHandler = Wx.Widget.extend({
		_init: function(p) {
			Wx.Widget.prototype._init.call(this, p);
			// Set properties
			this._event = p.event;
		},
		destroy: function() {
			Wx.Widget.prototype.destroy.call(this);
			if(this._target)
				this._target.unbind(this._event, this._handler);
		},
		bindHandler: function(target) {
			this._target = target;
			target.bind(this._event, $.proxy(this._handler, this));			
		},
		_handler: function(event) {
		}
	});

	Wx.ScriptHandler = Wx.EventHandler.extend({
		_init: function(p) {
			Wx.EventHandler.prototype._init.call(this, p);
			// Set properties
			this._script = p.script;
		},
		destroy: function() {
			Wx.EventHandler.prototype.destroy.call(this);
		},
		_handler: function(event) {
			var self = this._target;
			return eval("(function() { " + this._script + "})();");
		}
	});

	Wx.PropertyChangedHandler = Wx.EventHandler.extend({
		_init: function(properties) {
			properties.event = 'propertyChanged';
			Wx.EventHandler.prototype._init.call(this, properties);
			// Set properties
			this._property = properties.property;
			this._script = properties.script;
		},
		destroy: function() {
			Wx.EventHandler.prototype.destroy.call(this);
		},
		_handler: function(event) {
			if(event.propertyName !== this._property)
				return;
			return eval("(function() { " + this._script + "})();");
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Draggable implements drag functionality for a given element 

	Wx.Draggable = Wx.Base.extend({
		_init: function(p) {
			Wx.Base.prototype._init.call(this);
			// Set properties
			this._element = p.element;
			this._axis = p.axis;
			this._dragStopCallback = p.dragStop;
			this._dragStartCallback = p.dragStart;
			this._clone = p.clone;
			this._dragClass = p.dragClass || 'drag';
			this._dragging = false;
			this._minX = p.minX;
			this._maxX = p.maxX;
			this._minY = p.minY;
			this._maxY = p.maxY;
			this._startX = p.startX;
			this._startY = p.startY;
			// Bind drag events
			this._dragStartProxy = $.proxy(this._dragStart, this);
			this._dragMoveProxy = $.proxy(this._dragMove, this);
			this._dragStopProxy = $.proxy(this._dragStop, this);
			this._element.bind('mousedown', this._dragStartProxy).css({ position: 'absolute', zIndex: 1000 });
		},
		destroy: function() {
			this._element.unbind('mousedown', this._dragStartProxy).unbind('mousemove', this._dragMoveProxy).unbind('mouseup', this._dragStopProxy);
			Wx.Base.prototype.destroy.call(this);
		},
		set_minX: function(value) {
			this._minX = value;
		},
		set_maxX: function(value) {
			this._maxX = value;
		},
		set_minY: function(value) {
			this._minY = value;
		},
		set_maxY: function(value) {
			this._maxY = value;
		},
		set_startX: function(value) {
			this._startX = value;
		},
		set_startY : function(value) {
			this._startY;
		},
		_dragStart: function(event){
			event.stopPropagation();
			if(this._dragging) return; // We are already dragging (this can happen due to loss of mouse focus by the element)
			if(this._dragStartCallback) {
				if(this._dragStartCallback(this, {}) === false)
					return; // If the drag start callback returns false then it cancels the drag
			}
			this._dragging = true;
			if(this._clone)
				this._zombie = this._element.clone(false).insertAfter(this._element);
			if(this._dragClass)
				this._element.addClass(this._dragClass);
			this._mouseStart = { x: event.pageX, y: event.pageY };
			this._offsetStart = this._element.offset();
			$(document).bind('mousemove', this._dragMoveProxy).bind('mouseup', this._dragStopProxy);
		},
		_dragMove: function(event){
			event.stopPropagation();
			if(this._startX !== undefined){
				var x = this._axis == 'v' ? this._startX : this._startX + (event.pageX - this._mouseStart.x);
				if(this._minX !== undefined && x < this._minX)
					return;
				if(this._maxX !== undefined && x > this._maxX)
					return;
			}
			var left = this._axis == 'v' ? this._offsetStart.left : this._offsetStart.left + (event.pageX - this._mouseStart.x);
			var top = this._axis == 'h' ? this._offsetStart.top : this._offsetStart.top + (event.pageY - this._mouseStart.y);
			this._element.offset({ left: left, top: top });
		},
		_dragStop: function(event){
			event.stopPropagation();
			if(this._zombie) {
				this._zombie.remove();
				delete this._zombie;
			}
			if(this._dragClass)
				this._element.removeClass(this._dragClass);
			$(document).unbind('mousemove', this._dragMoveProxy).unbind('mouseup', this._dragStopProxy);
			if(this._dragStopCallback)
				this._dragStopCallback(this, { deltaX: event.pageX - this._mouseStart.x, deltaY: event.pageY - this._mouseStart.y });
			this._dragging = false;
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Splitter implements a resizeable horizontal or vertical splitter widget

	Wx.Splitter = Wx.Widget.extend({
		_init: function (p) {
			Wx.Widget.prototype._init.call(this, p);
			// Set properties
			this._split = p.split || null;
			this._isHorizontal = p.isHorizontal || null;
			this._min1 = p.min1 || 0;
			this._min2 = p.min2 || 0;
			this._isLocked = p.isLocked;
			this._hidePanel = p.hidePanel;
			this._isDocked = false;
			// Store references to child elements
			var children = $('>*', this._element);
			this._panel1 = $(children[0]);
			this._grip = $(children[1]);
			this._panel2 = $(children[2]);
			// Parameterise the refresh algorithm
			if(this._isHorizontal)
				this._parameters = { Dimension: 'Height', dimension: 'height', side: 'top' };
			else
				this._parameters = { Dimension: 'Width', dimension: 'width', side: 'left' };
			// Make the grip draggable
			if(!this._isLocked)
				this._gripDraggable = new Wx.Draggable({ clone: false, element: this._grip, axis: this._isHorizontal ? 'v' : 'h', dragStop: $.proxy(this._onDragStop, this), dragStart: $.proxy(this._onDragStart, this) });
			// Attach event handlers
			if(!this._isLocked)
				this._grip.dblclick($.proxy(this._onDblClick, this));
			this._element.addClass('wx-split');
			$(window).resize($.proxy(this.refresh, this));
		},
		destroy: function () {
			Wx.Widget.prototype.destroy.call(this);
		},
		// Property Accessors
		set_hidePanel: function(value) {
			this._hidePanel = value;
			$(window).resize();
		},
		// Event handlers
		_onDblClick: function(event) {
			this._isDocked = !this._isDocked;
			$(window).resize();
		},
		// Refresh UI
		refresh: function() {
			if(this._isLocked)
				this._element.addClass('loc');
			// Is one of the panels hidden?
			if(this._hidePanel == 1 || this._hidePanel == 2) {
				var totalSize = this._element['inner' + this._parameters.Dimension]();
				if(this._hidePanel == 1) {
					this._panel1.hide();
					this._grip.hide();			
					this._panel2.css('margin-' + this._parameters.side, 0);
					this._panel2.css(this._parameters.dimension, totalSize + 'px');
				} else {
					this._panel2.hide();
					this._grip.hide();
					this._panel1.css(this._parameters.dimension, totalSize + 'px');
				}
				return;
			}else{
				this._panel1.show();
				this._panel2.show();
				this._grip.show();
			}
			// Both panels are visible
			this._grip[this._isDocked ? 'addClass' : 'removeClass']('doc');
			// Parameterise the two code paths
			var totalSize = this._element['inner' + this._parameters.Dimension]();
			var gripSize = this._grip['outer' + this._parameters.Dimension]();
			if(this._split == null)
				this._split = (totalSize - gripSize) / 2;
			// Bound the split within the minimum regions
			var split = this._split;
			if(totalSize - split - gripSize < this._min2)
				split = totalSize - gripSize - this._min2;
			if(split < this._min1)
				split = this._min1;
			split = this._isDocked ? 0 : split;
			// Apply to the css
			this._panel1.css(this._parameters.dimension, split + 'px');
			this._grip.css(this._parameters.side, split + 'px');
			this._panel2.css('margin-' + this._parameters.side, ((this._isHorizontal ? 0 : split) + gripSize) + 'px');
			this._panel2.css(this._parameters.dimension, (totalSize - split - gripSize) + 'px');
		},
		// Event handlers
		_onDragStart: function(caller, event) {
			if(this._isDocked)
				return false;
			var totalSize = this._element['inner' + this._parameters.Dimension]();
			var gripSize = this._grip['outer' + this._parameters.Dimension]();
			if(this._isHorizontal) {
				caller.set_startY(this._split);
				caller.set_minY(this._min1);
				caller.set_maxY(totalSize - this._min2 - gripSize);			
			} else {
				caller.set_startX(this._split);
				caller.set_minX(this._min1);
				caller.set_maxX(totalSize - this._min2 - gripSize);			
			}
		},
		_onDragStop: function(caller, event) {
			this._isDocked = false;
			if(!this._isHorizontal) {
				this._split += event.deltaX;
			} else {
				this._split += event.deltaY;
			}
			// Trigger a global layout
			$(window).resize();
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Section implements a collapsible UI region with customisable header and footer regions
	// TODO: Is this interesting enough?  Should we just put this in Dx?

	Wx.Section = Wx.Widget.extend({
		_init: function (p) {
			Wx.Widget.prototype._init.call(this, p);
			// Set initial state
			this._isExpandable = p.isExpandable === true;
			this.set_isExpanded(p.isExpanded !== false);
			this.set_hideHeader(p.hideHeader === true);
			this.set_hideFooter(p.hideFooter !== false);
			// Bind event handlers
			if(this._isExpandable !== false)
				$('.wx-sect-hdr', this._element).click($.proxy(this.toggle, this));
			this._element.addClass('wx-sect');
		},
		destroy: function () {
			Wx.Widget.prototype.destroy.call(this);
		},
		expand: function() {
			if(this._isExpandable == false) return;
			this._element.removeClass('col').addClass('exp');
			$('.exp', this._element).hide();
			$('.col', this._element).show();
			$('.wx-sect-bdy', this._element).slideDown();
			this._isExpanded = true;
		},
		collapse: function() {
			if(this._isExpandable == false) return;
			this._element.addClass('col').removeClass('exp');
			$('.col', this._element).hide();
			$('.exp', this._element).show();
			$('.wx-sect-bdy', this._element).slideUp();
			this._isExpanded = false;
		},
		toggle: function() {
			this[this._isExpanded === true ? 'collapse' : 'expand']();
		},
		set_isExpanded: function(value) {
			this[value ? 'expand' : 'collapse']();		
		},
		set_hideHeader: function(value) {
			this._hideHeader = value;
			$('.wx-sect-hdr', this._element)[this._hideHeader ? 'hide' : 'show']();
		},
		set_hideFooter: function(value) {
			this._hideFooter = value;
			$('.wx-sect-ftr', this._element)[this._hideFooter ? 'hide' : 'show']();
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Button implements client side enhanced button functionality (currently just can be checkable)

	Wx.Button = Wx.Widget.extend({
		_init: function (p) {
			Wx.Widget.prototype._init.call(this, p);
			this._url = p.url;
			this._isCheckable = p.isCheckable === true;
			this._isChecked = p.isChecked === true;
		},
		destroy: function () {
			Wx.Widget.prototype.destroy.call(this);
		},
		// Property Accessors
		get_isCheckable: function() {
			return this._isCheckable;
		},
		get_isChecked: function() {
			return this._isChecked;
		},
		set_isChecked: function(value) {
			if(value) {
				if(this._isChecked)
					return;
				// If there are any checkable buttons in the same group make sure we uncheck them first
				var self = this;
				$.each(this._checkGroup(), function(i, b) { if(b != self) b.set_isChecked(false); });
				this._isChecked = true;
				this.trigger('check');
			} else {
				if(!this._isChecked)
					return;
				this._isChecked = false;
				this.trigger('uncheck');
			}
			this.refresh();
		},
		_checkGroup: function() {
			if(this.__checkGroup !== undefined) 
				return this.__checkGroup;
			if(this._group) {
				this.__checkGroup = [];
				var self = this;
				$.each(wx, function(k, v) { if((v instanceof Wx.Button) && v.get_group() == self._group && v.get_isCheckable && v.get_isCheckable()) { self.__checkGroup.push(v); } });
			}
			return this.__checkGroup;
		},
		refresh: function() {
			this._element[this._isChecked ? 'addClass' : 'removeClass']('chk');
		},

		// Event handlers
		_click: function(event) {
			if(this._url) {
				$.redirectTo(this._url);
				return;
			}
			if(this._isCheckable) {
				// If we are in a group of checkable buttons and we are checked then we can't uncheck 
				if(this._checkGroup().length == 0 || !this.get_isChecked()) {
					this.set_isChecked(!this.get_isChecked());
				}
			}
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Menu - popup menus and menu bars

	Wx.Menu = Wx.Widget.extend({
		_init: function (p) {
			Wx.Widget.prototype._init.call(this, p);
		},
		destroy: function () {
			Wx.Widget.prototype.destroy.call(this);
		},
		// Property accessors
		get_target: function() {
			return this._target;
		},
		get_parentMenu: function() {
			return this._parentMenu;
		},
		get_subMenu: function() {
			return this._subMenu;
		},
		set_subMenu: function(value) {
			this._subMenu = value;
		},
		openPopup: function(options) {
			if(this.isPopup()) {
				//if(options.closeIfOpen === true)
					this.closePopup();
				//else
				//	return;
			}
			this._target = options.target;
			this._parentMenu = options.parentMenu;

			// If we are opening a root menu then close the existing root menu
			if(!this._parentMenu) {
				if(Wx.Menu._rootMenu)
					Wx.Menu._rootMenu.closePopup();
				Wx.Menu._rootMenu = this;
			}
			// If parent menu implements sub menu then close the existing sub menu
			if(this._parentMenu && this._parentMenu.get_subMenu){
				var subMenu = this._parentMenu.get_subMenu();
				if(subMenu)
					subMenu.closePopup();
				this._parentMenu.set_subMenu(this);
			}
			Wx.Widget.prototype.openPopup.call(this, options);
		},
		closePopup: function() {
			if(!this.isPopup())
				return;
			// If we are the root menu then clear the root menu
			if(!this._parentMenu) {
				if(Wx.Menu._rootMenu == this)
					delete Wx.Menu._rootMenu;
			}
			// If parent menu implements sub menu then clear the sub menu
			if(this._parentMenu && this._parentMenu.get_subMenu) {
				var subMenu = this._parentMenu.get_subMenu();
				if(subMenu == this)
					this._parentMenu.set_subMenu(undefined);
			}
			// If we have a sub menu then close it
			if(this._subMenu) {
				this._subMenu.closePopup();
			}
			Wx.Widget.prototype.closePopup.call(this);
		}
	});

	Wx.MenuItem = Wx.Button.extend({
		_init: function (p) {
			Wx.Button.prototype._init.call(this, p);
			// Set properties
			this._isDivider = p.isDivider;
			this._subMenuId = p.subMenuId;
			this._parentMenuId = p.parentMenuId;
			this._parentMenu = wx[this._parentMenuId];
			// Attach event handlers
			$(".c3", this._element).mouseover($.proxy(function(event) {
				if(this._subMenuId) {
					this.openSubMenu();
					return false;
				}
			}, this));
		},
		destroy: function () {
			Wx.Button.prototype.destroy.call(this);
		},
		// Event handlers
		_click: function(event) {
			if(this._isDivider)
				return;
			if(this._subMenuId) {
				this.openSubMenu();
				return false;
			}
			// Call the button event handler which implements checkability
			Wx.Button.prototype._click.call(this, event);
			// Close our menu
			var parentMenu = this._parentMenu;
			if(parentMenu && parentMenu.isPopup()){
				parentMenu.closePopup();
			}
			// Add menuTarget to the event so it can be read by future bound handlers
			event.menuTarget = parentMenu.get_target();
		},
		_mouseenter: function(event) {
			if(this._subMenuId) {
				var parentSubMenu = this._parentMenu.get_subMenu();
				if(parentSubMenu && parentSubMenu != this)
					this.openSubMenu();
			}
		},
		openSubMenu: function() {
			var offset = this._element.offset();
			var parentMenu = this._parentMenu;
			wx[this._subMenuId].openPopup({ 
				left: offset.left + this._element.outerWidth(),
				top: offset.top,
				target: parentMenu.get_target(),
				parentMenu: parentMenu
			});
		}
	});
	
	Wx.MenuBar = Wx.Menu.extend({
		_init: function (p) {
			Wx.Menu.prototype._init.call(this, p);
		},
		destroy: function () {
			Wx.Menu.prototype.destroy.call(this);
		}
	});

	Wx.MenuBarItem = Wx.MenuItem.extend({
		_init: function (p) {
			Wx.MenuItem.prototype._init.call(this, p);
		},
		destroy: function () {
			Wx.MenuItem.prototype.destroy.call(this);
		},
		openSubMenu: function() {
			var offset = this._element.offset();
			var parentMenu = this._parentMenu;
			var parentTarget = parentMenu ? parentMenu.get_target() : undefined;
			wx[this._subMenuId].openPopup({ 
				left: offset.left,
				top: offset.top + this._element.outerHeight(),
				target: parentTarget,
				parentMenu: parentMenu
			});
		}
	});

	//---------------------------------------------------------------------------------------------------------------------------------------------------------
	// Wx.Condition is a generic declarative FINQ condition

	Wx.Condition = Wx.Widget.extend({
		_init: function(p) {
			Wx.Widget.prototype._init.call(this, p);
			// Set properties
			this._name = p.name;
			this._field = p.field;
			this._op = p.op || FINQ.Op.Equals;
			this._isOptional = p.isOptional === true;
			this._useOnInsert = p.useOnInsert === true;
		},
		destroy: function() {
			Wx.Widget.prototype.destroy.call(this);
		},
		get_field: function() {
			return this._field;
		},
		get_useOnInsert: function() {
			return this._useOnInsert;
		},
		// Method stub.  Returns the FQL associated with this condition
		toFQL: function() {

		}
	});

	Wx.StaticCondition = Wx.Condition.extend({
		_init: function(p) {
			Wx.Condition.prototype._init.call(this, p);
			// Set properties
			this._value = p.value;
		},
		destroy: function() {
			Wx.Condition.prototype.destroy.call(this);
		},
		toFQL: function() {
			// Return the FQL implied by this condition (or undefined if the condition is not defined)
			var value = this.evaluate();
			if(value === undefined) {
				if(this._isOptional)
					return '';
				return;
			}
			return FINQ.ConditionToFQL(this._field, this._op, value);
		},
		evaluate: function() {
			return this._value;
		}
	});

	Wx.DynamicCondition = Wx.Condition.extend({
		_init: function(p) {
			Wx.Condition.prototype._init.call(this, p);
			// Set properties
			this._expression = p.expression;
		},
		destroy: function() {
			Wx.Condition.prototype.destroy.call(this);
		},
		toFQL: function() {
			var value = this.evaluate();
			if(value === undefined) {
				if(this._isOptional)
					return '';
				return;
			}
			return FINQ.ConditionToFQL(this._field, this._op, value);
		},
		evaluate: function() {
			return eval(this._expression);
		}
	});

	Wx.FormFilterCondition = Wx.Condition.extend({
		_init: function(p) {
			Wx.Condition.prototype._init.call(this, p);
			// Set properties
			this._formId = p.formId;
		},
		destroy: function() {
			Wx.Condition.prototype.destroy.call(this);
		},
		toFQL: function() {
			return wx[this._formId].getFilters() || '';
		}
	})

})(jQuery);


