(function () {
	if (!self.Prism || !self.document || !document.querySelectorAll || ![].filter) return;

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

	/**
	 * 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.map(function (item) { return item.adapter; }).indexOf(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 jsonpcb = 0,
		loadMsg = "Loading\u2026";

	/**
	 * Highlights all `pre` elements with an `data-jsonp` 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.
	 */
	function highlight() {
		Array.prototype.slice.call(document.querySelectorAll("pre[data-jsonp]")).forEach(function (pre) {
			pre.textContent = "";

			var code = document.createElement("code");
			code.textContent = loadMsg;
			pre.appendChild(code);

			var adapterName = pre.getAttribute("data-adapter");
			var adapter = null;
			if (adapterName) {
				if (typeof window[adapterName] === "function") {
					adapter = window[adapterName];
				}
				else {
					code.textContent = "JSONP adapter function '" + adapterName + "' doesn't exist";
					return;
				}
			}

			var cb = "prismjsonp" + jsonpcb++;

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

			var timeout = setTimeout(function () {
				// we could clean up window[cb], but if the request finally succeeds, keeping it around is a good thing
				if (code.textContent === loadMsg) {
					code.textContent = "Timeout loading '" + src + "'";
				}
			}, 5000);

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

			window[cb] = function (rsp) {
				document.head.removeChild(script);
				clearTimeout(timeout);
				delete window[cb];

				var data = "";

				if (adapter) {
					data = adapter(rsp, pre);
				}
				else {
					for (var p in adapters) {
						data = adapters[p].adapter(rsp, pre);
						if (data !== null) {
							break;
						}
					}
				}

				if (data === null) {
					code.textContent = "Cannot parse response (perhaps you need an adapter function?)";
				}
				else {
					code.textContent = data;
					Prism.highlightElement(code);
				}
			};

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

	Prism.plugins.jsonphighlight = {
		registerAdapter: registerAdapter,
		removeAdapter: removeAdapter,
		highlight: highlight
	};

	highlight();
})();
