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