(function (root, doc, factory) {
	if (typeof define === "function" && define.amd) {
		// AMD. Register as an anonymous module.
		define(["jquery"], function ($) {
			return factory($, root, doc);
		});
	} else {
		// Browser globals
		factory(root.jQuery, root, doc);
	}
} (this, document, function ($, global, document, undefined) {/*!
 * Amplify 1.1.2
 *
 * Copyright 2011 - 2013 appendTo LLC. (http://appendto.com/team)
 * Dual licensed under the MIT or GPL licenses.
 * http://appendto.com/open-source-licenses
 *
 * http://amplifyjs.com
 */
	(function (global, undefined) {

		var slice = [].slice,
			subscriptions = {};

		var amplify = global.amplify = {
			publish: function (topic) {
				if (typeof topic !== "string") {
					throw new Error("You must provide a valid topic to publish.");
				}

				var args = slice.call(arguments, 1),
					topicSubscriptions,
					subscription,
					length,
					i = 0,
					ret;

				if (!subscriptions[topic]) {
					return true;
				}

				topicSubscriptions = subscriptions[topic].slice();
				for (length = topicSubscriptions.length; i < length; i++) {
					subscription = topicSubscriptions[i];
					ret = subscription.callback.apply(subscription.context, args);
					if (ret === false) {
						break;
					}
				}
				return ret !== false;
			},

			subscribe: function (topic, context, callback, priority) {
				if (typeof topic !== "string") {
					throw new Error("You must provide a valid topic to create a subscription.");
				}

				if (arguments.length === 3 && typeof callback === "number") {
					priority = callback;
					callback = context;
					context = null;
				}
				if (arguments.length === 2) {
					callback = context;
					context = null;
				}
				priority = priority || 10;

				var topicIndex = 0,
					topics = topic.split(/\s/),
					topicLength = topics.length,
					added;
				for (; topicIndex < topicLength; topicIndex++) {
					topic = topics[topicIndex];
					added = false;
					if (!subscriptions[topic]) {
						subscriptions[topic] = [];
					}

					var i = subscriptions[topic].length - 1,
						subscriptionInfo = {
							callback: callback,
							context: context,
							priority: priority
						};

					for (; i >= 0; i--) {
						if (subscriptions[topic][i].priority <= priority) {
							subscriptions[topic].splice(i + 1, 0, subscriptionInfo);
							added = true;
							break;
						}
					}

					if (!added) {
						subscriptions[topic].unshift(subscriptionInfo);
					}
				}

				return callback;
			},

			unsubscribe: function (topic, context, callback) {
				if (typeof topic !== "string") {
					throw new Error("You must provide a valid topic to remove a subscription.");
				}

				if (arguments.length === 2) {
					callback = context;
					context = null;
				}

				if (!subscriptions[topic]) {
					return;
				}

				var length = subscriptions[topic].length,
					i = 0;

				for (; i < length; i++) {
					if (subscriptions[topic][i].callback === callback) {
						if (!context || subscriptions[topic][i].context === context) {
							subscriptions[topic].splice(i, 1);
					
							// Adjust counter and length for removed item
							i--;
							length--;
						}
					}
				}
			}
		};

	} (this));

	(function (amplify, undefined) {

		var store = amplify.store = function (key, value, options) {
			var type = store.type;
			if (options && options.type && options.type in store.types) {
				type = options.type;
			}
			return store.types[type](key, value, options || {});
		};

		store.types = {};
		store.type = null;
		store.addType = function (type, storage) {
			if (!store.type) {
				store.type = type;
			}

			store.types[type] = storage;
			store[type] = function (key, value, options) {
				options = options || {};
				options.type = type;
				return store(key, value, options);
			};
		};
		store.error = function () {
			return "amplify.store quota exceeded";
		};

		var rprefix = /^__amplify__/;
		function createFromStorageInterface(storageType, storage) {
			store.addType(storageType, function (key, value, options) {
				var storedValue, parsed, i, remove,
					ret = value,
					now = (new Date()).getTime();

				if (!key) {
					ret = {};
					remove = [];
					i = 0;
					try {
						// accessing the length property works around a localStorage bug
						// in Firefox 4.0 where the keys don't update cross-page
						// we assign to key just to avoid Closure Compiler from removing
						// the access as "useless code"
						// https://bugzilla.mozilla.org/show_bug.cgi?id=662511
						key = storage.length;

						while (key = storage.key(i++)) {
							if (rprefix.test(key)) {
								parsed = JSON.parse(storage.getItem(key));
								if (parsed.expires && parsed.expires <= now) {
									remove.push(key);
								} else {
									ret[key.replace(rprefix, "")] = parsed.data;
								}
							}
						}
						while (key = remove.pop()) {
							storage.removeItem(key);
						}
					} catch (error) { }
					return ret;
				}

				// protect against name collisions with direct storage
				key = "__amplify__" + key;

				if (value === undefined) {
					storedValue = storage.getItem(key);
					parsed = storedValue ? JSON.parse(storedValue) : { expires: -1 };
					if (parsed.expires && parsed.expires <= now) {
						storage.removeItem(key);
					} else {
						return parsed.data;
					}
				} else {
					if (value === null) {
						storage.removeItem(key);
					} else {
						parsed = JSON.stringify({
							data: value,
							expires: options.expires ? now + options.expires : null
						});
						try {
							storage.setItem(key, parsed);
							// quota exceeded
						} catch (error) {
							// expire old data and try again
							store[storageType]();
							try {
								storage.setItem(key, parsed);
							} catch (error) {
								throw store.error();
							}
						}
					}
				}

				return ret;
			});
		}

		// localStorage + sessionStorage
		// IE 8+, Firefox 3.5+, Safari 4+, Chrome 4+, Opera 10.5+, iPhone 2+, Android 2+
		for (var webStorageType in { localStorage: 1, sessionStorage: 1 }) {
			// try/catch for file protocol in Firefox and Private Browsing in Safari 5
			try {
				// Safari 5 in Private Browsing mode exposes localStorage
				// but doesn't allow storing data, so we attempt to store and remove an item.
				// This will unfortunately give us a false negative if we're at the limit.
				window[webStorageType].setItem("__amplify__", "x");
				window[webStorageType].removeItem("__amplify__");
				createFromStorageInterface(webStorageType, window[webStorageType]);
			} catch (e) { }
		}

		// globalStorage
		// non-standard: Firefox 2+
		// https://developer.mozilla.org/en/dom/storage#globalStorage
		if (!store.types.localStorage && window.globalStorage) {
			// try/catch for file protocol in Firefox
			try {
				createFromStorageInterface("globalStorage",
					window.globalStorage[window.location.hostname]);
				// Firefox 2.0 and 3.0 have sessionStorage and globalStorage
				// make sure we default to globalStorage
				// but don't default to globalStorage in 3.5+ which also has localStorage
				if (store.type === "sessionStorage") {
					store.type = "globalStorage";
				}
			} catch (e) { }
		}

		// userData
		// non-standard: IE 5+
		// http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx
		(function () {
			// IE 9 has quirks in userData that are a huge pain
			// rather than finding a way to detect these quirks
			// we just don't register userData if we have localStorage
			if (store.types.localStorage) {
				return;
			}

			// append to html instead of body so we can do this from the head
			var div = document.createElement("div"),
				attrKey = "amplify";
			div.style.display = "none";
			document.getElementsByTagName("head")[0].appendChild(div);

			// we can't feature detect userData support
			// so just try and see if it fails
			// surprisingly, even just adding the behavior isn't enough for a failure
			// so we need to load the data as well
			try {
				div.addBehavior("#default#userdata");
				div.load(attrKey);
			} catch (e) {
				div.parentNode.removeChild(div);
				return;
			}

			store.addType("userData", function (key, value, options) {
				div.load(attrKey);
				var attr, parsed, prevValue, i, remove,
					ret = value,
					now = (new Date()).getTime();

				if (!key) {
					ret = {};
					remove = [];
					i = 0;
					while (attr = div.XMLDocument.documentElement.attributes[i++]) {
						parsed = JSON.parse(attr.value);
						if (parsed.expires && parsed.expires <= now) {
							remove.push(attr.name);
						} else {
							ret[attr.name] = parsed.data;
						}
					}
					while (key = remove.pop()) {
						div.removeAttribute(key);
					}
					div.save(attrKey);
					return ret;
				}

				// convert invalid characters to dashes
				// http://www.w3.org/TR/REC-xml/#NT-Name
				// simplified to assume the starting character is valid
				// also removed colon as it is invalid in HTML attribute names
				key = key.replace(/[^\-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/g, "-");
				// adjust invalid starting character to deal with our simplified sanitization
				key = key.replace(/^-/, "_-");

				if (value === undefined) {
					attr = div.getAttribute(key);
					parsed = attr ? JSON.parse(attr) : { expires: -1 };
					if (parsed.expires && parsed.expires <= now) {
						div.removeAttribute(key);
					} else {
						return parsed.data;
					}
				} else {
					if (value === null) {
						div.removeAttribute(key);
					} else {
						// we need to get the previous value in case we need to rollback
						prevValue = div.getAttribute(key);
						parsed = JSON.stringify({
							data: value,
							expires: (options.expires ? (now + options.expires) : null)
						});
						div.setAttribute(key, parsed);
					}
				}

				try {
					div.save(attrKey);
					// quota exceeded
				} catch (error) {
					// roll the value back to the previous value
					if (prevValue === null) {
						div.removeAttribute(key);
					} else {
						div.setAttribute(key, prevValue);
					}

					// expire old data and try again
					store.userData();
					try {
						div.setAttribute(key, parsed);
						div.save(attrKey);
					} catch (error) {
						// roll the value back to the previous value
						if (prevValue === null) {
							div.removeAttribute(key);
						} else {
							div.setAttribute(key, prevValue);
						}
						throw store.error();
					}
				}
				return ret;
			});
		} ());

		// in-memory storage
		// fallback for all browsers to enable the API even if we can't persist data
		(function () {
			var memory = {},
				timeout = {};

			function copy(obj) {
				return obj === undefined ? undefined : JSON.parse(JSON.stringify(obj));
			}

			store.addType("memory", function (key, value, options) {
				if (!key) {
					return copy(memory);
				}

				if (value === undefined) {
					return copy(memory[key]);
				}

				if (timeout[key]) {
					clearTimeout(timeout[key]);
					delete timeout[key];
				}

				if (value === null) {
					delete memory[key];
					return null;
				}

				memory[key] = value;
				if (options.expires) {
					timeout[key] = setTimeout(function () {
						delete memory[key];
						delete timeout[key];
					}, options.expires);
				}

				return value;
			});
		} ());

	} (this.amplify = this.amplify || {}));

	/*global amplify*/

	(function (amplify, undefined) {
		'use strict';

		function noop() { }
		function isFunction(obj) {
			return ({}).toString.call(obj) === "[object Function]";
		}

		function async(fn) {
			var isAsync = false;
			setTimeout(function () {
				isAsync = true;
			}, 1);
			return function () {
				var that = this,
					args = arguments;
				if (isAsync) {
					fn.apply(that, args);
				} else {
					setTimeout(function () {
						fn.apply(that, args);
					}, 1);
				}
			};
		}

		amplify.request = function (resourceId, data, callback) {
			// default to an empty hash just so we can handle a missing resourceId
			// in one place
			var settings = resourceId || {};

			if (typeof settings === "string") {
				if (isFunction(data)) {
					callback = data;
					data = {};
				}
				settings = {
					resourceId: resourceId,
					data: data || {},
					success: callback
				};
			}

			var request = { abort: noop },
				resource = amplify.request.resources[settings.resourceId],
				success = settings.success || noop,
				error = settings.error || noop;

			settings.success = async(function (data, status) {
				status = status || "success";
				amplify.publish("request.success", settings, data, status);
				amplify.publish("request.complete", settings, data, status);
				success(data, status);
			});

			settings.error = async(function (data, status) {
				status = status || "error";
				amplify.publish("request.error", settings, data, status);
				amplify.publish("request.complete", settings, data, status);
				error(data, status);
			});

			if (!resource) {
				if (!settings.resourceId) {
					throw "amplify.request: no resourceId provided";
				}
				throw "amplify.request: unknown resourceId: " + settings.resourceId;
			}

			if (!amplify.publish("request.before", settings)) {
				settings.error(null, "abort");
				return;
			}

			amplify.request.resources[settings.resourceId](settings, request);
			return request;
		};

		amplify.request.types = {};
		amplify.request.resources = {};
		amplify.request.define = function (resourceId, type, settings) {
			if (typeof type === "string") {
				if (!(type in amplify.request.types)) {
					throw "amplify.request.define: unknown type: " + type;
				}

				settings.resourceId = resourceId;
				amplify.request.resources[resourceId] =
				amplify.request.types[type](settings);
			} else {
				// no pre-processor or settings for one-off types (don't invoke)
				amplify.request.resources[resourceId] = type;
			}
		};

	} (amplify));


	(function (amplify, $, undefined) {
		'use strict';

		var xhrProps = ["status", "statusText", "responseText", "responseXML", "readyState"],
			rurlData = /\{([^\}]+)\}/g;

		amplify.request.types.ajax = function (defnSettings) {
			defnSettings = $.extend({
				type: "GET"
			}, defnSettings);

			return function (settings, request) {
				var xhr, handleResponse,
					url = defnSettings.url,
					abort = request.abort,
					ajaxSettings = $.extend(true, {}, defnSettings, { data: settings.data }),
					aborted = false,
					ampXHR = {
						readyState: 0,
						setRequestHeader: function (name, value) {
							return xhr.setRequestHeader(name, value);
						},
						getAllResponseHeaders: function () {
							return xhr.getAllResponseHeaders();
						},
						getResponseHeader: function (key) {
							return xhr.getResponseHeader(key);
						},
						overrideMimeType: function (type) {
							return xhr.overrideMimeType(type);
						},
						abort: function () {
							aborted = true;
							try {
								xhr.abort();
								// IE 7 throws an error when trying to abort
							} catch (e) { }
							handleResponse(null, "abort");
						},
						success: function (data, status) {
							settings.success(data, status);
						},
						error: function (data, status) {
							settings.error(data, status);
						}
					};

				handleResponse = function (data, status) {
					$.each(xhrProps, function (i, key) {
						try {
							ampXHR[key] = xhr[key];
						} catch (e) { }
					});
					// Playbook returns "HTTP/1.1 200 OK"
					// TODO: something also returns "OK", what?
					if (/OK$/.test(ampXHR.statusText)) {
						ampXHR.statusText = "success";
					}
					if (data === undefined) {
						// TODO: add support for ajax errors with data
						data = null;
					}
					if (aborted) {
						status = "abort";
					}
					if (/timeout|error|abort/.test(status)) {
						ampXHR.error(data, status);
					} else {
						ampXHR.success(data, status);
					}
					// avoid handling a response multiple times
					// this can happen if a request is aborted
					// TODO: figure out if this breaks polling or multi-part responses
					handleResponse = $.noop;
				};

				amplify.publish("request.ajax.preprocess",
					defnSettings, settings, ajaxSettings, ampXHR);

				$.extend(ajaxSettings, {
					isJSONP: function () {
						return (/jsonp/gi).test(this.dataType);
					},
					cacheURL: function () {
						if (!this.isJSONP()) {
							return this.url;
						}

						var callbackName = 'callback';

						// possible for the callback function name to be overridden
						if (this.hasOwnProperty('jsonp')) {
							if (this.jsonp !== false) {
								callbackName = this.jsonp;
							} else {
								if (this.hasOwnProperty('jsonpCallback')) {
									callbackName = this.jsonpCallback;
								}
							}
						}

						// search and replace callback parameter in query string with empty string
						var callbackRegex = new RegExp('&?' + callbackName + '=[^&]*&?', 'gi');
						return this.url.replace(callbackRegex, '');
					},
					success: function (data, status) {
						handleResponse(data, status);
					},
					error: function (_xhr, status) {
						handleResponse(null, status);
					},
					beforeSend: function (_xhr, _ajaxSettings) {
						xhr = _xhr;
						ajaxSettings = _ajaxSettings;
						var ret = defnSettings.beforeSend ?
							defnSettings.beforeSend.call(this, ampXHR, ajaxSettings) : true;
						return ret && amplify.publish("request.before.ajax",
							defnSettings, settings, ajaxSettings, ampXHR);
					}
				});

				// cache all JSONP requests
				if (ajaxSettings.cache && ajaxSettings.isJSONP()) {
					$.extend(ajaxSettings, {
						cache: true
					});
				}

				$.ajax(ajaxSettings);

				request.abort = function () {
					ampXHR.abort();
					abort.call(this);
				};
			};
		};



		amplify.subscribe("request.ajax.preprocess", function (defnSettings, settings, ajaxSettings) {
			var mappedKeys = [],
				data = ajaxSettings.data;

			if (typeof data === "string") {
				return;
			}

			data = $.extend(true, {}, defnSettings.data, data);

			ajaxSettings.url = ajaxSettings.url.replace(rurlData, function (m, key) {
				if (key in data) {
					mappedKeys.push(key);
					return data[key];
				}
			});

			// We delete the keys later so duplicates are still replaced
			$.each(mappedKeys, function (i, key) {
				delete data[key];
			});

			ajaxSettings.data = data;
		});



		amplify.subscribe("request.ajax.preprocess", function (defnSettings, settings, ajaxSettings) {
			var data = ajaxSettings.data,
				dataMap = defnSettings.dataMap;

			if (!dataMap || typeof data === "string") {
				return;
			}

			if ($.isFunction(dataMap)) {
				ajaxSettings.data = dataMap(data);
			} else {
				$.each(defnSettings.dataMap, function (orig, replace) {
					if (orig in data) {
						data[replace] = data[orig];
						delete data[orig];
					}
				});
				ajaxSettings.data = data;
			}
		});



		var cache = amplify.request.cache = {
			_key: function (resourceId, url, data) {
				data = url + data;
				var length = data.length,
					i = 0;

				/*jshint bitwise:false*/
				function chunk() {
					return data.charCodeAt(i++) << 24 |
						data.charCodeAt(i++) << 16 |
						data.charCodeAt(i++) << 8 |
						data.charCodeAt(i++) << 0;
				}

				var checksum = chunk();
				while (i < length) {
					checksum ^= chunk();
				}
				/*jshint bitwise:true*/

				return "request-" + resourceId + "-" + checksum;
			},

			_default: (function () {
				var memoryStore = {};
				return function (resource, settings, ajaxSettings, ampXHR) {
					// data is already converted to a string by the time we get here
					var cacheKey = cache._key(settings.resourceId,
						ajaxSettings.cacheURL(), ajaxSettings.data),
						duration = resource.cache;

					if (cacheKey in memoryStore) {
						ampXHR.success(memoryStore[cacheKey]);
						return false;
					}
					var success = ampXHR.success;
					ampXHR.success = function (data) {
						memoryStore[cacheKey] = data;
						if (typeof duration === "number") {
							setTimeout(function () {
								delete memoryStore[cacheKey];
							}, duration);
						}
						success.apply(this, arguments);
					};
				};
			} ())
		};

		if (amplify.store) {
			$.each(amplify.store.types, function (type) {
				cache[type] = function (resource, settings, ajaxSettings, ampXHR) {
					var cacheKey = cache._key(settings.resourceId,
						ajaxSettings.cacheURL(), ajaxSettings.data),
						cached = amplify.store[type](cacheKey);

					if (cached) {
						ajaxSettings.success(cached);
						return false;
					}
					var success = ampXHR.success;
					ampXHR.success = function (data) {
						amplify.store[type](cacheKey, data, { expires: resource.cache.expires });
						success.apply(this, arguments);
					};
				};
			});
			cache.persist = cache[amplify.store.type];
		}

		amplify.subscribe("request.before.ajax", function (resource) {
			var cacheType = resource.cache;
			if (cacheType) {
				// normalize between objects and strings/booleans/numbers
				cacheType = cacheType.type || cacheType;
				return cache[cacheType in cache ? cacheType : "_default"]
					.apply(this, arguments);
			}
		});



		amplify.request.decoders = {
			// http://labs.omniti.com/labs/jsend
			jsend: function (data, status, ampXHR, success, error) {
				if (data.status === "success") {
					success(data.data);
				} else if (data.status === "fail") {
					error(data.data, "fail");
				} else if (data.status === "error") {
					delete data.status;
					error(data, "error");
				} else {
					error(null, "error");
				}
			}
		};

		amplify.subscribe("request.before.ajax", function (resource, settings, ajaxSettings, ampXHR) {
			var _success = ampXHR.success,
				_error = ampXHR.error,
				decoder = $.isFunction(resource.decoder) ?
					resource.decoder :
					resource.decoder in amplify.request.decoders ?
						amplify.request.decoders[resource.decoder] :
						amplify.request.decoders._default;

			if (!decoder) {
				return;
			}

			function success(data, status) {
				_success(data, status);
			}
			function error(data, status) {
				_error(data, status);
			}
			ampXHR.success = function (data, status) {
				decoder(data, status, ampXHR, success, error);
			};
			ampXHR.error = function (data, status) {
				decoder(data, status, ampXHR, success, error);
			};
		});

	} (amplify, jQuery));
	return amplify;
}));