/* !
 * Polyfill from Google's Closure Library.
 * Copyright 2013 The Closure Library Authors. All Rights Reserved.
 */

'use strict';

import {isServerSide} from '../coreNamed';

const async = {};

/**
 * Throw an item without interrupting the current execution context.  For
 * example, if processing a group of items in a loop, sometimes it is useful
 * to report an error while still allowing the rest of the batch to be
 * processed.
 * @param {*} exception
 */
async.throwException = function(exception) {
	// Each throw needs to be in its own context.
	async.nextTick(function() {
		throw exception;
	});
};

/**
 * Fires the provided callback just before the current callstack unwinds, or as
 * soon as possible after the current JS execution context.
 * @param {function(this:THIS)} callback
 * @param {THIS=} context Object to use as the "this value" when calling
 *     the provided function.
 * @template THIS
 */
async.run = function(callback, context) {
	if (!async.run.workQueueScheduled_) {
		// Nothing is currently scheduled, schedule it now.
		async.nextTick(async.run.processWorkQueue);
		async.run.workQueueScheduled_ = true;
	}

	async.run.workQueue_.push(new async.run.WorkItem_(callback, context));
};

/** @private {boolean} */
async.run.workQueueScheduled_ = false;

/** @private {!Array.<!async.run.WorkItem_>} */
async.run.workQueue_ = [];

/**
 * Run any pending async.run work items. This function is not intended
 * for general use, but for use by entry point handlers to run items ahead of
 * async.nextTick.
 */
async.run.processWorkQueue = function() {
	// NOTE: additional work queue items may be pushed while processing.
	while (async.run.workQueue_.length) {
		// Don't let the work queue grow indefinitely.
		const workItems = async.run.workQueue_;
		async.run.workQueue_ = [];
		for (let i = 0; i < workItems.length; i++) {
			const workItem = workItems[i];
			try {
				workItem.fn.call(workItem.scope);
			} catch (e) {
				async.throwException(e);
			}
		}
	}

	// There are no more work items, reset the work queue.
	async.run.workQueueScheduled_ = false;
};

/**
 * @constructor
 * @final
 * @struct
 * @private
 *
 * @param {function()} fn
 * @param {Object|null|undefined} scope
 */
async.run.WorkItem_ = function(fn, scope) {
	/** @const */
	this.fn = fn;
	/** @const */
	this.scope = scope;
};

/**
 * Fires the provided callbacks as soon as possible after the current JS
 * execution context. setTimeout(…, 0) always takes at least 5ms for legacy
 * reasons.
 * @param {function(this:SCOPE)} callback Callback function to fire as soon as
 *     possible.
 * @param {SCOPE=} context Object in whose scope to call the listener.
 * @template SCOPE
 */
async.nextTick = function(callback, context) {
	let cb = callback;
	if (context) {
		cb = callback.bind(context);
	}
	cb = async.nextTick.wrapCallback_(cb);
	// Look for and cache the custom fallback version of setImmediate.
	if (!async.nextTick.setImmediate_) {
		if (
			typeof setImmediate === 'function' &&
			isServerSide({checkEnv: false})
		) {
			async.nextTick.setImmediate_ = setImmediate;
		} else {
			// eslint-disable-next-line
			async.nextTick.setImmediate_ = async.nextTick.getSetImmediateEmulator_();
		}
	}
	async.nextTick.setImmediate_(cb);
};

/**
 * Cache for the setImmediate implementation.
 * @type {function(function())}
 * @private
 */
async.nextTick.setImmediate_ = null;

/**
 * Determines the best possible implementation to run a function as soon as
 * the JS event loop is idle.
 * @return {function(function())} The "setImmediate" implementation.
 * @private
 */
async.nextTick.getSetImmediateEmulator_ = function() {
	// Create a private message channel and use it to postMessage empty messages
	// to ourselves.
	let Channel;

	// Verify if variable is defined on the current runtime (i.e., node, browser).
	// Can't use typeof enclosed in a function (such as core.isFunction) or an
	// exception will be thrown when the function is called on an environment
	// where the variable is undefined.
	if (typeof MessageChannel === 'function') {
		Channel = MessageChannel;
	}

	// If MessageChannel is not available and we are in a browser, implement
	// an iframe based polyfill in browsers that have postMessage and
	// document.addEventListener. The latter excludes IE8 because it has a
	// synchronous postMessage implementation.
	if (
		typeof Channel === 'undefined' &&
		typeof window !== 'undefined' &&
		window.postMessage &&
		window.addEventListener
	) {
		/** @constructor */
		Channel = function() {
			// Make an empty, invisible iframe.
			const iframe = document.createElement('iframe');
			iframe.style.display = 'none';
			iframe.src = '';
			iframe.title = '';
			document.documentElement.appendChild(iframe);
			const win = iframe.contentWindow;
			const doc = win.document;
			doc.open();
			doc.write('');
			doc.close();
			const message = 'callImmediate' + Math.random();
			const origin = win.location.protocol + '//' + win.location.host;
			const onmessage = function(e) {
				// Validate origin and message to make sure that this message was
				// intended for us.
				if (e.origin !== origin && e.data !== message) {
					return;
				}
				this.port1.onmessage();
			}.bind(this);
			win.addEventListener('message', onmessage, false);
			this.port1 = {};
			this.port2 = {
				postMessage: function() {
					win.postMessage(message, origin);
				},
			};
		};
	}
	if (typeof Channel !== 'undefined') {
		const channel = new Channel();
		// Use a fifo linked list to call callbacks in the right order.
		let head = {};
		let tail = head;
		channel.port1.onmessage = function() {
			head = head.next;
			const cb = head.cb;
			head.cb = null;
			cb();
		};
		return function(cb) {
			tail.next = {
				cb: cb,
			};
			tail = tail.next;
			channel.port2.postMessage(0);
		};
	}
	// Implementation for IE6-8: Script elements fire an asynchronous
	// onreadystatechange event when inserted into the DOM.
	if (
		typeof document !== 'undefined' &&
		'onreadystatechange' in document.createElement('script')
	) {
		return function(cb) {
			let script = document.createElement('script');
			script.onreadystatechange = function() {
				// Clean up and call the callback.
				script.onreadystatechange = null;
				script.parentNode.removeChild(script);
				script = null;
				cb();
				cb = null;
			};
			document.documentElement.appendChild(script);
		};
	}
	// Fall back to setTimeout with 0. In browsers this creates a delay of 5ms
	// or more.
	return function(cb) {
		setTimeout(cb, 0);
	};
};

/**
 * Helper function that is overrided to protect callbacks with entry point
 * monitor if the application monitors entry points.
 * @param {function()} callback Callback function to fire as soon as possible.
 * @return {function()} The wrapped callback.
 * @private
 */
async.nextTick.wrapCallback_ = function(callback) {
	return callback;
};

export default async;
