1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * Enqueue a method into the given chainback objects's call chain. If a
  7 	 * mutex is locking this chain, then place the call in the queued calls
  8 	 * instead.
  9 	 *
 10 	 * @private
 11 	 * @param {Chainback} chainback The object whose queue we want to push
 12 	 *                                  the given method to.
 13 	 * @param {function} method The method to chain.
 14 	 */
 15 	function addCallToQueue(chainback, method) {
 16 		if (!chainback.__gcnmutex__) {
 17 			chainback.__gcncallqueue__.push(method);
 18 		} else {
 19 			chainback.__gcncallchain__.push(method);
 20 			if (0 === chainback.__gcnajaxcount__
 21 					&& 1 === chainback.__gcncallchain__.length) {
 22 				method.call(chainback);
 23 			}
 24 		}
 25 	}
 26 
 27 	/**
 28 	 * Dequeue the function at the top of the given chainback's call chain and
 29 	 * invoke the next function in the queue.
 30 	 *
 31 	 * @private
 32 	 * @param {Chainback} chainback
 33 	 */
 34 	function callNext(chainback) {
 35 		// Waiting for an ajax call to complete.  Go away, and try again when
 36 		// another call completes.
 37 		if (chainback.__gcnajaxcount__ > 0) {
 38 			return;
 39 		}
 40 
 41 		if (0 === chainback.__gcncallchain__.length
 42 				&& 0 === chainback.__gcncallqueue__.length) {
 43 			return; // We should never reach here. Just so you know...
 44 		}
 45 
 46 		// Discard the empty shell...
 47 		chainback.__gcncallchain__.shift();
 48 
 49 		// Load and fire the next bullet...
 50 		if (chainback.__gcncallchain__.length) {
 51 			chainback.__gcncallchain__[0].call(chainback);
 52 		} else if (chainback.__gcncallqueue__.length) {
 53 			chainback.__gcncallqueue__.shift().call(chainback);
 54 		}
 55 	}
 56 
 57 	/**
 58 	 * Wraps the given method in a closure that provides scaffolding to chain
 59 	 * invocations of the method correctly.
 60 	 *
 61 	 * @private
 62 	 * @param {function} method The original function we want to wrap.
 63 	 * @param {string} name The method name as it was defined in its object.
 64 	 * @return {function} A function that wraps the original function.
 65 	 */
 66 	function makeMethodChainable(method, name) {
 67 		return function () {
 68 			var args = arguments;
 69 			var that = this;
 70 			var func = function () {
 71 				method.apply(that, args);
 72 				callNext(that);
 73 			};
 74 			func.__gcncallname__ = name; // For debugging
 75 			addCallToQueue(this, func);
 76 			return this;
 77 		};
 78 	}
 79 
 80 	/**
 81 	 * The Chainback constructor.
 82 	 *
 83 	 * Surfaces the chainback constructor in such a way as to be able to use
 84 	 * call apply() on it.
 85 	 *
 86 	 * http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
 87 	 *
 88 	 * @private
 89 	 * @param {Chainback} ctor The Chainback class we wish to initialize.
 90 	 * @param {Array} args
 91 	 * @param {object} continuation
 92 	 * @return {Chainback}
 93 	 */
 94 	var Chainback = (function () {
 95 		var Chainback = function (ctor, args) {
 96 			return ctor.apply(this, args);
 97 		};
 98 		return function (ctor, args, continuation) {
 99 			Chainback.prototype = ctor.prototype;
100 			return new Chainback(ctor, args);
101 		};
102 	}());
103 
104 	/**
105 	 * Get a chainback instance's current channel.
106 	 *
107 	 * @param {Chainback} chainback
108 	 * @return {number} The channel id or 0 if no channel is set.
109 	 */
110 	function getChannel(chainback) {
111 		return chainback._channel || 0;
112 	}
113 
114 	/**
115 	 * Get an instance of the given chainback class from its constructor's
116 	 * cache.  If the object for this hash does not exist in the cache, then
117 	 * instantiate a new object, place into the cache using the hash as the
118 	 * cache key.
119 	 *
120 	 * If no hash is passed to this function, then the Chainback instance that
121 	 * is returned will not be fully realized.  It will be a "fetus" instance
122 	 * that has yet to be bound to an id. Once it receives an id it will keep
123 	 * it for the remainder of its life.  These "fetus" instances are not be
124 	 * placed in the cache until they have aquired an id.
125 	 *
126 	 * @ignore
127 	 * @private
128 	 * @param {Chainback} ctor A chainback constructor.
129 	 * @param {string} hash A hash string that represents this chainback.
130 	 * @param {Chainback} callee The Chainback instance from which this
131 	 *                           invocation originated.
132 	 * @param {Array.<*>} args Arguments that will be applied to the chainback
133 	 *                         when (re-) initializing it.  This array should
134 	 *                         contain the following elements in the following
135 	 *                         order:
136 	 *                              id : string|array
137 	 *                         success : function|null
138 	 *                           error : function|null
139 	 *                         setting : object
140 	 * @return {Chainback}
141 	 */
142 	GCN.getChainback = function (ctor, hash, callee, args) {
143 		var chainback = hash && ctor.__gcncache__[hash];
144 		if (chainback) {
145 			// Reset the cached instance and re-initialize it.
146 			chainback._chain = callee;
147 			return chainback._init.apply(chainback, args);
148 		}
149 		args.push(callee);
150 		var isFetus = !hash;
151 		var hasCallee = !!callee;
152 		if (isFetus && hasCallee) {
153 			// TODO: Creating a hash form just the ctor argument is
154 			//       insufficient.  We must also consider
155 			//       ctor._needsChainedHash.  For example, createTag() will
156 			//       cause a new chanback to be created which will have a hash
157 			//       value of 'TagAPI:id' but it should be
158 			//       'Page::id::TagAPI:id'.
159 			hash = ctor._makeHash(getChannel(callee));
160 			chainback = callee.__gcntempcache__[hash];
161 			if (!chainback) {
162 				chainback =
163 					callee.__gcntempcache__[hash] =
164 						new Chainback(ctor, args);
165 			}
166 			return chainback;
167 		}
168 		return new Chainback(ctor, args);
169 	};
170 
171 	/**
172 	 * Create a class which allows for chainable callback methods.
173 	 *
174 	 * @ignore
175 	 * @private
176 	 * @param {object<string, *>} props Definition of the class to be created.
177 	 *                                  All function are wrapped to allow them
178 	 *                                  to be as chainable callbacks unless
179 	 *                                  their name is prefixed with a "!" .
180 	 * @return {Chainback}
181 	 */
182 	GCN.defineChainback = function (props) {
183 
184 		/**
185 		 * @TODO: use named arguments
186 		 *
187 		 * @constructor
188 		 * @param {number|string} id
189 		 * @param {?function(Chainback)} success
190 		 * @param {?function(GCNError):boolean} error
191 		 * @param {?object} chainlink
192 		 */
193 		var chainback = function () {
194 			var args = Array.prototype.slice.call(arguments);
195 
196 			this._chain = args.pop();
197 			this._channel = this._chain ? this._chain._channel : GCN.channel();
198 
199 			// Please note: We prefix and suffix these values with a double
200 			// underscore because they are not to be relied on whatsoever
201 			// outside of this file!  Although they need to be carried around
202 			// on chainback instances, they are nevertheless soley for internal
203 			// wiring.
204 			this.__gcnmutex__     = true;
205 			this.__gcncallchain__ = [];
206 			this.__gcncallqueue__ = [];
207 			this.__gcntempcache__ = {};
208 
209 			// This is used to synchronize ajax calls with non-ajax calls in a
210 			// chainback call queue.
211 			//
212 			// It serves as a type of countdown latch, or reverse counting
213 			// semaphore (msdn.microsoft.com/en-us/magazine/cc163427.aspx).
214 			//
215 			// Upon each invocation of `_queueAjax()', on a chainback object,
216 			// its `__gcnajaxcount__' counter will be incremented.  And each
217 			// time a queued ajax call completes (successfully or otherwise),
218 			// the counter is decremented.  Before any chainback object can
219 			// move on to the next call in its call queue, it will check the
220 			// value of this counter to determine whether it is permitted to do
221 			// so.  If the counter's value is 0, access is granted;  otherwise
222 			// the requesting chainback will wait until a pending ajax call
223 			// completes to trigger a retry.
224 			this.__gcnajaxcount__ = 0;
225 
226 			var obj = args[0];
227 			var ids;
228 
229 			switch (jQuery.type(obj)) {
230 			case 'null':
231 			case 'undefined':
232 				break;
233 			case 'object':
234 				if (typeof obj.id !== 'undefined') {
235 					ids = [obj.id];
236 				}
237 				break;
238 			case 'array':
239 				ids = obj;
240 				break;
241 			default:
242 				ids = [obj];
243 			}
244 
245 			// If one or more id is provided in the instantialization of this
246 			// object, only then will this instance be added to its class'
247 			// cache.
248 			if (ids) {
249 				this._setHash(ids.sort().join(','));
250 				this._addToCache();
251 			}
252 
253 			this._init.apply(this, args);
254 		};
255 
256 		/**
257 		 * Causes the chainback call queue to start running again once a lock
258 		 * has been released.
259 		 *
260 		 * @private
261 		 */
262 		props.__release__ = function () {};
263 
264 		// inheritance
265 		if (props._extends) {
266 			var inheritance = (jQuery.type(props._extends) === 'array')
267 			                ? props._extends
268 			                : [props._extends];
269 			var i;
270 			for (i = 0; i < inheritance.length; ++i) {
271 				jQuery.extend(chainback.prototype, inheritance[i].prototype);
272 			}
273 			delete props._extends;
274 		}
275 
276 		// static fields and methods
277 		jQuery.extend(chainback, {
278 
279 			/**
280 			 * @private
281 			 * @static
282 			 * @type {object<string, Chainback>} An associative array holding
283 			 *                                   instances of this class.  Each
284 			 *                                   instance is mapped against a
285 			 *                                   hash key generated through
286 			 *                                   `_makehash()'.
287 			 */
288 			__gcncache__: {},
289 
290 			/**
291 			 * @private
292 			 * @static
293 			 * @type {string} A string that represents this chainback's type.
294 			 *                It is used in generating hashs for instances of
295 			 *                this class.
296 			 */
297 			__chainbacktype__: props.__chainbacktype__ ||
298 				Math.random().toString(32),
299 
300 			/**
301 			 * @private
302 			 * @static
303 			 * @type {boolean} Whether or not we need to use the hash of this
304 			 *                 object's parent chainback object in order to
305 			 *                 generate a unique hash key when instantiating
306 			 *                 objects for this class.
307 			 */
308 			_needsChainedHash: false,
309 
310 			/**
311 			 * Given the arguments "one", "two", "three", will return something
312 			 * like: "one::two::ChainbackType:three".
313 			 *
314 			 * @private
315 			 * @static
316 			 * @param {...string} One or more strings to concatenate into the
317 			 *                    hash.
318 			 * @return {string} The hash string.
319 			 */
320 			_makeHash: function () {
321 				var ids = Array.prototype.slice.call(arguments);
322 				var id = ids.pop();
323 				ids.push(chainback.__chainbacktype__ + ':' + id);
324 				return ids.join('::');
325 			}
326 
327 		});
328 
329 		var DONT_MERGE = {
330 			__gcnmutex__     : true,
331 			__gcnorigin__    : true,
332 			__gcncallchain__ : true,
333 			__gcncallqueue__ : true,
334 			__gcnajaxcount__ : true,
335 			__gcntempcache__ : true
336 		};
337 
338 		// Prototype chainback methods and properties
339 
340 		jQuery.extend(chainback.prototype, {
341 
342 			/**
343 			 * @type {Chainback} Each object holds a reference to its
344 			 *                   constructor.
345 			 */
346 			_constructor: chainback,
347 
348 			/**
349 			 * Facilitates chaining from one chainback object to another.
350 			 *
351 			 * Uses "chainlink" objects to grow and internal linked list of
352 			 * chainback objects which make up a sort of callee chain.
353 			 *
354 			 * A link is created every time a context switch happens
355 			 * (ie: moving from one API to another).  Consider the following:
356 			 *
357 			 * page('1').tags().tag('content').render('#content');
358 			 *
359 			 * Accomplishing the above chain of execution will involve 3
360 			 * different chainable APIs, and 2 different API switches: a page
361 			 * API flows into a tags collection API, which in turn flows to a
362 			 * tag API.  This method is invoked each time that the exposed API
363 			 * mutates in this way.
364 			 *
365 			 * @private
366 			 * @param {Chainback} ctor The Chainback class we want to continue
367 			 *                         with.
368 			 * @param {number|string|Array.<number|string>|object} settings
369 			 *      If this argument is not defined, a random hash will be
370 			 *      generated as the object's hash.
371 			 *      An object can be provided instead of an id to directly
372 			 *      instantiate it from JSON data received from the server.
373 			 * @param {function} success
374 			 * @param {function} error
375 			 * @return {Chainback}
376 			 * @throws UNKNOWN_ARGUMENT If `settings' is not a number, string,
377 			 *                          array or object.
378 			 */
379 			_continue: function (ctor, settings, success, error) {
380 				// Is this a fully realized Chainback, or is it a Chainback
381 				// which has yet to determine which id it is bound to, from its
382 				// parent?
383 				var isFetus = false;
384 				var ids;
385 				var hashInputs = [];
386 
387 				switch (jQuery.type(settings)) {
388 				case 'undefined':
389 				case 'null':
390 					isFetus = true;
391 					break;
392 				case 'array':
393 					ids = settings.sort().join(',');
394 					break;
395 				case 'number':
396 				case 'string':
397 					ids = settings;
398 					break;
399 				case 'object':
400 					ids = settings.id;
401 					break;
402 				default:
403 					GCN.error('UNKNOWN_ARGUMENT',
404 						'Don\'t know what to do with the object ' + settings);
405 					return;
406 				}
407 
408 				var hash;
409 				if (isFetus) {
410 					hash = null;
411 				} else {
412 					var channel = getChannel(this);
413 					hash = ctor._needsChainedHash
414 					     ? ctor._makeHash(this.__gcnhash__, channel + '/' + ids)
415 					     : ctor._makeHash(channel + '/' + ids);
416 				}
417 
418 				var chainback = GCN.getChainback(ctor, hash, this,
419 					[settings, success, error, {}]);
420 
421 				return chainback;
422 			},
423 
424 			/**
425 			 * Works backward to read this object's ancestor before continuing
426 			 * with the callback.
427 			 *
428 			 * This method should be overridden.
429 			 *
430 			 * @private
431 			 * @param {function(Chainback)} success Callback.
432 			 * @param {function} error Custom error handler.
433 			 */
434 			/*
435 			_onContinue: function (success, error) {
436 				if (success) {
437 					success(this);
438 				}
439 			},
440 			*/
441 
442 			/**
443 			 * Terminates any further exection of the functions that remain in
444 			 * the call queue.
445 			 * TODO: Kill all ajax calls.
446 			 *
447 			 * @private
448 			 * @return {Array.<function>} A list of functions that we in this
449 			 *                            Chainback's call queue when an abort
450 			 *                            happend.
451 			 */
452 			_abort: function () {
453 				this._clearCache();
454 				var callchain = this.__gcncallchain__.concat(
455 						this.__gcncallqueue__
456 					);
457 				this.__gcnmutex__     = true;
458 				this.__gcncallchain__ = [];
459 				this.__gcncallqueue__ = [];
460 				return callchain;
461 			},
462 
463 			/**
464 			 * Gets the chainback from which this object was `_continue'd()
465 			 * from.
466 			 *
467 			 * @private
468 			 * @param {Chainback}
469 			 * @return {Chainback} This Chainback's ancestor.
470 			 */
471 			_ancestor: function () {
472 				return this._chain;
473 			},
474 
475 			/**
476 			 * Locks the semaphore.
477 			 *
478 			 * @private
479 			 * @return {Chainback} This Chainback.
480 			 */
481 			_procure: function () {
482 				this.__gcnmutex__ = false;
483 				return this;
484 			},
485 
486 			/**
487 			 * Unlocks the semaphore.
488 			 *
489 			 * @private
490 			 * @return {Chainback} This Chainback.
491 			 */
492 			_vacate: function () {
493 				this.__gcnmutex__ = true;
494 				this.__release__();
495 				return this;
496 			},
497 
498 			/**
499 			 * Halts and forks the main call chain of this chainback object.
500 			 * Creates a derivitive object that will be used to accomplish
501 			 * operations that need to complete before the main chain is
502 			 * permitted to proceed.  Before execution on the main chainback
503 			 * object is restarted, the forked derivitive object is merged into
504 			 * the original chainback instance.
505 			 *
506 			 * @private
507 			 * @return {Chainback} A derivitive Chainback object forked from
508 			 *                     this Chainback instance.
509 			 */
510 			_fork: function () {
511 				var that = this;
512 				this._procure();
513 				var Fork = function ChainbackFork() {
514 					var prop;
515 					for (prop in that) {
516 						if (that.hasOwnProperty(prop) && !DONT_MERGE[prop]) {
517 							this[prop] = that[prop];
518 						}
519 					}
520 					this.__gcnorigin__    = that;
521 					this.__gcnmutex__     = true;
522 					this.__gcncallchain__ = [];
523 					this.__gcncallqueue__ = [];
524 				};
525 				Fork.prototype = new this._constructor();
526 				return new Fork();
527 			},
528 
529 			/**
530 			 * Transfers the state of this derivitive into its origin.
531 			 *
532 			 * @private
533 			 */
534 			_merge: function () {
535 				if (!this.__gcnorigin__) {
536 					return;
537 				}
538 				var origin = this.__gcnorigin__;
539 				var prop;
540 				for (prop in this) {
541 					if (this.hasOwnProperty(prop) && !DONT_MERGE[prop]) {
542 						origin[prop] = this[prop];
543 					}
544 				}
545 				origin._vacate();
546 				return origin;
547 			},
548 
549 			/**
550 			 * Wraps jQuery's `ajax' method.  Queues the callbacks in the chain
551 			 * call so that they can be invoked synchonously.  Without blocking
552 			 * the browser thread.
553 			 *
554 			 * @private
555 			 * @param {object} settings
556 			 */
557 			_queueAjax: function (settings) {
558 				if (settings.json) {
559 					settings.data = JSON.stringify(settings.json);
560 					delete settings.json;
561 				}
562 
563 				settings.dataType = 'json';
564 				settings.contentType = 'application/json; charset=utf-8';
565 				settings.error = (function (onError) {
566 					return function (xhr, status, error) {
567 						var throwException = true;
568 						if (onError) {
569 							throwException = onError(GCN.createError('HTTP_ERROR', error, xhr));
570 						}
571 						if (throwException !== false) {
572 							GCN.error('AJAX_ERROR', error, xhr);
573 						}
574 					};
575 				}(settings.error));
576 
577 				// Duck-type the complete callback, or add one if not provided.
578 				// We use complete to forward the continuation because it is
579 				// the last callback to be executed the jQuery ajax callback
580 				// sequence.
581 				settings.complete = (function (chainback, onComplete, opts) {
582 					return function () {
583 						--chainback.__gcnajaxcount__;
584 						onComplete.apply(chainback, arguments);
585 						callNext(chainback);
586 					};
587 				}(this, settings.complete || function () {}, settings));
588 
589 				++this.__gcnajaxcount__;
590 
591 				GCN.ajax(settings);
592 			},
593 
594 			/**
595 			 * Clears the cache for this individual object.
596 			 *
597 			 * @private
598 			 * @return {Chainback} This Chainback.
599 			 */
600 			_clearCache: function () {
601 				if (chainback.__gcncache__[this.__gcnhash__]) {
602 					delete chainback.__gcncache__[this.__gcnhash__];
603 				}
604 				return this;
605 			},
606 
607 			/**
608 			 * Add this object to the cache, using its hash as the key.
609 			 *
610 			 * @private
611 			 * @return {Chainback} This Chainback.
612 			 */
613 			_addToCache: function () {
614 				this._constructor.__gcncache__[this.__gcnhash__] = this;
615 				return this;
616 			},
617 
618 			/**
619 			 * Removes the given chainback instance from the temporary cache,
620 			 * usually after the chainback instance has matured from a "fetus"
621 			 * into a fully realized chainback object.
622 			 *
623 			 * @param {Chainback} instance The chainback instance to remove.
624 			 * @return {boolean} True if this chainback instance was found and
625 			 *                   removed, false if it could not be found.
626 			 */
627 			_removeFromTempCache: function (instance) {
628 				var hash;
629 				var cache = this.__gcntempcache__;
630 				for (hash in cache) {
631 					if (cache.hasOwnProperty(hash) && instance === cache[hash]) {
632 						delete cache[hash];
633 						return true;
634 					}
635 				}
636 				return false;
637 			},
638 
639 			/**
640 			 * @private
641 			 * @param {string|number} str
642 			 * @return {Chainback} This Chainback.
643 			 */
644 			_setHash: function (str) {
645 				var ctor = this._constructor;
646 				var addAncestorHash = ctor._needsChainedHash && this._chain;
647 				var channel = getChannel(this);
648 				var hash = addAncestorHash
649 				         ? ctor._makeHash(this._chain.__gcnhash__, channel + '/' + str)
650 				         : ctor._makeHash(channel + '/' + str);
651 				this.__gcnhash__ = hash;
652 				return this;
653 			},
654 
655 			/**
656 			 * Invokes the given callback function while ensuring that any
657 			 * exceptions that occur during the invocation of the callback,
658 			 * will be caught and allow the Chainback object to complete its
659 			 * all remaining queued calls.
660 			 *
661 			 * @param {function} callback The function to invoke.
662 			 * @param {Array.<*>=} args A list of object that will be passed as
663 			 *                          arguments into the callback function.
664 			 */
665 			_invoke: function (callback, args) {
666 				if (typeof callback !== 'function') {
667 					return;
668 				}
669 				try {
670 					if (args && args.length) {
671 						callback.apply(null, args);
672 					} else {
673 						callback();
674 					}
675 				} catch (ex) {
676 					setTimeout(function () {
677 						throw ex;
678 					}, 1);
679 				}
680 			}
681 
682 		});
683 
684 		/**
685 		 * Causes all promises that are made by this Chainback object, as well
686 		 * as all derived promises, to be resolved before the given success
687 		 * callback is invoked to receive this object in a "fulfilled" state.
688 		 * If a problem is encountered at any point during the resolution of a
689 		 * promise in the resolution chain, the error function is called, and
690 		 * any further resolution is aborted.
691 		 *
692 		 * @private
693 		 * @param {function(Chainback)} success Callback function to be invoked
694 		 *                                      when all the promises on which
695 		 *                                      this object depends have been
696 		 *                                      completed.
697 		 * @param {function(GCNError):boolean=} error Optional custom error
698 		 *                                      handler.
699 		 */
700 		props._fulfill = function (success, error) {
701 			if (this._chain) {
702 				this._chain._fulfill(success, error);
703 			} else {
704 				success(this);
705 			}
706 		};
707 
708 		var propName;
709 		var propValue;
710 
711 		// Generates the chainable callback methods.  Transforms all functions
712 		// whose names do not start with the "!" character into chainable
713 		// callback prototype methods.
714 
715 		for (propName in props) {
716 			if (props.hasOwnProperty(propName)) {
717 				propValue = props[propName];
718 				if (jQuery.type(propValue) === 'function' &&
719 						propName.charAt(0) !== '!') {
720 					chainback.prototype[propName] = makeMethodChainable(
721 						propValue,
722 						propName
723 					);
724 				} else {
725 					if (propName.charAt(0) === '!') {
726 						propName = propName.substring(1, propName.length);
727 					}
728 					chainback.prototype[propName] = propValue;
729 				}
730 			}
731 		}
732 
733 		return chainback;
734 	};
735 
736 }(GCN));
737