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