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