/*!
 * gumshoejs v5.1.2
 * A simple, framework-agnostic scrollspy script.
 * (c) 2019 Chris Ferdinandi
 * MIT License
 * http://github.com/cferdinandi/gumshoe
 */

(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		define([], function () {
			return factory(root);
		});
	} else if (typeof exports === 'object') {
		module.exports = factory(root);
	} else {
		root.Gumshoe = factory(root);
	}
})(
	typeof global !== 'undefined'
		? global
		: typeof window !== 'undefined'
		? window
		: this,
	function (window) {
		'use strict';

		//
		// Defaults
		//

		var defaults = {
			// Active classes
			navClass: 'active',
			contentClass: 'active',

			// Nested navigation
			nested: false,
			nestedClass: 'active',

			// Offset & reflow
			offset: 0,
			reflow: false,

			// Event support
			events: true,
		};

		//
		// Methods
		//

		/**
		 * Merge two or more objects together.
		 * @param   {Object}   objects  The objects to merge together
		 * @returns {Object}            Merged values of defaults and options
		 */
		var extend = function () {
			var merged = {};
			Array.prototype.forEach.call(arguments, function (obj) {
				for (var key in obj) {
					if (!obj.hasOwnProperty(key)) return;
					merged[key] = obj[key];
				}
			});
			return merged;
		};

		/**
		 * Emit a custom event
		 * @param  {String} type   The event type
		 * @param  {Node}   elem   The element to attach the event to
		 * @param  {Object} detail Any details to pass along with the event
		 */
		var emitEvent = function (type, elem, detail) {
			// Make sure events are enabled
			if (!detail.settings.events) return;

			// Create a new event
			var event = new CustomEvent(type, {
				bubbles: true,
				cancelable: true,
				detail: detail,
			});

			// Dispatch the event
			elem.dispatchEvent(event);
		};

		/**
		 * Get an element's distance from the top of the Document.
		 * @param  {Node} elem The element
		 * @return {Number}    Distance from the top in pixels
		 */
		var getOffsetTop = function (elem) {
			var location = 0;
			if (elem.offsetParent) {
				while (elem) {
					location += elem.offsetTop;
					elem = elem.offsetParent;
				}
			}
			return location >= 0 ? location : 0;
		};

		/**
		 * Sort content from first to last in the DOM
		 * @param  {Array} contents The content areas
		 */
		var sortContents = function (contents) {
			if (contents) {
				contents.sort(function (item1, item2) {
					var offset1 = getOffsetTop(item1.content);
					var offset2 = getOffsetTop(item2.content);
					if (offset1 < offset2) return -1;
					return 1;
				});
			}
		};

		/**
		 * Get the offset to use for calculating position
		 * @param  {Object} settings The settings for this instantiation
		 * @return {Float}           The number of pixels to offset the calculations
		 */
		var getOffset = function (settings) {
			// if the offset is a function run it
			if (typeof settings.offset === 'function') {
				return parseFloat(settings.offset());
			}

			// Otherwise, return it as-is
			return parseFloat(settings.offset);
		};

		/**
		 * Get the document element's height
		 * @private
		 * @returns {Number}
		 */
		var getDocumentHeight = function () {
			return Math.max(
				document.body.scrollHeight,
				document.documentElement.scrollHeight,
				document.body.offsetHeight,
				document.documentElement.offsetHeight,
				document.body.clientHeight,
				document.documentElement.clientHeight
			);
		};

		/**
		 * Determine if an element is in view
		 * @param  {Node}    elem     The element
		 * @param  {Object}  settings The settings for this instantiation
		 * @param  {Boolean} bottom   If true, check if element is above bottom of viewport instead
		 * @return {Boolean}          Returns true if element is in the viewport
		 */
		var isInView = function (elem, settings, bottom) {
			var bounds = elem.getBoundingClientRect();
			var offset = getOffset(settings);
			if (bottom) {
				return (
					parseInt(bounds.bottom, 10) <
					(window.innerHeight ||
						document.documentElement.clientHeight)
				);
			}
			return parseInt(bounds.top, 10) <= offset;
		};

		/**
		 * Check if at the bottom of the viewport
		 * @return {Boolean} If true, page is at the bottom of the viewport
		 */
		var isAtBottom = function () {
			if (window.innerHeight + window.pageYOffset >= getDocumentHeight())
				return true;
			return false;
		};

		/**
		 * Check if the last item should be used (even if not at the top of the page)
		 * @param  {Object} item     The last item
		 * @param  {Object} settings The settings for this instantiation
		 * @return {Boolean}         If true, use the last item
		 */
		var useLastItem = function (item, settings) {
			if (isAtBottom() && isInView(item.content, settings, true))
				return true;
			return false;
		};

		/**
		 * Get the active content
		 * @param  {Array}  contents The content areas
		 * @param  {Object} settings The settings for this instantiation
		 * @return {Object}          The content area and matching navigation link
		 */
		var getActive = function (contents, settings) {
			var last = contents[contents.length - 1];
			if (useLastItem(last, settings)) return last;
			for (var i = contents.length - 1; i >= 0; i--) {
				if (isInView(contents[i].content, settings)) return contents[i];
			}
		};

		/**
		 * Deactivate parent navs in a nested navigation
		 * @param  {Node}   nav      The starting navigation element
		 * @param  {Object} settings The settings for this instantiation
		 */
		var deactivateNested = function (nav, settings) {
			// If nesting isn't activated, bail
			if (!settings.nested || !nav.parentNode) return;

			// Get the parent navigation
			var li = nav.parentNode.closest('li');
			if (!li) return;

			// Remove the active class
			li.classList.remove(settings.nestedClass);

			// Apply recursively to any parent navigation elements
			deactivateNested(li, settings);
		};

		/**
		 * Deactivate a nav and content area
		 * @param  {Object} items    The nav item and content to deactivate
		 * @param  {Object} settings The settings for this instantiation
		 */
		var deactivate = function (items, settings) {
			// Make sure there are items to deactivate
			if (!items) return;

			// Get the parent list item
			var li = items.nav.closest('li');
			if (!li) return;

			// Remove the active class from the nav and content
			li.classList.remove(settings.navClass);
			items.content.classList.remove(settings.contentClass);

			// Deactivate any parent navs in a nested navigation
			deactivateNested(li, settings);

			// Emit a custom event
			emitEvent('gumshoeDeactivate', li, {
				link: items.nav,
				content: items.content,
				settings: settings,
			});
		};

		/**
		 * Activate parent navs in a nested navigation
		 * @param  {Node}   nav      The starting navigation element
		 * @param  {Object} settings The settings for this instantiation
		 */
		var activateNested = function (nav, settings) {
			// If nesting isn't activated, bail
			if (!settings.nested) return;

			// Get the parent navigation
			var li = nav.parentNode.closest('li');
			if (!li) return;

			// Add the active class
			li.classList.add(settings.nestedClass);

			// Apply recursively to any parent navigation elements
			activateNested(li, settings);
		};

		/**
		 * Activate a nav and content area
		 * @param  {Object} items    The nav item and content to activate
		 * @param  {Object} settings The settings for this instantiation
		 */
		var activate = function (items, settings) {
			// Make sure there are items to activate
			if (!items) return;

			// Get the parent list item
			var li = items.nav.closest('li');
			if (!li) return;

			// Add the active class to the nav and content
			li.classList.add(settings.navClass);
			items.content.classList.add(settings.contentClass);

			// Activate any parent navs in a nested navigation
			activateNested(li, settings);

			// Emit a custom event
			emitEvent('gumshoeActivate', li, {
				link: items.nav,
				content: items.content,
				settings: settings,
			});
		};

		/**
		 * Create the Constructor object
		 * @param {String} selector The selector to use for navigation items
		 * @param {Object} options  User options and settings
		 */
		var Constructor = function (selector, options) {
			//
			// Variables
			//

			var publicAPIs = {};
			var navItems, contents, current, timeout, settings;

			//
			// Methods
			//

			/**
			 * Set variables from DOM elements
			 */
			publicAPIs.setup = function () {
				// Get all nav items
				navItems = document.querySelectorAll(selector);

				// Create contents array
				contents = [];

				// Loop through each item, get it's matching content, and push to the array
				Array.prototype.forEach.call(navItems, function (item) {
					// Get the content for the nav item
					var content = document.getElementById(
						decodeURIComponent(item.hash.substr(1))
					);
					if (!content) return;

					// Push to the contents array
					contents.push({
						nav: item,
						content: content,
					});
				});

				// Sort contents by the order they appear in the DOM
				sortContents(contents);
			};

			/**
			 * Detect which content is currently active
			 */
			publicAPIs.detect = function () {
				// Get the active content
				var active = getActive(contents, settings);

				// if there's no active content, deactivate and bail
				if (!active) {
					if (current) {
						deactivate(current, settings);
						current = null;
					}
					return;
				}

				// If the active content is the one currently active, do nothing
				if (current && active.content === current.content) return;

				// Deactivate the current content and activate the new content
				deactivate(current, settings);
				activate(active, settings);

				// Update the currently active content
				current = active;
			};

			/**
			 * Detect the active content on scroll
			 * Debounced for performance
			 */
			var scrollHandler = function (event) {
				// If there's a timer, cancel it
				if (timeout) {
					window.cancelAnimationFrame(timeout);
				}

				// Setup debounce callback
				timeout = window.requestAnimationFrame(publicAPIs.detect);
			};

			/**
			 * Update content sorting on resize
			 * Debounced for performance
			 */
			var resizeHandler = function (event) {
				// If there's a timer, cancel it
				if (timeout) {
					window.cancelAnimationFrame(timeout);
				}

				// Setup debounce callback
				timeout = window.requestAnimationFrame(function () {
					sortContents(contents);
					publicAPIs.detect();
				});
			};

			/**
			 * Destroy the current instantiation
			 */
			publicAPIs.destroy = function () {
				// Undo DOM changes
				if (current) {
					deactivate(current, settings);
				}

				// Remove event listeners
				window.removeEventListener('scroll', scrollHandler, false);
				if (settings.reflow) {
					window.removeEventListener('resize', resizeHandler, false);
				}

				// Reset variables
				contents = null;
				navItems = null;
				current = null;
				timeout = null;
				settings = null;
			};

			/**
			 * Initialize the current instantiation
			 */
			var init = function () {
				// Merge user options into defaults
				settings = extend(defaults, options || {});

				// Setup variables based on the current DOM
				publicAPIs.setup();

				// Find the currently active content
				publicAPIs.detect();

				// Setup event listeners
				window.addEventListener('scroll', scrollHandler, false);
				if (settings.reflow) {
					window.addEventListener('resize', resizeHandler, false);
				}
			};

			//
			// Initialize and return the public APIs
			//

			init();
			return publicAPIs;
		};

		//
		// Return the Constructor
		//

		return Constructor;
	}
);
