// AsyncQueue -- a clean way of breaking up expensive operations to avoid
// the dreaded "stop this script" dialog.
// Copyright 2006 Tom W.M.

// Dependencies: Dean Edwards' Base class
// Optionally: MochiKit.Async.Deferred

// Usage:
/*
	(new AsyncQueue(this)).next(function(){
		// do stuff
	}).next(function(){
		// do more stuff
		// Note that the this in this function is the same
		// as that which was passed to the AsyncQueue constructor.
	}).loop(function(){
		// stuff to repeat
		if (stuff_is_done) {
			// leave the loop
			throw AsyncQueue.StopLoop;
		}
	}).loopResult(this.get_function_to_repeat)
	.defer(this.deferred_to_wait_for)
	.defer(function func_returning_a_deferred(){
		return new MochiKit.Async.Deferred();
	}).go(); // get moving!
	// If MochiKit.Async is loaded, go() returns a deferred.
	
	// If you don't like the chained method, more 
	// normal usage works just fine.
	var a = new AsyncQueue(this);
	a.next(function() {
		// do stuff
	});
	var deferred = a.go();
*/

AsyncQueue = Base.extend({
	constructor: function(this_) {
		this.this_ = this_;
		this.queue = [];
		if (window.MochiKit && window.MochiKit.Async) {
			this.deferred = new MochiKit.Async.Deferred();
		} else {
			this.deferred = null;
		}
	},
	// Calls f().
	next: function(f) {
		this.queue.push(this._next(f));
		return this;
	},
	// Calls f() until it throws AsyncQueue.StopLoop.
	loop: function(f) {
		// Pass on to loopResult();
		return this.loopResult(function(){
			return f;
		});
	},
	// Calls *the result of* f() until it throws AsyncQueue.StopLoop.
	loopResult: function(f) {
		this.queue.push(this._loop(f));
		return this;
	},
	// Waits for a Deferred to callback.
	// defer(f) -> calls f(), waits on the Deferred it returns.
	// defer(d) -> waits on Deferred d.
	defer: function(o) {
		this.queue.push(this._defer(o));
		return this;
	},
	// Start executing functions.  If MochiKit.Async is loaded, return
	// a Deferred that will succeed when all have completed.
	go: function() {
		this._advance();
		return this.deferred;
	},
	_next: function(f) {
		var self = this;
		return function() {
			f.apply(self.this_, []);
			self._advance();
		};
	},
	_loop: function(f) {
		var self = this;
		return function() {
			var t = self.this_;
			// Get the result of f.  It's return value is a function,
			// which is what needs to be repeated.
			var res = f.apply(t);
			var start = (new Date).getTime();
			function run() {
				try {
					while (true) {
						self._multi_call(res);
						var now = (new Date).getTime();
						if (now - start > self.MAX_BLOCK) {
							window.setTimeout(run, self.BREAK_DELAY);
							start = now;
							return;
						}
					}
				} catch(e) {
					if (e !== AsyncQueue.StopLoop) { throw e; }
					self._advance();
				}
			};
			run();
		};
	},
	_multi_call: function(f) {
		var t = this.this_;
		f.apply(t); f.apply(t); f.apply(t);
		f.apply(t); f.apply(t); f.apply(t);
		f.apply(t); f.apply(t); f.apply(t);
		f.apply(t); f.apply(t); f.apply(t);
	},
	_defer: function(o) {
		var self = this;
		var a = function() {
			self._advance();
		};
		
		if (o instanceof MochiKit.Async.Deferred) { // Deferreds
			return function() {
				o.addCallback(a);
			};
		} else { // It's a Deferred factory
			return function() {
				o.apply(self.this_).addCallback(a);
			};
		}
	},
	// Calls the next stage, if there is one, or succeeds the deferred.
	_advance: function() {
		//log('_advance() called.  queue.length = ' + this.queue.length);
		var q = this.queue;
		if (q.length) {
			window.setTimeout(q.shift(), this.BREAK_DELAY);
		} else {
			//log("Queue completed.");
			if (this.deferred)
				this.deferred.callback(this.this_);
		}
	},
	toString: function() {
		return "[AsyncQueue instance]";
	},
	MAX_BLOCK: 3000, // ms
	BREAK_DELAY: 1
}, {
	StopLoop: {
		toString: function() {
			return "[AsyncQueue.StopLoop]";
		}
	},
	stop_loop: function() {
		throw AsyncQueue.StopLoop;
	}
});
