/**
 * Bounceback.js v1.0.0
 * 
 * Copyright 2014 Avi Kohn
 * Distributable under the MIT license
 */

(function(root, factory) {
	// The Istanbul comments stop the UMD from being counted in coverage reports
	
	// AMD
	/* istanbul ignore next */
	if (typeof define === 'function' && define.amd) {
		define(function() {
			return factory(root, document, {});
		});
	}

	// Node.js and CommonJS, for testing
	else if (typeof exports !== 'undefined') {
		// This is a test run, inject the test environment
		if (global && global.testEnv) {
			factory(global.testEnv, global.testEnv.document, exports);
		}
		/* istanbul ignore next */
		else {
			factory(root, document, exports);
		}
	}

	// Normal browser usage
	/* istanbul ignore next */
	else {
		root.Bounceback = factory(root, document, {});
	}

	// `root` and `doc` allow for better compression
})(window, function(root, doc, Bounceback) {
	/**
	 * Attaches an event to the window.
	 * 
	 * This could accept an element as an argument but that would make testing more difficult.
	 *
	 * @api    private
	 * @param  {Element}   elm The element to attach the event to
	 * @param  {String}    evt The name of the event to attach
	 * @param  {Function}  cb  The event callback
	 */
	var addEvent = function(elm, evt, cb) {
		if (elm.attachEvent) {
			elm.attachEvent('on' + evt, cb);
		}
		else {
			elm.addEventListener(evt, cb, false);
		}
	};


	// There isn't any other library called Bounceback that would use the
	// variable, but might as well
	var oldBounceback = root.Bounceback;

	/**
	 * Restores the Bounceback variable in the global scope to its previous value
	 *
	 * @return {Object} Bounceback
	 */
	Bounceback.noConflict = function() {
		root.Bounceback = oldBounceback;

		return this;
	};

	Bounceback.version = '1.0.0';

	Bounceback.options = {
		distance: 100, // The minimum distance in px from the top to consider triggering for
		maxDisplay: 1, // The maximum number of times the dialog may be shown on one page, or 0 for unlimited.  Only applicable when using the mouse based method
		method: 'auto', // The bounce detection method
		sensitivity: 10, // The minimum distance the mouse has to have moved in the last 10 mouse events for onBounce to be triggered
		cookieLife: 365, // The cookie (when localStorage isn't available) expiry age, in days
		scrollDelay: 500, // The amount of time in ms that bouncing should be ignored for after scrolling, or 0 to disable
		aggressive: false, // Whether or not to ignore the cookie that blocks initialization unless it's the first pageview
		checkReferrer: true, // Whether or not to check the referring page to see if it's on the same domain and this isn't the first pageview
		storeName: 'bounceback-visited', // The key to store the cookie (or localStorage item) under
		onBounce: function() { return Bounceback; } // The default onBounce handler
	};


	Bounceback.data = {
		/**
		 * Gets an item's value by key from storage
		 *
		 * @api    public
		 * @param  {String} key The key to retrieve the value from
		 * @return {String}     The retrieved value
		 */
		get: function(key) {
			if (root.localStorage) {
				return root.localStorage.getItem(key) || '';
			}
			else {
				var cookies = doc.cookie.split(';');

				var i = -1,
					data = [],
					cVal = '',
					cName = '',
					length = cookies.length;

				while (++i < length) {
					data = cookies[i].split('=');

					if (data[0] == key) {
						data.shift();

						return data.join('=');
					}
				}

				return '';
			}
		},


		/**
		 * Sets a key to the specified value in storage
		 *
		 * @api    public
		 * @param  {String} key   The key to store under
		 * @param  {String} value The value to store
		 * @return {Object}       The data store, for chained calls
		 */
		set: function(key, value) {
			if (root.localStorage) {
				root.localStorage.setItem(key, value);
			}
			else {
				var dt = new Date();

				dt.setDate(dt.getDate() + Bounceback.options.cookieLife);
				
				doc.cookie = key + '=' + value + '; expires=' + dt.toUTCString() + ';path=/;';
			}

			return this;
		}
	};


	var shown = 0;

	/**
	 * This proxies calls to onBounce to ensure that it isn't triggered
	 * more than the limit specified in the options allows
	 */
	Bounceback.onBounce = function() {
		shown++;

		/* istanbul ignore else */
		if (!this.options.maxDisplay || shown <= this.options.maxDisplay) {
			this.options.onBounce();
		}
	};


	/**
	 * Whether or not the current browser is mobile.
	 * 
	 * This is used to decide if the mouse or pushState method should be used.
	 *
	 * @type {Boolean}
	 */
	Bounceback.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(root.navigator.userAgent);


	/**
	 * Whether or not Bounceback is disabled, toggled by default on scroll
	 *
	 * @type {Boolean}
	 */
	Bounceback.disabled = false;


	/**
	 * Whether or not Bounceback has been activated.  This prevents activate
	 * from executing more than once
	 *
	 * @type {Boolean}
	 */
	Bounceback.activated = false;


	/**
	 * Disables Bounceback.
	 * 
	 * This does _not_ remove the event handlers since that would involve
	 * more complicated code to handle each of the handlers when only one
	 * would ever be attached at a given time.
	 *
	 * @api    public
	 * @return {Object} Bounceback
	 */
	Bounceback.disable = function() {
		this.disabled = true;

		return this;
	};


	/**
	 * Enables Bounceback
	 *
	 * @api    public
	 * @return {Object} Bounceback
	 */
	Bounceback.enable = function() {
		this.disabled = false;

		return this;
	};


	/**
	 * Attaches handlers as necessary and sets up Bounceback
	 */
	Bounceback.activate = function(method) {
		if (method == 'history') {
			// The history API for modern browsers
			if ('replaceState' in root.history) {
				// Set data in the current state to let Bounceback know that it should
				// fire the onBounce handler
				root.history.replaceState({
					isBouncing: true
				}, root.title);

				// Then add a new state to the history so hitting back navigates to
				// the previous added state and fires onBounce
				root.history.pushState(null, root.title);

				// Handle popstate events
				addEvent(root, 'popstate', function(e) {
					/* istanbul ignore else */
					if (root.history.state && root.history.state.isBouncing) {
						Bounceback.onBounce();
					}
				});
			}

			// And the hash for others
			/* istanbul ignore else */
			else if ('onhashchange' in root) {
				// BHT -> Bounceback Hash Trigger
				root.location.replace('#bht');

				root.location.hash = '';

				addEvent(root, 'hashchange', function() {
					/* istanbul ignore else */
					if (root.location.hash.substr(-3) === 'bht') {
						Bounceback.onBounce();
					}
				});
			}
		}
		else {
			var timer = null,
				movements = [],
				scrolling = false;


			addEvent(doc, 'mousemove', function(e) {
				movements.unshift({
					x: e.clientX,
					y: e.clientY
				});

				movements = movements.slice(0, 10);
			});


			addEvent(doc, 'mouseout', function(e) {
				/* istanbul ignore else */
				if (!Bounceback.disabled) {
					var from = e.relatedTarget || e.toElement;

					/* istanbul ignore else */
					if (
						(!from || from.nodeName == 'HTML') && 
						e.clientY <= Bounceback.options.distance && 
						movements.length == 10 &&
						movements[0].y < movements[9].y &&
						movements[9].y - movements[0].y > Bounceback.options.sensitivity
					) {
						Bounceback.onBounce();
					}
				}
			});


			// While scrolling using the mouse if it leaves the body the mouseout event is
			// delayed until scrolling stops.  This ensures that the event fired then is ignored.
			/* istanbul ignore else */
			if (this.options.scrollDelay) {
				addEvent(root, 'scroll', function(e) {
					/* istanbul ignore else */
					if (!Bounceback.disabled) {
						Bounceback.disabled = true;

						clearTimeout(timer);

						timer = setTimeout(function() {
							Bounceback.disabled = false;
						}, Bounceback.options.scrollDelay);
					}
				});
			}
		}
	};


	/**
	 * Initializes Bounceback.
	 * 
	 * Multiple calls will update options that are not already in use.
	 *
	 * @api    public
	 * @param  {Object} [options] Any options to initialize with
	 * @return {Object}           Bounceback
	 */
	Bounceback.init = function(options) {
		options = options || {};

		var key;

		for (key in this.options) {
			if (this.options.hasOwnProperty(key) && !options.hasOwnProperty(key)) {
				options[key] = this.options[key];
			}
		}

		this.options = options;

		if (options.checkReferrer && doc.referrer) {
			var a = doc.createElement('a');
			
			a.href = doc.referrer;

			if (a.host == root.location.host) {
				this.data.set(options.storeName, '1');
			}
		}

		if (!this.activated && (options.aggressive || !this.data.get(options.storeName))) {
			this.activated = true;

			if (options.method === 'history' || (options.method === 'auto' && this.isMobile)) {
				this.activate('history');
			}
			else {
				this.activate('mouse');
			}

			this.data.set(options.storeName, '1');
		}

		return this;
	};

	return Bounceback;
});