/***************************/
/* INITIALIZATION REGISTRY */
/***************************/

/*	Polyfill for requestIdleCallback in Apple and Microsoft browsers. */
if (!window.requestIdleCallback) {
	window.requestIdleCallback = (fn) => { setTimeout(fn, 0); };
}

/*	TBC. */
GW.initializersDone = { };
GW.initializers = { };
function registerInitializer(name, tryEarly, precondition, fn) {
	GW.initializersDone[name] = false;
	GW.initializers[name] = fn;
	let wrapper = function () {
		if (GW.initializersDone[name]) return;
		if (!precondition()) {
			if (tryEarly) {
				setTimeout(() => requestIdleCallback(wrapper, { timeout: 1000 }), 50);
			} else {
				document.addEventListener("readystatechange", wrapper, { once: true });
			}
			return;
		}
		GW.initializersDone[name] = true;
		fn();
	};
	if (tryEarly) {
		requestIdleCallback(wrapper, { timeout: 1000 });
	} else {
		document.addEventListener("readystatechange", wrapper, { once: true });
		requestIdleCallback(wrapper);
	}
}
function forceInitializer(name) {
	if (GW.initializersDone[name]) return;
	GW.initializersDone[name] = true;
	GW.initializers[name]();
}

/***********/
/* COOKIES */
/***********/

/*	Sets a cookie. */
function setCookie(name, value, days) {
	var expires = "";
	if (!days) days = 36500;
	if (days) {
		var date = new Date();
		date.setTime(date.getTime() + (days*24*60*60*1000));
		expires = "; expires=" + date.toUTCString();
	}
	document.cookie = name + "=" + (value || "")  + expires + "; path=/";
}

/*	Reads the value of named cookie.
	Returns the cookie as a string, or null if no such cookie exists. */
function readCookie(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i = 0; i < ca.length; i++) {
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1, c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
	}
	return null;
}

/****************************************************/
/* CSS CLASS MANIPULATION (polyfill for .classList) */
/****************************************************/

Element.prototype.addClass = function(className) {
	if (!this.hasClass(className))
		this.className = (this.className + " " + className).trim();
}
Element.prototype.addClasses = function(classNames) {
	let elementClassNames = this.className.trim().split(/\s/);
	
	classNames.forEach(className => {
		if (!this.hasClass(className))
			elementClassNames.push(className);
	});
	
	this.className = elementClassNames.join(" ");
}
Element.prototype.removeClass = function(className) {
	this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
	if (this.className == "") this.removeAttribute("class");
}
Element.prototype.removeClasses = function(classNames) {
	classNames.forEach(className => {
		this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
	});
	if (this.className == "") this.removeAttribute("class");
}
Element.prototype.hasClass = function(className) {
	return (new RegExp("(^|\\s+)" + className + "(\\s+|$)")).test(this.className);
}
Element.prototype.toggleClass = function(className, on) {
	if ((typeof on == "undefined" && !this.hasClass(className)) || 
		on == true) {
		this.addClass(className);
	} else {
		this.removeClass(className);
	}
}

/********************/
/* QUERYING THE DOM */
/********************/

function queryAll(selector, context) {
    context = context || document;
    // Redirect simple selectors to the more performant function
    if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) {
        switch (selector.charAt(0)) {
            case '#':
                // Handle ID-based selectors
                let element = document.getElementById(selector.substr(1));
                return element ? [ element ] : [ ];
            case '.':
                // Handle class-based selectors
                // Query by multiple classes by converting the selector 
                // string into single spaced class names
                var classes = selector.substr(1).replace(/\./g, ' ');
                return [].slice.call(context.getElementsByClassName(classes));
            default:
                // Handle tag-based selectors
                return [].slice.call(context.getElementsByTagName(selector));
        }
    }
    // Default to `querySelectorAll`
    return [].slice.call(context.querySelectorAll(selector));
}
function query(selector, context) {
	let all = queryAll(selector, context);
	return (all.length > 0) ? all[0] : null;
}
Object.prototype.queryAll = function (selector) {
	return queryAll(selector, this);
}
Object.prototype.query = function (selector) {
	return query(selector, this);
}

/*******************************/
/* EVENT LISTENER MANIPULATION */
/*******************************/

/*	Adds an event listener to a button (or other clickable element), attaching 
	it to both ‘click’ and ‘keyup’ events (for use with keyboard navigation).
	Optionally also attaches the listener to the ‘mousedown’ event, making the 
	element activate on mouse down instead of mouse up. */
Element.prototype.addActivateEvent = function(func, includeMouseDown) {
	let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) };
	if (includeMouseDown) this.addEventListener("mousedown", ael);
	this.addEventListener("click", ael);
	this.addEventListener("keyup", ael);
}

/*	Removes event listener from a clickable element, automatically detaching it
	from all relevant event types. */
Element.prototype.removeActivateEvent = function() {
	let ael = this.activateEventListener;
	this.removeEventListener("mousedown", ael);
	this.removeEventListener("click", ael);
	this.removeEventListener("keyup", ael);
}

/*	Adds a scroll event listener to the page. */
function addScrollListener(fn, name) {
	let wrapper = (event) => {
		requestAnimationFrame(() => {
			fn(event);
			document.addEventListener("scroll", wrapper, { once: true, passive: true });
		});
	}
	document.addEventListener("scroll", wrapper, { once: true, passive: true });

	// Retain a reference to the scroll listener, if a name is provided.
	if (typeof name != "undefined")
		GW[name] = wrapper;
}

/************************/
/* ACTIVE MEDIA QUERIES */
/************************/

/*	This function provides two slightly different versions of its functionality,
	depending on how many arguments it gets.

	If one function is given (in addition to the media query and its name), it
	is called whenever the media query changes (in either direction).

	If two functions are given (in addition to the media query and its name), 
	then the first function is called whenever the media query starts matching, 
	and the second function is called whenever the media query stops matching.

	If you want to call a function for a change in one direction only, pass an
	empty closure (NOT null!) as one of the function arguments.

	There is also an optional fifth argument. This should be a function to be 
	called when the active media query is canceled.
	*/
function doWhenMatchMedia(mediaQuery, name, ifMatchesOrAlwaysDo, otherwiseDo = null, whenCanceledDo = null) {
	if (typeof GW.mediaQueryResponders == "undefined")
		GW.mediaQueryResponders = { };

	let mediaQueryResponder = (event, canceling = false) => {
		if (canceling) {
			GWLog(`Canceling media query “${name}”`);

			if (whenCanceledDo != null)
				whenCanceledDo(mediaQuery);
		} else {
			let matches = (typeof event == "undefined") ? mediaQuery.matches : event.matches;

			GWLog(`Media query “${name}” triggered (matches: ${matches ? "YES" : "NO"})`);

			if (otherwiseDo == null || matches) ifMatchesOrAlwaysDo(mediaQuery);
			else otherwiseDo(mediaQuery);
		}
	};
	mediaQueryResponder();
	mediaQuery.addListener(mediaQueryResponder);

	GW.mediaQueryResponders[name] = mediaQueryResponder;
}

/*	Deactivates and discards an active media query, after calling the function
	that was passed as the whenCanceledDo parameter when the media query was
	added.
	*/
function cancelDoWhenMatchMedia(name) {
	GW.mediaQueryResponders[name](null, true);

	for ([ key, mediaQuery ] of Object.entries(GW.mediaQueries))
		mediaQuery.removeListener(GW.mediaQueryResponders[name]);

	GW.mediaQueryResponders[name] = null;
}

/****************/
/* MISC HELPERS */
/****************/

/*	Returns the passed object if it’s truthy, or a newly created HTMLElement.
	Æ(x) is the element analogue of (x||{}).
	*/
function Æ(x) {
	return x || document.createElement(null);
}

/*	If top of element is not at or above the top of the screen, scroll it into
	view. */
Element.prototype.scrollIntoViewIfNeeded = function() {
	GWLog("scrollIntoViewIfNeeded");
	let rect = this.getBoundingClientRect();
	if ((rect.bottom > window.innerHeight && rect.top > 0) ||
		rect.top < 0) {
		this.scrollIntoView(true);
	}
}

/*	Return the currently selected text, as HTML (rather than unstyled text).
	*/
function getSelectionHTML() {
	var container = document.createElement("div");
	container.appendChild(window.getSelection().getRangeAt(0).cloneContents());
	return container.innerHTML;
}

/*	Return the value of a GET (i.e., URL) parameter.
	*/
function getQueryVariable(variable) {
	var query = window.location.search.substring(1);
	var vars = query.split("&");
	for (var i = 0; i < vars.length; i++) {
		var pair = vars[i].split("=");
		if (pair[0] == variable)
			return pair[1];
	}

	return false;
}

/*	Given an element or a selector, removes that element (or the element 
	identified by the selector).

	If multiple elements match the selector, only the first is removed.
	*/
function removeElement(elementOrSelector, ancestor = document) {
	if (typeof elementOrSelector == "string") elementOrSelector = ancestor.query(elementOrSelector);
	if (elementOrSelector) elementOrSelector.parentElement.removeChild(elementOrSelector);
}

/*	Returns true if the string begins with the given prefix.
	*/
String.prototype.hasPrefix = function (prefix) {
	return (this.lastIndexOf(prefix, 0) === 0);
}

/*	Toggles whether the page is scrollable.
	*/
function togglePageScrolling(enable) {
	if (!enable) {
		window.addEventListener('keydown', GW.scrollingDisabledKeyDown = (event) => {
			let forbiddenKeys = [ " ", "Spacebar", "ArrowUp", "ArrowDown", "Up", "Down" ];
			if (forbiddenKeys.contains(event.key) &&
				event.target == document.body) {
				event.preventDefault();
			}
		});
	} else {
		window.removeEventListener('keydown', GW.scrollingDisabledKeyDown);
	}
}

/*	Copies a string to the clipboard.
	*/
function copyTextToClipboard(string) {
	let scratchpad = query("#scratchpad");
	scratchpad.value = string;
	scratchpad.select();
	document.execCommand("copy");
}

/*	Returns the next element sibling of the element, wrapping around to the 
	first child of the parent if the element is the last child.
	Returns the element itself, if it has no siblings or no parent.
	*/
Element.prototype.nextElementSiblingCyclical = function() {
	if (this.parentElement == null) return this;
	
	return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) + 1) % this.parentElement.children.length];
}

/*	Returns the previous element sibling of the element, wrapping around to the 
	last child of the parent if the element is the first child.
	Returns the element itself, if it has no siblings or no parent.
	*/
Element.prototype.previousElementSiblingCyclical = function() {
	if (this.parentElement == null) return this;
	
	return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) - 1) % this.parentElement.children.length];
}

/********************/
/* DEBUGGING OUTPUT */
/********************/

function GWLog (string) {
	if (GW.loggingEnabled == true || (GW.loggingEnabled == null && localStorage.getItem("logging-enabled") == "true"))
		console.log(string);
}
GW.enableLogging = (permanently = false) => {
	if (permanently)
		localStorage.setItem("logging-enabled", "true");
	else
		GW.loggingEnabled = true;
};
GW.disableLogging = (permanently = false) => {
	if (permanently)
		localStorage.removeItem("logging-enabled");
	else
		GW.loggingEnabled = false;
};

/**********/
/* NAV UI */
/**********/

/*	Hide the site nav UI on scroll down; show it on scroll up.

	Called by the ‘updateSiteNavUIStateScrollListener’ scroll listener.
	*/
function updateSiteNavUIState(event) {
	GWLog("updateSiteNavUIState");

	let newScrollTop = window.pageYOffset || document.documentElement.scrollTop;
	GW.scrollState.unbrokenDownScrollDistance = (newScrollTop > GW.scrollState.lastScrollTop) ? 
														(GW.scrollState.unbrokenDownScrollDistance + newScrollTop - GW.scrollState.lastScrollTop) : 
													 	0;
	GW.scrollState.unbrokenUpScrollDistance = (newScrollTop < GW.scrollState.lastScrollTop) ?
													 (GW.scrollState.unbrokenUpScrollDistance + GW.scrollState.lastScrollTop - newScrollTop) :
													 0;
	GW.scrollState.lastScrollTop = newScrollTop;

	// Hide site nav UI when scrolling a full page down.
	if (GW.scrollState.unbrokenDownScrollDistance > window.innerHeight) {
		if (!GW.scrollState.siteNavUI[0].hasClass("hidden")) toggleSiteNavUI();
	}

	// Make site nav UI translucent when scrolling down.
	GW.scrollState.siteNavUI.forEach(element => {
		if (GW.scrollState.unbrokenDownScrollDistance > 0) element.addClass("translucent-on-scroll");
		else element.removeClass("hidden");
	});

	// On desktop, show site nav UI when scrolling a full page up, or to the
	// the top of the page.
	// On mobile, show site nav UI on ANY scroll up.
	if (GW.mediaQueries.mobileNarrow.matches) {
		if (GW.scrollState.unbrokenUpScrollDistance > 0 || GW.scrollState.lastScrollTop <= 0)
			showSiteNavUI();
	} else if (   GW.scrollState.unbrokenUpScrollDistance > window.innerHeight
			   || GW.scrollState.lastScrollTop == 0) {
		showSiteNavUI();
	}
}

function toggleSiteNavUI() {
	GWLog("toggleSiteNavUI");

	GW.scrollState.siteNavUI.forEach(element => {
		element.toggleClass("hidden");
		element.removeClass("translucent-on-scroll");
	});
}

function showSiteNavUI() {
	GWLog("showSiteNavUI");

	GW.scrollState.siteNavUI.forEach(element => {
		element.removeClass("hidden");
		element.removeClass("translucent-on-scroll");
	});
}

/******************/
/* INITIALIZATION */
/******************/

registerInitializer('earlyInitialize', true, () => (query("#content") != null), () => {
	GWLog("INITIALIZER earlyInitialize");
	// Check to see whether we’re on a mobile device (which we define as a touchscreen^W narrow viewport).
// 	GW.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
//	GW.isMobile = ('ontouchstart' in document.documentElement);
	GW.mediaQueries = {
		mobileNarrow: matchMedia("(max-width: 520px)"),
		mobileWide: matchMedia("(max-width: 900px)"),
		mobileMax: matchMedia("(max-width: 960px)"),
		hover: matchMedia("only screen and (hover: hover) and (pointer: fine)")
	};
	GW.isMobile = GW.mediaQueries.mobileMax.matches;
	GW.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;

	GW.siteNavUISelector = "main nav";
});

/*************************/
/* POST-LOAD ADJUSTMENTS */
/*************************/

registerInitializer('pageLayoutFinished', false, () => (document.readyState == "complete"), () => {
	forceInitializer('earlyInitialize');

	GWLog("INITIALIZER pageLayoutFinished");

	// We pre-query the relevant elements, so we don’t have to run queryAll on
	// every firing of the scroll listener.
	GW.scrollState = {
		"lastScrollTop":					window.pageYOffset || document.documentElement.scrollTop,
		"unbrokenDownScrollDistance":		0,
		"unbrokenUpScrollDistance":			0,
		"siteNavUI":						queryAll(GW.siteNavUISelector),
	};
	addScrollListener(updateSiteNavUIState, "updateSiteNavUIStateScrollListener");
	GW.scrollState.siteNavUI.forEach(element => {
		element.addEventListener("mouseover", () => { showSiteNavUI(); });
	});

	// Wrap all images in a span.
	document.querySelectorAll("main > nav ~ * img").forEach(image => {
		let wrapper = document.createElement("span");
		wrapper.classList.add("img");
		image.parentElement.insertBefore(wrapper, image);
		wrapper.appendChild(image);
		wrapper.classList.add(...image.classList);
		image.classList.remove(...image.classList);
	});	

	imageFocusSetup();
	focusImageSpecifiedByURL();
});

