(function () {
	if (typeof self === 'undefined' || !self.Prism || !self.document) {
		return;
	}

	/**
	 * @callback Adapter
	 * @param {any} response
	 * @param {HTMLPreElement} [pre]
	 * @returns {string | null}
	 */

	/**
	 * The list of adapter which will be used if `data-adapter` is not specified.
	 *
	 * @type {Array<{adapter: Adapter, name: string}>}
	 */
	var adapters = [];

	/**
	 * Adds a new function to the list of adapters.
	 *
	 * If the given adapter is already registered or not a function or there is an adapter with the given name already,
	 * nothing will happen.
	 *
	 * @param {Adapter} adapter The adapter to be registered.
	 * @param {string} [name] The name of the adapter. Defaults to the function name of `adapter`.
	 */
	function registerAdapter(adapter, name) {
		name = name || adapter.name;
		if (typeof adapter === "function" && !getAdapter(adapter) && !getAdapter(name)) {
			adapters.push({ adapter: adapter, name: name });
		}
	}
	/**
	 * Returns the given adapter itself, if registered, or a registered adapter with the given name.
	 *
	 * If no fitting adapter is registered, `null` will be returned.
	 *
	 * @param {string|Function} adapter The adapter itself or the name of an adapter.
	 * @returns {Adapter} A registered adapter or `null`.
	 */
	function getAdapter(adapter) {
		if (typeof adapter === "function") {
			for (var i = 0, item; item = adapters[i++];) {
				if (item.adapter.valueOf() === adapter.valueOf()) {
					return item.adapter;
				}
			}
		}
		else if (typeof adapter === "string") {
			for (var i = 0, item; item = adapters[i++];) {
				if (item.name === adapter) {
					return item.adapter;
				}
			}
		}
		return null;
	}
	/**
	 * Remove the given adapter or the first registered adapter with the given name from the list of
	 * registered adapters.
	 *
	 * @param {string|Function} adapter The adapter itself or the name of an adapter.
	 */
	function removeAdapter(adapter) {
		if (typeof adapter === "string") {
			adapter = getAdapter(adapter);
		}
		if (typeof adapter === "function") {
			var index = adapters.findIndex(function (item) {
				return item.adapter === adapter;
			});
			if (index >= 0) {
				adapters.splice(index, 1);
			}
		}
	}

	registerAdapter(function github(rsp, el) {
		if (rsp && rsp.meta && rsp.data) {
			if (rsp.meta.status && rsp.meta.status >= 400) {
				return "Error: " + (rsp.data.message || rsp.meta.status);
			}
			else if (typeof (rsp.data.content) === "string") {
				return typeof (atob) === "function"
					? atob(rsp.data.content.replace(/\s/g, ""))
					: "Your browser cannot decode base64";
			}
		}
		return null;
	}, 'github');
	registerAdapter(function gist(rsp, el) {
		if (rsp && rsp.meta && rsp.data && rsp.data.files) {
			if (rsp.meta.status && rsp.meta.status >= 400) {
				return "Error: " + (rsp.data.message || rsp.meta.status);
			}

			var files = rsp.data.files;
			var filename = el.getAttribute("data-filename");
			if (filename == null) {
				// Maybe in the future we can somehow render all files
				// But the standard <script> include for gists does that nicely already,
				// so that might be getting beyond the scope of this plugin
				for (var key in files) {
					if (files.hasOwnProperty(key)) {
						filename = key;
						break;
					}
				}
			}

			if (files[filename] !== undefined) {
				return files[filename].content;
			}
			return "Error: unknown or missing gist file " + filename;
		}
		return null;
	}, 'gist');
	registerAdapter(function bitbucket(rsp, el) {
		if (rsp && rsp.node && typeof (rsp.data) === "string") {
			return rsp.data;
		}
		return null;
	}, 'bitbucket');


	var jsonpCallbackCounter = 0;

	var LOADING_MESSAGE = 'Loading…';
	var MISSING_ADAPTER_MESSAGE = function (name) {
		return '✖ Error: JSONP adapter function "' + name + '" doesn\'t exist';
	};
	var TIMEOUT_MESSAGE = function (url) {
		return '✖ Error: Timeout loading ' + url;
	};
	var UNKNOWN_FAILURE_MESSAGE = '✖ Error: Cannot parse response (perhaps you need an adapter function?)';

	var STATUS_ATTR = 'data-jsonp-status';
	var STATUS_LOADING = 'loading';
	var STATUS_LOADED = 'loaded';
	var STATUS_FAILED = 'failed';

	var SELECTOR = 'pre[data-jsonp]:not([' + STATUS_ATTR + '="' + STATUS_LOADED + '"])'
		+ ':not([' + STATUS_ATTR + '="' + STATUS_LOADING + '"])';


	Prism.hooks.add('before-highlightall', function (env) {
		env.selector += ', ' + SELECTOR;
	});

	Prism.hooks.add('before-sanity-check', function (env) {
		var pre = /** @type {HTMLPreElement} */ (env.element);
		if (pre.matches(SELECTOR)) {
			env.code = ''; // fast-path the whole thing and go to complete

			// mark as loading
			pre.setAttribute(STATUS_ATTR, STATUS_LOADING);

			// add code element with loading message
			var code = pre.appendChild(document.createElement('CODE'));
			code.textContent = LOADING_MESSAGE;

			// set language
			var language = env.language;
			code.className = 'language-' + language;

			// preload the language
			var autoloader = Prism.plugins.autoloader;
			if (autoloader) {
				autoloader.loadLanguages(language);
			}

			var adapterName = pre.getAttribute('data-adapter');
			var adapter = null;
			if (adapterName) {
				if (typeof window[adapterName] === 'function') {
					adapter = window[adapterName];
				} else {
					// mark as failed
					pre.setAttribute(STATUS_ATTR, STATUS_FAILED);

					code.textContent = MISSING_ADAPTER_MESSAGE(adapterName);
					return;
				}
			}

			var callbackName = 'prismjsonp' + jsonpCallbackCounter++;

			var uri = document.createElement('a');
			var src = uri.href = pre.getAttribute('data-jsonp');
			uri.href += (uri.search ? '&' : '?') + (pre.getAttribute('data-callback') || 'callback') + '=' + callbackName;


			var timeout = setTimeout(function () {
				// we could clean up window[cb], but if the request finally succeeds, keeping it around is a good thing

				// mark as failed
				pre.setAttribute(STATUS_ATTR, STATUS_FAILED);

				code.textContent = TIMEOUT_MESSAGE(src);
			}, Prism.plugins.jsonphighlight.timeout);


			var script = document.createElement('script');
			script.src = uri.href;

			// the JSONP callback function
			window[callbackName] = function (response) {
				// clean up
				document.head.removeChild(script);
				clearTimeout(timeout);
				delete window[callbackName];

				// interpret the received data using the adapter(s)
				var data = null;
				if (adapter) {
					data = adapter(response, pre);
				} else {
					for (var i = 0, l = adapters.length; i < l; i++) {
						data = adapters[i].adapter(response, pre);
						if (data !== null) {
							break;
						}
					}
				}

				if (data === null) {
					// mark as failed
					pre.setAttribute(STATUS_ATTR, STATUS_FAILED);

					code.textContent = UNKNOWN_FAILURE_MESSAGE;
				} else {
					// mark as loaded
					pre.setAttribute(STATUS_ATTR, STATUS_LOADED);

					code.textContent = data;
					Prism.highlightElement(code);
				}
			};

			document.head.appendChild(script);
		}
	});


	Prism.plugins.jsonphighlight = {
		/**
		 * The timeout after which an error message will be displayed.
		 *
		 * __Note:__ If the request succeeds after the timeout, it will still be processed and will override any
		 * displayed error messages.
		 */
		timeout: 5000,
		registerAdapter: registerAdapter,
		removeAdapter: removeAdapter,

		/**
		 * Highlights all `pre` elements under the given container with a `data-jsonp` attribute by requesting the
		 * specified JSON and using the specified adapter or a registered adapter to extract the code to highlight
		 * from the response. The highlighted code will be inserted into the `pre` element.
		 *
		 * Note: Elements which are already loaded or currently loading will not be touched by this method.
		 *
		 * @param {Element | Document} [container=document]
		 */
		highlight: function (container) {
			var elements = (container || document).querySelectorAll(SELECTOR);

			for (var i = 0, element; element = elements[i++];) {
				Prism.highlightElement(element);
			}
		}
	};

})();
