/*jslint devel: true, bitwise: true, regexp: true, browser: true, confusion: true, unparam: true, eqeq: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
/*globals jQuery */

/*!
 * Coverflow
 *
 * Copyright (c) 2013-2015 Martijn W. van der Lee
 * Licensed under the MIT.
 */

/* Lightweight and flexible coverflow effect using CSS3 transforms.
 * For modern browsers with some amount of graceful degradation.
 * Optional support for jQuery.interpolate() plugin.
 * Optional support for .reflect() plugins.
 *
 * Requires jQuery 1.7+ and jQueryUI 1.9+.
 * Recommended jQuery 1.8+ and jQueryUI 1.9+.
 */

;(function($, undefined) {
	"use strict";

	var sign		= function(number) {
						return number < 0 ? -1 : 1;
					},
		scl			= function(number, fromMin, fromMax, toMin, toMax) {
						return ((number - fromMin) * (toMax - toMin) / (fromMax - fromMin)) + toMin;
					},
		wheelEvents	= ('onwheel' in document) ? 'wheel' : 'mousewheel',	// FF
		getWheel	= function(event) {
						if ('deltaY' in event.originalEvent) {
							return 0 - event.originalEvent.deltaY;
						} else if ('wheelDelta' in event.originalEvent) { 
							return event.originalEvent.wheelDelta;	// IE
						}
					};

	$.widget("vanderlee.coverflow", {
		options: {
			animateComplete:	undefined,
			animateStep:		undefined,
			density:			1,
			duration:			'normal',
			easing:				undefined,
			enableKeyboard:		true,
			enableClick:		true,
			enableWheel:		true,
			index:				0,
			innerAngle:			-75,
			innerCss:			undefined,
			innerOffset:		100 / 3,
			innerScale:			0.75,
			outerAngle:			-30,
			outerCss:			undefined,
			outerScale:			0.25,
			selectedCss:		undefined,
			visible:			'density',		// 'density', 'all', NNN (exact)
			width:				undefined,

			change:				undefined,		// Whenever index is changed
			confirm:			undefined,		// Whenever clicking on the current item
			select:				undefined		// Whenever index is set (also on init)
		},

		_window_handler_resize:		null,
		_window_handler_keydown:	null,

		_create: function() {
			var that = this,
				covers = that._getCovers(),
				images = covers.filter('img').add('img', covers).filter(function() {
					return !(this.complete || this.height > 0);
				}),
				maxHeight = covers.height(),
				height;

			// Internal event prefix
			that.widgetEventPrefix	= 'vanderlee-coverflow';

			that.hovering			= false;
			that.pagesize			= 1;
			that.currentIndex		= that.options.index;
			
			// Fix height
			that.element.height(maxHeight);
			images.load(function() {
				height = that._getCovers().height();
				if (height > maxHeight) {
					maxHeight = height;
					that.element.height(maxHeight);
				}
			});

			// Hide all covers and set position to absolute
			covers.hide();

			// Enable click-jump
			that.element.on('mousedown tap', '> *', function(event) {
				if (that.options.enableClick) {
					var index = that._getCovers().index(this);
					if (index === that.currentIndex) {
						that._callback('confirm', event);
					} else {
						that._setIndex(index, true);
					}
				}
			});

			// Mousewheel
			that.element.on(wheelEvents, function(event) {
				if (that.options.enableWheel) {
					var delta = getWheel(event) > 0 ? 1 : -1;

					event.preventDefault();
					that._setIndex(that.options.index - delta, true);
				}
			});

			// Swipe
			if ($.isFunction(that.element.swipe)) {
				that.element.swipe({
					swipe: function(event, direction, distance, duration, fingerCount) {
						var count = Math.round((direction === 'left' ? 1 : -1) * 1.25 * that.pagesize * distance / that.element.width());
						that._setIndex(that.options.index + count, true);
					}
				});
			}

			// Keyboard
			that.element.hover(
				function() { that.hovering = true; }
			,	function() { that.hovering = false; }
			);

			// Refresh on resize
			that._window_handler_resize = function() {
				that.refresh();
			};
			$(window).on('resize', that._window_handler_resize);
			
			that._window_handler_keydown = function(event) {						
				if (that.options.enableKeyboard && that.hovering) {
					switch (event.which) {
						case 36:	// home
							event.preventDefault();
							that._setIndex(0, true);
							break;

						case 35:	// end
							event.preventDefault();
							that._setIndex(that._getCovers().length - 1, true);
							break;

						case 38:	// up
						case 37:	// left
							event.preventDefault();
							that._setIndex(that.options.index - 1, true);
							break;

						case 40:	// down
						case 39:	// right
							event.preventDefault();
							that._setIndex(that.options.index + 1, true);
							break;

						case 33:	// page up (towards home)
							event.preventDefault();
							that._setIndex(that.options.index - that.pagesize, true);
							break;

						case 34:	// page down (towards end)
							event.preventDefault();
							that._setIndex(that.options.index + that.pagesize, true);
							break;
					}
				}
			};
			$(window).on('keydown', that._window_handler_keydown);

			// Initialize
			that._setIndex(that.options.index, false, true);

			return that;
		},


		/**
		 * Destroy this object
		 * @returns {undefined}
		 */
		_destroy: function() {
			$(window).off('resize', this._window_handler_resize);
			$(window).off('keydown', this._window_handler_keydown);
			this.element.height('');
		},

		/**
		 * Returns the currently selected cover
		 * @returns {jQuery} jQuery object
		 */
		cover: function() {
			return $(this._getCovers()[this.options.index]);
		},

		/**
		 *
		 * @returns {unresolved}
		 */
		_getCovers: function() {
			return $('> *', this.element);
		},

		_setIndex: function(index, animate, initial) {
			var that = this,
				covers = that._getCovers();

			index = Math.max(0, Math.min(index, covers.length - 1));

			if (index !== that.options.index) {
				// Fix reflections
				covers.css('position', 'absolute');
				this._frame(that.options.index);						

				if (animate === true || that.options.duration === 0) {
					that.options.index	= Math.round(index);
					
					var duration	= typeof that.options.duration === "number"
									? that.options.duration
									: jQuery.fx.speeds[that.options.duration] || jQuery.fx.speeds._default;
					
					this.refresh(duration, that.options.index);
				} else {
					that.options.index = Math.round(index);
					that.refresh(0);
				}
			} else if (initial === true) {
				that.refresh();
				that._callback('select');
			}
		},

		_callback: function(callback, event) {
			this._trigger(callback, event, [this._getCovers().get(this.currentIndex), this.currentIndex]);
		},

		index: function(index) {
			if (index === undefined) {
				return this.options.index;
			}

			while (index < 0) {
				index += this._getCovers().length;
			}

			this._setIndex(index, true);
		},
		
		_frame: function(frame) {
			frame = frame.toFixed(6);		
				
			var that		= this,
				covers		= that._getCovers(),
				count		= covers.length,
				parentWidth	= that.element.innerWidth(),
				coverWidth	= that.options.width || covers.first().outerWidth(),
				visible		= that.options.visible === 'density'	? Math.round(parentWidth * that.options.density / coverWidth)
							: $.isNumeric(that.options.visible)		? that.options.visible
							: count,
				parentLeft	= that.element.position().left - ((1 - that.options.outerScale) * coverWidth * 0.5),
				space		= (parentWidth - (that.options.outerScale * coverWidth)) * 0.5;
		
			that.pagesize	= visible;
			
			covers.removeClass('current').each(function(index, cover) {
				var $cover		= $(cover),
					position	= index - frame,
					offset		= Math.min(Math.max(-1., position / visible), 1),
					isMiddle	= position == 0,
					zIndex		= count - Math.abs(Math.round(position)),
					isVisible	= Math.abs(position) <= visible,
					sin			= Math.sin(offset * Math.PI * 0.5),
					cos			= Math.cos(offset * Math.PI * 0.5),
					left		= sign(sin) * scl(Math.abs(sin), 0, 1, that.options.innerOffset * that.options.density, space),
					scale		= isVisible ? scl(Math.abs(cos), 1, 0, that.options.innerScale, that.options.outerScale) : 0,
					angle		= sign(sin) * scl(Math.abs(sin), 0, 1, that.options.innerAngle, that.options.outerAngle),
					css			= isMiddle ? that.options.selectedCss || {}
								: ( $.interpolate && that.options.outerCss && !$.isEmptyObject(that.options.outerCss) ? (
									isVisible ? $.interpolate(that.options.innerCss || {}, that.options.outerCss, Math.abs(sin))
											  : that.options.outerCss
									) : {}
								),
					transform;
							
				// bad behaviour for being in the middle
				if (Math.abs(position) < 1) {
					angle	= 0 - (0 - angle) * Math.abs(position);
					scale	= 1 - (1 - scale) * Math.abs(position);
					left	= 0 - (0 - left) * Math.abs(position);
				}
				
				//@todo Test CSS for middle behaviour (or does $.interpolate handle it?)

				transform = 'scale(' + scale + ',' + scale + ') perspective(' + (parentWidth * 0.5) + 'px) rotateY(' + angle + 'deg)';
				
				$cover[isMiddle ? 'addClass' : 'removeClass']('current');
				$cover[isVisible ? 'show' : 'hide']();				
						
				$cover.css($.extend(css, {
					'left':					parentLeft + space + left,
					'z-index':				zIndex,
					'-webkit-transform':	transform,
					'-ms-transform':		transform,
					'transform':			transform
				}));
				
				// Optional callback
				that._trigger('animateStep', null, [cover, offset, isVisible, isMiddle, sin, cos]);
				
				if (frame == that.options.index) {
					// Optional callback
					that._trigger('animateComplete', null, [cover, offset, isVisible, isMiddle, sin, cos]);					
				}
			});
		},

		refresh: function(duration, index) {	
			var that = this,
				previous = that.currentIndex,
				covers = that._getCovers(),
				covercount = covers.length,
				triggered = false;
		
			covers.css('position', 'absolute');
			that.element.stop().animate({
				'__coverflow_frame':	index || that.options.index
			}, {
				'easing':	that.options.easing,
				'duration': duration || 0,
				'step':		function(now, fx) {					
					that._frame(now);					
					
					that.currentIndex = Math.max(0, Math.min(Math.round(now), covercount - 1));
					if (previous !== that.currentIndex) {
						previous = that.currentIndex;
						that._callback('change');
						if (that.currentIndex === that.options.index) {
							triggered = true;
						}
					}
				},
				'complete':		function() {				
					that.currentIndex	= that.options.index;
					if (!triggered) {
						that._callback('change');
					}
					that._callback('select');
				}
			});
		}
	});
}(jQuery));
