(function () {

	if (typeof Prism === 'undefined' || typeof document === 'undefined') {
		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') {
			// eslint-disable-next-line no-redeclare
			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) {
		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) {
		if (rsp && rsp.node && typeof (rsp.data) === 'string') {
			return rsp.data;
		}
		return null;
	}, 'bitbucket');


	var jsonpCallbackCounter = 0;
	/**
	 * Makes a JSONP request.
	 *
	 * @param {string} src The URL of the resource to request.
	 * @param {string | undefined | null} callbackParameter The name of the callback parameter. If falsy, `"callback"`
	 * will be used.
	 * @param {(data: unknown) => void} onSuccess
	 * @param {(reason: "timeout" | "network") => void} onError
	 * @returns {void}
	 */
	function jsonp(src, callbackParameter, onSuccess, onError) {
		var callbackName = 'prismjsonp' + jsonpCallbackCounter++;

		var uri = document.createElement('a');
		uri.href = src;
		uri.href += (uri.search ? '&' : '?') + (callbackParameter || 'callback') + '=' + callbackName;

		var script = document.createElement('script');
		script.src = uri.href;
		script.onerror = function () {
			cleanup();
			onError('network');
		};

		var timeoutId = setTimeout(function () {
			cleanup();
			onError('timeout');
		}, Prism.plugins.jsonphighlight.timeout);

		function cleanup() {
			clearTimeout(timeoutId);
			document.head.removeChild(script);
			delete window[callbackName];
		}

		// the JSONP callback function
		window[callbackName] = function (response) {
			cleanup();
			onSuccess(response);
		};

		document.head.appendChild(script);
	}

	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 src = pre.getAttribute('data-jsonp');

			jsonp(
				src,
				pre.getAttribute('data-callback'),
				function (response) {
					// 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);
					}
				},
				function () {
					// mark as failed
					pre.setAttribute(STATUS_ATTR, STATUS_FAILED);

					code.textContent = TIMEOUT_MESSAGE(src);
				}
			);
		}
	});


	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);
			}
		}
	};

}());
