/*!
 * meny 1.4
 * http://lab.hakim.se/meny
 * MIT licensed
 *
 * Created by Hakim El Hattab (http://hakim.se, @hakimel)
 */

(function( root, factory ) {
    if( typeof define === 'function' && define.amd ) {
        // AMD module
        define( factory );
    } else {
        // Browser global
        root.Meny = factory();
    }
}(this, function () {

// Date.now polyfill
if( typeof Date.now !== 'function' ) Date.now = function() { return new Date().getTime(); };

var Meny = {

	// Creates a new instance of Meny
	create: function( options ) {
		return (function(){

			// Make sure the required arguments are defined
			if( !options || !options.menuElement || !options.contentsElement ) {
				throw 'You need to specify which menu and contents elements to use.';
			}

			// Make sure the menu and contents have the same parent
			if( options.menuElement.parentNode !== options.contentsElement.parentNode ) {
				throw 'The menu and contents elements must have the same parent.';
			}

			// Constants
			var POSITION_T = 'top',
				POSITION_R = 'right',
				POSITION_B = 'bottom',
				POSITION_L = 'left';

			// Feature detection for 3D transforms
			var supports3DTransforms =  'WebkitPerspective' in document.body.style ||
										'MozPerspective' in document.body.style ||
										'msPerspective' in document.body.style ||
										'OPerspective' in document.body.style ||
										'perspective' in document.body.style;

			// Default options, gets extended by passed in arguments
			var config = {
				width: 300,
				height: 300,
				position: POSITION_L,
				threshold: 40,
				angle: 30,
				overlap: 6,
				transitionDuration: '0.5s',
				transitionEasing: 'ease',
				gradient: 'rgba(0,0,0,0.20) 0%, rgba(0,0,0,0.65) 100%)',
				mouse: true,
				touch: true
			};

			// Cache references to DOM elements
			var dom = {
				menu: options.menuElement,
				contents: options.contentsElement,
				wrapper: options.menuElement.parentNode,
				cover: null
			};

			// State and input
			var indentX = dom.wrapper.offsetLeft,
				indentY = dom.wrapper.offsetTop,
				touchStartX = null,
				touchStartY = null,
				touchMoveX = null,
				touchMoveY = null,
				isOpen = false,
				isMouseDown = false;

			// Precalculated transform and style states
			var menuTransformOrigin,
				menuTransformClosed,
				menuTransformOpened,
				menuStyleClosed,
				menuStyleOpened,

				contentsTransformOrigin,
				contentsTransformClosed,
				contentsTransformOpened,
				contentsStyleClosed,
				contentsStyleOpened;

			var originalStyles = {},
				addedEventListeners = [];

			// Ongoing animations (for fallback mode)
			var menuAnimation,
				contentsAnimation,
				coverAnimation;

			configure( options );

			/**
			 * Initializes Meny with the specified user options,
			 * may be called multiple times as configuration changes.
			 */
			function configure( o ) {
				// Extend the default config object with the passed in
				// options
				Meny.extend( config, o );

				setupPositions();
				setupWrapper();
				setupCover();
				setupMenu();
				setupContents();

				bindEvents();
			}

			/**
			 * Prepares the transforms for the current positioning
			 * settings.
			 */
			function setupPositions() {
				menuTransformOpened = '';
				contentsTransformClosed = '';
				menuAngle = config.angle;
				contentsAngle = config.angle / -2;

				switch( config.position ) {
					case POSITION_T:
						// Primary transform:
						menuTransformOrigin = '50% 0%';
						menuTransformClosed = 'rotateX( ' + menuAngle + 'deg ) translateY( -100% ) translateY( '+ config.overlap +'px )';
						contentsTransformOrigin = '50% 0';
						contentsTransformOpened = 'translateY( '+ config.height +'px ) rotateX( ' + contentsAngle + 'deg )';

						// Position fallback:
						menuStyleClosed = { top: '-' + (config.height-config.overlap) + 'px' };
						menuStyleOpened = { top: '0px' };
						contentsStyleClosed = { top: '0px' };
						contentsStyleOpened = { top: config.height + 'px' };
						break;

					case POSITION_R:
						// Primary transform:
						menuTransformOrigin = '100% 50%';
						menuTransformClosed = 'rotateY( ' + menuAngle + 'deg ) translateX( 100% ) translateX( -2px ) scale( 1.01 )';
						contentsTransformOrigin = '100% 50%';
						contentsTransformOpened = 'translateX( -'+ config.width +'px ) rotateY( ' + contentsAngle + 'deg )';

						// Position fallback:
						menuStyleClosed = { right: '-' + (config.width-config.overlap) + 'px' };
						menuStyleOpened = { right: '0px' };
						contentsStyleClosed = { left: '0px' };
						contentsStyleOpened = { left: '-' + config.width + 'px' };
						break;

					case POSITION_B:
						// Primary transform:
						menuTransformOrigin = '50% 100%';
						menuTransformClosed = 'rotateX( ' + -menuAngle + 'deg ) translateY( 100% ) translateY( -'+ config.overlap +'px )';
						contentsTransformOrigin = '50% 100%';
						contentsTransformOpened = 'translateY( -'+ config.height +'px ) rotateX( ' + -contentsAngle + 'deg )';

						// Position fallback:
						menuStyleClosed = { bottom: '-' + (config.height-config.overlap) + 'px' };
						menuStyleOpened = { bottom: '0px' };
						contentsStyleClosed = { top: '0px' };
						contentsStyleOpened = { top: '-' + config.height + 'px' };
						break;

					default:
						// Primary transform:
						menuTransformOrigin = '100% 50%';
						menuTransformClosed = 'translateX( -100% ) translateX( '+ config.overlap +'px ) scale( 1.01 ) rotateY( ' + -menuAngle + 'deg )';
						contentsTransformOrigin = '0 50%';
						contentsTransformOpened = 'translateX( '+ config.width +'px ) rotateY( ' + -contentsAngle + 'deg )';

						// Position fallback:
						menuStyleClosed = { left: '-' + (config.width-config.overlap) + 'px' };
						menuStyleOpened = { left: '0px' };
						contentsStyleClosed = { left: '0px' };
						contentsStyleOpened = { left: config.width + 'px' };
						break;
				}
			}

			/**
			 * The wrapper element holds the menu and contents.
			 */
			function setupWrapper() {
				// Add a class to allow for custom styles based on
				// position
				Meny.addClass( dom.wrapper, 'meny-' + config.position );

				originalStyles.wrapper = dom.wrapper.style.cssText;

				dom.wrapper.style[ Meny.prefix( 'perspective' ) ] = '800px';
				dom.wrapper.style[ Meny.prefix( 'perspectiveOrigin' ) ] = contentsTransformOrigin;
			}

			/**
			 * The cover is used to obfuscate the contents while
			 * Meny is open.
			 */
			function setupCover() {
				if( dom.cover ) {
					dom.cover.parentNode.removeChild( dom.cover );
				}

				dom.cover = document.createElement( 'div' );

				// Disabled until a falback fade in animation is added
				dom.cover.style.position = 'absolute';
				dom.cover.style.display = 'block';
				dom.cover.style.width = '100%';
				dom.cover.style.height = '100%';
				dom.cover.style.left = 0;
				dom.cover.style.top = 0;
				dom.cover.style.zIndex = 1000;
				dom.cover.style.visibility = 'hidden';
				dom.cover.style.opacity = 0;

				// Silence unimportant errors in IE8
				try {
					dom.cover.style.background = 'rgba( 0, 0, 0, 0.4 )';
					dom.cover.style.background = '-ms-linear-gradient('+ config.position +','+ config.gradient;
					dom.cover.style.background = '-moz-linear-gradient('+ config.position +','+ config.gradient;
					dom.cover.style.background = '-webkit-linear-gradient('+ config.position +','+ config.gradient;
				}
				catch( e ) {}

				if( supports3DTransforms ) {
					dom.cover.style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
				}

				dom.contents.appendChild( dom.cover );
			}

			/**
			 * The meny element that folds out upon activation.
			 */
			function setupMenu() {
				// Shorthand
				var style = dom.menu.style;

				switch( config.position ) {
					case POSITION_T:
						style.width = '100%';
						style.height = config.height + 'px';
						break;

					case POSITION_R:
						style.right = '0';
						style.width = config.width + 'px';
						style.height = '100%';
						break;

					case POSITION_B:
						style.bottom = '0';
						style.width = '100%';
						style.height = config.height + 'px';
						break;

					case POSITION_L:
						style.width = config.width + 'px';
						style.height = '100%';
						break;
				}

				originalStyles.menu = style.cssText;

				style.position = 'fixed';
				style.display = 'block';
				style.zIndex = 1;

				if( supports3DTransforms ) {
					style[ Meny.prefix( 'transform' ) ] = menuTransformClosed;
					style[ Meny.prefix( 'transformOrigin' ) ] = menuTransformOrigin;
					style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
				}
				else {
					Meny.extend( style, menuStyleClosed );
				}
			}

			/**
			 * The contents element which gets pushed aside while
			 * Meny is open.
			 */
			function setupContents() {
				// Shorthand
				var style = dom.contents.style;

				originalStyles.contents = style.cssText;

				if( supports3DTransforms ) {
					style[ Meny.prefix( 'transform' ) ] = contentsTransformClosed;
					style[ Meny.prefix( 'transformOrigin' ) ] = contentsTransformOrigin;
					style[ Meny.prefix( 'transition' ) ] = 'all ' + config.transitionDuration +' '+ config.transitionEasing;
				}
				else {
					style.position = style.position.match( /relative|absolute|fixed/gi ) ? style.position : 'relative';
					Meny.extend( style, contentsStyleClosed );
				}
			}

			/**
			 * Attaches all input event listeners.
			 */
			function bindEvents() {

				if( 'ontouchstart' in window ) {
					if( config.touch ) {
						Meny.bindEvent( document, 'touchstart', onTouchStart );
						Meny.bindEvent( document, 'touchend', onTouchEnd );
					}
					else {
						Meny.unbindEvent( document, 'touchstart', onTouchStart );
						Meny.unbindEvent( document, 'touchend', onTouchEnd );
					}
				}

				if( config.mouse ) {
					Meny.bindEvent( document, 'mousedown', onMouseDown );
					Meny.bindEvent( document, 'mouseup', onMouseUp );
					Meny.bindEvent( document, 'mousemove', onMouseMove );
				}
				else {
					Meny.unbindEvent( document, 'mousedown', onMouseDown );
					Meny.unbindEvent( document, 'mouseup', onMouseUp );
					Meny.unbindEvent( document, 'mousemove', onMouseMove );
				}
			}

			/**
			 * Expands the menu.
			 */
			function open() {
				if( !isOpen ) {
					isOpen = true;

					Meny.addClass( dom.wrapper, 'meny-active' );

					dom.cover.style.height = dom.contents.scrollHeight + 'px';
					dom.cover.style.visibility = 'visible';

					// Use transforms and transitions if available...
					if( supports3DTransforms ) {
						// 'webkitAnimationEnd oanimationend msAnimationEnd animationend transitionend'
						Meny.bindEventOnce( dom.wrapper, 'transitionend', function() {
							Meny.dispatchEvent( dom.menu, 'opened' );
						} );

						dom.cover.style.opacity = 1;

						dom.contents.style[ Meny.prefix( 'transform' ) ] = contentsTransformOpened;
						dom.menu.style[ Meny.prefix( 'transform' ) ] = menuTransformOpened;
					}
					// ...fall back on JS animation
					else {
						menuAnimation && menuAnimation.stop();
						menuAnimation = Meny.animate( dom.menu, menuStyleOpened, 500 );
						contentsAnimation && contentsAnimation.stop();
						contentsAnimation = Meny.animate( dom.contents, contentsStyleOpened, 500 );
						coverAnimation && coverAnimation.stop();
						coverAnimation = Meny.animate( dom.cover, { opacity: 1 }, 500 );
					}

					Meny.dispatchEvent( dom.menu, 'open' );
				}
			}

			/**
			 * Collapses the menu.
			 */
			function close() {
				if( isOpen ) {
					isOpen = false;

					Meny.removeClass( dom.wrapper, 'meny-active' );

					// Use transforms and transitions if available...
					if( supports3DTransforms ) {
						// 'webkitAnimationEnd oanimationend msAnimationEnd animationend transitionend'
						Meny.bindEventOnce( dom.wrapper, 'transitionend', function() {
							Meny.dispatchEvent( dom.menu, 'closed' );
						} );

						dom.cover.style.visibility = 'hidden';
						dom.cover.style.opacity = 0;

						dom.contents.style[ Meny.prefix( 'transform' ) ] = contentsTransformClosed;
						dom.menu.style[ Meny.prefix( 'transform' ) ] = menuTransformClosed;
					}
					// ...fall back on JS animation
					else {
						menuAnimation && menuAnimation.stop();
						menuAnimation = Meny.animate( dom.menu, menuStyleClosed, 500 );
						contentsAnimation && contentsAnimation.stop();
						contentsAnimation = Meny.animate( dom.contents, contentsStyleClosed, 500 );
						coverAnimation && coverAnimation.stop();
						coverAnimation = Meny.animate( dom.cover, { opacity: 0 }, 500, function() {
							dom.cover.style.visibility = 'hidden';
							Meny.dispatchEvent( dom.menu, 'closed' );
						} );
					}
					Meny.dispatchEvent( dom.menu, 'close' );
				}
			}

			/**
			 * Unbinds Meny and resets the DOM to the state it
			 * was at before Meny was initialized.
			 */
			function destroy() {
				dom.wrapper.style.cssText = originalStyles.wrapper
				dom.menu.style.cssText = originalStyles.menu;
				dom.contents.style.cssText = originalStyles.contents;

				if( dom.cover && dom.cover.parentNode ) {
					dom.cover.parentNode.removeChild( dom.cover );
				}

				Meny.unbindEvent( document, 'touchstart', onTouchStart );
				Meny.unbindEvent( document, 'touchend', onTouchEnd );
				Meny.unbindEvent( document, 'mousedown', onMouseDown );
				Meny.unbindEvent( document, 'mouseup', onMouseUp );
				Meny.unbindEvent( document, 'mousemove', onMouseMove );

				for( var i in addedEventListeners ) {
					this.removeEventListener( addedEventListeners[i][0], addedEventListeners[i][1] );
				}

				addedEventListeners = [];
			}


			/// INPUT: /////////////////////////////////

			function onMouseDown( event ) {
				isMouseDown = true;
			}

			function onMouseMove( event ) {
				// Prevent opening/closing when mouse is down since
				// the user may be selecting text
				if( !isMouseDown ) {
					var x = event.clientX - indentX,
						y = event.clientY - indentY;

					switch( config.position ) {
						case POSITION_T:
							if( y > config.height ) {
								close();
							}
							else if( y < config.threshold ) {
								open();
							}
							break;

						case POSITION_R:
							var w = dom.wrapper.offsetWidth;
							if( x < w - config.width ) {
								close();
							}
							else if( x > w - config.threshold ) {
								open();
							}
							break;

						case POSITION_B:
							var h = dom.wrapper.offsetHeight;
							if( y < h - config.height ) {
								close();
							}
							else if( y > h - config.threshold ) {
								open();
							}
							break;

						case POSITION_L:
							if( x > config.width ) {
								close();
							}
							else if( x < config.threshold ) {
								open();
							}
							break;
					}
				}
			}

			function onMouseUp( event ) {
				isMouseDown = false;
			}

			function onTouchStart( event ) {
				touchStartX = event.touches[0].clientX - indentX;
				touchStartY = event.touches[0].clientY - indentY;
				touchMoveX = null;
				touchMoveY = null;

				Meny.bindEvent( document, 'touchmove', onTouchMove );
			}

			function onTouchMove( event ) {
				touchMoveX = event.touches[0].clientX - indentX;
				touchMoveY = event.touches[0].clientY - indentY;

				var swipeMethod = null;

				// Check for swipe gestures in any direction

				if( Math.abs( touchMoveX - touchStartX ) > Math.abs( touchMoveY - touchStartY ) ) {
					if( touchMoveX < touchStartX - config.threshold ) {
						swipeMethod = onSwipeRight;
					}
					else if( touchMoveX > touchStartX + config.threshold ) {
						swipeMethod = onSwipeLeft;
					}
				}
				else {
					if( touchMoveY < touchStartY - config.threshold ) {
						swipeMethod = onSwipeDown;
					}
					else if( touchMoveY > touchStartY + config.threshold ) {
						swipeMethod = onSwipeUp;
					}
				}

				if( swipeMethod && swipeMethod() ) {
					event.preventDefault();
				}
			}

			function onTouchEnd( event ) {
				Meny.unbindEvent( document, 'touchmove', onTouchMove );

				// If there was no movement this was a tap
				if( touchMoveX === null && touchMoveY === null ) {
					onTap();
				}
			}

			function onTap() {
				var isOverContent = ( config.position === POSITION_T && touchStartY > config.height ) ||
									( config.position === POSITION_R && touchStartX < dom.wrapper.offsetWidth - config.width ) ||
									( config.position === POSITION_B && touchStartY < dom.wrapper.offsetHeight - config.height ) ||
									( config.position === POSITION_L && touchStartX > config.width );

				if( isOverContent ) {
					close();
				}
			}

			function onSwipeLeft() {
				if( config.position === POSITION_R && isOpen ) {
					close();
					return true;
				}
				else if( config.position === POSITION_L && !isOpen ) {
					open();
					return true;
				}
			}

			function onSwipeRight() {
				if( config.position === POSITION_R && !isOpen ) {
					open();
					return true;
				}
				else if( config.position === POSITION_L && isOpen ) {
					close();
					return true;
				}
			}

			function onSwipeUp() {
				if( config.position === POSITION_B && isOpen ) {
					close();
					return true;
				}
				else if( config.position === POSITION_T && !isOpen ) {
					open();
					return true;
				}
			}

			function onSwipeDown() {
				if( config.position === POSITION_B && !isOpen ) {
					open();
					return true;
				}
				else if( config.position === POSITION_T && isOpen ) {
					close();
					return true;
				}
			}


			/// API: ///////////////////////////////////

			return {
				configure: configure,

				open: open,
				close: close,
				destroy: destroy,

				isOpen: function() {
					return isOpen;
				},

				/**
				 * Forward event binding to the menu DOM element.
				 */
				addEventListener: function( type, listener ) {
					addedEventListeners.push( [type, listener] );
					dom.menu && Meny.bindEvent( dom.menu, type, listener );
				},
				removeEventListener: function( type, listener ) {
					dom.menu && Meny.unbindEvent( dom.menu, type, listener );
				}
			};

		})();
	},

	/**
	 * Helper method, changes an element style over time.
	 */
	animate: function( element, properties, duration, callback ) {
		return (function() {
			// Will hold start/end values for all properties
			var interpolations = {};

			// Format properties
			for( var p in properties ) {
				interpolations[p] = {
					start: parseFloat( element.style[p] ) || 0,
					end: parseFloat( properties[p] ),
					unit: ( typeof properties[p] === 'string' && properties[p].match( /px|em|%/gi ) ) ? properties[p].match( /px|em|%/gi )[0] : ''
				};
			}

			var animationStartTime = Date.now(),
				animationTimeout;

			// Takes one step forward in the animation
			function step() {
				// Ease out
				var progress = 1 - Math.pow( 1 - ( ( Date.now() - animationStartTime ) / duration ), 5 );

				// Set style to interpolated value
				for( var p in interpolations ) {
					var property = interpolations[p];
					element.style[p] = property.start + ( ( property.end - property.start ) * progress ) + property.unit;
				}

				// Continue as long as we're not done
				if( progress < 1 ) {
					animationTimeout = setTimeout( step, 1000 / 60 );
				}
				else {
					callback && callback();
					stop();
				}
			}

			// Cancels the animation
			function stop() {
				clearTimeout( animationTimeout );
			}

			// Starts the animation
			step();


			/// API: ///////////////////////////////////

			return {
				stop: stop
			};
		})();
	},

	/**
	 * Extend object a with the properties of object b.
	 * If there's a conflict, object b takes precedence.
	 */
	extend: function( a, b ) {
		for( var i in b ) {
			a[ i ] = b[ i ];
		}
	},

	/**
	 * Prefixes a style property with the correct vendor.
	 */
	prefix: function( property, el ) {
		var propertyUC = property.slice( 0, 1 ).toUpperCase() + property.slice( 1 ),
			vendors = [ 'Webkit', 'Moz', 'O', 'ms' ];

		for( var i = 0, len = vendors.length; i < len; i++ ) {
			var vendor = vendors[i];

			if( typeof ( el || document.body ).style[ vendor + propertyUC ] !== 'undefined' ) {
				return vendor + propertyUC;
			}
		}

		return property;
	},

	/**
	 * Adds a class to the target element.
	 */
	addClass: function( element, name ) {
		element.className = element.className.replace( /\s+$/gi, '' ) + ' ' + name;
	},

	/**
	 * Removes a class from the target element.
	 */
	removeClass: function( element, name ) {
		element.className = element.className.replace( name, '' );
	},

	/**
	 * Adds an event listener in a browser safe way.
	 */
	bindEvent: function( element, ev, fn ) {
		if( element.addEventListener ) {
			element.addEventListener( ev, fn, false );
		}
		else {
			element.attachEvent( 'on' + ev, fn );
		}
	},

	/**
	 * Removes an event listener in a browser safe way.
	 */
	unbindEvent: function( element, ev, fn ) {
		if( element.removeEventListener ) {
			element.removeEventListener( ev, fn, false );
		}
		else {
			element.detachEvent( 'on' + ev, fn );
		}
	},

	bindEventOnce: function ( element, ev, fn ) {
		var me = this;
		var listener = function() {
			me.unbindEvent( element, ev, listener );
			fn.apply( this, arguments );
		};
		this.bindEvent( element, ev, listener );
	},

	/**
	 * Dispatches an event of the specified type from the
	 * menu DOM element.
	 */
	dispatchEvent: function( element, type, properties ) {
		if( element ) {
			var event = document.createEvent( "HTMLEvents", 1, 2 );
			event.initEvent( type, true, true );
			Meny.extend( event, properties );
			element.dispatchEvent( event );
		}
	},

	/**
	 * Retrieves query string as a key/value hash.
	 */
	getQuery: function() {
		var query = {};

		location.search.replace( /[A-Z0-9]+?=([\w|:|\/\.]*)/gi, function(a) {
			query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
		} );

		return query;
	}

};

return Meny;

}));
