﻿/// <reference path="~/Scripts/jquery-1.4.1-vsdoc.js" />
/*!
 * Nm.js
 * 
 * Provides methods for loading scripts, retrieving server data,
 * and a few other minor utilities.
 */
(function ($) {
	// define local copies of window and Nm
	var window, Nm, _, $;
	
	window = this;
	
	Nm = window.Nm = {

		loadScripts: function (scripts, callback) {
			/// <summary>
			///     1: loadScripts(scripts, callback) - Loads the array of scripts before calling the callback.
			///     2: loadScripts(context) - Parses the context element for server data instructions to load scripts. Calls: context.trigger("scriptsloaded");
			/// </summary>
			/// <param name="scripts" type="Array">
			///     1: scripts - The array of scripts to load.
			///     2: context - The new dom element.
			/// </param>
			/// <param name="callback" type="Function">
			///     The function to call after the scripts have loaded. Optional.
			/// </param>
			/// <returns type="undefined" />
			var serverData, context, scriptCount;

			if (arguments.length === 2 || (scripts && scripts.constructor == Array)) {
				scriptCount = scripts.length;
				$.each(scripts, function (i, url) {
					Nm.loadScript(url, function() {
						scriptCount--;
						if (scriptCount === 0) {
							Nm.console.log("Nm calling loadScripts callback.");
							callback && callback.call();
							return false;
						}
					});					
				});
				return;
			}

			// undocumented method - Html.IncludeScript -> registering scripts as loaded
			if (arguments.length === 3 && arguments[2] === true) {
				_.loadedScripts[arguments[0]] = "loaded"; // target
				_.loadedScripts[arguments[1]] = "loaded"; // source				
				return;
			}

			// loadscript - load the script with ajax.
			context = $(arguments[0]);
			serverData = context.serverData("nm.web.mvc.ui.scriptloader.loadscript");
			$.each(serverData, function (i, data) {
				data.context = context;
				_.loadScriptGroup(data);
			});

			context.trigger("scriptsloaded");

			// autofocus - could use something like Modernizr to detect html support
			context.find("[autofocus]:not([readonly])").trigger("focus");
		}, 		

		scriptLoaded: function (key, callback) {
			/// <summary>
			///		Executes the callback after the script is loaded.
			///	</summary>
			/// <param name="key" type="String">
			///		 The script key given with LoadScript.
			/// </param>
			/// <param name="callback" type="Function">
			///		The method to be called when the script is loaded. callback(context, serverData)
			///        - context - The element that the scripts should act on
			///        - serverData - Data set with the Html.LoadScript helper.
			/// </param>
			/// <returns type="undefined />

			// the key is what is used to trigger a loadScript after it has been loaded.
			$(document).bind(key, function (event) {
				var context = $(event.target);
				callback.call(this, context, context.serverData(key));
			});
		},

		loadScript: function (url, callback) {
			///	<summary>
			///		Loads a JavaScript or css file. Uses a local cache for optimization.
			///	</summary>
			/// <param name="url" type="String">
			///     The url of the script to load. Supports the app root "~" syntax.
			/// </param>
			///	<param name="callback" type="Function">
			///		The function called after the script has been loaded successfully.
			/// </param>
			///	<returns type="undefined" />
			_.loadScript(Nm.resolveUrl(url), callback);
		},

		clientCache: function (name, value, days) {
			/// <summary>
			///		OVERLOADED - Provides access to cookies.
			///		1: Nm.clientCache(name, value) - Sets the cookie, or provides a function to return a default value.
			///		2: Nm.clientCache(name) - Gets the cookie.
			///     3: Nm.clientCache(name, value, days) - Sets the cookie good for the specified # of days.
			///	</summary>
			/// <param name="name" type="String">The data key.</param>
			///	<param name="value" type="Object">
			///		1,3: value - Object - The value to set (can by any serializable object).
			///		1,3: value - Function - If the value to get is null, the value function will be called to return the default value.
			///	</param>
			/// <param>
			///     3: days - # - Number of days the cookie should be good for.
			/// </param>
			/// <returns type="Object">Returns the value if a get, otherwise will return undefined.</returns>
			var retValue, nOffset, nEnd, date;

			if (value === undefined || $.isFunction(value) === true) { // get
				if (document.cookie.length === 0) {
					retValue = null;
				}
				if (retValue !== null) {
					nOffset = document.cookie.indexOf(name + "=")
					if (nOffset === -1) {
						retValue = null;
					}
				}
				if (retValue !== null) {
					nOffset += name.length + 1;
					nEnd = document.cookie.indexOf(";", nOffset)
					if (nEnd === -1) {
						nEnd = document.cookie.length
					}
					retValue = unescape(document.cookie.substring(nOffset, nEnd));
					try {
						retValue = JSON.parse(retValue);
					} catch (e) { }
				}
				if (retValue === null && $.isFunction(value) === true) {
					retValue = value.call();
				}
				return retValue;
			} else { // set
				value = JSON.stringify(value);
				date = new Date();
				date.setDate(date.getDate() + (arguments[2] || 365));
				document.cookie = [name, "=", escape(value), "; expires=", date.toGMTString(), "; path=/;"].join("");
				return;
			}
		},

		window: function (name, url) { 
			/// <summary>
			///		OVEROADED - Opens a new browser window and returns it.
			///		1: Nm.window(name, url)
			///		2: Nm.window(options)
			///		Default window specs: "resizable=1,toolbars=0,location=1,status=1,scrollbars=1,height=450,width=700".
			///	</summary>
			///	<param name="name" type="String">
			///		1: name - The name of the window to open. Can be null. The default is "_self".
			///		2: options - An object containing: name, url, specs, error.
			///         *name
			///         *url
			///         *specs (string, function) - window specifications.
			///			*error (function) - callback if the window failed to open.
			///	</param>
			/// <param>
			///		1: url - The url of the window to open. The default is "about:blank".
			/// </param>
			/// <returns type="window" />
			var options, oWindow;
			if (typeof arguments[0] === "string") {
				options = { name: name, url: url };
			} else {
				options = arugments[0];
			}

			$.extend({
				name: "_self",
				url: "about:blank",
				specs: "resizable=1,toolbars=0,location=1,status=1,scrollbars=1,height=450,width=700",
				error: function (name, url) {
					Nm.console.error("An error occured while attempting to open a window. Do you have popups blocked?");
				}
			}, options);

			oWindow = window.open(options.url, options.name, options.specs);
			if (!oWindow && options.error) {
				options.error(options.name, options.url);
			} else {
				oWindow.focus();
			}
			return oWindow;
		},

		setAppPath: function (path) {
			/// <summary>
			///     Sets the applicatio path. Used when resolving virtual urls.
			///     This is automatically set if LoadBaseScripts is called.
			/// </summary>
			/// <params name="path" type="string">
			///     The application path.
			/// </params>
			_.appRoot = path;
		},

		resolveUrl: function (url) {
			///	<summary>
			///		Resolves the url replacing ~ with the application path.
			/// </summary>
			/// <param name="url" type="String"
			///		The url to resolve.
			///	</param>
			/// <returns type="string" />
			return String(url).replace(/~/g, _.appRoot);
		},

		alert: function (format, arg0) {
			/// <summary>
			///    Uses Nm.format to format the string and alert it using a javascript alert.
			/// </summary>
			/// <param name="format" type="string">
			///     The string to format.
			/// </param>
			/// <param name="arg0" type="object">
			///      The object to format.
			/// </param>
			/// <returns type="string" />
			alert(Nm.format.apply(null, arguments));
		},

		format: function (format, arg0) {
			/// <summary>
			///    Replaces one or more format items in a specified string with the string representation of an object.
			/// </summary>
			/// <param name="format" type="string">
			///     The string to format.
			/// </param>
			/// <param name="arg0" type="object">
			///      The object to format.
			/// </param>
			/// <returns type="string" />
			$.each(arguments, function (i, arg) {
				if (i === 0) {
					return;
				}
				format = format.replace(new RegExp("\\{" + (i - 1) + "\\}", "g"), arg);
			});
			return format;
		},

		waitFor: function (keys, callback) {
			/// <summary>
			///     1. A queing helper for multiple asyncronous callbacks.
			///     2. Notifies of a key completion.
			///     ex: Nm.waitFor("method1, method2", myCallback);
			///       Nm.waitFor("method2");
			///       Nm.waitFor("method1");
			///       -at this point myCallback is called.
			/// </summary>
			/// <param type="string" name="keys">
			///     1. A comma seperated list of keys to wait for.
			///     2. Notifies that a key that has finished executing.
			/// </param>
			/// <param type="function" name="callback">
			///     1. The callback to execte once all keys have been notified.
			/// <param>
			if (arguments.length === 2) {
				if (_.waitForQueue[keys] === undefined) {
					_.waitForQueue[keys] = {
						callback: callback,
						keys: keys.replace(" ", "").split(",")
					};
				}
				return;
			}

			var key = keys;
			$.each(_.waitForQueue, function (name, data) {
				var index = $.inArray(key, data.keys);
				if (index > -1) {
					_.waitForQueue[name].keys.splice(index, 1);
					if (_.waitForQueue[name].keys.length === 0) {
						_.waitForQueue[name].callback.call();
						delete _.waitForQueue[name];
						return false;
					}
				}
			});
		}
	};

	$.fn.loadScripts = function () {
		/// <summary>
		///     Calls Nm.loadScripts(this).
		/// </summary>
		/// <returns type="jQuery" />
		var el = $(this);
		Nm.loadScripts(el);
		return el;
	};

	$.fn.fields = function (name, value) {
		/// <summary>
		///		OVERLOADED - Adds fields to a form.
		///		1. fields(name, value)
		///		2. fields(fields)
		/// </summary>
		/// <param name="name" type="string">
		///     1. name - The name of a form field to add to the form submission.
		///     2. fields - An object where the keys are the field names and the values are the field values.
		/// </param>
		/// <param name="value" type="string">
		///     1. value - The value of the form field to add.
		/// </param>
		/// <returns type="jQuery/>
		var form;
		
		form = $(this);
		if (form.is("form") === false || form.length === 0) {
			Nm.console.warn("Could not add fields to a non existing form.");
			return form;
		}

		if (arguments[1] !== undefined) {
			fields = {};
			fields[arguments[0]] = arguments[1];
		} else {
			fields = arguments[0];
		}

		if (!fields) {
			Nm.console.error("Attempted to add empty fields to the form.");
			return form;
		}

		$.each(fields, function (name, value) {
			var field;
			field = form.find("[name=" + name + "]");
			if (field.length === 0 || field.attr("type") === "submit") {
				field = $('<input type="hidden" name="' + name +'">').appendTo(form);
			}
			field.val(value);
		});

		return form;
	};

	$.fn.serverData = function (name) {
		/// <summary>
		///		Returns server data with the specified name.
		///		The return array will never be null. Test for length === 0.	
		///		Also makes the properties of the first item available at the top level for convenience.
		///		This will also REMOVE the server data tag.
		/// </summary>
		/// <param name="name" type="string">
		///		The name of the server data to retrieve.
		///	</param>
		/// <returns type="Array" />
		var data = [];
		$(this).find("[name=" + name + "]").each(function () {
			var $this = $(this);
			data.push(JSON.parse(decodeURIComponent($this.attr("data-server"))));
			$this.remove();
		});

		// add the first items properties to the top level for convenience
		return $.extend(data, data[0]);
	};

	$.fn.value = function (value) {
		/// <summary>
		///		OVERLOADED - Gets or sets the value of a form field whose name is the same as the element id.
		///		1: $(element).value()
		///		2: $(element).value(value)
		///		NOTE ON GET:
		///		If no elements are found, null will be returned.
		///		If two or more elements are found, the value will be a comma seperated string of values.
		///		NOTE ON SET:
		///		If the value is an array, it will be converted to a comma seperated string.
		/// </summary>
		/// <param name="value" type="Object">
		///		The value to set. Or undefined for a get.
		///	</param>
		/// <returns type="var">Returns the value if a get, or jQuery if a set.</returns>

		var value, values, name;

		// see if we are setting the value
		if (arguments[0] !== undefined) {
			value = $.isArray(arguments[0]) ? arguments[0].join(", ") : arguments[0];
			$("[name=" + this.attr("id") + "]").attr("value", value);
			return this;
		}

		// handle if there is more than one element with the same name
		values = [];
		name = this.attr("id");
		$("[name=" + this.attr("id") + "]").each(function () {
			values.push($(this).attr("value"));
		});
		if (values.length === 0) {
			return null;
		}
		if (values.length === 1) {
			return values[0];
		}
		return values.join(", ");
	};

	$.fn.values = function () {
		/// <summary>
		///		Performs a split(", ") on the $.fn.value() method.
		///	</summary>
		///	<returns type="Array" />
		if (this.isNullOrEmpty()) {
			return [];
		}
		return this.value().split(", ");
	};

	$.fn.isNullOrEmpty = function () {
		/// <summary>
		///		Returns true if the value returned from $.fn.value() is null or an empty string.
		/// </summary>
		/// <returns type="Boolean" />
		var value = this.value();
		return (value == null || $.trim(value) == "") ? true : false;
	};

	$.fn.disable = function (disable) {
		/// <summary>
		///		OVERLOADED - Disables element(s) and adds the "ui-state-disabled" class.
		///		1: Nm.dislabe()
		///		2: Nm.disable(disable)
		///	</summary>
		///	<param name="disable" type="Boolean">
		///		2: Calls $.fn.enable() if false.
		/// </param>
		/// <returns type="jQuery" />
		var disable = arguments[0];
		this.each(function () {
			if (disable === undefined) {
				$(this).trigger("blur").attr("disabled", "disabled")
						.removeClass("ui-state-hover").addClass("ui-state-disabled");
			} else {
				disable ? $(this).disable() : $(this).enable();
			};
		});
		return this;
	};

	$.fn.enable = function (enable) {
		/// <summary>
		///		OVERLOADED - Enables element(s) and removes the "ui-state-disabled" class.
		///		1: Nm.enable()
		///		2: Nm.enable(enable)
		///	</summary>
		///	<param name="enable" type="Boolean">
		///		2: Calls $.fn.enable() if false.
		/// </param>
		/// <returns type="jQuery" />
		var enable = arguments[0];
		this.each(function () {
			if (enable === undefined) {
				$(this).removeAttr("disabled").removeClass("ui-state-disabled");
			} else {
				enable ? $(this).enable() : $(this).disable();
			};
		});
		return this;
	};

	$.fn.center = function (options) {
		/// <summary>
		///    Centers the element.
		/// </summary>
		/// <param name="options" type="object">
		///     *horizontal: Centers horizontal (true).
		///     *vertical: Centers virtically (true).
		///     *resize: Centers on window.resize (true).
		/// </param>
		/// <returns type="jQuery" />
		var win, _this;

		options = $.extend({
				horizontal: true,
				vertical: true,
				resize: true
			}, options);

		win = $(window);
		_this = this;
		_this.css("position", "absolute");
		if (options.vertical) {
			_this.css("top", Math.max((win.height() - this.outerHeight(true)) / 2, 0) + "px");
		}
		if (options.horizontal) {
			_this.css("left", Math.max((win.width() - _this.outerWidth(true)) / 2, 0) + "px");
		};
		if (options.resize) {
			win.resize(function () {
				_this.center();
			});
		}
		return this;
	}

	_ = {
		appRoot: "",

		waitForQueue: {},

		loadedScripts: {},		
		
		loadScript: function (url, callback) {
			var callCallback;
			
			callCallback = function () {				
				if (callback) {
					callback.call(null);
				}
			}; 			
						
			if (_.loadedScripts[url] === "loaded") {						// script is loaded
				callCallback();
			} else {
				if (url.toLowerCase().indexOf(".js") > -1) {				// load javascript file
					if (_.loadedScripts[url] === "queued") {				// script is queued, wait
						setTimeout(function () {
							_.loadScript(url, callback);
						}, 1);
					} else {												// queue the script
						_.loadedScripts[url] = "queued";
						$.getScript(url, function (data, textStatus) {
							_.loadedScripts[url] = "loaded";
							Nm.console.log("Nm loaded js: [%s]", url);
							callCallback();
						});
					}
				} else {													// load stylesheet
					$('head').append('<link rel="stylesheet" href="' + url + '" type="text/css"/>');
					_.loadedScripts[url] = "loaded";
					Nm.console.log("Nm loaded css: [%s]", url);
					callCallback();
				}
			}
		},

		loadScriptGroup: function(options) {
			/// <summary>
			///	    Loads scripts from the server.
			///		options has three properties:
			///			Key: The key used for the callback.
			///			Dependencies (Array of ScriptUrl)
			///			context (jQuery)
			///     ScriptUrl: Source, Target
			/// </summary>			
			var dependentCount;

			dependentCount = options.Dependencies.length || 0;

			$.each(options.Dependencies, function (i, scriptUrl) {				
				Nm.loadScript(scriptUrl.Target, function() {
					dependentCount--;
					if (dependentCount === 0) {
						Nm.console.log("Nm calling scriptLoaded callback: \"%s\"", options.Key);
						_.loadedScripts[scriptUrl.Source] = "loaded";
						options.context.trigger(options.Key); // for scriptLoaded
						return false;
					}
				});
			});
		}
	};

	$(document).ready(function () {
		
		// set up handling ajax errors and traditional parameter serialization
		jQuery.ajaxSetup({
			error: function (xmlHttpRequest, status, error) {
				Nm.console.error(xmlHttpRequest.responseText);
			},
			traditional: true
		});

		// load JSON if browser has not implemented it
		if (window.JSON === undefined) {
			jQuery.ajax({
				method: "get",
				url: Nm.resolveUrl("~/Scripts/Nm/minified/json2.min.js"),
				async: false,
				success: function () {
					Nm.console.log("Nm document.ready: JSON loaded.");
				},
				error: function (response) {
					Nm.console.error("Nm document.ready: Failed to load JSON. %o", response);
				}
			});
		}
		Nm.loadScripts(document);
	});
 })(jQuery);
/*
 * Nm.js - console
 * 
 * Provides a wrapper around window.console for Nm script logging.
 */
 (function ($) {
	var _;

	Nm.console = {
		log: function (message, objectsN) {
			/// <summary>
			///     Calls console.log.
			/// </summary>
			///	<param name="message" type="string">
			///    The message to log. 
			/// </param>
			/// <param name="objectsN" type="object">
			///		 Additional parameters used for string formatting and variable replacement.
			/// </param>
			_.log("log", arguments);
		},
		
		info: function (message) {
			/// <summary>
			///     Calls console.info.
			/// </summary>
			///	<param name="message" type="string">
			///    The message to log. 
			/// </param>
			/// <param name="objectsN" type="object">
			///		 Additional parameters used for string formatting and variable replacement.
			/// </param>			
			$(document).trigger("nminfo", _.formatConsoleMsg(arguments));
			_.log("info", arguments);
		},

		warn: function (message) {
			/// <summary>
			///     Calls console.warn.
			/// </summary>
			///	<param name="message" type="string">
			///    The message to log. 
			/// </param>
			/// <param name="objectsN" type="object">
			///		 Additional parameters used for string formatting and variable replacement.
			/// </param>
			_.log("warn", arguments);
		},

		error: function (message) {
			/// <summary>
			///     Calls console.error.
			/// </summary>
			///	<param name="message" type="string">
			///    The message to log. 
			/// </param>
			/// <param name="objectsN" type="object">
			///		 Additional parameters used for string formatting and variable replacement.
			/// </param>
			try {
				$(document).trigger("nmerror", _.formatConsoleMsg(arguments));
				_.log("error", arguments);
			} catch (e) { }
		},

		level: function (level) {
			/// <summary>
			///	    Limits the logging of Nm scripts to the set level.
			/// </summary>
			///	<param name="level" type="string">
			///    The maximum level to log. Can be "log", "info", "warn", "error".
			///    The default is "log".
			/// </param>
			if (level in _.levels) {
				_.currentLevel = level;
			}
		}
	};

	_ = {
		levels: {
			"log": 4, "info": 3, "warn": 2, "error": 1
		},

		currentLevel: "log",
		
		log: function (level, args) {
			var msg;
			if (_.nativeConsoleExists && _.levels[level] <= _.levels[_.currentLevel]) {			
				try {
					window.console[level].apply(window.console, args);
				} catch (e) {
					try {
						$.each(args, function (i, arg) {
							if (!msg) {
								msg = String(arg);
							} else {
								msg = msg.replace(/%s|%o|%n|%b/, String(arg));
							}
						});
					} catch (e) {
						msg = String(args);
					}
					window.console.log(msg);
				}
			}
		},

		nativeConsoleExists: false,

		emptyFn: function () { },

		formatConsoleMsg: function (args) {
			var msg;
			try {	
				args = Array.prototype.slice.apply(args); // turn it into a real array
				msg = args.shift() || ""; // get the text
				try {
					$.each(args, function (i, arg) {
						msg = msg.replace(/%s|%o|%n|%b/, String(arg));
					});
				} catch (e) {
					msg = String(args);
				}
				return msg;
			} catch (e) {
				return args[0];
			}
		}
	};
	
	// ensure a basic window.console object in case attempting to log directly.
	// (lessens the chance of a js error if window.console does not exist)
	if (window.console === undefined) {
		window.console = { log: _.emptyFn, info: _.emptyFn, warn: _.emptyFn, error: _.emptyFn };
	} else {
		_.nativeConsoleExists = true;
	}
	
 })(jQuery);
