/*!
 * stroll.js 1.2 - CSS scroll effects
 * http://lab.hakim.se/scroll-effects
 * MIT licensed
 *
 * Copyright (C) 2012 Hakim El Hattab, http://hakim.se
 */
(function(){

	"use strict";

	// When a list is configured as 'live', this is how frequently
	// the DOM will be polled for changes
	var LIVE_INTERVAL = 500;

	var IS_TOUCH_DEVICE = !!( 'ontouchstart' in window );

	// All of the lists that are currently bound
	var lists = [];

	// Set to true when there are lists to refresh
	var active = false;

	/**
	 * Updates all currently bound lists.
	 */
	function refresh() {
		if( active ) {
			requestAnimFrame( refresh );

			for( var i = 0, len = lists.length; i < len; i++ ) {
				lists[i].update();
			}
		}
	}

	/**
	 * Starts monitoring a list and applies classes to each of
	 * its contained elements based on its position relative to
	 * the list's viewport.
	 *
	 * @param {HTMLElement} element
	 * @param {Object} options Additional arguments;
	 * 	- live; Flags if the DOM should be repeatedly checked for changes
	 * 			repeatedly. Useful if the list contents is changing. Use
	 * 			scarcely as it has an impact on performance.
	 */
	function add( element, options ) {
		// Only allow ul/ol
		if( !element.nodeName || /^(ul|ol)$/i.test( element.nodeName ) === false ) {
			return false;
		}
		// Delete duplicates (but continue and re-bind this list to get the
		// latest properties and list items)
		else if( contains( element ) ) {
			remove( element );
		}

		var list = IS_TOUCH_DEVICE ? new TouchList( element ) : new List( element );

		// Handle options
		if( options && options.live ) {
			list.syncInterval = setInterval( function() {
				list.sync.call( list );
			}, LIVE_INTERVAL );
		}

		// Synchronize the list with the DOM
		list.sync();

		// Add this element to the collection
		lists.push( list );

		// Start refreshing if this was the first list to be added
		if( lists.length === 1 ) {
			active = true;
			refresh();
		}
	}

	/**
	 * Stops monitoring a list element and removes any classes
	 * that were applied to its list items.
	 *
	 * @param {HTMLElement} element
	 */
	function remove( element ) {
		for( var i = 0; i < lists.length; i++ ) {
			var list = lists[i];

			if( list.element == element ) {
				list.destroy();
				lists.splice( i, 1 );
				i--;
			}
		}

		// Stopped refreshing if the last list was removed
		if( lists.length === 0 ) {
			active = false;
		}
	}

	/**
	 * Checks if the specified element has already been bound.
	 */
	function contains( element ) {
		for( var i = 0, len = lists.length; i < len; i++ ) {
			if( lists[i].element == element ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Calls 'method' for each DOM element discovered in
	 * 'target'.
	 *
	 * @param target String selector / array of UL elements /
	 * jQuery object / single UL element
	 * @param method A function to call for each element target
	 */
	function batch( target, method, options ) {
		var i, len;

		// Selector
		if( typeof target === 'string' ) {
			var targets = document.querySelectorAll( target );

			for( i = 0, len = targets.length; i < len; i++ ) {
				method.call( null, targets[i], options );
			}
		}
		// Array (jQuery)
		else if( typeof target === 'object' && typeof target.length === 'number' ) {
			for( i = 0, len = target.length; i < len; i++ ) {
				method.call( null, target[i], options );
			}
		}
		// Single element
		else if( target.nodeName ) {
			method.call( null, target, options );
		}
		else {
			throw 'Stroll target was of unexpected type.';
		}
	}

	/**
	 * Checks if the client is capable of running the library.
	 */
	function isCapable() {
		return !!document.body.classList;
	}

	/**
	 * The basic type of list; applies past & future classes to
	 * list items based on scroll state.
	 */
	function List( element ) {
		this.element = element;
	}

	/**
	 * Fetches the latest properties from the DOM to ensure that
	 * this list is in sync with its contents.
	 */
	List.prototype.sync = function() {
		this.items = Array.prototype.slice.apply( this.element.children );

		// Caching some heights so we don't need to go back to the DOM so much
		this.listHeight = this.element.offsetHeight;

		// One loop to get the offsets from the DOM
		for( var i = 0, len = this.items.length; i < len; i++ ) {
			var item = this.items[i];
			item._offsetHeight = item.offsetHeight;
			item._offsetTop = item.offsetTop;
			item._offsetBottom = item._offsetTop + item._offsetHeight;
			item._state = '';
		}

		// Force an update
		this.update( true );
	}

	/**
	 * Apply past/future classes to list items outside of the viewport
	 */
	List.prototype.update = function( force ) {
		var scrollTop = this.element.pageYOffset || this.element.scrollTop,
			scrollBottom = scrollTop + this.listHeight;

		// Quit if nothing changed
		if( scrollTop !== this.lastTop || force ) {
			this.lastTop = scrollTop;

			// One loop to make our changes to the DOM
			for( var i = 0, len = this.items.length; i < len; i++ ) {
				var item = this.items[i];

				// Above list viewport
				if( item._offsetBottom < scrollTop ) {
					// Exclusion via string matching improves performance
					if( item._state !== 'past' ) {
						item._state = 'past';
						item.classList.add( 'past' );
						item.classList.remove( 'future' );
					}
				}
				// Below list viewport
				else if( item._offsetTop > scrollBottom ) {
					// Exclusion via string matching improves performance
					if( item._state !== 'future' ) {
						item._state = 'future';
						item.classList.add( 'future' );
						item.classList.remove( 'past' );
					}
				}
				// Inside of list viewport
				else if( item._state ) {
					if( item._state === 'past' ) item.classList.remove( 'past' );
					if( item._state === 'future' ) item.classList.remove( 'future' );
					item._state = '';
				}
			}
		}
	}

	/**
	 * Cleans up after this list and disposes of it.
	 */
	List.prototype.destroy = function() {
		clearInterval( this.syncInterval );

		for( var j = 0, len = this.items.length; j < len; j++ ) {
			var item = this.items[j];

			item.classList.remove( 'past' );
			item.classList.remove( 'future' );
		}
	}


	/**
	 * A list specifically for touch devices. Simulates the style
	 * of scrolling you'd see on a touch device but does not rely
	 * on webkit-overflow-scrolling since that makes it impossible
	 * to read the up-to-date scroll position.
	 */
	function TouchList( element ) {
		this.element = element;
		this.element.style.overflow = 'hidden';

		this.top = {
			value: 0,
			natural: 0
		};

		this.touch = {
			value: 0,
			offset: 0,
			start: 0,
			previous: 0,
			lastMove: Date.now(),
			accellerateTimeout: -1,
			isAccellerating: false,
			isActive: false
		};

		this.velocity = 0;
	}
	TouchList.prototype = new List();

	/**
	 * Fetches the latest properties from the DOM to ensure that
	 * this list is in sync with its contents. This is typically
	 * only used once (per list) at initialization.
	 */
	TouchList.prototype.sync = function() {
		this.items = Array.prototype.slice.apply( this.element.children );

		this.listHeight = this.element.offsetHeight;

		var item;

		// One loop to get the properties we need from the DOM
		for( var i = 0, len = this.items.length; i < len; i++ ) {
			item = this.items[i];
			item._offsetHeight = item.offsetHeight;
			item._offsetTop = item.offsetTop;
			item._offsetBottom = item._offsetTop + item._offsetHeight;
			item._state = '';

			// Animating opacity is a MAJOR performance hit on mobile so we can't allow it
			item.style.opacity = 1;
		}

		this.top.natural = this.element.scrollTop;
		this.top.value = this.top.natural;
		this.top.max = item._offsetBottom - this.listHeight;

		// Force an update
		this.update( true );

		this.bind();
	}

	/**
	 * Binds the events for this list. References to proxy methods
	 * are kept for unbinding if the list is disposed of.
	 */
	TouchList.prototype.bind = function() {
		var scope = this;

		this.touchStartDelegate = function( event ) {
			scope.onTouchStart( event );
		};

		this.touchMoveDelegate = function( event ) {
			scope.onTouchMove( event );
		};

		this.touchEndDelegate = function( event ) {
			scope.onTouchEnd( event );
		};

		this.element.addEventListener( 'touchstart', this.touchStartDelegate, false );
		this.element.addEventListener( 'touchmove', this.touchMoveDelegate, false );
		this.element.addEventListener( 'touchend', this.touchEndDelegate, false );
	}

	TouchList.prototype.onTouchStart = function( event ) {
		event.preventDefault();

		if( event.touches.length === 1 ) {
			this.touch.isActive = true;
			this.touch.start = event.touches[0].clientY;
			this.touch.previous = this.touch.start;
			this.touch.value = this.touch.start;
			this.touch.offset = 0;

			if( this.velocity ) {
				this.touch.isAccellerating = true;

				var scope = this;

				this.touch.accellerateTimeout = setTimeout( function() {
					scope.touch.isAccellerating = false;
					scope.velocity = 0;
				}, 500 );
			}
			else {
				this.velocity = 0;
			}
		}
	}

	TouchList.prototype.onTouchMove = function( event ) {
		if( event.touches.length === 1 ) {
			var previous = this.touch.value;

			this.touch.value = event.touches[0].clientY;
			this.touch.lastMove = Date.now();

			var sameDirection = ( this.touch.value > this.touch.previous && this.velocity < 0 )
								|| ( this.touch.value < this.touch.previous && this.velocity > 0 );

			if( this.touch.isAccellerating && sameDirection ) {
				clearInterval( this.touch.accellerateTimeout );

				// Increase velocity significantly
				this.velocity += ( this.touch.previous - this.touch.value ) / 10;
			}
			else {
				this.velocity = 0;

				this.touch.isAccellerating = false;
				this.touch.offset = Math.round( this.touch.start - this.touch.value );
			}

			this.touch.previous = previous;
		}
	}

	TouchList.prototype.onTouchEnd = function( event ) {
		var distanceMoved = this.touch.start - this.touch.value;

		if( !this.touch.isAccellerating ) {
			// Apply velocity based on the start position of the touch
			this.velocity = ( this.touch.start - this.touch.value ) / 10;
		}

		// Don't apply any velocity if the touch ended in a still state
		if( Date.now() - this.touch.lastMove > 200 || Math.abs( this.touch.previous - this.touch.value ) < 5 ) {
			this.velocity = 0;
		}

		this.top.value += this.touch.offset;

		// Reset the variables used to determne swipe speed
		this.touch.offset = 0;
		this.touch.start = 0;
		this.touch.value = 0;
		this.touch.isActive = false;
		this.touch.isAccellerating = false;

		clearInterval( this.touch.accellerateTimeout );

		// If a swipe was captured, prevent event propagation
		if( Math.abs( this.velocity ) > 4 || Math.abs( distanceMoved ) > 10 ) {
			event.preventDefault();
		}
	};

	/**
	 * Apply past/future classes to list items outside of the viewport
	 */
	TouchList.prototype.update = function( force ) {
		// Determine the desired scroll top position
		var scrollTop = this.top.value + this.velocity + this.touch.offset;

		// Only scroll the list if there's input
		if( this.velocity || this.touch.offset ) {
			// Scroll the DOM and add on the offset from touch
			this.element.scrollTop = scrollTop;

			// Keep the scroll value within bounds
			scrollTop = Math.max( 0, Math.min( this.element.scrollTop, this.top.max ) );

			// Cache the currently set scroll top and touch offset
			this.top.value = scrollTop - this.touch.offset;
		}

		// If there is no active touch, decay velocity
		if( !this.touch.isActive || this.touch.isAccellerating ) {
			this.velocity *= 0.95;
		}

		// Cut off early, the last fraction of velocity doesn't have
		// much impact on movement
		if( Math.abs( this.velocity ) < 0.15 ) {
			this.velocity = 0;
		}

		// Only proceed if the scroll position has changed
		if( scrollTop !== this.top.natural || force ) {
			this.top.natural = scrollTop;
			this.top.value = scrollTop - this.touch.offset;

			var scrollBottom = scrollTop + this.listHeight;

			// One loop to make our changes to the DOM
			for( var i = 0, len = this.items.length; i < len; i++ ) {
				var item = this.items[i];

				// Above list viewport
				if( item._offsetBottom < scrollTop ) {
					// Exclusion via string matching improves performance
					if( this.velocity <= 0 && item._state !== 'past' ) {
						item.classList.add( 'past' );
						item._state = 'past';
					}
				}
				// Below list viewport
				else if( item._offsetTop > scrollBottom ) {
					// Exclusion via string matching improves performance
					if( this.velocity >= 0 && item._state !== 'future' ) {
						item.classList.add( 'future' );
						item._state = 'future';
					}
				}
				// Inside of list viewport
				else if( item._state ) {
					if( item._state === 'past' ) item.classList.remove( 'past' );
					if( item._state === 'future' ) item.classList.remove( 'future' );
					item._state = '';
				}
			}
		}
	};

	/**
	 * Cleans up after this list and disposes of it.
	 */
	TouchList.prototype.destroy = function() {
		List.prototype.destroy.apply( this );

		this.element.removeEventListener( 'touchstart', this.touchStartDelegate, false );
		this.element.removeEventListener( 'touchmove', this.touchMoveDelegate, false );
		this.element.removeEventListener( 'touchend', this.touchEndDelegate, false );
	}


	/**
	 * Public API
	 */
	window.stroll = {
		/**
		 * Binds one or more lists for scroll effects.
		 *
		 * @see #add()
		 */
		bind: function( target, options ) {
			if( isCapable() ) {
				batch( target, add, options );
			}
		},

		/**
		 * Unbinds one or more lists from scroll effects.
		 *
		 * @see #remove()
		 */
		unbind: function( target ) {
			if( isCapable() ) {
				batch( target, remove );
			}
		}
	}

	window.requestAnimFrame = (function(){
		return  window.requestAnimationFrame       ||
				window.webkitRequestAnimationFrame ||
				window.mozRequestAnimationFrame    ||
				window.oRequestAnimationFrame      ||
				window.msRequestAnimationFrame     ||
				function( callback ){
					window.setTimeout(callback, 1000 / 60);
				};
	})()

})();
