/** @license MIT License (c) copyright 2012-2013 original author or authors */

/**
 * poll.js
 *
 * Helper that polls until cancelled or for a condition to become true.
 *
 * @author Scott Andrews
 */

(function (define) { 'use strict';
define(function(require) {

	var when = require('./when');
	var attempt = when['try'];
	var cancelable = require('./cancelable');

	/**
	 * Periodically execute the work function on the msec delay. The result of
	 * the work may be verified by watching for a condition to become true. The
	 * returned deferred is cancellable if the polling needs to be cancelled
	 * externally before reaching a resolved state.
	 *
	 * The next vote is scheduled after the results of the current vote are
	 * verified and rejected.
	 *
	 * Polling may be terminated by the verifier returning a truthy value,
	 * invoking cancel() on the returned promise, or the work function returning
	 * a rejected promise.
	 *
	 * Usage:
	 *
	 * var count = 0;
	 * function doSomething() { return count++ }
	 *
	 * // poll until cancelled
	 * var p = poll(doSomething, 1000);
	 * ...
	 * p.cancel();
	 *
	 * // poll until condition is met
	 * poll(doSomething, 1000, function(result) { return result > 10 })
	 *     .then(function(result) { assert result == 10 });
	 *
	 * // delay first vote
	 * poll(doSomething, 1000, anyFunc, true);
	 *
	 * @param work {Function} function that is executed after every timeout
	 * @param interval {number|Function} timeout in milliseconds
	 * @param [verifier] {Function} function to evaluate the result of the vote.
	 *     May return a {Promise} or a {Boolean}. Rejecting the promise or a
	 *     falsey value will schedule the next vote.
	 * @param [delayInitialWork] {boolean} if truthy, the first vote is scheduled
	 *     instead of immediate
	 *
	 * @returns {Promise}
	 */
	return function poll(work, interval, verifier, delayInitialWork) {
		var deferred, canceled, reject;

		canceled = false;
		deferred = cancelable(when.defer(), function () { canceled = true; });
		reject = deferred.reject;

		verifier = verifier || function () { return false; };

		if (typeof interval !== 'function') {
			interval = (function (interval) {
				return function () { return when().delay(interval); };
			})(interval);
		}

		function certify(result) {
			deferred.resolve(result);
		}

		function schedule(result) {
			attempt(interval).then(vote, reject);
			if (result !== void 0) {
				deferred.notify(result);
			}
		}

		function vote() {
			if (canceled) { return; }
			when(work(),
				function (result) {
					when(verifier(result),
						function (verification) {
							return verification ? certify(result) : schedule(result);
						},
						function () { schedule(result); }
					);
				},
				reject
			);
		}

		if (delayInitialWork) {
			schedule();
		} else {
			// if work() is blocking, vote will also block
			vote();
		}

		// make the promise cancelable
		deferred.promise = Object.create(deferred.promise);
		deferred.promise.cancel = deferred.cancel;

		return deferred.promise;
	};

});
})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); });
