/*!
 *  Kwicks: Sexy Sliding Panels for jQuery - v2.2.1
 *  http://devsmash.com/projects/kwicks
 *
 *  Copyright 2013 Jeremy Martin (jmar777)
 *  Contributors: Duke Speer (Duke3D), Guillermo Guerrero (gguerrero)
 *  Released under the MIT license
 *  http://www.opensource.org/licenses/mit-license.php
 */

(function($) {

	/**
	 *  API methods for the plugin
	 */
	var methods = {
		init: function(opts) {
			var defaults = {
				// general options:
				maxSize: -1,
				minSize: -1,
				spacing: 5,
				duration: 500,
				isVertical: false,
				easing: undefined,
				autoResize: true,
				behavior: null,
				// menu behavior options:
				delayMouseIn: 0,
				delayMouseOut: 0,
				selectOnClick: true,
				deselectOnClick: false,
				// slideshow behavior options:
				interval: 2500,
				interactive: true
			};
			var o = $.extend(defaults, opts);

			// validate and normalize options
			if (o.minSize !== -1 && o.maxSize !== -1)
				throw new Error('Kwicks options minSize and maxSize may not both be set');
			if (o.behavior && o.behavior !== 'menu' && o.behavior !== 'slideshow')
				throw new Error('Unrecognized Kwicks behavior specified: ' + o.behavior);
			$.each(['minSize', 'maxSize', 'spacing'], function(i, prop) {
				var val = o[prop];
				switch (typeof val) {
					case 'number':
						o[prop + 'Units'] = 'px';
						break;
					case 'string':
						if (val.slice(-1) === '%') {
							o[prop + 'Units'] = '%';
							o[prop] = +val.slice(0, -1) / 100;
						} else if (val.slice(-2) === 'px') {
							o[prop + 'Units'] = 'px';
							o[prop] = +val.slice(0, -2);
						} else {
							throw new Error('Invalid value for Kwicks option ' + prop + ': ' + val);
						}
						break;
					default:
						throw new Error('Invalid value for Kwicks option ' + prop + ': ' + val);
				}
			});
						
			return this.each(function() {
				$(this).data('kwicks', new Kwick(this, o));
			});
		},
		expand: function(index, opts) {
			if (typeof index === 'object') {
				opts = index;
				index = undefined;
			}

			var delay = opts && opts.delay || 0;
			
			return this.each(function() {
				var $this = $(this),
					kwick = $this.data('kwicks');

				// assume this is the container
				if (kwick) {
					index = typeof index === 'number' ? index : -1;
				}
				// otherwise, assume we have a panel
				else if (kwick = $this.parent().data('kwicks')) {
					index = $this.index();
				} else {
					return;
				}

				var expand = function() {
					// bail out if the panel is already expanded
					if (index === kwick.expandedIndex) return;

					var $panels = kwick.$panels,
						expanded = $panels[index] || null;

					kwick.$container.trigger('expand.kwicks', {
						index: index,
						expanded: expanded,
						collapsed: $panels.not(expanded).get(),
						oldIndex: kwick.expandedIndex,
						oldExpanded: kwick.getExpandedPanel(),
						isAnimated: kwick.isAnimated
					});
				};

				var timeoutId = kwick.$container.data('kwicks-timeout-id');
				if (timeoutId) {
					kwick.$container.removeData('kwicks-timeout-id');
					clearTimeout(timeoutId);
				}
				if (delay > 0) {
					kwick.$container.data('kwicks-timeout-id', setTimeout(expand, delay));
				} else {
					expand();
				}
			});
		},
		expanded: function() {
			var kwick = this.first().data('kwicks');
			if (!kwick) return;
			return kwick.expandedIndex;
		},
		select: function(index) {
			return this.each(function() {
				var $this = $(this),
					kwick = $this.data('kwicks');
				
				// assume this is the container
				if (kwick) {
					index = typeof index === 'number' ? index : -1;
				}
				// otherwise, assume we have a panel
				else if (kwick = $this.parent().data('kwicks')) {
					index = $this.index();
				} else {
					return;
				}

				// don't trigger event if its already selected
				if (index !== kwick.selectedIndex) {
					var $panels = kwick.$panels,
						selected = $panels[index] || null;

					kwick.$container.trigger('select.kwicks', {
						index: index,
						selected: selected,
						unselected: $panels.not(selected).get(),
						oldIndex: kwick.selectedIndex,
						oldSelected: kwick.getSelectedPanel()
					});
				}

				// call expand
				kwick.$container.kwicks('expand', index);
			});
		},
		selected: function() {
			var kwick = this.first().data('kwicks');
			if (!kwick) return;
			return kwick.selectedIndex;
		},
		resize: function() {
			return this.each(function() {
				var $this = $(this),
					kwick = $this.data('kwicks');

				if (!kwick) return;

				kwick.resize();
			});
		},
		destroy: function() {
			return this.each(function() {
				var $this = $(this),
					kwick = $this.data('kwicks');

				if (!kwick) return;

				kwick.destroy();
			});
		}
	};

	/**
	 *  Expose the actual plugin
	 */
	$.fn.kwicks = function(opts) {
		if (methods[opts]) {
			return methods[opts].apply(this, Array.prototype.slice.call(arguments, 1));
		} else if (typeof opts === 'object' || !opts) {
			return methods.init.apply(this, arguments);
		} else {
			throw new Error('Unrecognized kwicks method: ' + opts);
		}
	};

	/**
	 *  Special event for triggering default behavior on 'expand.kwicks' events
	 */
	$.event.special.expand = {
		_default: function(e, data) {
			if (e.namespace !== 'kwicks') return;
			var kwick = $(e.target).data('kwicks');
			if (kwick) kwick.expand(data.index);
		}
	};

	/**
	 *  Special event for triggering default behavior on 'select.kwicks' events
	 */
	$.event.special.select = {
		_default: function(e, data) {
			if (e.namespace !== 'kwicks') return;
			var kwick = $(e.target).data('kwicks');
			if (kwick) kwick.select(data.index);
		}
	};

	/**
	 *  Instantiates a new Kwick instance using the provided container and options.
	 */
	var Kwick = function Kwick(container, opts) {
		var self = this;

		this.opts = opts;

		// an array of callbacks to invoke if 'destroy' is invoked
		this.onDestroyHandlers = [];

		// references to our DOM elements
		var orientation = opts.isVertical ? 'vertical' : 'horizontal';
		this.$container = $(container);
		this.$panels = this.$container.children();

		// semi-smart add/remove around container classes so that we don't bork
		// the styling if/when destroy is called
		var containerClasses = ['kwicks', 'kwicks-' + orientation];
		$.each(containerClasses, function(className) {
			if (self.$container.hasClass(className)) return;
			self.$container.addClass(className);
			self.onDestroy(function() {
				self.$container.removeClass(className);
			});
		});

		// zero-based, -1 for "none"
		this.selectedIndex = this.$panels.filter('.kwicks-selected').index();
		this.expandedIndex = this.selectedIndex;

		// each instance has a primary and a secondary dimension (primary is the animated dimension)
		this.primaryDimension = opts.isVertical ? 'height' : 'width';
		this.secondaryDimension = opts.isVertical ? 'width' : 'height';

		// initialize panel sizes
		this.calculatePanelSizes();

		// likewise, we have primary and secondary alignments (all panels but the last use primary,
		// which uses the secondary alignment). this is to allow the first and last panels to have
		// fixed offsets. this reduces jittering, which is much more noticeable on the last item.
		this.primaryAlignment = opts.isVertical ? 'top' : 'left';
		this.secondaryAlignment = opts.isVertical ? 'bottom' : 'right';

		// object for creating a "master" animation loop for all panel animations
		this.$timer = $({ progress: 0 });

		// keeps track of whether or not an animation is in progress
		this.isAnimated = false;

		// the current offsets for each panel
		this.offsets = this.getOffsetsForExpanded();

		this.updatePanelStyles();
		this.initBehavior();
		this.initWindowResizeHandler();

		// somewhat of a blind stab at handling rare/sporadic failures to initialize styles.
		// https://github.com/jmar777/kwicks/issues/31
		setTimeout(function() {
			self.updatePanelStyles();
		}, 100);
	};

	/**
	 * Calculates size, minSize, maxSize, and spacing based on the current size of the container and
	 * the user-provided options.  The results will be stored on this.panelSize, this.panelMinSize,
	 * this.panelMaxSize, and this.panelSpacing.  This should be run on initialization and whenever
	 * the container's primary dimension may have changed in size.
	 */
	Kwick.prototype.calculatePanelSizes = function() {
		var opts = this.opts,
			containerSize = this.getContainerSize(true);

		// calculate spacing first
		if (opts.spacingUnits === '%') {
			this.panelSpacing = containerSize * opts.spacing;
		} else {
			this.panelSpacing = opts.spacing;
		}

		var numPanels = this.$panels.length,
			sumSpacing = this.panelSpacing * (numPanels - 1),
			sumPanelSize = containerSize - sumSpacing;

		this.panelSize = sumPanelSize / numPanels;

		if (opts.minSize === -1) {
			if (opts.maxSize === -1) {
				// if neither minSize or maxSize or set, then we try to pick a sensible default
				if (numPanels < 5) {
					this.panelMaxSize = containerSize / 3 * 2;
				} else {
					this.panelMaxSize = containerSize / 3;
				}
			} else if (opts.maxSizeUnits === '%') {
				this.panelMaxSize = sumPanelSize * opts.maxSize;
			} else {
				this.panelMaxSize = opts.maxSize;
			}

			// at this point we know that this.panelMaxSize is set
			this.panelMinSize = (sumPanelSize - this.panelMaxSize) / (numPanels - 1);
		} else if (opts.maxSize === -1) {
			// at this point we know that opts.minSize is set
			if (opts.minSizeUnits === '%') {
				this.panelMinSize = sumPanelSize * opts.minSize;
			} else {
				this.panelMinSize = opts.minSize;
			}

			// at this point we know that this.panelMinSize is set
			this.panelMaxSize = sumPanelSize - (this.panelMinSize * (numPanels - 1));
		}
	};

	/**
	 *  Returns the calculated panel offsets based on the currently expanded panel.
	 */
	Kwick.prototype.getOffsetsForExpanded = function() {
		// todo: cache the offset values
		var expandedIndex = this.expandedIndex,
			numPanels = this.$panels.length,
			spacing = this.panelSpacing,
			size = this.panelSize,
			minSize = this.panelMinSize,
			maxSize = this.panelMaxSize;

		//first panel is always offset by 0
		var offsets = [0];

		for (var i = 1; i < numPanels; i++) {
			// no panel is expanded
			if (expandedIndex === -1) {
				offsets[i] = i * (size + spacing);
			}
			// this panel is before or is the expanded panel
			else if (i <= expandedIndex) {
				offsets[i] = i * (minSize + spacing);
			}
			// this panel is after the expanded panel
			else {
				offsets[i] = maxSize + (minSize * (i - 1)) + (i * spacing);
			}
		}

		return offsets;
	};

	/**
	 *  Sets the style attribute on the specified element using the provided value.  This probably
	 *  doesn't belong on Kwick.prototype, but here it is...
	 */
	Kwick.prototype.setStyle = (function() {
		if ($.support.style) {
			return function(el, style) { el.setAttribute('style', style); };
		} else {
			return function (el, style) { el.style.cssText = style; };
		}
	})();

	/**
	 *  Updates the offset and size styling of each panel based on the current values in
	 *  `this.offsets`.  Also does some special handling to convert panels to absolute positioning
	 *  the first time this is invoked.
	 */
	Kwick.prototype.updatePanelStyles = function() {
		var offsets = this.offsets,
			$panels = this.$panels,
			pDim = this.primaryDimension,
			pAlign = this.primaryAlignment,
			sAlign = this.secondaryAlignment,
			spacing = this.panelSpacing,
			containerSize = this.getContainerSize();

		// the kwicks-processed class ensures that panels are absolutely positioned, but on our
		// first pass we need to set offsets, width|length, and positioning atomically to prevent
		// mid-update repaints
		var stylePrefix = !!this._stylesInited ? '' : 'position:absolute;',
			offset, size, prevOffset, style;

		// loop through remaining panels
		for (var i = $panels.length; i--;) {
			prevOffset = offset;
			// todo: maybe we do one last pass at the end and round offsets, rather than on every
			// update
			offset = Math.round(offsets[i]);
			if (i === $panels.length - 1) {
				size = containerSize - offset;
				style = sAlign + ':0;' + pDim + ':' + size + 'px;';
			} else {
				size = prevOffset - offset - spacing;
				style = pAlign + ':' + offset + 'px;' + pDim + ':' + size + 'px;';
			}
			this.setStyle($panels[i], stylePrefix + style);
		}

		if (!this._stylesInited) {
			this.$container.addClass('kwicks-processed');
			this._stylesInited = true;
		}
	};

	/**
	 *  Assuming for a moment that out-of-the-box behaviors aren't a horrible idea, this method
	 *  encapsulates the initialization logic thereof.
	 */
	Kwick.prototype.initBehavior = function() {
		if (!this.opts.behavior) return;

		switch (this.opts.behavior) {
			case 'menu':
				this.initMenuBehavior();
				break;
			case 'slideshow':
				this.initSlideshowBehavior();
				break;
			default:
				throw new Error('Unrecognized behavior option: ' + this.opts.behavior);
		}
	};

	/**
	 * Initializes the menu behavior.
	 */
	Kwick.prototype.initMenuBehavior = function() {
		var self = this,
			opts = self.opts;

		this.addEventHandler(this.$container, 'mouseleave', function() {
			self.$container.kwicks('expand', -1, { delay: opts.delayMouseOut });
		});

		this.addEventHandler(this.$panels, 'mouseenter', function() {
			$(this).kwicks('expand', { delay: opts.delayMouseIn });
		});

		if (!opts.selectOnClick && !opts.deselectOnClick) return;

		this.addEventHandler(this.$panels, 'click', function() {
			var $this = $(this),
				isSelected = $this.hasClass('kwicks-selected');

			if (isSelected && opts.deselectOnClick) {
				$this.parent().kwicks('select', -1);
			} else if (!isSelected && opts.selectOnClick) {
				$this.kwicks('select');
			}
		});
	};

	/**
	 * Initializes the slideshow behavior.
	 */
	Kwick.prototype.initSlideshowBehavior = function() {
		var self = this,
			numSlides = this.$panels.length,
			curSlide = 0,
			// flag to handle weird corner cases
			running = false,
			intervalId;

		var start = function() {
			if (running) return;
			intervalId = setInterval(function() {
				self.$container.kwicks('expand', ++curSlide % numSlides);
			}, self.opts.interval);
			running = true;
		};
		var pause = function() {
			clearInterval(intervalId);
			running = false;
		};

		start();
		this.onDestroy(pause);

		if (!this.opts.interactive) return;

		this.addEventHandler(this.$container, 'mouseenter', pause);
		this.addEventHandler(this.$container, 'mouseleave', start);
		this.addEventHandler(this.$panels, 'mouseenter', function() {
			curSlide = $(this).kwicks('expand').index();
		});
	};

	/**
	 * Sets up a throttled window resize handler that triggers resize logic for the panels
	 * todo: hideous code, needs refactor for the eye bleeds
	 */
	Kwick.prototype.initWindowResizeHandler = function() {
		if (!this.opts.autoResize) return;

		var self = this,
			prevTime = 0,
			execScheduled = false,
			$window = $(window);

		var onResize = function(e) {
			// if there's no event, then this is a scheduled from our setTimeout
			if (!e) { execScheduled = false; }

			// if we've already run in the last 20ms, then delay execution
			var now = +new Date();
			if (now - prevTime < 20) {
				// if we already scheduled a run, don't do it again
				if (execScheduled) return;
				setTimeout(onResize, 20 - (now - prevTime));
				execScheduled = true;
				return;
			}

			// throttle rate is satisfied, go ahead and run
			prevTime = now;
			self.resize();
		};

		this.addEventHandler($window, 'resize', onResize);
	};

	/**
	 * Returns the size in pixels of the container's primary dimension. This value is cached as it
	 * is used repeatedly during animation loops, but the cache can be cleared by passing `true`.
	 * todo: benchmark to see if this caching business is even at all necessary.
	 */
	Kwick.prototype.getContainerSize = function(clearCache) {
		var containerSize = this._containerSize;
		if (clearCache || !containerSize) {
			containerSize = this._containerSize = this.$container[this.primaryDimension]();
		}
		return containerSize;
	};

	/**
	 *  Gets a reference to the currently expanded panel (if there is one)
	 */
	Kwick.prototype.getExpandedPanel = function() {
		return this.$panels[this.expandedIndex] || null;
	};

	/**
	 *  Gets a reference to the currently collapsed panels
	 */
	Kwick.prototype.getCollapsedPanels = function() {
		if (this.expandedIndex === -1) return [];
		return this.$panels.not(this.getExpandedPanel()).get();
	};

	/**
	 *  Gets a reference to the currently selected panel (if there is one)
	 */
	Kwick.prototype.getSelectedPanel = function() {
		return this.$panels[this.selectedIndex] || null;
	};

	/**
	 * Gets a reference to the currently unselected panels
	 */
	Kwick.prototype.getUnselectedPanels = function() {
		return this.$panels.not(this.getSelectedPanel()).get();
	};

	/**
	 * Registers a handler to be invoked if/when 'destroy' is invoked
	 */
	Kwick.prototype.onDestroy = function(handler) {
		this.onDestroyHandlers.push(handler);
	};

	/**
	 * Adds an event handler and automatically registers it to be removed if/when
	 * the plugin is destroyed.
	 */
	Kwick.prototype.addEventHandler = function($el, eventName, handler) {
		$el.on(eventName, handler);
		this.onDestroy(function() {
			$el.off(eventName, handler);
		});
	};

	/**
	 * "Destroys" this Kwicks instance plugin by performing the following:
	 * 1) Stops any currently running animations
	 * 2) Invokes all destroy handlers
	 * 3) Clears out all style attributes on panels
	 * 4) Removes all kwicks class names from panels and container
	 * 5) Removes the 'kwicks' data value from the container
	 */
	Kwick.prototype.destroy = function() {
		this.$timer.stop();
		for (var i = 0, len = this.onDestroyHandlers.length; i < len; i++) {
			this.onDestroyHandlers[i]();
		}
		this.$panels
			.attr('style', '')
			.removeClass('kwicks-expanded kwicks-selected kwicks-collapsed');
		this.$container
			// note: kwicks and kwicks-<orientation> classes have extra smarts around them
			// back in the constructor
			.removeClass('kwicks-processed')
			.removeData('kwicks');
	};

	/**
	 *  Forces the panels to be updated in response to the container being resized.
	 */
	Kwick.prototype.resize = function() {
		// bail out if container size hasn't changed
		if (this.getContainerSize() === this.getContainerSize(true)) return;

		this.calculatePanelSizes();
		this.offsets = this.getOffsetsForExpanded();

		// if the panels are currently being animated, we'll just set a flag that can be detected
		// during the next animation step
		if (this.isAnimated) {
			this._dirtyOffsets = true;
		} else {
			// otherwise update the styles immediately
			this.updatePanelStyles();
		}
	};

	/**
	 *  Selects the panel with the specified index (use -1 to select none)
	 */
	Kwick.prototype.select = function(index) {
		// make sure the panel isn't already selected
		if (index === this.selectedIndex) return;

		$(this.getSelectedPanel()).removeClass('kwicks-selected');
		this.selectedIndex = index;
		$(this.getSelectedPanel()).addClass('kwicks-selected');
	};

	/**
	 *  Expands the panel with the specified index (use -1 to expand none)
	 */
	Kwick.prototype.expand = function(index) {
		var self = this,
			// used for expand-complete event later on
			oldIndex = this.expandedIndex,
			oldExpanded = this.getExpandedPanel();

		// if the index is -1, then default it to the currently selected index (which will also be
		// -1 if no panels are currently selected)
		if (index === -1) index = this.selectedIndex;

		// make sure the panel isn't already expanded
		if (index === this.expandedIndex) return;

		$(this.getExpandedPanel()).removeClass('kwicks-expanded');
		$(this.getCollapsedPanels()).removeClass('kwicks-collapsed');
		this.expandedIndex = index;
		$(this.getExpandedPanel()).addClass('kwicks-expanded');
		$(this.getCollapsedPanels()).addClass('kwicks-collapsed');

		// handle panel animation
		var $timer = this.$timer,
			numPanels = this.$panels.length,
			startOffsets = this.offsets.slice(),
			offsets = this.offsets,
			targetOffsets = this.getOffsetsForExpanded();

		$timer.stop()[0].progress = 0;
		this.isAnimated = true;
		$timer.animate({ progress: 1 }, {
			duration: this.opts.duration,
			easing: this.opts.easing,
			step: function(progress) {
				// check if we've resized mid-animation (yes, we're thorough)
				if (self._dirtyOffsets) {
					offsets = self.offsets;
					targetOffsets = self.getOffsetsForExpanded();
					self._dirtyOffsets = false;
				}
				offsets.length = 0;
				for (var i = 0; i < numPanels; i++) {
					var targetOffset = targetOffsets[i],
						newOffset = targetOffset - ((targetOffset - startOffsets[i]) * (1 - progress));
					offsets[i] = newOffset;
				}
				self.updatePanelStyles();
			},
			complete:  function() {
				self.isAnimated = false;
				self.$container.trigger('expand-complete.kwicks', {
					index: index,
					expanded: self.getExpandedPanel(),
					collapsed: self.getCollapsedPanels(),
					oldIndex: oldIndex,
					oldExpanded: oldExpanded,
					// note: this will always be false but is included to match expand event
					isAnimated: false
				});
			}
		});
	};

})(jQuery);
