;(function($, window, undefined) {
'use strict';

O_O.lib.ModeStack = function($elem) {
	this.$elem = $elem;

	this.$win = $(window);
};

O_O.lib.ModeStack.prototype = {
	/* constants */

	CSS_ID_PREFIX: 'mode-',

	CSS_CLASS_MODE: 'mode',
	CSS_CLASS_STACK: 'stack',
	CSS_CLASS_TOP: 'stack-top',

	PREFIX_DELAY: Modernizr.prefixed('transitionDelay'),
	PREFIX_DURATION: Modernizr.prefixed('transitionDuration'),

	DEFAULT_FADE: 500, // overridden by fade arg (if present)

	/* O_O */

	_delete: function() {
		// TODO
	},

	/* constructor */

	$elem: null,
	$win: null,

	/* state */

	current: null,
	last: null,
	fadeCount: null,

	/* utils */

	reset: function() {
		this.current = null;
		this.last = null;
		this.fadeCount = null;

		this.$elem.empty();
	},

	_getDiv: function(modeName) {
		return $('#' + this.CSS_ID_PREFIX + modeName);
	},

	getMode: function(modeName) {
		return this._getDiv(modeName); // TODO: collapse into one function?
	},

	getCurrent: function() {
		return this.current;
	},

	getLast: function() {
		return this.last;
	},

	modeExists: function(modeName) {
		return this._getDiv(modeName).length > 0;
	},

	/* regular modes */

	addMode: function(modeName) {
		if (this.modeExists(modeName)) {
			throw new Error('Mode ' + modeName + ' already exists in stack');
		}

		var $modeElem = $('<div>')
			.attr('class', this.CSS_CLASS_MODE)
			.attr('id', this.CSS_ID_PREFIX + modeName)
			.appendTo(this.$elem);

		// automatically switch to first mode
		if (!this.current) {
			this.current = modeName;
			$modeElem.attr('class', this.CSS_CLASS_TOP);
		}

		return $modeElem;
	},

	switchMode: function(modeName, delay, fade) {
		var $dfd = $.Deferred();

		if (this.current === modeName) return $dfd.resolve().promise();

		var $current = this._getDiv(this.current);
		var $next = this._getDiv(modeName);
		if (!$next) throw new Error('Could not get div for mode ' + modeName);

		var switchCb = scopeC(function() {
			if (!this.fadeCount) this.fadeCount = 0;
			this.fadeCount++;

			// transition complete
			if (this.fadeCount === 2) {
				this.last = this.current;
				this.current = modeName;
				this.fadeCount = null;

				$current.addClass(this.CSS_CLASS_STACK)
					.removeAttr('style');
				$next.removeAttr('style')[0].scrollIntoView(true);

				$dfd.resolve();
			}
		}, this);

		var opts = {};
		opts[this.PREFIX_DURATION] = (typeof fade === 'undefined') ?
			this.DEFAULT_FADE : fade + 'ms'; // no explicit css default
		if (delay) opts[this.PREFIX_DELAY] = delay + 'ms'; // css default zero

		$current.oneC(O_O.browser.transEndEventName(), switchCb, this)
			.css(opts)
			.attr('class', this.CSS_CLASS_MODE);
		$next.oneC(O_O.browser.transEndEventName(), switchCb, this)
			.css(opts)
			.attr('class', this.CSS_CLASS_TOP);
		if (!Modernizr.prefixed('transition')) {
			this.fadeCount = 1;
			switchCb();
		}

		// HACK: no event if transition is none (so invoke manually)
		if (delay === 0 && fade === 0) {
			this.fadeCount = 1; // means next switchCb call will resolve
			switchCb();
		}

		return $dfd.promise();
	}
};

}(ps$, window));
;(function($, undefined) {
	"use strict";

	// Private classes, not meant for usage by anything other than Fetcher
	var ImgRequest = function(id, size, url, loadCB) {
		this.size = size || 500;
		this.url = url;
		this.loadCB = loadCB || null;
		this.dfr = $.Deferred();
		if (typeof id === "string") {
			this.$img = $('<img>').psImg({id: id});
		} else {
			// Was passed an already-created element
			// instead of an id string
			this.$img = id;
		}
	};
	ImgRequest.prototype = {
		$img: null,
		size: null,
		url: null,
		loadCB: null,
		dfr: null,

		fetch: function() {
			var dfr = this.dfr;
			this.$img.onC('load', function(evt) {
				dfr.resolve();
				
				if (typeof this.loadCB === 'function') {
					this.loadCB();
				}

				var $t = $(evt.target);
				$t.nativeSize = {
					width: $t.width,
					height: $t.height
				};
			}, this);
			this.$img.psImg('load', {width: 1000, height: this.size});
			return dfr.promise();
		}
	};
	
	O_O.lib.NewFetcher = function(index, images, opt) {
		this._images = images.slice(0);
		this._imgLen  = images.length;
		this.opt = opt;

		this._processIndex(index);
	};

	O_O.lib.NewFetcher.prototype = {
		PADDING: 5,
		CIRCLING: false,

		_imgLen: null,
		opt: null,

		reachedRightEnd: null,
		reachedLeftEnd: null,
		groupCounter: 1,

		_mkImg: function(id, indexIntoImages) {
			var $img = $('<img>');
			$img.attr({
				'data-index': indexIntoImages,
				'data-id': id
			}).psImg('init', {id: id});
			return $img;
		},

		_fixBounds: function(start, end) {
			var imgLen = this._imgLen;
			if (this.CIRCLING) {
				throw "Circling not implemented yet.";
			} else {
				if (start < 0) start = 0;
				if (end >= imgLen) end = imgLen-1;
			}
			
			return [start, end];
		},

		_getPaddedBoundsFrom: function(index) {
			// start and end are inclusive
			var start = index-this.PADDING, end = index+this.PADDING;
			return this._fixBounds(start, end);
		},

		_loadCell: function(i, group) {
			var imgs = this._images;
			var req;
			if (!is_array(imgs[i])) {
				var imgModel = imgs[i];
				var $imgEl = this._mkImg(imgModel.getId(), i);
				//var url = imgModel.getLinkForSize({width: 500, height: 500});
				req = O_O.mm.oNew(ImgRequest, $imgEl, this.opt.size);
				imgs[i] = [imgModel, $imgEl, req.dfr.promise()];
				group.addRequest(req);
			}
			return imgs[i][2];
		},

		_loadRange: function(start, end) {
			var group = O_O.mm.oNew(O_O.lib.RequestGroup, this.opt.priority, null, this.opt.C_ID);
			var dfrs = [];
			for (var i=start; i<=end; i++) {
				dfrs.push(this._loadCell(i, group));
			}
			if (group.requests.length > 0) O_O.scheduler.addGroup(group);
			return dfrs;
		},

		_processRange: function(start, end) {
			var bound1 = this._getPaddedBoundsFrom(start), bound2 = this._getPaddedBoundsFrom(end);
			start = bound1[0];
			end = bound2[1];

			return this._loadRange(start, end);
		},

		_processIndex: function(index) {
			var bounds = this._getPaddedBoundsFrom(index);
			var start = bounds[0], end = bounds[1];

			this._loadRange(start, end);
		},

		getRange: function(start, end, cb) {
			var dfrs = this._processRange(start, end);
			var bounds = this._fixBounds(start, end);
			start = bounds[0], end = bounds[1];

			var range = [], imgs=this._images;
			for (var i=start; i<=end; i++) {
				range.push(imgs[i][1]);
			}
			
			if (typeof cb === 'function') {
				$.when.apply(this, dfrs).done(cb);
			}

			return range;
		}
	};
	
	O_O.lib.Fetcher = O_O.lib.NewFetcher;
})(ps$);
;(function($, undefined) {
var scopeC = O_O.scopeC;
'use strict';
O_O.lib.Scheduler = function(groups) {
	this.counter = 0;
	this.groups = groups || [];

	var i, len;
	if (this.IFRAME) {
		this.iframePool = [];
		for (i=0; i<this.CONCURRENCY; i++) {
			this.iframePool.push($('<iframe>', {
				width: 0,
				height: 0
			})
				.appendTo($('body')));
		}
	}
	for (i=0, len=this.groups.length; i<len; i++) {
		this.addGroup(groups[i]);
	}
};

O_O.lib.Scheduler.prototype = {
	counter: 0,
	groups: null,
	locked: false,
	processing: false,
	numProcessing: 0,
	transitioning: null,
	transitionsRunning: 0,
	transitionEndCb: null,
	forcedUnlock: null,

	CONCURRENCY: 6,
	PROCESS_WAIT: 0,
	PROCESS_LOCK_WAIT: 100,
	TRANSITION_WAIT: 1000,
	THREADING: false,
	IFRAME: false,
	WORKER_PATH: 'js/2.0/lib/workers/fetcher.js',

	iframePool: null,

	_process: function() {
		this.processing = true;
		if (this.groups.length === 0) {
			this.processing = false;
			return;
		}

		if (this.numProcessing >= this.CONCURRENCY) return;

		if (this.locked) {
			console.log('LOCKED!');
			setTimeout(scopeC(function() {
				this._process();
			}, this), this.PROCESS_LOCK_WAIT);
			return;
		}

		if (this.transitioning) {
			if (!this.transitionEndCb) {
				this.transitionEndCb = scopeC(function() {
					this._process();
				}, this);
			}
			return;
		}

		var group = this.groups[0];
		var requests = group.popN(this.CONCURRENCY-this.numProcessing);

		if (group.requests.length === 0) {
			this.groups.shift();
		}

		// All _fetch* functions must call _process to loop
		if (this.IFRAME) {
			this._fetchIframe(requests);
		} else if (!this.THREADING) {
			this._fetchNormal(requests);
		} else if (group.canThread) {
			this._fetchThreaded(requests);
		}

		if (this.groups.length === 0) {
			this.processing = false;
		}
	},

	_getFreeIframe: function() {
		return this.iframePool.shift();
	},

	_bindIframe: function($iframe, req) {
		var dfr = $.Deferred();
		$iframe.onC('load', function() {
			req.fetch();
			dfr.resolve();
			$iframe.data('ready', true);
			this.iframePool.push($iframe);
			this.numProcessing--;
			this._process();
		}, this);
		return dfr;
	},

	_fetchIframe: function(requests) {
		var dfr;
		var req;
		var $iframe;

		var doneFunc = scopeC(function() {
			this.numProcessing--;
			this._process();
		}, this);

		for (var i=0, len=requests.length; i<len; i++) {
			req = requests[i];
			this.numProcessing++;
			if (req.url) {
				$iframe = this._getFreeIframe();
				dfr = this._bindIframe($iframe, req);
				$iframe.attr('src', req.url);
			} else {
				// Fall back to single-threaded fetching
				req.fetch().done(doneFunc);
			}
		}
	},

	_fetchNormal: function(requests) {
		var doneFunc = scopeC(function() {
			this.numProcessing--;
			this._process();
		}, this);

		for (var i=0, len=requests.length; i<len; i++) {
			this.numProcessing++;
			requests[i].fetch().done(doneFunc);
		}
	},

	_fetchThreaded: function(requests) {
		var worker = new Worker(this.WORKER_PATH, requests);
		if (this.groups.length === 0) {
			this.processing = false;
		} else {
			worker.onMessage = scopeC(function() {
				this._process();
			}, this);
		}
	},

	_transitionUnlock: function() {
		this.transitioning = false;
		if (this.transitionEndCb) this.transitionEndCb();
		this.transitionEndCb = null;
	},

	transitionLock: function(n) {
		n = n || 1;
		this.transitioning = true;
		this.transitionsRunning += n;
		this.forcedUnlock = setTimeout(scopeC(this._transitionUnlock, this),
			this.TRANSITION_WAIT);
	},

	bindTransitions: function($elem) {
		$elem.onC(O_O.browser.transEndEventName(), function() {
			if(!this.transitioning) return;
			this.transitionsRunning--;
			if (this.transitionsRunning !== 0) return;
			clearTimeout(this.forcedUnlock);
			this._transitionUnlock();
		}, this);
	},

	addGroup: function(group) {
		var alreadyLocked = this.locked;
		this.locked = true;
		var g, inserted=false;
		for (var i=0,len=this.groups.length; i<len; i++) {
			g = this.groups[i];
			if (!inserted) {
				if (g.priority < group.priority) {
					this.groups.splice(i, 0, group);
					inserted = true;
				}
			} else {
				g.index++;
			}
		}
		if (!inserted) {
			this.groups.push(group);
		}
		group.index = i;
		this.locked = alreadyLocked;

		if (!this.processing) {
			this._process();
		}
	},

	removeGroup: function(group) {
		var alreadyLocked = this.locked;
		this.locked = true;

		var groups = this.groups;
		var idx = groups.indexOf(group);

		if (idx < 0) {
			this.locked = alreadyLocked;
			return;
		}

		var i, len;
		for (i=0, len=group.requests.length; i<len; i++) {
			group.requests[i].forceResolve();
		}

		for (i=idx, len=groups.length; i<len; i++) {
			groups[i].index--;
		}

		this.locked = alreadyLocked;
	},

	changePriority: function(group, newP) {
		group.priority = newP;
		var oldIndex = this.groups.indexOf(group);
		this.groups.splice(oldIndex, 1);
		this.addGroup(group);
		if (group.index > oldIndex) {
			for (var i=oldIndex; i<group.index; i++) {
				if (this.groups[i]) this.groups[i].index--;
			}
		}
	}
};

O_O.lib.RequestGroup = function(priority, requests, id, loadCB) {
	requests = requests || [];
	id = id === undefined ? null : id;
	loadCB = loadCB || null;

	this.id = id;
	this.priority = priority;
	this.requests = requests;
	this.loadCB = loadCB;
};

O_O.lib.RequestGroup.prototype = {
	id: null,
	priority: null,
	index: null,
	requests: null,
	loadCB: null,

	isEmpty: function() {
		return this.requests.length === 0;
	},

	popN: function(n) {
		return this.requests.splice(0, n);
	},

	addRequest: function(request) {
		this.requests.push(request);
	},

	removeRequest: function(request) {
		var idx = this.requests.indexOf(request);
		this.requests.splice(idx, 1);
		if (typeof request.forceResolve === 'function') {
			request.forceResolve();
		}
	}
};

O_O.lib.Request = {};

O_O.lib.Request.PSImage = function($img, id, size, loadCB) {
	this.size = size || 500;
	this.loadCB = loadCB;
	this.$img = $img.psImg('init', {id: id});
	this.dfr = $.Deferred();
};

O_O.lib.Request.PSImage.prototype = {
	$img: null,
	size: null,
	loadCB: null,
	dfr: null,

	fetch: function() {
		var dfr = this.dfr;
		this.dfr = dfr;
		this.$img.onC('load', function() {
			var $img = this.$img;

			dfr.resolve();

			$img.off('load');
			if (this.loadCB) this.loadCB($img);
		}, this);

		this.$img.psImg('load', {width: this.size, height: this.size});
		return dfr.promise();
	},

	forceResolve: function() {
		this.dfr.resolve();
	}
};

O_O.lib.Request.Image = function($img, url, loadCB) {
	this.$img = $img;
	this.url = url;
	this.loadCB = loadCB;
	this.dfr = $.Deferred();
};

O_O.lib.Request.Image.prototype = {
	$img: null,
	url: null,
	loadCB: null,
	dfr: null,

	fetch: function() {
		var dfr = this.dfr;
		this.$img
			.onC('load', function() {
				var $img = this.$img;

				$img.data('origSz', {width: $img.width(), height: $img.height()});

				dfr.resolve();
				$img.off('load');
				if (this.loadCB) this.loadCB($img);
			}, this)
			.attr('src', this.url);
		return dfr.promise();
	},

	forceResolve: function() {
		this.dfr.resolve();
	}
};

O_O.lib.Request.ImageBackground = function($elm, url, loadCB) {
	this.$elm = $elm;
	this.url = url;
	this.loadCB = loadCB;
	this.dfr = $.Deferred();
};

O_O.lib.Request.ImageBackground.prototype = {
	$elm: null,
	url: null,
	loadCB: null,
	dfr: null,

	fetch: function() {
		var dfr = this.dfr,
			$img = $('<img>');
		this.dfr = dfr;
		$img
			.onC('load', function() {
				var $elm = this.$elm;

				if (this.loadCB) this.loadCB($elm);
				$elm.data('origSz', {width: $img.width(), height: $img.height()})
					.css({
						'background-image': 'url('+$img.attr('src')+')'
					});

				// Force a repaint of the div after the image loads
				var div = $elm[0];
				div.style.display = 'none';
				div.offsetHeight;
				div.style.display = 'block';

				dfr.resolve();
				$img.off('load');
			}, this)
			.attr('src', this.url);
		return dfr.promise();
	},

	forceResolve: function() {
		this.dfr.resolve();
	}
};

O_O.scheduler = O_O.mm.oNew(O_O.lib.Scheduler);
})(ps$);
;(function($, undefined) {
	"use strict";
	O_O.lib.Swiper = function(sel, domScope, opt) {
		domScope = domScope || null;
		this.$elem = domScope ? domScope.find(sel) : $(sel);
		this.settings = $.extend({
			'threshold': 120,
			'snapping': true
		}, opt);

		this.SLIDE_THRESHOLD = this.settings.threshold;
		this.ignoreNextEnd = false;
		this.touches = 0;
		this._swiping = false;
		this._swipeEnding = false;
		this._startX = null;
		this._startLeft = null;
		this._currentLeft = 0;
		this._minLeft = 0;
		this._xDiff = null;

		this.setSizeConstants(this.settings.viewportWidth, this.settings.slideWidth);
		this._bindTouchEvents();
	};

	O_O.lib.Swiper.prototype = {
		SLIDE_THRESHOLD: null,
		VIEWPORT_WIDTH: null,
		SLIDE_WIDTH: null,
		settings: null,

		swipeAmount: null,
		goingForward: null,

		ignoreNextEnd: null,

		imgsPerSwipe: 1,
		$elem: null,

		touches: null,
		_swiping: false,
		_swipeEnding: null,
		_swipeE: null,

		_initialOffset: null,

		_startX: null,
		_startLeft: null,
		_currentLeft: 0,
		_minLeft: 0,
		_xDiff: null,

		_prevImg: null,
		_curImg: null,
		_nextImg: null,

		_initSlide: function(forward) {
			this._curImg = this.$elem.find('.viewing');
			var next=this._curImg, prev=this._curImg;
			for (var i=0; i<this.imgsPerSwipe; i++) {
				next = next.next();
				prev = prev.prev();
			}
			this.goingForward = forward;
			this._nextImg = next;
			this._prevImg = prev;
			this._startLeft = this._getTranslateX(this.$elem);
		},

		inSwipe: function() {
			return this.touches > 0;
		},

		_swipeStart: function(e) {
			this.touches++;
			if (this._swipeEnding || this.touches > 1) return;
			this._initSlide();
			this._startX = e.originalEvent.changedTouches[0].pageX;
			this.$elem.addClass('no-transition');
		},

		_swipe: function(e) {
			this._swipeE = e;
			var x = this._swipeE.originalEvent.changedTouches[0].pageX;
			var xDiff = x - this._startX;
			if (Math.abs(xDiff) > 15) {
				e.preventDefault();
				e.stopPropagation();
				this._swiping = true;
			}
		},

		_doSwipe: function() {
			if (this._swiping && !this._swipeEnding){
				this._swiping = false;
				if (this._swipeEnding || this.touches > 1) return;
				var x = this._swipeE.originalEvent.changedTouches[0].pageX;
				var xDiff = x - this._startX;
				if (x > 0) {
					this._currentLeft = this._startLeft + xDiff;
					this.$elem.css(
						Modernizr.prefixed('transform'),
						this._translate3dStr(this._startLeft + xDiff)
					);
				}
			}
			window.requestAnimationFrame(scopeC(this._doSwipe, this));
		},

		_swipeEnd: function(e) {
			this._swiping = false;
			this.touches--;
			if (this._swipeEnding || this.touches > 0) return;
			this._swipeEnding = true;
			var x = e.originalEvent.changedTouches[0].pageX;
			var xDiff = x - this._startX;
			this._minLeft = -1 * (this.getWidth() - this.VIEWPORT_WIDTH);
			if (this._currentLeft > 0) {
				this.slideStart(xDiff);
			//} else if (this._currentLeft < this._minLeft) {
				//this.slideEnd(xDiff);
			} else if (xDiff < -this.SLIDE_THRESHOLD) {
				this.slideForward(xDiff);
			} else if (xDiff > this.SLIDE_THRESHOLD) {
				this.slideBackward(xDiff);
			} else {
				this.slideCurrent(xDiff);
			}
			setTimeout(scopeC(function() {
				// Possibly force swipeEnding to be set to false just in case
				// transitionEnd has not been fired
				this._swipeEnding = false;
			}, this), 4000);
		},

		setInitialOffset: function(offset) {
			if (!this._initialOffset) {
				this._initialOffset = offset;
			}
			return this._initialOffset;
		},

		getWidth: function() {
			var $elem = this.$elem;
			var width=0, $imgs=$elem.find('img');
			for (var i=0,len=$imgs.length; i<len; i++) {
				width += $($imgs[i]).width();
			}
			return width;
		},

		setSizeConstants: function(viewport, slide) {
			this.VIEWPORT_WIDTH = viewport;
			this.SLIDE_WIDTH = slide;
			this.imgsPerSwipe = Math.max(1, Math.round(this.VIEWPORT_WIDTH/this.SLIDE_WIDTH));
		},

		slideStart: function(xDiff) {
			if (!this._swipeEnding) this._initSlide();
			this._xDiff = xDiff;
			this.$elem.removeClass('no-transition');
			this.$elem.css(
				Modernizr.prefixed('transform'),
				this._translate3dStr(0)
			);
			this._swipeEnding = false;
			this._currentLeft = 0;
		},

		slideEnd: function(xDiff) {
			if (!this._swipeEnding) this._initSlide();
			this._xDiff = xDiff;
			this.$elem.removeClass('no-transition');
			this.$elem.css(
				Modernizr.prefixed('transform'),
				this._translate3dStr(this._minLeft)
			);
			this._swipeEnding = false;
			this._currentLeft = this._minLeft;
		},

		slideCurrent: function(xDiff) {
			if (!this._swipeEnding) this._initSlide();
			this._xDiff = xDiff;
			this.$elem.removeClass('no-transition');
			if (this.settings.snapping) {
				this.$elem.css(
					Modernizr.prefixed('transform'),
					this._translate3dStr(this._startLeft)
				);
				this._currentLeft = this._startLeft;
			}
			this._swipeEnding = false;
		},

		slideBackward: function(xDiff) {
			if (!this._swipeEnding) this._initSlide(false);
			this._curImg.removeClass('viewing');
			this._prevImg.addClass('viewing');
			this._xDiff = xDiff;

			if (xDiff && this.settings.backwardPreload) this.settings.backwardPreload();
			this.$elem.removeClass('no-transition');

			if (this.settings.snapping && (xDiff || this.swipeAmount)) {
				var amount;
				if (this.swipeAmount) {
					amount = this.swipeAmount;
					this.swipeAmount = null;
				} else {
					amount = this.SLIDE_WIDTH * this.imgsPerSwipe;
				}
				if (this._startLeft+amount >= 200) {
					amount = Math.round(amount/3);
				}
				this._currentLeft = this._startLeft + amount;
				this.$elem.css(
					Modernizr.prefixed('transform'),
					this._translate3dStr(this._currentLeft)
				);
			} else if (!this.settings.snapping) {
				this._currentLeft = this._startLeft + xDiff + 200;
				if(this._currentLeft > 0) this._currentLeft = 0;
				this.$elem.css(
					Modernizr.prefixed('transform'),
					this._translate3dStr(this._currentLeft)
				);
			}
		},

		slideForward: function(xDiff) {
			if (!this._swipeEnding) this._initSlide(true);
			this._curImg.removeClass('viewing');
			this._nextImg.addClass('viewing');
			this._xDiff = xDiff;

			if (xDiff && this.settings.forwardPreload) this.settings.forwardPreload();
			this.$elem.removeClass('no-transition');

			this._minLeft = -1 * (this.getWidth() - this.VIEWPORT_WIDTH);
			if (this.settings.snapping && (xDiff || this.swipeAmount)) {
				var amount;
				if (this.swipeAmount) {
					amount = this.swipeAmount;
					this.swipeAmount = null;
				} else {
					amount = this.SLIDE_WIDTH * this.imgsPerSwipe;
				}

				var width = this.getWidth();
				var amountInScreen = width - Math.abs(this._getTranslateX(this.$elem));
				if (amountInScreen <= this.VIEWPORT_WIDTH) {
					//amount = Math.round(amount/3);
				}
				this._currentLeft = this._startLeft - amount;
				//if(this._currentLeft < this._minLeft) this._currentLeft = this._minLeft;
				this.$elem.css(
					Modernizr.prefixed('transform'),
					this._translate3dStr(this._currentLeft)
				);
			} else if (!this.settings.snapping) {
				this._currentLeft = this._startLeft + xDiff - 200;
				if(this._currentLeft < this._minLeft) this._currentLeft = this._minLeft;
				this.$elem.css(
					Modernizr.prefixed('transform'),
					this._translate3dStr(this._currentLeft)
				);
				this._swipeEnding = false;
			}
		},

		// Used when the client code does anything that might move the strip
		// Resets it to the original position so that client nudging can happen
		adjust: function() {
			this.$elem.css(
				Modernizr.prefixed('transform'),
				this._translate3dStr(this._startLeft)
			);
		},

		diff: function() {
			return this.$elem.offset().left - this._startLeft;
		},


		_bindTouchEvents: function() {
			this.$elem.onC('touchstart', this._swipeStart, this);
			this.$elem.onC('touchmove', this._swipe, this);
			this.$elem.onC('touchend', this._swipeEnd, this);
			this.$elem.onC('touchcancel', function() {
				this.touches--;
			}, this);
			this.$elem.onC(O_O.browser.transEndEventName(), '', this._transitionEnd, this);

			if(window.requestAnimationFrame)
				this._doSwipe();
		},

		_transitionEnd: function() {
			if (this.ignoreNextEnd) {
				this.ignoreNextEnd = false;
				return;
			}

			this._swipeEnding = false;
			var dir;
			if (this.goingForward || this._xDiff < -this.SLIDE_THRESHOLD) {
				dir = 'forward';
			} else if (!this.goingForward || this._xDiff > this.SLIDE_THRESHOLD) {
				dir = 'backward';
			}
			this.settings.transitionEnd(dir);
			this._xDiff = 0;
		},

		/* utils */

		_translate3dStr: function(x) {
			x = Math.floor(x);
			if(Modernizr.csstransforms3d)
				return 'translate3d(' + x + 'px,0,0)';
			else
				return 'translate(' + x + 'px, 0)';
		},

		_getTranslateX: function($el) {
			var transform = $el.css(Modernizr.prefixed('transform'));
			if (transform === 'none') {
				return 0;
			} else {
				transform = transform.split(',');
				if(transform.length===16)   //matrix3d gets returned by IE10.
					return parseFloat(transform[12]);
				else						//regular matrix is getting returned elsewhere.
					return parseFloat(transform[4]);
			}
		}
	};
})(ps$);
;(function($, undefined) {
O_O.lib.psImg = function() {};
var psImgMethods = {
	init: function (img_args)
	{
		var params = {
			id: null,
			type: 'GalleryImage'
		};

		params = $.extend(params, img_args);

		this.data('psImg', params);
		this.addClass('psimg unloaded');

		return this;
	},
	
	model: function ()
	{
		var params = this.data('psImg');
		return O_O.model.get(params.type, params.id);
	},
	
	resize: function (size_args, cb)
	{
		var size = $.extend({width: 500, height: 500}, size_args);
		var mode = size_args.mode || 'fit';

		//this should almost always be synchronous, unless we stop returning size params with gallery images
		this.psImg('model').getImageModel().load().done(scopeC(function (data) {
			var img = {width: parseInt(data.width), height: parseInt(data.height) };
			var scale = O_O.app.imgScaleCalc(img, size, 'fit');
			var newW = Math.round(img.width * scale), newH = Math.round(img.height * scale);
			this.width(newW);
			this.height(newH);
			if (cb) {
				cb(newW, newH);
			}
		}, this));
		
		return this;
	},
	
	resizeTo: function (target)
	{
		return this.psImg('resize', {width: $(target).width(), height: $(target).height()});
	},

	resizeByRatio: function (ratio)
	{
		var newWidth = parseInt(this.width() * ratio, 10);
		var newHeight;
		if (this.attr('data-heightSet') === 'set') {
			// Only set height if it has been set before
			newHeight = parseInt(this.height() * ratio, 10);
		} else {
			newHeight = false;
		}

		this.width(newWidth);
		if (newHeight) {
			this.height(newHeight);
		}
	},
	
	load: function (size_args)
	{
		var args = {
			width: this.width(),
			height: this.height(),
		};
		
		args = $.extend(args, size_args);
		
		this.removeClass('unloaded');
		this.addClass('psimg_loading');
		//this.psImg('resize', args); // TODO: Refactor resizing from the View out
		this.onC('load', psImgMethods._loadCb, this);
		this.attr('src', this.psImg('model').getLinkForSize(args));

		return this;
	},
	
	_loadCb: function ()
	{
		this.removeClass('psimg_loading');
		this.offC('load', psImgMethods._loadCb, this);
	}
};

$.fn.psImg = function (method) {
	if ( psImgMethods[method] ) {
		return psImgMethods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
	} else if ( typeof method === 'object' || !method ) {
		return psImgMethods.init.apply( this, arguments );
	} else {
		$.error( 'Method ' +  method + ' does not exist on jQuery.psImg' );
	}
};
})(ps$);
;(function(undefined) {
'use strict';

// TODO: fold this lean functionality into models

O_O.lib.Dirty = function(ptr, saveCb) {
	if (typeof ptr === 'object') this.init(ptr, saveCb);

	return this;
};

O_O.lib.Dirty.prototype = {
	/* state */

	f_dirty: null,
	ptr: null,
	mod: null,
	subA: null,
	saveCb: null,

	/* utils */

	init: function(ptr, saveCb) {
		if (typeof ptr === 'undefined') {
			throw new Error('Must initialize with reference');
		}

		this.f_dirty = false;
		this.ptr = ptr;
		this.mod = null;
		this.subA = [];
		this.saveCb = saveCb;
	},

	destroy: function() {
		this.f_dirty = null;
		this.ptr = null;
		this.mod = null;
		this.subA = null;
		this.saveCb = null;
	},

	_guard: function() {
		if (this.f_dirty === null) {
			throw new Error('Cannot complete; not yet initialized');
		}
	},

	/* subscriptions */

	sub: function(saveCb, scope) {
		this._guard();

		this.subA.push([saveCb, scope]);
	},

	_dispatch: function(d) {
		this._guard();

		var i, l, saveCb, scope;
		for (i = 0, l = this.subA.length; i < l; i++) {
			saveCb = this.subA[i][0];
			scope = this.subA[i][1];

			if (typeof scope !== 'undefined') saveCb.call(scope, d);
			else saveCb(d);
		}
	},

	/* accessors */

	getOriginal: function() {
		this._guard();

		return this.ptr;
	},

	getCurrent: function() {
		this._guard();

		var r = this.f_dirty ? this.mod : this.ptr;

		// copy for safety
		return O_O.obj.clone(r);
	},

	/* mutators */

	update: function(d, f_silent) {
		this._guard();

		this.f_dirty = true;
		this.mod = d;

		// copy for safety
		var r = O_O.obj.clone(this.mod);

		if (!f_silent) this._dispatch(r);

		return r;
	},

	revert: function(f_silent) {
		this._guard();

		if (this.f_dirty) {
			this.f_dirty = false;
			this.mod = null;
		}

		// copy for safety
		var r = O_O.obj.clone(this.ptr);

		if (!f_silent) this._dispatch(r);

		return r;
	},

	save: function() {
		this._guard();

		if (this.f_dirty) {
			this.f_dirty = false;
			this.ptr = this.mod;
			this.mod = null;
		}

		// copy for safety
		var r = O_O.obj.clone(this.ptr);

		if (typeof this.saveCb === 'function') return this.saveCb(r);

		return r;
	}
};

}());
;(function($, undefined) {
'use strict';

if (!O_O.lib) O_O.lib = {};
if (!O_O.mode) O_O.mode = {};

O_O.lib.Mode = function($elem, name, msgSys, env) {
	this.$elem = $elem;
	this.name = name;
	this.msgSys = msgSys;
	this.env = env;

	this._uid = O_O.lib.Util.makeUid();
};

O_O.lib.Mode.prototype = {
	/* constants */

	PHANTOM_DELAY: 1000,
	CSS_CLASS_WIDGET: 'widget',
	GLOBAL_MODE: 'global',

	/* state */

	$elem: null,
	name: null,
	msgSys: null,
	env: null,

	_uid: null,

	// TODO: bundle as one object (with sections) that keeps controllers
	globalA: null,
	widgetA: null,
	sets: null,

	_tupleA: null, // HACK: maintains state between init and render
	_f_rendered: null,
	_contIdx: null, // keep track of index for arrays of delegates in sets

	metaName: null,
	desc: null,
	metaImgs: null,

	/* Set ID regex consts from Util library */
	GAL_ID_REGEX: O_O.lib.Util.GAL_ID_REGEX,
	CLC_ID_REGEX: O_O.lib.Util.CLC_ID_REGEX,
	IMG_ID_REGEX: O_O.lib.Util.IMG_ID_REGEX,
	VID_ID_REGEX: O_O.lib.Util.VID_ID_REGEX,
	AUD_ID_REGEX: O_O.lib.Util.AUD_ID_REGEX,
	DOC_ID_REGEX: O_O.lib.Util.DOC_ID_REGEX,

	/* shell commands */

	destroy: function() {
		var chan = 'Mode:' + this.name.toLowerCase();
		this.msgSys.unsub(chan, this._uid);

		this.getWidgets().map(function(controller) {
			controller.destroy(); // bai2u
		});

		this.globalA = null;
		this.widgetA = null;
		this.sets = null;

		this._tupleA = null;
		this._f_rendered = null;

		this.$elem.off().empty();

		for (var k in this) {
			if (this.hasOwnProperty(k)) k = null;
		}
	},

	init: function(tpl, modeCfg, globalCfg) {
		if (typeof tpl !== 'function') {
			throw new Error('Could not find template for mode ' + this.name);
		}

		// TODO: move into meta tags functions
		var siteName = this.env('custom').site_name;
		siteName = siteName ? siteName + ' ' : '';
		this.metaName = siteName + window.location;
		this.desc = modeCfg.meta_description || this.metaName;
		this.$elem.html(tpl(modeCfg)); // tpl eval context is mode config
		
		var $dfd;
		// check config version for feature usage
		$dfd = this._waitForWC(modeCfg);
		$dfd = $dfd.then(this._tplDependentInit.bind(this, modeCfg, globalCfg));

		var chan = 'Mode:' + this.name.toLowerCase();
		this.msgSys.sub(chan, this._uid, this._msgCB, this);

		return $dfd.promise();
	},

	/**
	 * Inits aspects of mode (widgets & delegates) that depend on
	 * the mode template dom being completely ready.
	 * @param  {Object} modeCfg
	 * @param  {Object} globalCfg
	 * @return {Deferred}           Deferred that resolves when init is done.
	 */
	_tplDependentInit: function (modeCfg, globalCfg) {
		//filter out any undefined ordinals
		//note: this is jQuery.filter(), which is a bit different from the modern Array.filter
		var tplOrdinals = $('[data-ordinal]', this.$elem).filter(function() {
			return typeof $(this).data('ordinal') !== 'undefined';
		});

		var widgetInitA = this._initWidgets(tplOrdinals, modeCfg, globalCfg);

		//wait for widgets to init before initializing delegates
		var $dfd = new $.Deferred();
		$.when.apply($, widgetInitA).done(scopeC(function() {
			this._tupleA = Array.prototype.slice.call(arguments); // save state
			// controllers have been loaded now but not init-ed
			var delegatesDfd = this._initDelegates(this._tupleA);
			delegatesDfd.fail(function() {
				$dfd.reject();
			}).done(scopeC(function() {
				this._checkDelegates(); // throws error if bad
				this._checkForWebComponentsOnPage();
				$dfd.resolve();
			}, this));
		}, this));

		return $dfd;
	},

	/**
	 * Init each widget, bundling deferred promise of each init into an array
	 * @param  {jqArray} tplOrdinals jqArray for tpl ordinals
	 * @param  {Object} modeCfg
	 * @param  {Object} globalCfg
	 * @return {Array}             Array of deferreds
	 */
	_initWidgets: function (tplOrdinals, modeCfg, globalCfg) {
	
		//note: using jQuery.map(), not Array.map()
		return tplOrdinals.map(function(index, ordinalElement) {
			var $element = $(ordinalElement);
			var ordinal = parseInt($element.data('ordinal'), 10);
			var f_global = ($element.filter('[data-global]').length === 1);

			var set = $element.data('set');
			var inject = $element.data('inject');

			var widgets;
			if (set) {
				widgets = modeCfg.sets[set];
			}
			else {
				widgets = f_global ? globalCfg.widgets : modeCfg.widgets;
			} 
			return this._initWidget($element, widgets[ordinal], ordinal,
				f_global, set, inject);
		}.bind(this));
	},

	getWidgets: function(group) {
		var wA = [];

		// groups are all (default), global, and local (including any sets)
		if ((!group || group === 'all' || group === 'global') && this.globalA) {
			wA = wA.concat(this.globalA);
		}
		if (!group || group === 'all' || group === 'local') {
			if (this.widgetA) wA = wA.concat(this.widgetA);
			if (this.sets) {
				for (var s in this.sets) {
					if (this.sets.hasOwnProperty(s)) {
						var set = this.sets[s];
						for (var i = 0, l = set.length; i < l; i++) {
							if (set[i].length) wA = wA.concat(set[i]);
							else wA.push(set[i]);
						}
					}
				}
			}
		}

		return wA;
	},

	isRendered: function() {
		return this._f_rendered; // false is state between init and render
	},

	render: function() {
		this._f_rendered = true;

		var whenA = [];
		for (var i = 0, l = this._tupleA.length; i < l; i++) {
			var t = this._tupleA[i];
			whenA.push(this._renderWidget(t.controller, t.cfg, t.ordinal,
				t.f_global, t.set, t.inject));
		}
		this._tupleA = null;

		this.setMetaTags();

		return $.when.apply($, whenA).done(scopeC(function() {
			var phantom = this.env('phantom');
			if (phantom) {
				setTimeout(function() {
					phantom('phantomExit');
				}, this.PHANTOM_DELAY);
			}
		}, this)); // must return deferred for shell
	},

	args: function() {
		// TODO: always rewrite to clear bad args (fix content pages first)
	},

	/* widget setup */

	_initWidget: function($elem, cfg, ordinal, f_global, set, inject) {
		if (typeof cfg === 'undefined') {
			throw new Error('Cannot add widget; widget config is undefined');
		}
		if (typeof cfg.widget === 'undefined') {
			throw new Error('Cannot add widget; no widget name key in config');
		}

		$elem.addClass(this.CSS_CLASS_WIDGET + ' ' + cfg.widget); // convention

		return this._shellMsg('loadWidget', {
			name: cfg.widget
		}).then(scopeC(function(props) {
			if (!props) throw new Error('Theme does not include ' + cfg.widget);

			/******************************************************************/

			// TODO: clean up (see note about bundling global/local/sets)

			if (set && !this.sets) this.sets = {};
			else if (f_global && !this.globalA) this.globalA = [];
			else if (!f_global && !this.widgetA) this.widgetA = [];
			if (set && !this.sets[set]) this.sets[set] = [];

			var controller = O_O.mm.oNew(props.Controller, $elem, props,
				this.msgSys, this.env);

			if (set) {
				if (!this.sets[set][ordinal]) {
					this.sets[set][ordinal] = controller;
				}
				else if (!this.sets[set][ordinal].length) {
					var a = [this.sets[set][ordinal]];
					a.push(controller);
					this.sets[set][ordinal] = a;
				}
				else {
					this.sets[set][ordinal].push(controller); // pack multiple
				}
			}
			else if (f_global) {
				this.globalA[ordinal] = controller;
			}
			else {
				this.widgetA[ordinal] = controller;
			}

			/******************************************************************/

			// data needed to invoke render on each widget's controller
			return {
				controller: controller,
				cfg: cfg,
				ordinal: ordinal,
				f_global: f_global,
				set: set,
				inject: inject
			};
		}, this));
	},

	_renderWidget: function(controller, cfg, ordinal, f_global, set, inject) {
		var $dfd = new $.Deferred();

		var tuple = {
			mode: f_global ? this.GLOBAL_MODE : this.name,
			widget: controller.props.name,
			ordinal: ordinal
		};
		if (set) tuple.set = set;

		if (this.env('edit')) {
			this._shellMsg('cfgWidget', {
				action: 'make',
				tuple: tuple,
				cfg: cfg,
				controller: controller,
				inject: inject
			}).fail(function() {
				$dfd.reject();
			}).done(scopeC(function(d) {
				controller.setCfg(d.id);
				controller.render($.extend({}, d.cfg, inject)).fail(function() {
					$dfd.reject();
				}).done(scopeC(function() {
					$dfd.resolve();
				}));
			}, this));
		}
		else {
			this._shellMsg('getWidgetId', {
				tuple: tuple
			}).done(scopeC(function(d) {
				controller.setCfg(d.id);
				controller.render($.extend({}, cfg, inject)).fail(function() {
					$dfd.reject();
				}).done(scopeC(function() {
					$dfd.resolve();
				}, this));
			}, this));
		}

		return $dfd.promise(); // synced to initial widget render deferred
	},

	/* inter-widget communication */

	_initDelegates: function(tupleA) {
		var i, l, t, del, whenA = [];

		for (i = 0, l = tupleA.length; i < l; i++) {
			t = tupleA[i];

			del = t.cfg.delegate;
			if (del) {
				whenA.push(this._delegateInterface(t.controller, del, t.set));
			}
		}

		return $.when.apply($, whenA).done(scopeC(function() {
			delete this._contIdx; // no longer needed
		}, this));
	},

	_getController: function(mode, set, ordinal) {
		var $dfd = new $.Deferred();
		if (mode === this.name) {
			if (set) {
				$dfd.resolve(this.sets[set][ordinal]); // FIXME: use getWidgets
			}
			else {
				$dfd.resolve(this.getWidgets('local')[ordinal]);
			}
		}
		else {
			this._shellMsg('initMode', {name: mode}).fail(function() {
				$dfd.reject();
			}).done(function(m) {
				if (set) {
					$dfd.resolve(m.sets[set][ordinal]); // FIXME: use getWidgets
				}
				else {
					$dfd.resolve(m.getWidgets('local')[ordinal]);
				}
			});
		}

		return $dfd.promise();
	},

	_objKeysMatchArr: function(obj, arr) {
		var count = 0;

		for (var k in obj) {
			if (obj.hasOwnProperty(k)) {
				if (arr.indexOf(k) < 0) return false;
				count++;
			}
		}

		return count === arr.length;
	},

	_delegateInterface: function(cont, delegates, s) {
		var n = cont.prop('name');

		if (!cont.delegateInterfaces) {
			throw new Error('Cannot attach implementers to widget ' + n + ';' +
				' does not delegate any interfaces');
		}

		var mode, set, ordinal, ifName, ifA, i, l, $dfd, whenA = [];

		var doneCb = scopeC(function(controller) {
			if (!controller) {
				throw new Error('Cannot fetch widget ordinal ' + ordinal +
					' of mode ' + mode + ' to delegate');
			}

			var c;
			if (controller.length) {
				if (!this._contIdx) this._contIdx = {};
				if (!this._contIdx.hasOwnProperty(set)) this._contIdx[set] = 0;
				else this._contIdx[set]++;

				c = controller[this._contIdx[set]];
			}
			else {
				c = controller;
			}

			var widget = c.prop('name');

			if (!c.implementInterfaces) {
				throw new Error('Cannot attach ' + widget + ' to delegator ' +
					n + '; does not implement any interfaces');
			}
			if (typeof c.implementInterfaces[ifName] !== 'object') {
				throw new Error('Cannot attach ' + widget + ' to delegator ' +
					n + '; does not implement interface ' + ifName);
			}

			var impl = c.implementInterfaces[ifName];

			// check that implementer supplies exact delegated functions
			var del = cont.delegateInterfaces[ifName];
			var funcA = is_array(del) ? del : del.functions; // if optional
			if (!this._objKeysMatchArr(impl, funcA)) {
				throw new Error('Cannot attach ' + widget + ' to delegator ' +
					n + '; does not exactly implement interface ' + ifName);
			}

			cont.setDelegate(ifName, impl, c);
		}, this);

		var failCb = scopeC(function() {
			throw new Error('Cannot fetch widget ordinal ' + ordinal +
				' of mode ' + mode + ' to delegate');
		}, this);

		for (ifName in delegates) {
			if (delegates.hasOwnProperty(ifName)) {
				if (!cont.delegateInterfaces.hasOwnProperty(ifName)) {
					throw new Error('Cannot attach implementer to widget ' + n +
						'; does not delegate interface ' + ifName);
				}

				ifA = delegates[ifName];

				for (i = 0, l = ifA.length; i < l; i++) {
					mode = ifA[i].mode || this.name;
					set = s ? (ifA[i].set || s) : (ifA[i].set || undefined);
					ordinal = ifA[i].ordinal;

					$dfd = this._getController(mode, set, ordinal);
					whenA.push($dfd.done(doneCb).fail(failCb));
				}
			}
		}

		return $.when.apply($, whenA);
	},

	_checkDelegates: function() {
		var i, l, widgetA = this.getWidgets('local');

		for (i = 0, l = widgetA.length; i < l; i++) {
			this._checkDelegate(widgetA[i]); // controller
		}
	},

	_checkDelegate: function(c) {
		var ifName, ifDef, reqIf, ifObj = c.delegateInterfaces;
		if (!ifObj) return;

		for (ifName in ifObj) {
			if (ifObj.hasOwnProperty(ifName)) {
				ifDef = ifObj[ifName];
				reqIf = is_array(ifDef) ||
					(typeof ifDef === 'object' && !!ifDef.required);

				if (reqIf && !c.delegateMap[ifName]) {
					throw new Error('Cannot start ' + c.prop('name') +
						'; no implementers for interface ' + ifName);
				}
			}
		}
	},

	/**
	 * Collects components to wait for attachement in the 
	 * current mode, then returns a deferred that resolves when
	 * they are fully attached.
	 * @param  {Object} modeCfg Config object for the mode
	 * @return {Deferred}         Deferred object that tracks attachment.
	 */
	_waitForWC: function(modeCfg) {
		var widgets = modeCfg.widgets,
			sets = modeCfg.sets,
			wc = modeCfg.wc || [],
			$dfd = new $.Deferred();

		if (wc.length) {
			var wcWaitA = wc.map(function (webComponentName) {
				return this._waitForWcAttach(webComponentName, modeCfg);
			}.bind(this));

			$.when.apply($, wcWaitA)
				.always(function(){ $dfd.resolve(); });
		} else $dfd.resolve();

		return $dfd.promise();
	},

	/**
	 * Waits for a polymer web component in the mode to attach
	 * @param  {String} webComponentName tag name of element to wait for
	 * @return {Deferred} Deferred that resolves when element attaches.
	 */
	_waitForWcAttach: function (webComponentName, modeCfg) {
		var $dfd = new $.Deferred();
		var wcElement = this.$elem.find(webComponentName)[0];


		if (!wcElement) {
			throw new Error("Can't listen for attachment of element '"
				+ webComponentName + "' in mode '" + modeCfg.definition +
				"':  Can't find the wc element.");
		}

		if (wcElement.isAttached) { $dfd.resolve(); }
		else {
			var oldAttached = wcElement.attached
			wcElement.attached = function () {
				if (oldAttached) {
					oldAttached.apply(wcElement, arguments);
					wcElement.attached = oldAttached;
				}
				$dfd.resolve();
			}
		}

		return $dfd;
	},

	_checkForWebComponentsOnPage: function() {
		var HTMLImports = document.querySelectorAll('link[rel="import"]'),
			customElementIds = [];
		[].forEach.call(HTMLImports, function(HTMLImport) {
			if (HTMLImport.import) {
				var domModules = HTMLImport.import.getElementsByTagName('dom-module');
				[].forEach.call(domModules, function(module) {
					customElementIds.push(module.id);
				});
			}
		});
		customElementIds.forEach(function(elementName) {
			var elements = document.getElementsByTagName(elementName);
			[].forEach.call(elements, function(element) {
				if(element.constructor === HTMLElement) {
					PSGA.distributeToGaTrackers('send', {
		                'hitType': 'event',
						'eventCategory': 'JavaScript Error',
				        'eventAction': 'Polymer Component Error',
				        'eventLabel': '{"page":"' + location.pathname + '","element":"' + elementName + '"}'
		            });
				}
			})
		});
	},

	/* message system */

	_msgCB: function(type, data) {
		if (typeof this._eventH !== 'function') return;

		var d = data.data;
		this._eventH(d.type, d.data);
	},

	_eventH: null, // optionally implemented in children

	/* helpers */

	_shellMsg: function(msg, data) {
		return this.msgSys.pub('Shell', {
			msg: msg,
			data: data
		});
	},

	_shellEventMsg: function(type, data) {
		this._shellMsg('event', {
			type: type,
			data: data
		});
	},

	/* location hash */

	_isClc: function(clc) {
		return (typeof clc === 'string') &&
			!!clc.match(this.CLC_ID_REGEX);
	},

	_isGal: function(gal) {
		return (typeof gal === 'string') &&
			!!gal.match(this.GAL_ID_REGEX);
	},

	_isIdx: function(idx) {
		return (typeof idx === 'string') &&
			!!idx.match(/^\d+$/);
	},

	_isImg: function(id) {
		return (typeof id === 'string') &&
			!!id.match(this.IMG_ID_REGEX);
	},

	_isVid: function(id) {
		return (typeof id === 'string') &&
			!!id.match(this.VID_ID_REGEX);
	},

	_isAud: function(id) {
		return (typeof id === 'string') &&
			!!id.match(this.AUD_ID_REGEX);
	},

	_isDoc: function(id) {
		return (typeof id === 'string') &&
			!!id.match(this.DOC_ID_REGEX);
	},

	_pathInfoGetArr: function(argA) {
		var pathItemsO = {};
		argA = argA || [];
		for (var i = 0, ct = argA.length; i < ct; i++) {
			if (this._isClc(argA[i]))
				pathItemsO.C_ID = argA[i];
			else if (this._isGal(argA[i]))
				pathItemsO.G_ID = argA[i];
			else if (this._isVid(argA[i]))
				pathItemsO.VD_ID = argA[i];
			else if (this._isAud(argA[i]))
				pathItemsO.AD_ID = argA[i];
			else if (this._isDoc(argA[i]))
				pathItemsO.DO_ID = argA[i];
			else if (this._isImg(argA[i]))
				pathItemsO.I_ID = argA[i];
			else if (this._isIdx(argA[i]))
				pathItemsO.Idx = argA[i];
			else {
				pathItemsO.modifier = pathItemsO.modifier || [];
				pathItemsO.modifier.push(argA[i]);
			}

		}

		//convenience property catch-all for media ids
		pathItemsO.M_ID = pathItemsO.I_ID || pathItemsO.VD_ID || pathItemsO.AD_ID || pathItemsO.DO_ID || false;
		return pathItemsO;
	},

	/* meta tags */

	_makeMetaImgUrl: function(imgUrl, hasExt) {
		if (hasExt) imgUrl = imgUrl.substr(0, imgUrl.lastIndexOf('/'));
		return imgUrl.substr(0, imgUrl.lastIndexOf('/')) + '/fill=600x600';
	},

	setMetaTags: function(name, desc, url, imgs, twText) {
		name = name || this.metaName;
		desc = desc || this.desc;
		url = url || window.location;
		imgs = imgs || this.metaImgs;

		this._shellMsg('setMetaTags', {
			props: {
				'twitter:title': this.metaName,
				'og:title': this.metaName,
				'og:description': this.desc,
				'og:url': window.location,
				'og:image': this.metaImgs,
				'twtext': twText
			}
		});
	}
};

}(ps$));
;(function($, undefined) {
'use strict';

if (!O_O.lib) O_O.lib = {};

O_O.lib.Controller = function($elem, props, msgSys, env) {
	this.$elem = $elem;
	this.props = $.extend(true, {}, props); // copying for safety
	this.msgSys = msgSys;
	this.env = env;

	this._uid = O_O.lib.Util.makeUid();
	this.f_active = true;

	var chan = this.prop('name') + ':Controller';
	this.msgSys.sub(chan, this._uid, this._msgCb, this);
};

O_O.lib.Controller.prototype = {
	/* state */

	// TODO: these are all private and should be prefixed by underscores

	$elem: null,
	props: null,
	msgSys: null,
	env: null,

	_uid: null,
	f_active: false, // FIXME: change references to use accessor/mutator

	_cfg: null,

	/* utils */

	// TODO: deprecated (replace with just this.name)

	prop: function(p) {
		return this.props[p];
	},

	/* widget config */

	setCfg: function(cfg) {
		this._cfg = cfg;
	},

	getCfg: function() {
		return this._cfg;
	},

	toggleEdit: function(f_toggle, themeDfn, themeCfg) {
		var id = JSON.parse(this.getCfg());
		if (!id) return;

		var dfn, f_edit;

		/**********************************************************************/

		// FIXME: refactor this function once def parsing standardized
		// (block is fragmented because of how endpoint returns def; any
		// config that uses "define" section doesn't have aliases expanded)

		// TODO: rewrite settings widget to expect this true copy of def
		// (a block similar to this also exists in: --settings controller)

		var m = id.mode;
		var modeDfn = themeDfn.modes[m];
		var modeCfg = themeCfg.modes[m];

		var set = id.set;
		var ordinal = id.ordinal;

		// "default" as string because it's technically a reserved word
		var alias; // "widget variable" in define section of definition
		if (set) {
			alias = modeDfn.sets[set].widgets['default'][ordinal];
			dfn = modeDfn.define[alias];
			f_edit = modeCfg.sets[set][ordinal].f_editable;
		}
		else if (modeDfn.widgets['default']) {
			alias = modeDfn.widgets['default'][ordinal];
			dfn = modeDfn.define[alias];
			f_edit = modeCfg.widgets[ordinal].f_editable;
		}
		else {
			dfn = modeDfn.widgets[ordinal];
			f_edit = modeCfg.widgets[ordinal].f_editable;
		}

		/**********************************************************************/

		if (typeof f_edit === 'undefined' || f_edit) {
			this.view.toggleEdit(f_toggle, dfn);
		}
	},

	/* shell commands */

	destroy: function() {
		var channel = this.prop('name') + ':Controller';
		this.msgSys.unsub(channel, this._uid);

		this.view.destroy();

		for (var k in this) {
			if (this.hasOwnProperty(k)) k = null;
		}
	},

	render: function() { // defined in child
		throw new Error(this.prop('name') + '.Controller.render ' +
			'not implemented properly');
	},

	freeze: function() { // defined in child
		this.f_active = false;
	},

	thaw: function() { // defined in child
		this.f_active = true;
	},

	/* inter-widget communication */

	implementInterfaces: null,

	delegateInterfaces: null,
	delegateMap: null,

	setDelegate: function(ifName, funcMap, scope) {
		if (!this.delegateMap) this.delegateMap = {};
		if (!this.delegateMap[ifName]) this.delegateMap[ifName] = {};

		var delMap = this.delegateMap[ifName];
		for (var funcName in funcMap) {
			if (funcMap.hasOwnProperty(funcName)) {
				if (!delMap[funcName]) delMap[funcName] = [];

				delMap[funcName].push([funcMap[funcName], scope]);
			}
		}
	},

	delegate: function(ifName, funcName) {
		var n = this.prop('name');

		if (!this.delegateMap) {
			throw new Error('Cannot call function ' + funcName + '; ' + n +
				' does not delegate any interfaces');
		}
		if (!this.delegateMap[ifName]) {
			throw new Error('Cannot call function ' + funcName + '; ' + n +
				' does not delegate interface ' + ifName);
		}

		var funcA = this.delegateMap[ifName][funcName];
		if (!funcA || !funcA.length) {
			throw new Error('Cannot call function ' + funcName + '; not in' +
				' interface ' + ifName + ' as delegated by ' + n);
		}

		// return function that maps over array of scoped implementers
		return function() {
			for (var i = 0, l = funcA.length; i < l; i++) {
				funcA[i][0].apply(funcA[i][1], arguments); // original scope
			}
		};
	},

	/* message system */

	_msgCb: function(channel, payload) {
		var d = payload.data;

		switch (payload.msg) {
		case 'event':
			if (typeof this._eventH === 'function') {
				return this._eventH(d.type, d.data);
			}
		}
	},

	_eventH: function() { // defined in child
		throw new Error(this.prop('name') + '.Controller._eventH not ' +
			'implemented properly');
	},

	/* local functions */

	_controllerCb: function() { // defined in child
		throw new Error(this.prop('name') + '.Controller._controllerCb not ' +
			'implemented properly');
	}
};

}(ps$));
;(function($, undefined) {
'use strict';

if (!O_O.lib) O_O.lib = {};

O_O.lib.View = function($elem, props, controllerCb, env) {
	this.$elem = $elem;
	this.props = props;
	this.controllerCb = controllerCb;
	this.env = env;

	// TODO: deprecate and remove this._init (only used in a few views)

	if (typeof this._init === 'function') this._init(); // defined in child
};

O_O.lib.View.prototype = {
	/* constants */

	LOADING_HTML: '<div class="loading-c2"><div class="loading-bar"/></div>',

	EDIT_BUTTON_CN: 'gearHolder', // corresponds to css edit widget toggle

	TPL_WIDGET_DIR: '/js/2.0/widget/',
	TPL_FILE: '/templates.html',
	TPL_RETRY_DELAY: 10, // ms

	/* state */

	$elem: null,
	props: null,
	controllerCb: null,
	env: null,

	/* utils */

	// TODO: deprecated (replace with just this.name)

	prop: function(name) {
		return this.props[name];
	},

	_clearElem: function() {
		this.$elem.children(':not(.' + this.EDIT_BUTTON_CN +')')
			.remove(); // clear widget contents except edit toggle
	},

	/* widget config */

	$editButton: null,

	toggleEdit: function(f_toggle, dfn) {
		if (!this.$editButton) {
			var name = dfn.widgetName || dfn.widget,
				desc = dfn.widgetDesc || '',
				tooltip = '';
			if (name) tooltip += '<h1>' + ((typeof name.default === 'undefined') ? name : name.default) + '</h1>';
			if (desc) tooltip += '<p>' + ((typeof desc.default === 'undefined') ? desc : desc.default) + '</p>';
			if (tooltip) tooltip = '<div class="tooltip">' + tooltip + '</div>';

			this.$editButton = $('<div class="gearHolder"><button class="gear">edit' + tooltip + '</button></div>')
				.onC('click', this._editButtonCb, this)
				.prependTo(this.$elem);
		}

		this.$elem.toggleClass('editMode', f_toggle);
	},

	toggleEditing: function(f_toggle) {
		this.$elem.toggleClass('editing', f_toggle);
	},

	_editButtonCb: function() {
		this._gaEvent('Edit Widget', this.props.name, 'start');
		this.controllerCb('editWidget', {view: this});
	},

	/* shell commands */

	destroy: function() {
		this.$elem.off().empty();

		for (var k in this) {
			if (this.hasOwnProperty(k)) k = null;
		}
	},

	/* models */

	// TODO: deprecate and remove these (only used in a few widgets)

	modelSubCb: function() { // defined in child
		throw new Error(this.prop('name') + '.View.modelSubCb not ' +
			'implemented properly');
	},

	subModels: function() {
		var models = arguments;
		for (var i = 0, l = models.length; i < l; i++) {
			models[i].sub(this.modelSubCb, this);
		}
	},

	unsubModels: function() {
		var models = arguments;
		for (var i = 0, l = models.length; i < l; i++) {
			models[i].unsub(this.modelSubCb, this);
		}
	},

	/* templates */

	tpls: null,

	_tplNoDfr: function(name, args) {
		if (typeof args.tpl === 'undefined') {
			args.tpl = scopeC(function(name, args) {
				return this._tplNoDfr(name, args);
			}, this);
		}

		var key = this.props.name + '_' + name;
		return this.tpls[key](args); // bare value (not deferred object)
	},

	tpl: function(name, args, cb) {
		if (typeof args.tpl === 'undefined') {
			args.tpl = scopeC(function(name, args, cb) {
				return this._tplNoDfr(name, args, cb); // for in-tpl recursion
			}, this);
		}

		var $dfd = $.Deferred();

		var key = this.props.name + '_' + name;
		var resolve = scopeC(function() {
			var tpl = this.tpls[key];
			$dfd.resolve(tpl(args));
		}, this);

		if (!this.tpls) {
			this.tpls = {};

			var memoize = scopeC(function($sA) {
				var i, l, $s;
				for (i = 0, l = $sA.length; i < l; i++) {
					$s = $($sA[i]);
					this.tpls[$s.attr('id')] = O_O.tpl($s.html());
				}
				resolve();
			}, this);

			// check smarty template for tpls that came with init
			var $scriptA = $('[id ^= "' + this.props.name + '_"]');
			if ($scriptA.length > 0) {
				memoize($scriptA); // no need to load other files
			}
			else {
				var f;
				if (typeof this.props.version === 'undefined') {
					f = this.TPL_WIDGET_DIR + this.props.name + this.TPL_FILE;
				}
				else {
					f = this.TPL_WIDGET_DIR + this.props.name + '/' +
						this.props.version + this.TPL_FILE;
				}
				var $temp = $('<div>').load(f, scopeC(function() {
					$scriptA = $temp.children('script');

					if (!$scriptA.length) {
						throw new Error('No tpls found for ' + this.props.name);
					}

					$temp = null;
					memoize($scriptA);
				}, this));
			}
		}
		else if (!this.tpls[key]) {
			var retry = setInterval(scopeC(function() {
				if (this.tpls[key]) {
					clearInterval(retry);
					resolve();
				}
			}, this), this.TPL_RETRY_DELAY); // retry if mid-load
		}
		else {
			resolve();
		}

		return $dfd.promise().done(scopeC(function(tplHtml) {
			if (typeof cb === 'function') {
				cb(tplHtml);
			}
			else if (typeof cb !== 'undefined') {
				throw new Error('Optional callback for tpl must be a function');
			}
		}, this));
	},

	_gaEvent: function(cat, act, lbl) {
		try {
			PSGA.distributeToGaTrackers('send', 'event', {
			    eventCategory: cat,
			      eventAction: act,
			       eventLabel: lbl
			});
		}
		catch(err) {}
	}
};

}(ps$));
(function ($) {
//'use strict';

O_O.model = {};
O_O.model._env = {
	api_key: '',
	f_edit: false
};

//namespace for request mixins
O_O.model.requestBuilders = {};

O_O.model.setEnv = function (env)
{
	$.extend(O_O.model._env, env);
};

O_O.model.getEnv = function (key)
{
	var env = O_O.model._env;
	if (!env) throw 'Model._env not defined';
	if (!isset(env[key])) console.log("WARNING: Model environment variable '"+key+"' not set.");
	
	return O_O.model._env[key];
};

O_O.model.get = function (type, id)
{
	if (!type) throw "O_O.model.get: type required";
	
	if (!isset(O_O.model.modelCache)) O_O.model.modelCache = O_O.mm.oNew(O_O.model.ModelCache);
	type = type[0].toUpperCase() + type.slice(1);
	//validate that model type exists
	if (!O_O.model[type]) throw "O_O.model.get: Unknown Model Type: " + type;
	
	//for singleton models with only one ID (eg, PortfolioGalleries)
	if (O_O.model[type].prototype.STATIC_ID) id = O_O.model[type].prototype.STATIC_ID;
	
	var m = O_O.model.modelCache.getModel(type, id);
	if (!m) {
		m = O_O.mm.oNew(O_O.model[type], id)
		if (m.getUniqStr()) O_O.model.modelCache.addModel(m);
	}	
	
	return m;
};

O_O.model.Model = function (identifier, data)
{
	//identifier can be a string or an object
	
	if (!isset(identifier)) {
		console.log(this+': id undefined!');
		console.trace();
	}

	this._id = (typeof identifier === 'object' ? identifier : {id: identifier});
	this._data = data || null;
	this._subA = [];
	this._setPosH = {};
	
	this._subChildren();
};
O_O.model.Model.prototype = {
	type: 'Model',
	//API details vary with implementation
	_adapter: null,
	_api_root: null,
	_endpoint: null,
	_auth_prefix: null, 
	//
	_id: null,
	_f_short: true,
	_f_dirty: false,
	_f_connected: true,
	_f_connectable: true,
	_schema: null, //data schema
	_data: null,
	_data_orig: null,
	_subA: null,
	_CHILD_MODELS: null,
	//constants
	
		
	_delete: function ()
	{
		for (var i=0, l=this._subA.length, t; i<l; i++) {
			t = this._subA[i];
			t[0] = t[1] = null;
			delete t;
		}
		for (var k in this._setPosH) this._setPosH[k] = null;
		
		O_O.mm.delArr(this._subA);
		O_O.mm.delArr(this._data);
		this._data = this._subA = this._schema = this._setPosH = null;
	},
	
	/******* On/Offlining Funcs */
	
	is_connected: function ()
	{
		return this._f_connected && this._f_connectable;
	},
	
	disconnect: function ()
	{
		if (!this.is_connected()) return;
		
		this._f_connected = false;
		this._data_orig = this._data;
		//TODO: don't make this a deep extend for sets
		this._data = $.extend(true, {}, this._data);
	},
	
	reconnect: function ()
	{
		this._f_connected= true;
		this._data_orig = null;
		if (this._f_dirty) return this.update(this._data);
		else return $.Deferred().resolve();
	},
	
	revert: function ()
	{
		if (this.is_connected()) throw this + ":revert() - can only revert while disconnected";
		
		this._f_dirty = false;
		//TODO: don't make this a deep extend for sets
		this._data = $.extend(true, {}, this._data_orig);
		this._pub();
	},
	
	/******* Model data request Funcs */
	
	_request: function (opts)
	{
		return this._adapter.request(this._buildRequest(opts));
	},
	
	_buildRequest: function (user_opts)
	{
		throw 'Model._buildRequest unimplemented';
	},
	
	load: function (user_opts)
	{
		throw 'Model._load unimplemented';
	},

	//Experimental shortcut - should be refactored out through better model usage patterns
	extractPrimitiveData: function () {
		return $.extend(true, {}, this._data);
	},
		
	assignDataIfNewer: function (data)
	{
		if (!this._data) this._data = data;
		else {
			//mtime / dirtiness comparison magic here
			this._data = data;
		}		
		
		this._pub();
	},
	
	/******* Introspection **/
		
	getId: function () { return this._id.id; },
	_setId: function (id) { return this._id.id = id; },
	
	getTuple: function () { return $.extend({}, this._id); },
	
	/* Override this if your model is identified by a tuple object*/
	getUniqStr: function () { return this._id.id; },
	
	getType: function () { return this.type; },
	
	toString: function () {
		return 'Model ' + this.type + '['+this.getUniqStr()+']';
	},
	
	/******* Subscription Logic **/
	
	regSetPos: function (id, pos) { this._setPosH[id] = pos; },
	unregSetPos: function (id) { delete this._setPosH[id]; },
	getSetPos: function (id) { return this._setPosH[id]; },
	
	sub: function (cb, scope, range)
	{
		var sA = this._subA;
		var subA = [cb, scope, range];
		for (var i=0, l=sA.length; i<l; i++) {
			if (sA[i][0] == cb && sA[i][1] == scope) {
				sA[i] = subA;
				return; //overwrite duplicate subs
			}
		}
		sA.push(subA);
	},
	
	unsub: function (cb, scope)
	{
		for (var i=0, l=this._subA.length, t; i<l; i++) {
			t = this._subA[i];
			if (t[0] != cb || t[1] != scope) continue;
			this._subA.splice(i, 1);
			i--, l--;
			delete t;
		}
	},
	
	_pub: function (change_range)
	{
		var cd = this._data;
		cd = $.extend((is_array(cd) ? []: {}), cd);
		//TODO: record data age and only notify out of date subscribers?
		this._pubraw(cd, {model: this, change_range: change_range});
	},
	
	_pubraw: function (data, opt)
	{
		var sub, sub_range, rstart, rend, cstart, cend, limit, sub_opt, req_data, pag_opts, page_fmt;

		for (var i=0, l=this._subA.length; i<l; i++) {
			sub = this._subA[i];
			sub_range = sub[2];
			req_data = data;
			
			/* Pagination is a bit broken atm
			// TODO Abstract range items out into pagination mixin 
			if (opt.change_range && !sub_range) {
				var cr = opt.change_range;
				sub_range = {offset: cr.start, limit: (cr.end - cr.start) + 1 };
			}
			
			// TODO Abstract range items out into pagination mixin
			if (sub_range) { //is this sub event range based
				sub_opt = $.extend({}, opt);
				rstart = sub_range.offset;
				rend = rstart + sub_range.limit;
				if (!opt.path) { //is local event?
					if (is_array(data)) {
						sub_opt = $.extend({}, opt);
						if (opt.change_range) {
							cstart = opt.change_range.start;
							cend = opt.change_range.end;
							//calc if ranges interesect, publish
						
							if (!((cstart < rend && cend >= rend) ||
							      (rstart < cend && rend > cend) || 
							      (cstart <= rstart && rstart < cend) || 
							      (rstart < cstart && cstart < rend))) {
							 	continue; //no intersection
							}
						}
				
						pag_opts = {
							offset: sub_range.offset,
							limit: sub_range.limit,
							total: data.length
						}
						page_fmt = this._pageDataFmt(req_data, pag_opts);
						req_data = page_fmt[0];
						sub_opt.pag_opts = pag_opts;
					} //else drop through
				} else {
					var pos = opt.model.getSetPos(this.getUniqStr());
					if (!isset(pos)) {
						console.log(opt.model);
						throw this + '._pubraw(): child has no recorded position';
					}
					if (!(pos >= rstart && pos < rend)) continue; // not in range
				}
				this._subA[i][0].call(sub[1], req_data, sub_opt);
			} else */
				this._subA[i][0].call(sub[1], req_data, opt);
		}
	},
	
	_subChildren: function ()
	{
		if (!this._CHILD_MODELS || !this.getTuple()) return;
		var cM, cA = this._CHILD_MODELS;
		
		for (var i = 0, l = cA.length; i < l; i++) {
			cM = cA[i];
			if (!isset(O_O.model[cM])) throw this + "._subChildren(): O_O.model." + cM + " isn't defined";
			cM = O_O.model.get(cM, this.getTuple());
			cM.sub(this._childEventCB, this);
		} 
	},
	
	_childEventCB: function (data, opt)
	{
		if (opt && opt.path && opt.path.length > 30) {
			console.log(opt);
			throw this + '._childEventCB(): event path too long, check for cycle';
		}
		
		//clone for current context
		opt = $.extend({}, opt);
		if (!opt.path) opt.path = [opt.model];
		else opt.path = $.extend([], opt.path);
		
		opt.path.unshift(this);
		this._pubraw(data, opt);
	}
};

//helper function to make schema accessors
//TODO audit usage
O_O.model.mixinModelSchema = function (model) 
{
	var fname, s = model.prototype._schema;
	if (!s) return;
	for (var f in s) {
		fname = 'get'+f[0].toUpperCase() + f.slice(1); //poor man's camel case
		model.prototype[fname] = O_O.model._mixinModelSchemaCloser(f);
	}
};
//helper for the helper
O_O.model._mixinModelSchemaCloser = function (field) {
	return function () { return this.getDataField(field); }
}

O_O.model.ModelCache = function ()
{
	this.typeA = {};
};
O_O.model.ModelCache.prototype = {
	typeA: null,
	_delete: function ()
	{
		for (var t in this.typeA) {
			if (!this.typeA.hasOwnProperty(t)) continue;
			O_O.mm.delArr(this.typeA[t]);
		}
	},
	
	addModel: function (model)
	{
		var k, t = model.type;
		if (!isset(this.typeA[t])) this.typeA[t] = {};
	
		if ((k = model.getUniqStr()))
			this.typeA[t][k] = model;
		else throw "ModelCache.addModel(): " + model + " has no data, can't add.";
	},
	
	removeModel: function (model)
	{
		var k, t = model.type;
		if (!isset(this.typeA[t])) this.typeA[t] = {};
	
		if ((k = model.getUniqStr())) delete this.typeA[t][k];
		else throw "ModelCache.addModel(): " + model + " has no data, can't remove.";
	},
	
	getModel: function (type, identifier) 
	{
		if (!O_O.model[type]) throw "O_O.model.get: Unknown Model Type: " + type;
		var id_obj = (typeof identifier === 'object' ? identifier : {id: identifier});
		
		//Fake a model object to use UniqStr function ... need to find a nicer way to do this
		var id_str = O_O.model[type].prototype.getUniqStr.call({_id: id_obj});
		if (!id_str) console.log('WARNING: ' + type + '.getUniqStr() returning undefined for identifier ', identifier);
		
		if (isset(this.typeA[type]) && isset(this.typeA[type][id_str])) return this.typeA[type][id_str];
		else return false;
	}
};

/*** Model plurality mixins **/

O_O.model.ModelSolo = function () { };
O_O.model.ModelSolo.prototype = {
	load: function (user_opts)
	{
		var default_opts = {
			/*optional*/
			f_force: false, 
			fv: {fields: '*'},
			f_static: false
		};

		// allow models to set their own default fv
		var default_fv = this.default_fv || {};
		$.extend(default_opts.fv, default_fv);

		var opts = $.extend(true, default_opts, user_opts);
		
		return this._getLoad(opts);
	},
	
	_getLoad: function (user_opts)
	{
		var default_opts = {
			/*required*/
			tail: null,
			/*optional*/
			f_force: false, 
			fv: {fields: '*'},
			f_static: true			
		};
		
		var opts = $.extend(default_opts, user_opts);
		
		var cache = this._data;

		if (cache && !opts.f_force && (!this._f_dirty || !this.is_connected())) {
			this._pub();
			return $.Deferred().resolve(cache).promise();
		} else {

			return this._request(opts).then(scopeC(function(d, fv) {
				this.assignDataIfNewer(d);
				return d; //strip out fv
			},this));
		}
	},
	
	
	insert: function (data)
	{
		if (this.getId()) throw this + '.insert(): record already exists!';
		
		var d = this._encodeFields(data);
		var adapter_params = {fv: d, tail: 'insert', f_auth: true, f_static: true};
		//TODO check for required fields
		//TODO validate data
		if (!this.is_connected())	throw this + "insert(): model disconnected, can't insert";
		else return this._request(adapter_params).pipe(scopeC(function(rdata) {
			d.id = this._setId(rdata.id);
			this.assignDataIfNewer(d);
			//meh, adding the model to the cache here seems a little messy. But where else could this be done?
			O_O.model.modelCache.addModel(this); 
			return this._data;
		}, this));
	},
	
	update: function (data)
	{

		//TODO check for required fields
		//TODO validate data
		
		if (!this.is_connected())	{
			this._f_dirty = true; 		
			return $.Deferred().resolve(this.updateCb(data));
		} else {
			var d = this._encodeFields(data);
			var adapter_params = {fv: d, tail: '/update', f_auth: true};
			var dfr	=  this._request(adapter_params).pipe(scopeC(function (resp, fv) {
				//merge updates into current data
				return this.updateCb(d);
			} , this));
			//only way to resuse updateCb and still reset dirtying - not sure its actually worth it.
			dfr.done(scopeC(function (data) { this._f_dirty = false; return data; },this));
			return dfr;
		}
	},
	
	updateCb: function(data) 
	{
		var dm = $.extend({}, this._data, data);
		this.assignDataIfNewer(dm);
		return this._data;
	},
	
	delete: function ()
	{
		var adapter_params = {tail: '/delete', f_auth: true};
		
		if (!this.is_connected())	throw this + "delete(): model disconnected, can't delete"
		else return this._request(adapter_params).pipe(scopeC(function(rdata) {
			//this._pub(); //need a separate deletion event?
			O_O.model.modelCache.removeModel(this); 
			this._delete();
			return true;
		}, this));
	},
	
	/******* Internal Data Handling  */
	
	_decodeFields: function (data) { return this._transformFields(data, 'decode'); },
	_encodeFields: function (data) { return this._transformFields(data, 'encode'); },
	
	_transformFields: function (original_data, mode)
	{
		var data = $.extend({}, original_data);
		var schema = this._schema;
		for (var k in schema) {
			if (!isset(data[k])) continue;
			var h = this._schemaTransformHelpers[schema[k]];
			if (h && h[mode]) {
				data[k] = h[mode](data[k]);
			}
		}
		
		return data;
	},
	
	_schemaTransformHelpers: {
		json: {
			encode: function (val) { return JSON.stringify(val); },
			decode: function (val) { return $.parseJSON(val); }
		}
	},
	
	assignDataIfNewer: function (data)
	{
		data = this._decodeFields(data);
		
		O_O.model.Model.prototype.assignDataIfNewer.call(this, data);
	},
	
	/** TODO audit usage and consider removal */
	getDataField: function (field)
	{
		if (!isset(this._schema[field])) throw this + ".getDataField(): Unknown Field Type: " + field;
		
		//determine shortness of field
		//determine cache status
		var opts = {};
		if (!isset(this._data[field])) opts.f_force = true;
		
		return this.load(opts).pipe(function (data) { return data[field];  });
	}
};

O_O.model.ModelSet = function () { };
O_O.model.ModelSet.prototype = {
	_setPosH: null,
	
	_delete: function ()
	{
		for (var k in this._setPosH) this._setPosH[k] = null;
		O_O.model.Model.prototype._delete.apply(this, arguments);
	},
	
	/******* Pagination Funcs */
	
	_pageDataFmt: function (data, page_info)
	{
		var pag_opts = {
			total: page_info.total,
			offset: page_info.offset,
			limit: page_info.limit
		}

		return [data.slice(pag_opts.offset, pag_opts.limit+pag_opts.offset), pag_opts];
	},
	
	//formerly _qryLoad
	//TODO split out pagination into sub-mixin
	load: function (user_opts)
	{
		var default_opts = {
			/*required*/
			query_label: null,
			modelcfg_cb: null,
			endpoint_tail: null,
			/*optional*/
			f_force: false, 
			offset: 0, 
			limit: null,
			fv: null,
			f_return_primitive_data: false,
			f_auth: false,
			f_sub_to_children: true
		};
		
		//if (this.f_qrying) return;
		var opts = $.extend(default_opts, user_opts);
				
		if (opts.f_return_primitive_data) {
			opts.f_return_primitive_data = false;
			return this.load(opts).then(function (data, fv) {
				var primA = [], m;
				for (var i = 0, l = data.length; i < l; i++) {
					m = data[i];
					primA[i] = $.extend(true, {}, m._data);
				}
				return $.Deferred().resolve(primA, fv); 
			});
		}		

		var cache = this._data;
		var start, end, miss_start, miss_end, lookahead;
		
		var f_paging = !!opts.limit;
		
		if (f_paging) {
			
			//TODO: proper lookaheads
			//lookahead = Math.floor(opts.limit / 2);
			lookahead = 0;
			start = Math.max(opts.offset - lookahead, 0);
			end = start + (opts.limit + lookahead);
			
			if (!opts.f_force && cache) {
				
				//check for cache presence
				end = Math.min(end, cache.length)
			
				for (var i = start; i < end; i++) {
					if (!cache[i]) {
						if (!isset(miss_start)) miss_start = i;
						miss_end = i;
					}
				}
			
				if (isset(miss_start)) {
					start = miss_start;
					end = miss_end;
				}
			}
		} 
				
		//do more substantive qry cache lookup here?
		if (cache && !isset(miss_start) && !opts.f_force) {
			this._pub();
			if (f_paging) {
				opts.total = cache.length;
				var pageData = this._pageDataFmt(cache, opts);
				//may need to merge page_opts into data array instead - I don't like this nested array
			    return $.Deferred().resolve(pageData[0], pageData[1]).promise(); 
			} else  return $.Deferred().resolve(cache, opts.fv);
		} else {
			
			if (f_paging) {
				var ppg = end - start;
				var pg;
				
				//page fitting logic
				pg = Math.floor(start / ppg);
				
				if (Math.floor(start / ppg) != Math.floor(end / ppg)) {
					ppg *= 2;
					Math.floor(pg/2);
				}

				if (ppg > start) {
					pg = 0;
					ppg += start; 
				} 
				
				//TODO By passing page fitting for now, buggy
				 $.extend(opts.fv, {page: 1, ppg: opts.limit, limit: opts.limit, offset: opts.offset});
				/*
				opts.fv = {page: pg + 1, ppg: ppg,
						limit: opts.limit, offset: opts.offset};*/
			}
			//this.f_qrying = true;
			var adapter_params = {fv: opts.fv, tail: opts.endpoint_tail, f_static: true, f_auth: opts.f_auth};
			var child_sub = opts.f_sub_to_children;
			return this._request(adapter_params).pipe(scopeC(function(data, fv) {
				//this.f_qrying = false;
				
				var cache = this._data;
				//init - consider doing this init elsewhere
				if (!cache) this._data = cache = []; 
				var qry_data = data[opts.query_label];
				var total = data.total || qry_data.length;
				var start = (fv.ppg * (data.paging && data.paging.page-1)) || 0;
				
				//unsubscribe from old qry set children that won't be checked against new data
				if (total == qry_data.length && total < cache.length) {
					//splice off excess
					var overflow = cache.splice(total, cache.length);
					//unsub
					for (var i = 0, l = overflow.length; i < l; i++) {
						overflow[i].unsub(this._childEventCB, this);
						overflow[i].unregSetPos(this.getUniqStr());
					}
				}
				
				//expand data array when short
				if (total > cache.length) cache[total-1] = false;
					
				var d, t, new_model, current_model, change_start, change_end, iofs;
				for (var i = 0, l = qry_data.length; i < l; i++) {
					iofs = i + start;
					d = qry_data[i];
					//modelcfg_cb returns a model instance and the data to populate it with
					//kinda clunky, but not sure how else to sub to model
					//before updating its data
					t = opts.modelcfg_cb.call(this, d);
					new_model = t[0];
					current_model = cache[iofs];
					if ((current_model && current_model.getId() != new_model.getId()) 
					 || !current_model) {
						if (!isset(change_start)) change_start = iofs;
						change_end = iofs;
						
						//FIXME figure out better way to deal with cyclic events instead of sweeping it under the rug
						if (child_sub) new_model.sub(this._childEventCB, this);
						cache[iofs] = new_model;
						new_model.regSetPos(this.getUniqStr(), iofs);
						if (current_model) current_model.unregSetPos(this.getUniqStr());
						//must do this last since it triggers events
						new_model.assignDataIfNewer(t[1]);					
					}
				}
				this._pub({start: change_start, end: change_end});

			    	if (f_paging) {
					fv.total = total;
					fv.offset = fv.offset;
					fv.limit = fv.limit;
					var pageData = this._pageDataFmt(cache, fv);
					return $.Deferred().resolve(pageData[0], pageData[1]).promise();
			    	} else 
					return $.Deferred().resolve(cache, fv);


			},this));
		}

	}
};

O_O.model.batchDfrAction = function (modA, action, argA, errorF) {
	var loadA = [], mod, dfr;
	
	for (var i = 0, l = modA.length; i < l; i++) {
		mod = modA[i];
		dfr = mod[action].apply(mod, argA);
		if (errorF) dfr = dfr.then(null, O_O.model._batchErrorH);
		loadA.push(dfr);
	}
	
	return $.when.apply($, loadA).then(function() {
		var resultA = Array.prototype.slice.apply(arguments);
		var dataA = [], result;
		if (errorF) {
			for (var i = 0, l = resultA.length; i < l; i++) {
				result = resultA[i];
				if (result.__type == 'error') errorF(result.args);
				else dataA.push(result);
			}
			resultA = dataA;
		}
		
		return resultA;
	});
};

O_O.model._batchErrorH = function(errA) {
	//repackage error so it can be aggregated with $.when later
	return $.Deferred().resolve({__type: 'error', args: errA}).promise();
};

//TODO: Move Adapters into own file
/*** Adaptor Abstract Class ***/
O_O.adapter = {};
O_O.adapter.singletons = {};

O_O.adapter.Adapter = function ()
{
	this.dfrHash = {};
};
O_O.adapter.Adapter.prototype = {
	dfrHash: null,

	request: function ()
	{
		throw 'Adapter.request is abstract: Implement your api details here and return a deferred object';
		//return $.Deferred().resolve().promise()
	}
};

})(ps$);
(function ($) {
'use strict';

O_O.model.requestBuilders.PSApi_IDEndpoint = function () {};
O_O.model.requestBuilders.PSApi_IDEndpoint.prototype = {
	_endpoint: 'endpoint',
	
	_buildRequest: function (user_opts)
	{
		var opts = $.extend({
			fv: {},
			tail: '',
			f_auth: false,
			f_static: false
		}, user_opts);
		
		var params = {};
		params.fv = opts.fv;
		params.path = this._api_root;
		if (opts.f_auth) params.path += this._auth_prefix;
		params.path += this._endpoint + '/';
		if (!opts.f_static) params.path += this.getId();
		if (opts.tail) params.path += opts.tail;
		return params;
	}
};

O_O.model.PSModel = function (identifier, data)
{
	O_O.model.Model.call(this, identifier, data);
	//TODO: Assign adapters more cleanly
	this._adapter = O_O.adapter.singletons.PSApi;
};
O_O.model.PSModel.prototype = {
	type: 'PSModel',
	_api_root: '/psapi/v2.0/',
	_auth_prefix: 'mem/'
};

O_O.obj.inherit(O_O.model.requestBuilders.PSApi_IDEndpoint, O_O.model.PSModel); //mixin
O_O.obj.inherit(O_O.model.Model, O_O.model.PSModel);

O_O.adapter.PSApi = function ()
{
	O_O.adapter.Adapter.apply(this, arguments);
};
O_O.adapter.PSApi.prototype = {
	
	request: function (opts)
	{
		var dfr = $.Deferred();
		var fv = opts.fv;
		
		if (!fv) fv = {};
		fv.api_key = O_O.model.getEnv('api_key');
		
		var hash = opts.path;
		for (var k in fv) {
			if (fv.hasOwnProperty(k))
				hash += k+fv[k];
		}
		
		//prevent duplicate simultaneous queries
		var dfr_in_progress = this.dfrHash[hash];
		if (dfr_in_progress) return dfr_in_progress;
		else this.dfrHash[hash] = dfr;
		
		var cont = {
			fv: fv,
			dfr: dfr,
			hash: hash
		};
		
		$.ajax({
			url: opts.path,
			data: fv,
			type: 'post',
			dataType: 'json',
			bsapiCB: scopeC(this._actCB, this),
			bsapiErr: scopeC(this._bsapiErrCB, this),
			onTransportErr: scopeC(this._bsapiTransportErrCB, this),
			cont: cont
		});
		
		return dfr.promise();
	},
	
	_dfrHashCleanup: function (settings)
	{
		if (settings.cont.hash)
			delete this.dfrHash[settings.cont.hash];
	},
	
	_actCB: function (data, settings)
	{
		this._dfrHashCleanup(settings);
		
		var dfr = settings.cont.dfr;
		dfr.resolve(data, settings.cont.fv);
	},
	
	_bsapiErrCB: function (data, settings)
	{
		this._dfrHashCleanup(settings);
		
		var cont = settings.cont;
		var dfr = cont.dfr;
		
		//TODO: Handle this at the app level
		if (data && data['class'] === "SessionRequiredErr") {
			alert(data.message.split('<!')[0]);
			return location.reload(true);
		}
		
		if (!is_array(data)) data = [data];

		// // add default handler
		// if (!settings.f_fail) {
		//	dfr.fail(psApp.error);
		// }
		dfr.reject(data);
	},
	
	_bsapiTransportErrCB: function (evt, xhr, settings, excpt)
	{
		this._dfrHashCleanup(settings);
		
		var cont = settings.cont;
		var dfr = cont.dfr;
			
		dfr.reject([{'class': "TransportErr", 'message': excpt.message, 'var': excpt}]);
	}
};
O_O.obj.inherit(O_O.adapter.Adapter, O_O.adapter.PSApi);

O_O.adapter.singletons.PSApi = O_O.mm.oNew(O_O.adapter.PSApi);


/* ==== Utility Mixin for Models - should be partially outmoded with API v3 ==== */

/* ImageCompoundMixin - Used by GalleryKeyImage, CollectionKeyImage		 */
/* and StaticImage to keep ImgRec's distinct but maintain Img URL uniqueness	 */
/* that can occur within each of those image classes.				 */
O_O.model.ImageCompoundMixin = function (id, data) { };

O_O.model.ImageCompoundMixin.prototype = {
	
	assignDataIfNewer: function (data, channel)
	{
		var host_data = {};
		var image_data = null;
		var v;
		for (var k in data) {
			if (data.hasOwnProperty(k)) {
				v = data[k];
				if (isset(this._schema[k])) host_data[k] = v;
				else {
					if (!image_data) image_data = {};
					image_data[k] = v;
				}
			}
		}
		
		//TODO: implement this remapping in the API
		host_data.image_id = data.id;
		
		O_O.model.PSModel.prototype.assignDataIfNewer.call(this, host_data);
		
		if (image_data) {
			var img = this.getImageModel();
			img.sub(this._childEventCB, this);
			img.assignDataIfNewer(image_data);
		}
	},
	
	getImageModel: function ()
	{
		if (!this._data) throw this + ':getImageModel() called before model loaded';
		return O_O.model.get('Image', this._data.image_id);
	},
	
	//TODO: Deprecate in favor of direct model usage
	getLinkForSize: function (user_opts)
	{
		var merged_data = $.extend({}, this.getImageModel().extractPrimitiveData(), this._data);
		return O_O.app.imgMkUrlFromData(user_opts, merged_data);
	}
};

}(ps$));;(function($, window, undefined) {
'use strict';

O_O.app.Shell = function(elemId, cfg, opt, type) {
	this.$elem = $('#' + elemId);

	this.themeCfg = cfg.themeCfg;
	this.widgetVers = cfg.widgetVers;
	this.customEnv = cfg.customEnv;
	this.modeTpls = cfg.modeTpls;

	this.f_edit = opt.f_edit;
	this.f_debug = opt.f_debug;

	if (isset(opt.themeList))
		this.themeList = opt.themeList;

	if (isset(type)) {
		this.type = type;
	}

	this.path_theme = this.PATH_ROOT + this.type;

	var reqs = this._serializeReqs();
	O_O.lib.Util.loadFiles(reqs, !this.f_debug, this.customEnv.server.js)
		.done(scopeC(this._init, this));
};

O_O.app.Shell.prototype = {
	/* constants */

	CSS_CLASS_LOAD: 'loading-c2',
	CSS_CLASS_SITE: 'site',

	HASH_DEFAULT_MODE: 'index',
	HASH_DEFAULT_FADE: 200,

	MSG_SYS_NAME: 'C2',
	MSG_SYS_CHAN: 'Shell',

	CSS_ID_APP_SETTINGS: 'c2settings',
	CSS_ID_APP_CONTENT: 'c2content',
	CSS_CLASS_THEFT_GUARD: 'prevent-action',

	SETTINGS_WIDGET: 'Settings',
	SETTINGS_EXPANDED: 'editMode',

	GLOBAL_MODE: 'global',
	DELAY_SWITCH: 200, // delay before mode-switch spinner appears

	CSS_CLASS_HIDDEN: 'hidden',
	CSS_CLASS_LOADED: 'loaded',

	CFG_UPD_URL: '/psapi/v2/mem/user/theme/element/update',
	CFG_UPD_KEY: 'PS631731c7', // FIXME

	URL_THEME_DEF: '/psapi/v2/theme/',

	CFG_TYPE_THEME: 'THEME',
	CFG_TYPE_MODE: 'MODE',
	CFG_TYPE_WIDGET: 'WIDGET',

	// keys that are filtered out of config changes made via settings widget
	//CFG_RESERVED_KEYS_THEME: ['name', 'version', 'requires', 'start', 'modes'],
	CFG_RESERVED_KEYS_THEME: ['requires', 'start', 'modes'],
	CFG_RESERVED_KEYS_MODE: ['widgets', 'definition'],
	// CFG_RESERVED_KEYS_WIDGET: ['widget'], // unused

	PATH_ROOT: 'js/2.0/',
	PATH_WIDGET: 'js/2.0/widget',

	PREFIX_MODE: 'O_O.mode',
	PREFIX_WIDGET: 'O_O.widget',

	DELAY_RESIZE: 200,
	DELAY_SCROLL: 50,

	/* state */

	// TODO: these are all private and should be prefixed by underscores

	$elem: null,
	themeCfg: null,
	themeDfn: null,
	themeList: null,
	customEnv: null,
	modeTpls: null,
	f_edit: null,
	f_debug: false,

	type: 'theme',	// default
	path_theme: null,
	msgSys: null,
	env: null,
	$win: null,
	modeStack: null,
	_firstMode: null,
	$busy: null,

	$settings: null,
	settingsWidget: null,

	modes: null,
	_timerSwitch: null,

	hashDisabled: null,
	timerResize: null,
	timerScroll: null,
	scrollTop: null,
	scrollLeft: null,

	_cfgs: null,

/*******************************************************************************
	APP-WIDE ENVIRONMENT VARIABLES
*******************************************************************************/

	getEnv: function(k, opt) {
		if (k === '#c1_url') return this._env_c1_url(opt); // macros
		return this._getEnv(k);
	},

	_getEnv: function(k) {
		try {
			if (typeof this.env !== 'object') {
				throw new Error('Object this.env does not exist in Shell');
			}
			if (k && typeof this.env[k] === 'undefined') {
				throw new Error('Environment variable ' + k + ' not set');
			}
		} catch (e) {
			console.log(e.name + ': ' + e.message); // FIXME: implement
			return;
		}

		if (k) return this.env[k];
		return this.env;
	},

	_setEnv: function(k, v, f_overwrite) {
		try {
			if (typeof this.env === 'undefined') {
				throw new Error('Object this.env does not exist in Shell');
			}
			if (typeof this.env[k] !== 'undefined' && !f_overwrite) {
				throw new Error('Environment variable ' + k + ' already set');
			}
		} catch (e) {
			console.log(e.name + ': ' + e.message); // FIXME: implement
			return;
		}

		this.env[k] = v;
	},

	/* macros */

	_env_c1_url: function(d) {
		if (empty(d.id)) return '#';

		var url = '';

		// HACK: infer record type from ID prefix
		switch (d.id.charAt(0)) {
		case 'C':
		case 'G':
			url = (d.id.charAt(0) === 'C' ? '/gallery-collection/' :
				'/gallery/');
			if (!empty(d.name)) url += (O_O.str.dashify(d.name) + '/');
			url += d.id;
			break;
		case 'I':
			if (!empty(d.g_id)) {
				url = '/gallery-image/';
				if (!empty(d.g_name)) url += O_O.str.dashify(d.g_name);
				url += ('/' + d.g_id + '/');
			}
			else {
				url = '/image/';
			}
			url += d.id;
			if (!empty(d.c_id)) url += '/' + d.c_id;
			break;
		}

		return url;
	},

/*******************************************************************************
	HASHPATH
*******************************************************************************/

	_route: function(url) {
		url = url || this.parseUrl();

		var argA = !!url.argA.length ? url.argA : undefined;

		if ((url.argA.length < 1) && url.modeName === this.modeStack.current) {
			var mode = this.modes[url.modeName];
			if (typeof mode.resetState === 'function') mode.resetState();
		}

		this._trackEvent('route'); // TODO: abstract to event listener
		return this._switchMode(url.modeName, 0, this.HASH_DEFAULT_FADE, argA);
	},

	_updatePath: function(modeName, argA) {
		var modeName = modeName || this.HASH_DEFAULT_MODE,
		    query = location.search,
		    path = O_O.lib.Util.makePath(modeName, argA, query),
		    url = this.parseUrl();

		if ((location.pathname + query) !== path) {
			// to avoid creating loops that prevent backwards navigation,
			// legacy (hashbang) urls should be replaced
			if (url.legacy || url.pathname === '/') window.history.replaceState({}, '', path);
			else window.history.pushState({}, '', path);
		}
	},

	parseUrl: function(target) {
		return O_O.lib.Util.parseUrl(target);
	},

	// deprecated: breaks settings widget, no longer needed
	_resetHash: function() {},

/*******************************************************************************
	SHELL INIT & RESET
*******************************************************************************/

	_serializeReqs: function() {
		var reqA = [];

		var props, name, includes, vendor, fileA, i, l, venA = [];

		for (var k in O_O.widget) {
			if (O_O.widget.hasOwnProperty(k)) {
				props = O_O.widget[k];

				name = props.name;
				includes = props.includes;
				vendor = props.vendor;

				fileA = [];
				for (i = 0, l = includes.length; i < l; i++) {
					fileA.push(this.PREFIX_WIDGET + '.' + name + '.' +
						includes[i]);
				}
				reqA.push({
					path: 'widget' + '/' + name + '/' + this.widgetVers[name],
					fileA: fileA,
					failMsg: 'Shell failed to load includes for widget ' + name
				});

				if (vendor) {
					for (i = 0, l = vendor.length; i < l; i++) {
						if (venA.indexOf(vendor[i]) < 0) venA.push(vendor[i]);
					}
				}
			}
		}

		if (venA.length) {
			reqA.push({
				path: null,
				fileA: venA,
				failMsg: 'Shell failed to load vendor includes for widgets'
			});
		}

		return reqA;
	},

	_initEnv: function() {
		this.env = {};

		// constants
		this._setEnv('edit', this.f_edit);
		this._setEnv('custom', this.customEnv);

		// variables
		this._setEnv('window', {
			width: this.$win.width(),
			height: this.$win.height()
		});

		this._initScreenSz();

		// modernizr (included on page separate from c2)
		if (typeof window.Modernizr === 'object') {
			this._setEnv('modernizr', window.Modernizr);
		}

		this._setEnv('phantom', window.callPhantom || false);
	},

	_initScreenSz: function() {
		var sz = O_O.screen.maxDim();
		var v = null;

		// size "detents" - correlated with bitshelter sizes
		var szA = [600, 1024, 1440, 2040];

		for (var i=0; i<szA.length; i++) {
			v = szA[i];
			if (sz <= szA[i]) break;
		}

		this._setEnv('max_img_sz', v);
	},

	_initSettings: function() {
		var $container = $('<div>')
			.attr('id', this.CSS_ID_APP_SETTINGS)
			.appendTo(this.$elem);

		this.$settings = $('<div>')
			.addClass(this.SETTINGS_WIDGET) // by convention
			.appendTo($container);

		var $dfd = this._loadWidget(this.SETTINGS_WIDGET);
		return $dfd.done(scopeC(function(props) {
			this.settingsWidget = O_O.mm.oNew(props.Controller, this.$settings,
				props, this.msgSys, scopeC(this.getEnv, this));
			this.settingsWidget.render(this.themeCfg, this.themeDfn, this.themeList);
		}, this));
	},

	_init: function() {
		if (this.f_edit) this._makeThemeCfg(this.themeCfg.name, this.themeCfg);

		this.msgSys = O_O.mm.oNew(O_O.com.MsgSys, this.MSG_SYS_NAME);
		this.msgSys.sub(this.MSG_SYS_CHAN, 'shell', this._msgCb, this);

		this.$win = $(window); // need window size (via env) for settings init
		this._initEnv();

		// check for preview mode
		if (this.getEnv('custom').f_preview && parent) {
			parent.ps$('body').trigger('previewLoaded'); // iframe parent
		}

		// get theme definition and load settings panel if edit mode
		if (this.f_edit) {
			this._getThemeDfn(this.themeCfg).done(scopeC(function(d) {
				this.themeDfn = JSON.parse(d.json);
				this._initSettings().done(scopeC(function() {
					this._initCont();
				}, this));
			}, this));
		}
		else {
			this._initCont();
		}
	},

	_initCont: function() {
		var $container = $('<div>')
			.attr('id', this.CSS_ID_APP_CONTENT)
			.addClass(this.CSS_ID_APP_CONTENT)
			.appendTo(this.$elem);
		this.modeStack = O_O.mm.oNew(O_O.lib.ModeStack, $container);

		// image-theft guard via touch event and right-click disable
		var noMask = this.getEnv('custom').site_cfg.SC_IMG_NO_MASK;
		if (!this.f_debug &&
			(typeof noMask === 'undefined' || noMask !== 't')) {
				$('body').addClass(this.CSS_CLASS_THEFT_GUARD); // for touch
				$container.on('contextmenu', function() {
					return false; // overridden in debug mode
				});
		}

		this._firstMode = true;
		this.$busy = $('.' + this.CSS_CLASS_LOAD + '.' + this.CSS_CLASS_SITE);

		// load mode from hash (default if bad or unspecified)
		this._route().fail(scopeC(function() {
			this._switchMode(this.themeCfg.start);
		}, this));

		document.addEventListener('click', this._clickCb.bind(this), false);
		window.addEventListener('update-mode', this._updateModeHandler.bind(this), false);
		// Pure JavaScript errors handler
		window.addEventListener('error', function (err) {
		    var lineAndColumnInfo = err.colno ? ' line:' + err.lineno +', column:'+ err.colno : ' line:' + err.lineno;
		    PSGA.distributeToGaTrackers('send', {
		        'hitType': 'event',
		        'eventCategory': 'JavaScript Error',
		        'eventAction': err.message,
		        'eventLabel': err.filename + lineAndColumnInfo + ' -> ' +  navigator.userAgent,
		        'eventValue': 0
		    });
		});
		this.$win
			.onC('popstate', this._popstateCb, this)
			.onC('hashchange', this._hashchangeCb, this)
			.onC('keydown', this._keydownCb, this)
			.onC('resize', this._resizeCb, this)
			.onC('scroll', this._scrollCb, this);
	},

	_reset: function() {
		var current = this.modeStack.getCurrent();

		for (var k in this.modes) {
			if (this.modes.hasOwnProperty(k)) {
				this.modes[k].destroy();
				this.modes[k] = null;
				delete this.modes[k];
			}
		}
		this.modes = null;

		this.modeStack.reset();
		this._resetHash(); // reset (will be rewritten when mode switches)
		return this._switchMode(current); // restart
	},

/*******************************************************************************
	MODES
*******************************************************************************/
	_updateModeHandler: function(event) {
		this._route();
	},

	_loadWidget: function(name) {
		var file = this.PREFIX_WIDGET + '.' + name;
		var props = O_O.lib.Util.getWinPropPath(file); // manifest

		//inject widget version into props
		props['version'] = this.widgetVers[name];

		// FIXME: widgets now load at runtime; this call no longer needed
		return new $.Deferred().resolve(props).promise();
	},

	_initMode: function(name, f_render) {
		if (!this.modes) {
			this.modes = {};
		}
		else if (this.modes[name]) {
			throw new Error('Cannot init mode ' + name + '; already exists');
		}
		else if (!this.themeCfg.modes[name]) {
			throw new Error('Cannot init mode ' + name + '; not specified');
		}

		var $dfd = new $.Deferred();
		var modeObj;

		var contCb = scopeC(function() {
			if (this.modeStack.modeExists(name)) {
				$dfd.resolve();
				return;
			}

			var $elem = this.modeStack.addMode(name);
			this.modes[name] = O_O.mm.oNew(modeObj, $elem, name, this.msgSys,
				scopeC(this.getEnv, this));

			var cfg = this.themeCfg.modes[name];
			if (this.f_edit) this._makeModeCfg(name, cfg);

			var init = this.modes[name].init(this.modeTpls[name], cfg,
				this.themeCfg.modes[this.GLOBAL_MODE]);
			if (!is_deferred(init)) {
				throw new Error('Mode init did not return Deferred object');
			}

			init.fail(function() {
				$dfd.reject();
			}).done(scopeC(function() {
				if (f_render) {
					this.modes[name].render().fail(function() {
						$dfd.reject();
					}).done(function() {
						$dfd.resolve();
					});
				}
				else {
					$dfd.resolve();
				}
			}, this));
		}, this);

		var modeDef = this.themeCfg.modes[name].definition;
		if (modeDef) {
			var path = this.path_theme + '/' + this.themeCfg.name;
			var file = this.PREFIX_MODE + '.' + modeDef;

			if (!O_O.lib.Util.getWinPropPath(file)) {
				O_O.lib.Util.loadFiles([{
					path: null,
					fileA: [path + '/' + file],
					failMsg: 'lolwat'
				}], !this.f_debug, this.customEnv.server.js).done(function() {
					modeObj = O_O.lib.Util.getWinPropPath(file);
					contCb();
				});
			}
			else {
				modeObj = O_O.lib.Util.getWinPropPath(file);
				contCb();
			}
		}
		else {
			modeObj = O_O.lib.Mode;
			contCb();
		}

		return $dfd.promise().done(scopeC(function() {
			if (this._firstMode) {
				this._firstMode = false;
				this.$busy.addClass(this.CSS_CLASS_HIDDEN)
					.addClass(this.CSS_CLASS_LOADED);
			}
		}, this));
	},

	_switchMode: function(name, delay, fade, argA) {
		try {
			if (name === this.GLOBAL_MODE) {
				throw new Error('Global mode is already called ' + name);
			}
			if (!this.themeCfg.modes[name]) {
				throw new Error('Mode ' + name + ' not specified in theme');
			}
		} catch (e) {
			console.log(e.name + ': ' + e.message); // FIXME: implement
			return new $.Deferred().reject().promise();
		}

		var oldMode = this.modeStack.current;
		var $dfd = new $.Deferred();

		// used only on a given mode's initial load
		var cancelBarCb = scopeC(function() {
			if (this._timerSwitch) {
				clearTimeout(this._timerSwitch);
				this._timerSwitch = null;
				this.$busy.addClass(this.CSS_CLASS_HIDDEN);
			}
		}, this);

		// continue after branching (below) for possible mode loading
		var contCb = scopeC(function() {
			var d = delay ? parseInt(delay, 10) : 0;
			var f = fade ? parseInt(fade, 10) : 0;

			// HACK: setTimeout ensures new mode added to DOM so switchMode
			// resolves properly (had issues with no second transitionend)
			setTimeout(scopeC(function() {
				this.modeStack.switchMode(name, d, f).always(cancelBarCb)
					.fail(function() {
						$dfd.reject();
					}).done(function() {
						$dfd.resolve();
					});
			}, this), 0);
		}, this);

		// load or render mode before switching if needed
		if (!this.modeStack.modeExists(name)) {
			// progress bar appears if initial load takes too long
			if (!this._firstMode) {
				this._timerSwitch = setTimeout(scopeC(function() {
					this.$busy.removeClass(this.CSS_CLASS_HIDDEN);
				}, this), this.DELAY_SWITCH);
			}

			// init mode and roll up render into deferred (true)
			this._initMode(name, true).fail(function() {
				cancelBarCb();
				$dfd.reject();
			}).done(contCb);
		}
		else if (!this.modes[name].isRendered()) {
			this.modes[name].render().fail(function() {
				$dfd.reject();
			}).done(contCb);
		}
		else {
			contCb();
		}

		// EXPERIMENTAL: broadcast that we're starting a mode switch
		this._broadcastEvent('modeSwitchStart', {mode: name}, true);

		// TODO: use dispatch to DRY up freeze/thaw below?
		return $dfd.promise().done(scopeC(function() {
			for (var m in this.modes) {
				if (this.modes.hasOwnProperty(m)) {
					var c, widgetsA = this.modes[m].getWidgets();

					for (var i = 0, l = widgetsA.length; i < l; i++) {
						c = widgetsA[i];

						// freeze/thaw widgets after switch
						if (m === name) c.thaw();
						else c.freeze();
					}
				}
			}

			// update hash on call to switchMode, but not if real url change
			// (in which case it's done already, throwing off disable boolean)
			this._updatePath(name, argA);
			this.modes[name].args(argA);

			// FIXME: change this to use meta tags setter? (possible weird
			// states, e.g., certain properties are gone and can't be grabbed)
			var cfg = this.themeCfg.modes[name];
			var descr = cfg.meta_description || '';
			if(name != "p"){
				document.title = !empty(cfg.title) ?
					cfg.title : this.getEnv('custom').site_name;
			}
			// $('meta[property="og:title"]').attr('content', document.title);
			
			$('meta[name=description], meta[property="og:description"]')
				.attr('content', descr);

			// FIXME: used only by settings and nav; move them to above?
			this._broadcastEvent('modeSwitched', {
				path: location.pathname || '/' + name, // FIXME: remove from nav
				mode: name,
				oldMode: oldMode
			}, true);
		}, this));
	},

	_resetMode: function(name) {
		try {
			if (name === this.GLOBAL_MODE) {
				throw new Error('Cannot reset global mode ' + name);
			}
			if (!this.modeStack.modeExists(name)) {
				throw new Error('Cannot reset mode ' + name + '; not active');
			}
		} catch (e) {
			console.log(e.name + ': ' + e.message); // FIXME: implement
			return new $.Deferred().reject().promise();
		}

		var $dfd = new $.Deferred();

		// destroy and clean up old mode instances and its widgets
		this.modes[name].destroy();
		delete this.modes[name];

		// TODO: make a function and move to config section (destroy widget
		// configs on reset so updates to set configurations propagate)
		if (this._cfgs && this._cfgs[this.CFG_TYPE_WIDGET])
		for (var c in this._cfgs[this.CFG_TYPE_WIDGET]) {
			if (this._cfgs[this.CFG_TYPE_WIDGET].hasOwnProperty(c)) {
				if (JSON.parse(c).mode === name) {
					delete this._cfgs[this.CFG_TYPE_WIDGET][c];
				}
			}
		}

		// create new mode instance, save reference, and re-init
		var $elem = this.modeStack.getMode(name);
		var modeObj = this.themeCfg.modes[name].definition ?
			O_O.lib.Util.getWinPropPath(this.PREFIX_MODE + '.' +
			this.themeCfg.modes[name].definition) : O_O.lib.Mode; // FIXME
		this.modes[name] = O_O.mm.oNew(modeObj, $elem, name, this.msgSys,
			scopeC(this.getEnv, this));
		this.modes[name].init(this.modeTpls[name], this.themeCfg.modes[name],
			this.themeCfg.modes[this.GLOBAL_MODE]).fail(function() {
				$dfd.resolve();
			}).done(scopeC(function() {
				this.modes[name].render().fail(function() {
					$dfd.reject();
				}).done(function() {
					$dfd.resolve();
				});
			}, this));

		return $dfd.promise();
	},

/*******************************************************************************
	WIDGETS
*******************************************************************************/

	// Allows the Site Builder link in Ubernav to prevent page
	// forwarding while the slow page is genreated
	//
	// NOT FOR EVERYDAY USE.
	_freezeWidgets: function() {
		for (var m in this.modes) {
			if (this.modes.hasOwnProperty(m)) {
				var c, widgetsA = this.modes[m].getWidgets();

				for (var i = 0, l = widgetsA.length; i < l; i++) {
					c = widgetsA[i];

					// freeze/thaw widgets after switch
					if (m === name) c.thaw();
					else c.freeze();
				}
			}
		}
	},

/*******************************************************************************
	LOCAL EVENT HANDLERS
*******************************************************************************/

	/* hashchange */
	/* deprecated, but updated to trigger a route for legacy support */

	_hashchangeCb: function(e) {
		if (this.hashDisabled) this.hashDisabled = false;
		else {
			var url = this.parseUrl();

			// to avoid creating loops that prevent backwards navigation,
			// legacy (hashbang) urls should be replaced
			if (url.legacy) window.history.replaceState({}, '', url.path);
			else window.history.pushState({}, '', url.path);

			this._route(url);
		}
	},

	/* traversing history back/forward */

	_popstateCb: function(e) {
		// popstate fires after the path has been updated
		// so we can just use the current window location
		this._route();
	},

	/* in-app click routing */

	_clickCb: function(e) {
		var target, origin, sameOrigin;
		// cases where the target isn't a link, but may be nested inside a link
		if (e.target.tagName === 'A') {
			target = e.target;
		}
		// when Polymer is available you might be looking at things in shadowDOM
		// and doing so with our version of jQuery is bunk...
		else if (typeof Polymer !== 'undefined' && !!Polymer.version &&
				!!Polymer.Settings.useNativeShadow) {
			var eventPath = Polymer.dom(event).path;
			for (var i = 0; i < eventPath.length; i++) {
				var element = eventPath[i];
				if (element.tagName === 'A' && element.href) {
					target = element;
					break;
				}
			}
			if (typeof target === 'undefined') return;
		}
		// usest closest() to traverse the least amount of DOM being we're not
		// checking more than one preceeding `a`
		else {
			var parentAnchors = ps$(e.target).closest('a');

			if (parentAnchors.length > 0) target = parentAnchors[0];
			else return;
		}

		if (target.target === '_blank') return;

		// some IE11 versions lack origin on links
		origin = target.origin || target.protocol + '//' + target.host;
		if (origin === '//') sameOrigin = target.href.search(location.origin) === 0;
		else sameOrigin = origin === location.origin;

		if (!sameOrigin) return; // only local
		if (e.button !== 0) return; // only left-clicks & taps
		if (e.metaKey || e.ctrlKey) return; // no modified clicks, i.e. intended to open link in a new tab
		if (target.href.substr(target.href.length - 1) === "#") return; // placeholder links don't require routing

		e.preventDefault();
		var url = this.parseUrl(target);

		// HACK: libris to treat sso page as php page (exit c2)
		// remove once sso is moved out of /p
		var isSsoPage = (this.themeCfg.name === 'LibrisPortal' && url.modeName === 'p');

		// allow direct navigations to c1 pages (exit c2)
		if (!url.modeName.length || !this.themeCfg.modes[url.modeName] || isSsoPage) {
			window.location = target.href;
			return;
		}

		if (url.path === location.pathname) return;
		window.history.pushState({}, '', url.path);
		this._route(url);
	},

	/* keydown */

	_keydownCb: function(e) {
		var KEYS = {ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40};

		var k;
		switch (e.which) {
		case KEYS.ESC:
			k = 'esc';
			break;
		case KEYS.SPACE:
			k = 'space';
			break;
		case KEYS.LEFT:
			k = 'left';
			break;
		case KEYS.UP:
			k = 'up';
			break;
		case KEYS.RIGHT:
			k = 'right';
			break;
		case KEYS.DOWN:
			k = 'down';
			break;
		}

		if (k) this._broadcastEvent('keyDown', {key: k}, false);
	},

	/* resize */

	_resizeCb: function() {
		if (this.timerResize) clearTimeout(this.timerResize);

		this.timerResize = setTimeout(scopeC(this._resizeH, this),
			this.DELAY_RESIZE); // debounce
	},

	_resizeH: function() {
		var w = this.$win.width();
		var h = this.$win.height();

		if (this.$elem.hasClass(this.SETTINGS_EXPANDED)) {
			w -= this.$settings.children(':first-child').outerWidth();
		}

		this._setEnv('window', {
			width: w,
			height: h
		}, true); // force update

		this._broadcastEvent('windowResize', this.getEnv('window'), false);
	},

	/* scroll */

	_scrollCb: function() {
		if (this.timerScroll) clearTimeout(this.timerScroll);

		this.timerScroll = setTimeout(scopeC(this._scrollH, this),
			this.DELAY_SCROLL); // debounce
	},

	_scrollH: function() {
		var st = this.$win.scrollTop();
		var sl = this.$win.scrollLeft();

		// boundary for lion inertial scrolling
		if (st < 0) st = 0;
		if (sl < 0) sl = 0;

		if (this.scrollTop !== st || this.scrollLeft !== sl) {
			this.scrollTop = st;
			this.scrollLeft = sl;

			this._broadcastEvent('windowScroll', {
				top: st,
				left: sl
			}, false);
		}
	},

/*******************************************************************************
	MESSAGE SYSTEM
*******************************************************************************/

	/* dispatcher */

	_msgCb: function(channel, payload) {
		var name = '_' + payload.msg + 'MsgH'; // handler
		var func = this[name];

		if (typeof func !== 'function') {
			throw new Error('Could not dispatch to handler ' + name);
		}

		var r;
		// try {
			r = scopeC(func, this)(payload.data);
		// } catch (e) {
			// console.log(e.name + ': ' + e.message); // FIXME: implement
		// }
		return r;
	},

	_editModeCheck: function() {
		if (!this.f_edit) {
			throw new Error('Unavailable because app is not in edit mode');
		}
	},

	/* general handlers */

	_eventMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot handle message; no data');
		}
		if (typeof d.type === 'undefined') {
			throw new Error('Cannot handle message; type undefined');
		}

		// FIXME: HACK: for otis (adjust event broadcast to return deferred)
		if (d.type === 'heroTabLoad' && d.data && d.data.tabA) {
			return this.modes[this.HASH_DEFAULT_MODE]
				._heroTabLoadCB(d.data.tabA);
		}

		this._broadcastEvent(d.type, d.data, false);
	},

	_setMetaTagsMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot set meta tags; no data');
		}
		if (typeof d.props !== 'object') {
			throw new Error('Cannot set meta tags; no props');
		}

		$('meta[property]').remove(); // clear any existing tags

		var $head = $('head');
		var k, i, len, prop, props = d.props; // normal key-value pairs
		for (k in props) {
			if (props.hasOwnProperty(k)) {
				prop = props[k];
				if (prop === undefined) continue;
				if (is_array(prop)) {
					for (i=0, len=prop.length; i<len; i++) {
						$head.append('<meta property="' + k + '" content="' +
							O_O.tplescape(prop[i]) + '">');
					}
				} else {
					$head.append('<meta property="' + k + '" content="' +
						O_O.tplescape(prop) + '">');
				}
			}
		}
	},

	_setLinkTagMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot set link tag; no data');
		}
		if (typeof d.attrs !== 'object') {
			throw new Error('Cannot set link tag; no attributes');
		}

		var attrs = d.attrs;
		var $head = $('head');
		var $link = $head.find('link[rel="'+attrs.rel+'"]');
		if ($link.length === 0) {
			$link = $('<link>');
		} else {
			$link.remove();
		}
		var k;
		for (k in attrs) {
			if (attrs.hasOwnProperty(k)) {
				$link.attr(k, attrs[k]);
			}
		}
		$head.append($link);
	},

	_getModesMsgH: function() {
		var abridgedModes = {};

		var modes = this.themeCfg.modes;
		for (var k in modes) {
			if (modes.hasOwnProperty(k)) {
				abridgedModes[k] = {
					modeName: modes[k].modeName,
					user_hidden: modes[k].user_hidden
				};
			}
		}

		return abridgedModes;
	},

	_initModeMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot load mode; no data');
		}
		if (typeof d.name === 'undefined') {
			throw new Error('Cannot load mode; name undefined');
		}

		var controller = this.modes[d.name];
		if (controller) {
			return new $.Deferred().resolve(controller).promise();
		}

		return this._initMode(d.name, false).then(scopeC(function() {
			return this.modes[d.name];
		}, this));
	},

	_switchModeMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot switch mode; no data');
		}
		if (typeof d.name === 'undefined') {
			throw new Error('Cannot switch mode; name undefined');
		}

		return this._switchMode(d.name, d.delay, d.fade); // deferred
	},

	_loadWidgetMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot load widget; no data');
		}
		if (typeof d.name === 'undefined') {
			throw new Error('Cannot load widget; name undefined');
		}

		return this._loadWidget(d.name); // deferred
	},

	_getWidgetIdMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot make widget id; no data');
		}
		if (typeof d.tuple === 'undefined') {
			throw new Error('Cannot make widget id; tuple undefined');
		}

		return {id: this._makeWidgetCfgId(this._makeWidgetCfgTuple(d.tuple))};
	},

	_updatePathMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot update URL hash; no data');
		}
		if (typeof d.mode === 'undefined') {
			throw new Error('Cannot update URL hash; mode undefined');
		}
		if (typeof d.args === 'undefined') {
			throw new Error('Cannot update URL hash; args undefined');
		}

		this._updatePath(d.mode, d.args);
	},

	_routeMsgH: function(d) {
		if (typeof d === 'undefined') {
			throw new Error('Cannot perform route; no data');
		}
		if (typeof d.url === 'undefined') {
			throw new Error('Cannot perform route; url undefined');
		}

		this._route(d.url);
	},

	/* edit mode handlers */

	_toggleEditMsgH: function(d) {
		this._editModeCheck();

		if (typeof d === 'undefined') {
			throw new Error('Cannot toggle edit mode; no data');
		}
		if (typeof d.f_toggle === 'undefined') {
			throw new Error('Cannot toggle edit mode; f_toggle undefined');
		}

		this._dispatch(function(controller) {
			controller.toggleEdit(d.f_toggle, this.themeDfn, this.themeCfg);
		});
	},

	_toggleEditContextMsgH: function(d) {
		this._editModeCheck();

		if (typeof d === 'undefined') {
			throw new Error('Cannot toggle edit context; no data');
		}
		if (typeof d.f_global === 'undefined') {
			throw new Error('Cannot toggle edit context; f_global undefined');
		}

		var goOn = d.f_global ? this.GLOBAL_MODE : this.modeStack.current;
		this._dispatch(function(controller) {
			controller.toggleEdit(true, this.themeDfn, this.themeCfg);
		}, goOn); // turn this mode's widgets on

		var goOff = d.f_global ? this.modeStack.current : this.GLOBAL_MODE;
		this._dispatch(function(controller) {
			controller.toggleEdit(false, this.themeDfn, this.themeCfg);
		}, goOff); // turn this mode's widgets off
	},

	_toggleEditWidgetMsgH: function() {
		this._editModeCheck();

		// TODO: figure out how to select single widget

		this._dispatch(function(controller) {
			controller.toggleEdit(false, this.themeDfn, this.themeCfg);
		});
	},

	_editWidgetMsgH: function(d) {
		this._editModeCheck();

		if (typeof d === 'undefined') {
			throw new Error('Cannot edit widget; no data');
		}
		if (typeof d.view === 'undefined') {
			throw new Error('Cannot edit widget; view undefined');
		}
		if (typeof d.cfgId === 'undefined') {
			throw new Error('Cannot edit widget; cfgId undefined');
		}

		var cfg = this._getCfg(this.CFG_TYPE_WIDGET, d.cfgId);

		this.settingsWidget.editWidget(d.view, d.cfgId, cfg);
	},

	/* config handlers */

	_cfgThemeMsgH: function(d) {
		this._editModeCheck();

		if (typeof d !== 'object') {
			throw new Error('Cannot access or modify theme config; no data');
		}
		if (typeof d.action !== 'string') {
			throw new Error('Cannot access or modify theme config; no action');
		}

		var name = this.themeCfg.name;
		switch (d.action) {
		case 'make':
			break; // nop (available only to shell)
		case 'get':
			return this._getCfg(this.CFG_TYPE_THEME, name);
		case 'update':
			return this._updateCfg(this.CFG_TYPE_THEME, name, d.cfg);
		case 'revert':
			return this._revertCfg(this.CFG_TYPE_THEME, name);
		case 'save':
			return this._saveCfg(this.CFG_TYPE_THEME, name);
		}
	},

	modeEditing: null, // FIXME: HACK: state used below (get rid of this)

	_cfgModeMsgH: function(d) {
		this._editModeCheck();

		if (typeof d !== 'object') {
			throw new Error('Cannot access or modify mode config; no data');
		}
		if (typeof d.action !== 'string') {
			throw new Error('Cannot access or modify mode config; no action');
		}

		// FIXME: get rid of state (this.modeEditing)
		switch (d.action) {
		case 'make':
			break; // nop (available only to shell)
		case 'get':
			this.modeEditing = this.modeStack.getCurrent();
			return this._getCfg(this.CFG_TYPE_MODE, this.modeEditing);
		case 'update':
			return this._updateCfg(this.CFG_TYPE_MODE, this.modeEditing, d.cfg);
		case 'revert':
			return this._revertCfg(this.CFG_TYPE_MODE, this.modeEditing);
		case 'save':
			return this._saveCfg(this.CFG_TYPE_MODE, this.modeEditing);
		}
	},

	_cfgWidgetMsgH: function(d) {
		this._editModeCheck();

		if (typeof d !== 'object') {
			throw new Error('Cannot access or modify widget config; no data');
		}
		if (typeof d.action !== 'string') {
			throw new Error('Cannot access or modify widget config; no action');
		}

		switch (d.action) {
		case 'make':
			return this._makeWidgetCfg(d.tuple, d.controller, d.cfg, d.inject);
		case 'get':
			break; // nop (never called through message)
		case 'update':
			return this._updateCfg(this.CFG_TYPE_WIDGET, d.id, d.cfg);
		case 'revert':
			return this._revertCfg(this.CFG_TYPE_WIDGET, d.id);
		case 'save':
			return this._saveCfg(this.CFG_TYPE_WIDGET, d.id);
		}
	},

/*******************************************************************************
	CONFIG SYSTEM (EDIT MODE ONLY)
*******************************************************************************/

	/* interface */

	_sync: function(url, d) {
		if (typeof O_O.adapter.singletons.PSApi !== 'object') {
			throw new Error('Cannot sync configs without model adapter');
		}

		return O_O.adapter.singletons.PSApi.request({path: url, fv: d});
	},

	_syncElem: function(tuple, cfg) {
		return this._sync(this.CFG_UPD_URL, $.extend({}, tuple, {
			data: JSON.stringify(cfg)
		}));
	},

	_syncTheme: function(cfg) {
		var themeCfg = cfg || this.themeCfg;

		// write only theme since modes are synced by themselves
		themeCfg = $.extend({}, themeCfg);
		delete themeCfg.modes;

		return this._syncElem({
			theme: themeCfg.name,
			type: this.CFG_TYPE_THEME
		}, themeCfg);
	},

	/* base functions */

	_makeCfg: function(type, uid, cfg, subCb, saveCb) {
		if (typeof cfg !== 'object') {
			throw new Error('Cannot make config model; bad cfg');
		}

		if (!this._cfgs) this._cfgs = {};
		if (!this._cfgs[type]) this._cfgs[type] = {};

		var c = this._cfgs[type][uid];
		if (!c) {
			c = O_O.mm.oNew(O_O.lib.Dirty, cfg, saveCb);
			this._cfgs[type][uid] = c;
		}

		c.sub(subCb);

		return c;
	},

	_checkCfg: function(act, type, uid) {
		if (typeof this._cfgs !== 'object') {
			throw new Error('Cannot ' + act + ' config model; none exist');
		}
		if (typeof this._cfgs[type] !== 'object') {
			throw new Error('Cannot ' + act + ' config model; bad type');
		}
		if (typeof this._cfgs[type][uid] !== 'object') {
			throw new Error('Cannot ' + act + ' config model; bad uid');
		}
	},

	_getCfg: function(type, uid) {
		this._checkCfg('get', type, uid);
		return this._cfgs[type][uid].getCurrent();
	},

	_getThemeDfn: function(cfg) {
		var url = this.URL_THEME_DEF + cfg.name + '/definition';

		return O_O.adapter.singletons.PSApi.request({
			path: url,
			fv: {version: cfg.version}
		});
	},

	_updateCfg: function(type, uid, cfg) {
		this._checkCfg('update', type, uid);
		return this._cfgs[type][uid].update(cfg);
	},

	_revertCfg: function(type, uid) {
		this._checkCfg('revert', type, uid);
		return this._cfgs[type][uid].revert();
	},

	_saveCfg: function(type, uid) {
		this._checkCfg('save', type, uid);
		return this._cfgs[type][uid].save();
	},

	/* type-specific wrappers */

	_makeThemeCfg: function(name, cfg) {
		if (typeof name !== 'string') {
			throw new Error('Cannot make theme config model; bad name');
		}
		if (typeof cfg !== 'object') {
			throw new Error('Cannot make theme config model; bad cfg');
		}

		var fCfg = O_O.lib.Util.filterKeys(cfg, this.CFG_RESERVED_KEYS_THEME);

		// FIXME: add pipeCb option to DRY up double $.extend-s?
		var subCb = scopeC(function(d) {
			$.extend(this.themeCfg, d);
			this._reset();
		}, this);
		var saveCb = scopeC(function(d) {
			this.themeCfg = $.extend({}, this.themeCfg, d);
			return this._syncTheme().done(scopeC(function() {
				this._reset();
			}, this)); // deferred
		}, this);

		this._makeCfg(this.CFG_TYPE_THEME, name, fCfg, subCb, saveCb);
	},

	_makeModeCfg: function(name, cfg) {
		if (typeof name !== 'string') {
			throw new Error('Cannot make widget config model; bad name');
		}
		if (typeof cfg !== 'object') {
			throw new Error('Cannot make widget config model; bad cfg');
		}

		var fCfg = O_O.lib.Util.filterKeys(cfg, this.CFG_RESERVED_KEYS_MODE);

		// FIXME: add pipeCb option to DRY up double $.extend-s?
		var subCb = scopeC(function(d) {
			$.extend(this.themeCfg.modes[name], d);
			this._resetMode(name);
		}, this);
		var saveCb = scopeC(function(d) {
			var newCfg = $.extend({}, this.themeCfg.modes[name], d);
			return this._syncElem({
				type: this.CFG_TYPE_MODE,
				theme: this.themeCfg.name,
				mode: name
			}, newCfg).done(scopeC(function() {
				this._resetMode(name);
			}, this)); // deferred
		}, this);

		this._makeCfg(this.CFG_TYPE_MODE, name, fCfg, subCb, saveCb);
	},

	_makeWidgetCfgTuple: function(tuple) {
		return $.extend({}, tuple, {
			type: this.CFG_TYPE_WIDGET,
			theme: this.themeCfg.name
		});
	},

	_makeWidgetCfgId: function(tuple) {
		return JSON.stringify(tuple);
	},

	_makeWidgetCfg: function(tuple, controller, cfg, inject) {
		if (typeof tuple.mode !== 'string') {
			throw new Error('Cannot make widget config model; bad mode');
		}
		if (typeof tuple.widget !== 'string') {
			throw new Error('Cannot make widget config model; bad widget');
		}
		if (typeof tuple.ordinal !== 'number') {
			throw new Error('Cannot make widget config model; bad ordinal');
		}
		if (typeof tuple.set !== 'undefined' && typeof tuple.set !== 'string') {
			throw new Error('Cannot make widget config model; bad set');
		}
		if (typeof controller !== 'object') {
			throw new Error('Cannot make widget config model; bad controller');
		}
		if (typeof cfg !== 'object') {
			throw new Error('Cannot make widget config model; bad cfg');
		}
		if (typeof inject !== 'undefined' && typeof inject !== 'object') {
			throw new Error('Cannot make widget config model; bad inject');
		}

		var extendedTuple = this._makeWidgetCfgTuple(tuple);

		var subCb = scopeC(function(d) {
			controller.render($.extend({}, d, inject)); // modified config
		}, this);
		var saveCb = scopeC(function(d) {
			this._syncElem(extendedTuple, d);
		}, this);

		var id = this._makeWidgetCfgId(extendedTuple);
		this._makeCfg(this.CFG_TYPE_WIDGET, id, cfg, subCb, saveCb);

		return {
			id: id,
			cfg: this._getCfg(this.CFG_TYPE_WIDGET, id) // for changed globals
		};
	},

/*******************************************************************************
	EVENT BROADCAST & FUNCTION DISPATCH
*******************************************************************************/

	_broadcastEvent: function(type, data, f_settings) {
		this._dispatchModes(function(mode) {
			this.msgSys.pub('Mode:' + mode, { // TODO: how to target?
				msg: 'event',
				data: {type: type, data: data}
			});
		});

		this._dispatch(function(controller) {
			this.msgSys.pub(controller.prop('name') + ':Controller', {
				msg: 'event',
				data: {type: type, data: data}
			});
		});

		if (f_settings && this.f_edit) {
			this.msgSys.pub(this.SETTINGS_WIDGET + ':Controller', {
				msg: 'event',
				data: {type: type, data: data}
			});
		}
	},

	_dispatchModes: function(func) {
		for (var mode in this.modes) {
			if (this.modes.hasOwnProperty(mode)) func.call(this, mode);
		}
	},

	_dispatch: function(func, mode) {
		for (var m in this.modes) {
			if (this.modes.hasOwnProperty(m) &&
				(!mode || (mode === this.GLOBAL_MODE || mode === m))) {
					var group;
					if (!mode) group = 'all';
					else if (mode === this.GLOBAL_MODE) group = 'global';
					else group = 'local';

					var widgetsA = this.modes[m].getWidgets(group);
					for (var i = 0, l = widgetsA.length; i < l; i++) {
						func.call(this, widgetsA[i]); // controller as arg
					}
			}
		}
	},

/*******************************************************************************
	EXPERIMENTAL: EVENT TRACKING
*******************************************************************************/

	_trackEvent: function(type) {
		if (!window.ga)
			return;

		if (type === 'route') {
			var url = location.pathname + location.search + location.hash;
			PSGA.distributeToGaTrackers('send', 'pageview', url);
		}
	}
};

}(ps$, window));
(function ($) {
'use strict';

O_O.model.Collection = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.Collection.prototype = {
	type: 'Collection',
	_endpoint: 'collection',
	_schema: {name: 'string', f_list: 'boolean', mode: 'string', description: 'string'},
	_CHILD_MODELS: ['CollectionChildren'],
	default_fv: {f_https_link: 't'}, // f_https_link used for key image
	
	getChildCollections: function (opts)
	{
		var qry_opts = {
			filter_type: 'Collection'
		};
		$.extend(opts, qry_opts);
		return this.getChildren(opts);
	},
	
	getChildGalleries: function (opts)
	{
		var qry_opts = {
			filter_type: 'Gallery'
		};
		$.extend(opts, qry_opts);
		return this.getChildren(opts);
	},
	
	getChildren: function (opts)
	{
		return O_O.model.get('CollectionChildren', this.getId()).load(opts);
	},
	
	getKeyImage: function (f_force) {
		return O_O.model.get('CollectionKeyImage', this.getId()).load({f_force: f_force});
	},
	
	assignDataIfNewer: function (data, channel)
	{
		var key_image = data.key_image;
		//delete data.key_image;
		
		O_O.model.PSModel.prototype.assignDataIfNewer.call(this, data);
		
		if (key_image && !is_array(key_image)) {
			var kimg = O_O.model.get('CollectionKeyImage', this.getId());
			kimg.sub(this._childEventCB, this);
			kimg.assignDataIfNewer(key_image);
		}
	},
	
	getVisibilityModel: function ()
	{
		return O_O.model.get('Visibility', this.getTuple());
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Collection);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.Collection);
O_O.model.mixinModelSchema(O_O.model.Collection);

O_O.model.CollectionChildren = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.CollectionChildren.prototype = {
	type: 'CollectionChildren',
	_endpoint: 'collection',
	
	load: function (opts)
	{
		var dfr, qry_opts = {
			query_label: "children",
			modelcfg_cb: this._loadCb,
			endpoint_tail: this.getId() + '/children',
			f_force: false,
			filter_type: null,
			filter_mode: null,
			fv: {fields: 'name,id,description,f_list,mode'}
		};
		$.extend(qry_opts, opts);
		
		//TODO: generalize this filtering section
		var filterCB = (function (filter_type, filter_mode) {
			return function (childA) {
				var filteredA = [], child;
				for (var i = 0, l = childA.length; i < l; i++) {
					child = childA[i];
					if ((!filter_type || child.getType() === filter_type) &&
						(!filter_mode || child._data.mode === filter_mode))
							filteredA.push(child);
				}
				return filteredA;
			};
		}(qry_opts.filter_type, qry_opts.filter_mode));
		
		dfr = O_O.model.ModelSet.prototype.load.call(this, qry_opts);
		
		if (qry_opts.filter_type || qry_opts.filter_mode) dfr = dfr.then(filterCB);
		
		return dfr;
	},
	
	_loadCb: function (d)
	{
		var m;
		
		var data = d[d.type];
		m = O_O.model.get(d.type, {id: data.id, collection_id: this.getId() });
		return [m,data];
	}
};

O_O.obj.inherit(O_O.model.ModelSet, O_O.model.CollectionChildren);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.CollectionChildren);

O_O.model.CollectionKeyImage = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.CollectionKeyImage.prototype = {
	type: 'CollectionKeyImage',
	_endpoint: 'collection',
	_schema: {link: 'string', link_elements: 'object', id: 'string'},
	
	load: function (user_opts) {
		var opts = {
			tail: '/key_image',
			fv: {f_https_link: 't'}
		};
		
		$.extend(opts, user_opts);
		return O_O.model.ModelSolo.prototype.load.call(this, opts);
	}
};

O_O.obj.inherit(O_O.model.ImageCompoundMixin, O_O.model.CollectionKeyImage);
O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.CollectionKeyImage);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.CollectionKeyImage);

O_O.model.CollectionTest = function () {

	console.log('starting collection solo test');
	var cm = O_O.model.get('Collection', 'C0000hGGVDyR11tw');
	cm.sub(function () {
		console.log('cm.sub() results');
		console.log(arguments);
	}, this);
	
	cm.load().done(function () {
		console.log('cm.load() results');
		console.log(arguments);
	});
	
	console.log('starting collection children test');
	var cmc = O_O.model.get('CollectionChildren', 'C0000hGGVDyR11tw');
	cmc.sub(function () {
		console.log('cmc.sub() results');
		console.log(arguments);
	}, this);
	
	cm.getChildren().done(function () {
		console.log('cm.getChildren() results');
		console.log(arguments);
	});
};


O_O.model.CollectionRoot = function (id)
{
	//if (id !== "root") throw this + ": CollectionRoot id can only be 'root'";
	var data = $.extend({}, this.LISTED_DATA);
	O_O.model.Collection.call(this, id, data);
};

O_O.model.CollectionRoot.prototype = {
	type: 'CollectionRoot',
	LISTED_DATA: { name: "Root (listed)", f_list: "t", id: "root"},
	_f_connectable: false,
	
	getChildren: function (opts)
	{
		return O_O.model.get('CollectionChildren', 'root').load(opts);
	},
	
	getKeyImage: function (f_force) {
		//TODO return first key image found in children?
		return $.Deferred().resolve({}).promise();
	}
};

O_O.obj.inherit(O_O.model.Collection, O_O.model.CollectionRoot);

O_O.model.CollectionMemRoot = function (id)
{
	var data = $.extend({}, this.LISTED_DATA);
	
	O_O.model.CollectionRoot.call(this, id, data);
};

O_O.model.CollectionMemRoot.prototype = {
	type: 'CollectionMemRoot',
	LISTED_DATA: { name: "Images", f_list: "t", id: "root", description: "Your Archive", mode: 'everyone'},
	_f_connectable: false,
	
	getChildren: function (opts)
	{
		//TODO enforce _f_connectable better so this isn't needed
		if (opts) opts.f_force = false;
		return O_O.model.get('CollectionChildren', 'MemRoot').load(opts);
	}
};

O_O.obj.inherit(O_O.model.CollectionRoot, O_O.model.CollectionMemRoot);

O_O.model.CollectionMemSubRoot = function (id)
{
	if (!isset(this.DEF_DATA[id])) throw this + ": CollectionMemRoot id can only be 'listed' or 'unlisted'";
	
	O_O.model.CollectionRoot.call(this, id);
	
	this._data = $.extend({}, this.DEF_DATA[id]);
};

O_O.model.CollectionMemSubRoot.prototype = {
	type: 'CollectionMemSubRoot',
	DEF_DATA : {
		'listed' : { name: "Listed on Website", f_list: "t", mode: 'everyone', id: 'listed'},
		'unlisted' : { name: "Unlisted on Website", f_list: "t",  mode: 'everyone', id: 'unlisted'}
	},
	_f_connectable: false,
	
	//FIXME think of a better way to do this
	getChildren: function (opts)
	{
		return O_O.model.get('CollectionMemSubRootChildren', this.getId()).load(opts);
	}
};

O_O.obj.inherit(O_O.model.CollectionRoot, O_O.model.CollectionMemSubRoot);

O_O.model.CollectionMemSubRootChildren = function (id)
{
	if (!isset(O_O.model.CollectionMemSubRoot.prototype.DEF_DATA[id]))
		throw this + ": CollectionMemRoot id can only be 'listed' or 'unlisted'";
	O_O.model.CollectionChildren.call(this, id);
};

O_O.model.CollectionMemSubRootChildren.prototype = {
	type: 'CollectionMemSubRootChildren',
	
	//real mess ... TODO: ask BE to break listed/unlisted queries into separate API calls
	load: function (opts) {
		var qry_opts = {
			fv: {fields: '*' },
			f_return_primitive_data: false
		};
		
		$.extend(qry_opts, opts);

		//Must post process to aggregate return of all parents
		return  O_O.model.get('CollectionMemSubRootChildrenRaw', 'root').loadByType(this.getId(), qry_opts);
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.CollectionMemSubRootChildren);
O_O.obj.inherit(O_O.model.CollectionChildren, O_O.model.CollectionMemSubRootChildren);

/* TODO: Talk to BE about making this two different endpoints so CollectionMemSubRootChildren can access directly*/
O_O.model.CollectionMemSubRootChildrenRaw = function (id)
{
	O_O.model.CollectionChildren.call(this, 'root');
};

O_O.model.CollectionMemSubRootChildrenRaw.prototype = {
	type: 'CollectionMemSubRootChildrenRaw',
	
	//real mess ... TODO: ask BE to break listed/unlisted queries into separate API calls
	load: function (opts) {
		var qry_opts = {
			tail: '/children',
			fv: {fields: '*' },
			f_return_primitive_data: false,
			f_auth: true
		};
		
		$.extend(qry_opts, opts);

		//Must post process to aggregate return of all parents
		return  O_O.model.ModelSolo.prototype.load.call(this, qry_opts).then(function(data) {
			return data.children;
		});
	},
	
	loadByType: function (listed_type, opts) {
		return this.load(opts).then(scopeC(function (childA) {
			var m, md, model_pair, modelA = [];
			var childD;
			for (var i = 0, l = childA.length; i < l; i++) {
				childD = childA[i];
				if ((childD.listed === 't') === (listed_type === 'listed')) {
					model_pair = this._loadCb(childD);
					m = model_pair[0];
					md = model_pair[1];
					m.assignDataIfNewer(md);
					if (opts.f_return_primitive_data) modelA.push(md);
					else modelA.push(m);
				}
			}

			return modelA;
		},this));
	},
	
	_loadCb: function (d)
	{
		var m;
		
		var data = d[d.type];
		/* PSMX-993: the following line of code allows the listed key value to pass through with the data object. If we were to enable the feature to show the listed/unlisted text in the Featured Work selection of site builder this would be part of the fix. */
        //data.f_listed = d.listed;
		m = O_O.model.get(d.type, {id: data.id });
		return [m,data];
	}
};

O_O.obj.inherit(O_O.model.Collection, O_O.model.CollectionMemSubRootChildrenRaw);

//init & pre-cache imutable models
//TODO delay execution of this?
(function () {
	var g = O_O.model.get;
	
	var mA = [
		//TODO - make immutable models here?
		{
			type: 'CollectionChildren', id: 'MemRoot',
			data: [ g('CollectionMemSubRoot', 'listed'), g('CollectionMemSubRoot', 'unlisted') ]
		}
	];
	
	for (var m, mc, i = 0, l = mA.length; i < l; i++) {
		mc = mA[i];
		m = g(mc.type, mc.id);
		m.assignDataIfNewer(mc.data);
		m._f_connectable = false;
	}
}());


}(ps$));
(function ($) {
'use strict';

O_O.model.Content = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.Content.prototype = {
	type: 'Content',
	_endpoint: 'content',
	_schema: {type: 'string', category: 'string', subcategory : 'string', content : 'string',
		metadata:'json', created_at: 'datetime', modified_at:'datetime'},
	
	_buildRequest: function(user_opts) {
		var opts = $.extend({
			fv: {},
			tail: '',
			f_auth: false,
			f_static: false
		}, user_opts);

		var params = {};
		params.fv = opts.fv;
		params.path = this._api_root;
		if (opts.f_auth) params.path += this._auth_prefix;
		params.path += this._endpoint;// + '/';
		if (!opts.f_static) {
			if (this._data && this._data.id)
				params.path += '/' + this._data.id;
			params.fv = $.extend({}, params.fv, this._id);
		}
			
		if (opts.tail) params.path += opts.tail;
		//if (opts.tail) params.path += opts.tail.substr(1); // HACK: fix slash

		return params;
	},
	
	getUniqStr: function () { return this._id.type + "-" + this._id.category + "-" + this._id.subcategory; },
	
	load: function (user_opts)
	{
		var default_opts = {
			/*optional*/
			'f_force': false,
			'static': true,
			'fv': {}
		};
		
		var opts = $.extend(default_opts, user_opts);
		
		var channel_cache = this._data;

		if (channel_cache && !opts.f_force && (!this._f_dirty || !this.is_connected())) {
			this._pub();
			return $.Deferred().resolve(channel_cache).promise();
		} else {

			return this._request(opts).then(scopeC(function(d) {
				d = d.content;
				if (d.length) { d = d[0];
					this.assignDataIfNewer(d);
					return this._data;
				} else return $.Deferred().reject("404"); //TODO reject with standardized error object
			},this));
		}
	},
	
	insert: function (data)
	{
		var d = this._encodeFields(data);
		var adapter_params = {fv: d, tail: '/insert', f_auth: true, f_static: false};
		
		if (!this.is_connected())	throw this + "insert(): model disconnected, can't insert";
		else return this._request(adapter_params).then(scopeC(function(rdata) {
			d.id = rdata.id;
			this.assignDataIfNewer(d);
			//O_O.model.ModelCache.addModel(this);
			return this._data;
		}, this));
	},
	
	update: function (data)
	{
		if (!this._data || !this._data.id) return this.insert($.extend({}, this._data, data));
		
		return O_O.model.ModelSolo.prototype.update.call(this, data);
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Content);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.Content);
O_O.model.mixinModelSchema(O_O.model.Content);

O_O.model.ContentQuery = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.ContentQuery.prototype = {
	type: 'ContentQuery',
	_endpoint: 'content',
	_schema: {},

	getUniqStr: function ()
	{
		return this._id.type + ":" + (this._id.category || '') + ":" + (this._id.subcategory || '');
	},
	
	load: function (opts)
	{
		var qry_opts = {
			query_label: "content",
			modelcfg_cb: this._pageCb,
			endpoint_tail: 'query',
			f_force: null,
			fv: this.getId()
		};
		
		$.extend(qry_opts, opts);
		
		return O_O.model.ModelSet.prototype.load.call(this, qry_opts);
	},
	
	getFirstContent: function (opts) {
		
		return this.load(opts).pipe(function (pageA) {
			return pageA.shift();
		});
	},
	
	_pageCb: function (d)
	{
		var m = O_O.model.get('Content', {
			type: d.type,
			category: d.category,
			subcategory: d.subcategory
		});
		
		return [m, d];
	}
};

O_O.obj.inherit(O_O.model.ModelSet, O_O.model.ContentQuery);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.ContentQuery);
O_O.model.mixinModelSchema(O_O.model.ContentQuery);

O_O.model.Content.arbPageTuple = function (subcat) {
	return {type: 'c2_page', category: 'arbpage', subcategory: subcat};
};

O_O.model.Content.contentTest = function() {
	var g = O_O.model.get('Content', {type: 'c2_page', category: 'test', subcategory: 'test'});

	var logFunc = function(tag) { return function() { console.log(tag , arguments);  }; };

	//g.sub(logFunc("section A"), this, {offset: 1, limit:2});
	g.load().done(logFunc("section A"));
	
	return g;
};

}(ps$));
(function ($) {
'use strict';

O_O.model.GalleryAbstract = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.GalleryAbstract.prototype = {
	type: 'GalleryAbstract',
	_endpoint: 'gallery',
	
	getUniqStr: function () {
		var tuple = this._id;
		//apply parent collection id if present
		return tuple.id + (tuple.collection_id || "");
	},
	
	_addCollectionId: function (obj) {
		var c_id = this._id.collection_id;
		if (c_id) obj.collection_id = c_id;
		
		return obj;
	}
};

O_O.obj.inherit(O_O.model.PSModel, O_O.model.GalleryAbstract);

O_O.model.Gallery = function (id, data)
{
	O_O.model.GalleryAbstract.call(this, id, data);
};

O_O.model.Gallery.prototype = {
	type: 'Gallery',
	_schema: {name: 'string', f_list: 'boolean', description: 'string', display_order: 'number'},
	_CHILD_MODELS: ['GalleryImages', 'GalleryKeyImage'],
	default_fv: {f_https_link: 't'},
	
	assignDataIfNewer: function (data, channel)
	{
		var key_image = data.key_image;
		//delete data.key_image;
		
		O_O.model.PSModel.prototype.assignDataIfNewer.call(this, data);
		
		if (key_image && !is_array(key_image)) {
			var kimg = O_O.model.get('GalleryKeyImage', this.getId());
			kimg.sub(this._childEventCB, this);
			kimg.assignDataIfNewer(key_image);
		}
	},
	
	load: function (user_opts)
	{
		if (!user_opts) user_opts = {};
		if (!user_opts.fv) user_opts.fv = {};
		this._addCollectionId(user_opts.fv);
		return O_O.model.ModelSolo.prototype.load.call(this, user_opts);
	},
	
	getImages: function (opts)
	{
		return this.getImagesModel().load(opts);
	},
	
	getImagesModel: function ()
	{
		return O_O.model.get('GalleryImages', this.getTuple());
	},
	
	getKeyImage: function (opts)
	{
		return this.getKeyImageModel().load(opts);
	},
	
	getKeyImageModel: function ()
	{
		return O_O.model.get('GalleryKeyImage', this.getTuple());
	},
	
	getParents: function (opts)
	{
		return O_O.model.get('GalleryParents', this.getTuple()).load(opts);
	},
	
	getVisibilityModel: function ()
	{
		return O_O.model.get('Visibility', this.getTuple());
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Gallery);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.Gallery);
O_O.model.mixinModelSchema(O_O.model.Gallery);

O_O.model.GalleryImages = function (id, data)
{
	O_O.model.GalleryAbstract.call(this, id, data);
};

O_O.model.GalleryImages.prototype = {
	type: 'GalleryImages',

	load: function (opts)
	{
		var qry_opts = {
			query_label: "images",
			modelcfg_cb: this._getImageCb,
			endpoint_tail: this.getId() + '/images',
			f_force: null,
			limit: 250,
			fv: {
				fields: '*',
				f_https_link: 't'
			}
		};

		this._addCollectionId(qry_opts.fv);
		
		$.extend(qry_opts, opts);
		return O_O.model.ModelSet.prototype.load.call(this, qry_opts);
	},
	
	_getImageCb: function (d)
	{
		d.gallery_id = this.getId();
		var tuple = this._addCollectionId({image_id:d.id, gallery_id: d.gallery_id});
		var m = O_O.model.get('GalleryImage', tuple);
		return [m, d];
	},
	
	getGalleryModel: function ()
	{
		return O_O.model.get('Gallery', this.getTuple());
	}
};

O_O.obj.inherit(O_O.model.ModelSet, O_O.model.GalleryImages);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.GalleryImages);

O_O.model.GalleryKeyImage = function (id, data)
{
	O_O.model.GalleryAbstract.call(this, id, data);
};

O_O.model.GalleryKeyImage.prototype = {
	type: 'GalleryKeyImage',
	_schema: {link: 'string', link_elements: 'object', id: 'string'},
	
	load: function (user_opts)
	{
		var opts = {
			query_label: "children",
			modelcfg_cb: this._getImageCb,
			tail: '/key_image',
			f_force: false,
			fv: {fields: '*', f_https_link: 't'}
		};
		this._addCollectionId(opts.fv);
		$.extend(opts, user_opts);
		return O_O.model.ModelSolo.prototype.load.call(this, opts);
	},
	
	getLinkForSize: function (user_opts)
	{
		return O_O.app.imgMkUrlFromData(user_opts, this._data);
	}
};

O_O.obj.inherit(O_O.model.ImageCompoundMixin, O_O.model.GalleryKeyImage);
O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.GalleryKeyImage);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.GalleryKeyImage);

O_O.model.GalleryParents = function (id, data)
{
	O_O.model.GalleryAbstract.call(this, id, data);
};

O_O.model.GalleryParents.prototype = {
	type: 'GalleryParents',
	
	load: function (opts)
	{
		var qry_opts = {
			tail: '/parents',
			fv: {fields: '*' }
		};
		
		this._addCollectionId(qry_opts.fv);
		
		$.extend(qry_opts, opts);
		
		//Must post process to aggregate return of all parents
		//TODO have API endpoint return array of collections instead of JSON encoded string
		//TODO add option to return model stubs instead of primitve data
		return O_O.model.ModelSolo.prototype.load.call(this, qry_opts).then(scopeC(function (data) {
			var collections = data.parents;
			var parentA = [];
			var pcollection, p_id = this.getTuple().collection_id;
			
			//sift out parent that we're interested in
			for (var i = 0, l = collections.length; i < l; i++) {
				pcollection = collections[i];
				if (p_id === pcollection.id) break;
			}
			
			//decode path, pop off root pseudo collection
			var path = JSON.parse(pcollection.root_path).slice(0,-1);
			
			for (i = 0, l = path.length; i < l; i++) {
				pcollection = path[i];
				//map vals
				parentA.unshift({id: pcollection.C_ID,
						name: pcollection.C_NAME,
						f_list: pcollection.C_F_LIST});
			}
			
			return parentA;
		},this));
	}
	
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.GalleryParents);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.GalleryParents);

O_O.model.GalleryImage = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.GalleryImage.prototype = {
	type: 'GalleryImage',
	_schema: {link: 'string', gallery_id: 'string', link_elements: 'object'},
	
	assignDataIfNewer: function (data, channel)
	{
		var gallery_image_data = {};
		var image_data = null;
		var v;
		for (var k in data) {
			if (data.hasOwnProperty(k)) {
				v = data[k];
				if (isset(this._schema[k])) gallery_image_data[k] = v;
				else {
					if (!image_data) image_data = {};
					image_data[k] = v;
				}
			}
		}
		
		
		O_O.model.PSModel.prototype.assignDataIfNewer.call(this, gallery_image_data);
		
		if (image_data) {
			var img = this.getImageModel();
			img.sub(this._childEventCB, this);
			img.assignDataIfNewer(image_data);
		}
	},
	
	//GalleryImage has no "id" on the front end, so we use UniqStr
	//or not??
	getId: function () { return this._id; },
	getUniqStr: function () {
		var tuple = this._id;
		return tuple.image_id + tuple.gallery_id + (tuple.collection_id || '');
	},
	parseUniqStr: function (str)
	{
		return {image_id: str.slice(0,16),
			gallery_id: str.slice(16,32),
			collection_id: str.slice(32)};
	},
	
	getGallery: function()
	{
		var t = this.getTuple();
		return O_O.model.get('Gallery', {id: t.gallery_id, collection_id: t.collection_id || ''});
	},
	
	_buildRequest: function (user_opts)
	{
		var opts = $.extend(true, {
			fv: {},
			tail: '',
			f_auth: false,
			f_static: false
		}, user_opts);
		
		var params = {};
		params.fv = opts.fv;
		//collection context
		this._addCollectionId(opts.fv);
		params.path = this._api_root;
		if (opts.f_auth) params.path += this._auth_prefix;
		params.path += this._endpoint + '/' + this._id.gallery_id + '/images/';
		if (!opts.f_static) params.path += this._id.image_id;
		if (opts.tail) params.path += opts.tail;
		return params;
	},
	
	getLinkForSize: function (user_opts)
	{
		return O_O.app.imgMkUrlFromData(user_opts, this._data);
	},
	
	getImageModel: function ()
	{
		return O_O.model.get('Image', this.getTuple().image_id);
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.GalleryImage);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.GalleryImage);
O_O.model.mixinModelSchema(O_O.model.GalleryImage);

O_O.model.GallerySet = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.GallerySet.prototype = {
	type: 'GallerySet',
	_tail: null,

	load: function (opts)
	{
		if (!this._tail)
			throw 'GallerySet is Abstract - provide an endpoint tail for your particular needs';
		
		var qry_opts = {
			query_label: "galleries",
			modelcfg_cb: this._galleryCb,
			f_force: null,
			endpoint_tail: this._tail,
			fv: {fields: '*'}
		};
		
		$.extend(qry_opts, opts);
		
		return O_O.model.ModelSet.prototype.load.call(this, qry_opts);
	},
	
	_galleryCb: function (d)
	{
		var m = O_O.model.get('Gallery', d.id);
		
		return [m, d];
	}
};

O_O.obj.inherit(O_O.model.ModelSet, O_O.model.GallerySet);
O_O.obj.inherit(O_O.model.GalleryAbstract, O_O.model.GallerySet);

O_O.model.PortfolioGalleries = function ()
{
	O_O.model.PSModel.call(this, this.STATIC_ID);
};

O_O.model.PortfolioGalleries.prototype = {
	STATIC_ID: 'PortfolioGalleries',
	type: 'PortfolioGalleries',
	_tail: 'portfolios'
};

O_O.obj.inherit(O_O.model.GallerySet, O_O.model.PortfolioGalleries);

O_O.model.RecentGalleries = function ()
{
	O_O.model.PSModel.call(this, this.STATIC_ID);
};

O_O.model.RecentGalleries.prototype = {
	STATIC_ID: 'RecentGalleries',
	type: 'RecentGalleries',
	_tail: 'recently_updated'
};

O_O.obj.inherit(O_O.model.GallerySet, O_O.model.RecentGalleries);

O_O.model.Gallery.paginationTest = function() {
	var g = O_O.model.get('Gallery', 'G00003q59o2lnoNg');

	var logFunc = function(tag) { return function() { console.log(tag , arguments);  };};

	g.sub(logFunc("section A"), this, {offset: 1, limit:2});
	g.sub(logFunc("section B"), this, {offset: 3, limit:4});
	g.getImages({offset: 0, limit:5}).done(logFunc("section deferred")).done(function () {
		console.log(g._data.gallery_images);
		g.getImages({offset: 10, limit:5}).done(function () {
			logFunc("final deferred")(arguments);
			console.log(g._data.gallery_images);
		});
	});
	
	return g;
};

}(ps$));
(function ($) {
'use strict';

O_O.model.ImageAbstract = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.ImageAbstract.prototype = {
	type: 'ImageAbstract',
	_endpoint: 'image'
};

O_O.obj.inherit(O_O.model.PSModel, O_O.model.ImageAbstract);

O_O.model.Image = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.Image.prototype = {
	type: 'Image',
	_schema: {file_name: 'string', f_searchable: 'boolean',
		file_size: 'number', height: 'number', width: 'number',
		caption: 'string'},
	default_fv: {f_https_link: 't'},
	
	//TODO Consolidate with GalleryImage.getLinkForSize
	getLinkForSize: function (user_opts)
	{
		return O_O.app.imgMkUrlFromData(user_opts, this._data);
	},
	
	getPricingProfiles: function (opts)
	{
		return this.getPricingProfilesModel().load(opts);
	},
	
	getPricingProfilesModel: function ()
	{
		return O_O.model.get('ImagePricingProfiles', this.getTuple());
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Image);
O_O.obj.inherit(O_O.model.ImageAbstract, O_O.model.Image);
O_O.model.mixinModelSchema(O_O.model.Image);

O_O.model.StaticImage = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.StaticImage.prototype = {
	type: 'StaticImage',
	_endpoint: 'image',
	_schema: {link: 'string', link_elements: 'object', id: 'string'},
	
	getUniqStr: function () { return JSON.stringify(this._id); },
	
	_buildRequest: function (user_opts)
	{
		var f_edit = O_O.model.getEnv('f_edit');
		var fv = {image_size: '2048x2048', f_https_link: 't'};
		var tuple = this.getTuple();
		
		if (f_edit) fv.user_config = tuple.config;
		else {
			var ct;
			try {
				ct = JSON.parse(tuple.config_tuple);
			} catch (e) { throw this + ": Invalid JSON config TUPLE - " + tuple.config_tuple; }
			
			for (var k in ct) {
				if (ct.hasOwnProperty(k)) {
					fv[k] = ct[k];
				}
			}
		}
		
		var opts = $.extend(true, {
			fv: fv
		}, user_opts);
		
		if (f_edit) opts.f_auth = true;
		
		return O_O.model.PSModel.prototype._buildRequest.call(this, opts);
	}
};

O_O.obj.inherit(O_O.model.ImageCompoundMixin, O_O.model.StaticImage);
O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.StaticImage);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.StaticImage);

O_O.model.AssetImage = function (id, data)
{
	O_O.model.PSModel.call(this, id, data);
};

O_O.model.AssetImage.prototype = {
	type: 'AssetImage',
	_endpoint: 'asset',
	_schema: {link: 'string', link_elements: 'object', id: 'string'},

	getUniqStr: function () { return this._id.id; },

	getLink: function () {
		if (!this._data.Asset)
			return null;

		var asset = this._data.Asset,
		    base = '/asset-get',
		    id = asset.asset_id,
		    name = asset.name,
		    url = base + '/' + id + '/' + O_O.str.dashifyFilename(name);

		return url;
	},

	_buildRequest: function (user_opts)
	{
		var f_edit = O_O.model.getEnv('f_edit');
		var fv = {image_size: '2048x2048'};
		var tuple = this.getTuple();

		if (f_edit) fv.user_config = tuple.config;
		else {
			var ct;
			try {
				ct = JSON.parse(tuple.config_tuple);
			} catch (e) { throw this + ": Invalid JSON config TUPLE - " + tuple.config_tuple; }

			for (var k in ct) {
				if (ct.hasOwnProperty(k)) {
					fv[k] = ct[k];
				}
			}
		}

		this._api_root = '/psapi/v3.0/';

		var opts = $.extend(true, {
			fv: fv
		}, user_opts);

		if (f_edit) opts.f_auth = true;

		return O_O.model.PSModel.prototype._buildRequest.call(this, opts);
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.AssetImage);
O_O.obj.inherit(O_O.model.PSModel, O_O.model.AssetImage);


O_O.model.ImagePricingProfiles = function (id, data)
{
	O_O.model.ImageAbstract.call(this, id, data);
};

O_O.model.ImagePricingProfiles.prototype = {
	type: 'ImagePricingProfiles',

	load: function (opts)
	{
		var qry_opts = {
			query_label: "profiles",
			modelcfg_cb: this._getPricingProfileCb,
			endpoint_tail: this.getId() + '/pricing',
			f_force: null,
			limit: 250,
			fv: {fields: 'id,name,type' }
		};
				
		$.extend(qry_opts, opts);
		return O_O.model.ModelSet.prototype.load.call(this, qry_opts);
	},
	
	_getPricingProfileCb: function (d)
	{
		var m = O_O.model.get('ImagePricingProfile', d.id);
		return [m, d];
	},
	
	getImageModel: function ()
	{
		return O_O.model.get('Image', this.getTuple());
	}
};

O_O.obj.inherit(O_O.model.ModelSet, O_O.model.ImagePricingProfiles);
O_O.obj.inherit(O_O.model.ImageAbstract, O_O.model.ImagePricingProfiles);

O_O.model.ImagePricingProfile = function (id, data)
{
	O_O.model.ImageAbstract.call(this, id, data);
};

O_O.model.ImagePricingProfile.prototype = {
	type: 'ImagePricingProfile',
	_schema: {name: 'string', type: 'string'}
	
	//no individual endpoint yet
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.ImagePricingProfile);
O_O.obj.inherit(O_O.model.ImageAbstract, O_O.model.ImagePricingProfile);
O_O.model.mixinModelSchema(O_O.model.ImagePricingProfile);
	
}(ps$));
;(function($, undefined) {
'use strict';

O_O.model.Generic = function(id, data) {
	this.DATA_SOURCES = [this.CHANNEL_PRIM];

	O_O.model.Model.call(this, id, data);

	this.disconnect();
};

O_O.model.Generic.prototype = {
	type: 'Generic'
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Generic);
O_O.obj.inherit(O_O.model.Model, O_O.model.Generic);
O_O.model.mixinModelSchema(O_O.model.Generic);

O_O.model.Cart = function (id, data)
{
	O_O.model.Model.call(this, this.STATIC_ID);
	//TODO: Assign adapters more cleanly
	this._adapter = O_O.adapter.singletons.PSApi;
};

O_O.model.Cart.prototype = {
	STATIC_ID: 'CART',
	type: 'Cart',
	_tail: 'cart/summary',
	_api_root: '/psapi/v2.0/',

	_buildRequest: function (user_opts)
	{
		var opts = $.extend(true, {
			fv: {},
			f_auth: false,
			f_static: false
		}, user_opts);
		
		var params = {};
		params.fv = opts.fv;

		params.path = this._api_root;
		if (opts.f_auth) params.path += this._auth_prefix;
		if (this._tail) params.path += this._tail;
		return params;
	}
};

O_O.obj.inherit(O_O.model.ModelSolo, O_O.model.Cart);
O_O.obj.inherit(O_O.model.Model, O_O.model.Cart);
O_O.model.mixinModelSchema(O_O.model.Cart);

}(ps$));
(function($) {

O_O.widget.Content = {
	name: 'Content',
	includes: [
		'Controller',
		'View',
		'Editor'
	],
	vendor: []
};

})(ps$);
