// javascript support code for the GAVO data center


////////////////////////////// possibly generic functions

function decodeGetPars(queryString) {
// an incredibly crappy approach to getting whatever was in the query string
// into javascript.
	var pars = new Object();
	var pairs = queryString.slice(1).split("&");
	for (var ind in pairs) {
		var pair = pairs[ind].split("=");
		var key = 'arg'+unescape(pair[0]).replace("+", " ");
		var value = unescape(pair[1]).replace("+", " ");
		if (pars[key]==undefined) {
			pars[key] = new Array();
		}
		pars[key].push(value);
	}
	return pars;
}


function isIn(item, arr) {
// does a linear search through arr to see if item is in there
// (can't we use assoc. arrs for that?)
	for (var ind in arr) {
		if (arr[ind]==item) {
			return true;
		}
	}
	return false;
}


function getJD(date) {
	return date/86400000+2440587.5;
}


function getJYear(date) {
	return (getJD(date)-2451545)/365.25+2000;
}


///////////// Micro templating.  See develNotes
function htmlEscape(str) {
	return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;')
		.replace(/'/g, '&apos;').replace(/</g, '&lt;')
		.replace(/>/g, '&gt;');
}


function jqueryOrRaw(arg) {
	return arg.html ? arg.html() : arg;
}


(function () {
	var _tmplCache = {};
	this.renderTemplate = function (templateId, data) {
		var err = "";
		var func = _tmplCache[templateId];
		if (!func) {
			str = document.getElementById(templateId).innerHTML;
			var strFunc =
				"var p=[],print=function(){p.push.apply(p,arguments);};"
				+ "with(obj){p.push('"
				+ str.replace(/[\r\t\n]/g, " ")
				.split("'").join("\\'")
				.split("\t").join("'")
				.replace(/\$([a-zA-Z_]+)/g, "',htmlEscape($1),'")
				.replace(/\$!([a-zA-Z_]+)/g, "',jqueryOrRaw($1),'")
				+ "');}return $.trim(p.join(''));";
				func = new Function("obj", strFunc);
				_tmplCache[str] = func;
		}
		return func(data);
	}
})()




///////////// Code for generating GET-URLs for forms

function getEnclosingForm(element) {
// returns the form element immediately enclosing element.
	if (element.nodeName=="FORM") {
		return element;
	}
	return getEnclosingForm(element.parentNode);
}

function getSelectedEntries(selectElement) {
// returns an array of all selected entries from a select element
// in url encoded form
	var result = new Array();
	var i;

	for (i=0; i<selectElement.length; i++) {
		if (selectElement.options[i].selected) {
			result.push(selectElement.name+"="+encodeURIComponent(
				selectElement.options[i].value))
		}
	}
	return result;
}

function makeQueryItem(element, keepEmpties) {
// returns an url-encoded query tag item out of a form element
	var val=null;

	switch (element.nodeName.toUpperCase()) {
		case "INPUT":
		case "TEXTAREA":
			if (element.type=="radio" || element.type=="checkbox") {
				if (element.checked) {
					val = element.name+"="+encodeURIComponent(element.value);
				}
			} else if (element.name) {
				if (element.value || keepEmpties) {
					val = element.name+"="+encodeURIComponent(element.value);
				}
			}
			break;
		case "SELECT":
			return getSelectedEntries(element).join("&");
			break;
		case "BUTTON":  // no state here
			break;
		default:
			alert("No handler for "+element.nodeName);
	}
	if (val) {
		return val;
	} else {
		return element.NodeName;
	}
}


function completeURL(uriOrPath) {
	// guesses whether uriOrPath has a host part and adds the
	// current one if necessary.
	// This for now does not support relative paths.
	if (uriOrPath[0]=="/") {
		return window.location.protocol+"//"+window.location.host+uriOrPath;
	}
	return uriOrPath;
}

function getFormQuery(form, ignoreNames, keepEmpties) {
	// returns a link to the result sending the HTML form form would
	// yield.
	var fragments = new Array();
	var i;

	items = form.elements;
	for (i=0; i<items.length; i++) {
		var fragment = makeQueryItem(items[i], keepEmpties);
		if (fragment && ignoreNames[items[i].name]==undefined) {
			fragments.push(fragment);
		}
	}
	return completeURL(form.getAttribute("action")+"?"+fragments.join("&"));
}


function makeResultLink(form) {
	return getFormQuery(form, []);
}


function makeBookmarkLink(form) {
	return getFormQuery(form, {'__nevow_form__': 1});
}


///////////// Functions for the sidebar

function followEmbeddedLink(parent) {
// forwards the browser to the href of the first child that has one.
// this is for the button-type decoration around links.
	for (index in parent.childNodes) {
		child = parent.childNodes[index];
		if (child.href) {
			window.location = child.href;
			break;
		}
	}
}

///////////// letting people bookmark anchors

function _add_anchor(ev) {
	var anchor = document.createElement("a");
	anchor.setAttribute("class", "linkAnchor");
	anchor.innerHTML = "¶";
	anchor.href = "#"+this.id;
	this.appendChild(anchor);
}


function _remove_anchor(ev) {
	var parent = this;
	parent.querySelectorAll(".linkAnchor").forEach(
		function(c) {
			parent.removeChild(c);
		});
}


function _add_anchor_shower(el) {
	if (el.id) {
		el.addEventListener("mouseenter", _add_anchor);
		el.addEventListener("mouseleave", _remove_anchor);
	}
}

function add_anchors_to_heads() {
// adds linkable anchors to all h? elements with ids.
// this is supposed to be run from a document.ready callback.
	document.querySelectorAll("h1,h2,h3,h4,h5"
		).forEach(_add_anchor_shower);
}


///////////// Functions dealing with the output format widget
// This incredibly verbose crap hides and shows widgets selecting aspects
// of the output format.  Basically, you have widgets in output_bussedElements
// that get notified when the output format changes and then (un)attach
// themselves to a container.
//
// To use this, you need:
//  * a block element with id "genForm-_OUTPUT" in which the subwidgets are
//    displayed
//  * a form element calling output_broadcast(this.value) on a change
//
//  In the DC, the static QueryMeta method getOutputWidget worries about this.


function output_BussedElement(domNode, id, visibleFor) {
	// An element that can be passed to output_broadcast and does something
	// in response.
	domNode.id = id;
	for (var ind in visibleFor) {
		domNode["visibleFor_"+visibleFor[ind]] = true;
	}
	return domNode;
}


function output_verbSelector(pars) {
	// returns a BussedElement for the selector for output verbosity
	var verbosities = new Array("H", "1", "2", "3");
	var root = document.createElement("span")
	var sel = document.createElement("select");
	var curSetting;

	root["class"] = "op_widget";
	root.appendChild(document.createTextNode(" output verbosity "));
	sel.name = "_VERB";
	if (pars['arg'+sel.name]!=undefined) {
		curSetting = pars['arg'+sel.name][0];
	} else {
		curSetting = "H";
	}
	for (verbInd in verbosities) {
		var el = sel.appendChild(document.createElement("option"));
		var verb = verbosities[verbInd];
		el.appendChild(document.createTextNode(verb));
		if (verb==curSetting) {
			el.selected = "selected";
		}
	}
	root.appendChild(sel);
	return output_BussedElement(root, "op_verb", ["VOTable", "FITS",
		"CSV", "TSV", "JSON"]);
}


function output_tdEncSelector(pars) {
	// returns a BussedElement to select VOTable encoding
	var root = document.createElement("span");
	var box = document.createElement("input");
	var curSetting;

	root["class"] = "op_widget";
	box.name = "_TDENC";
	if (pars['arg'+box.name]!=undefined) {
		curSetting = pars['arg'+box.name][0];
	} else {
		curSetting = "false";
	}
	box.type = "checkbox";
	box.style.width = "auto";
	root.appendChild(box);
	root.appendChild(document.createTextNode(" human-readable "));
	if (curSetting=="true") {
		box.checked = "checked";
	}
	return output_BussedElement(root, "op_tdenc", ["VOTable"]);
}


function output_setFormat(format) {
	var opts=document.getElementById("genForm-_FORMAT").options;
	for (var optInd=0; optInd<opts.length; optInd++) {
		if (opts[optInd].value==format) {
			opts[optInd].selected = true;
		} else {
			opts[optInd].selected = false;
		}
	}
}

var output_bussedElements = new Array();


function output_init() {
	var pars = decodeGetPars(location.search);
	var format = pars["arg_FORMAT"];

	if (!document.getElementById("genForm-_OUTPUT")) { // no form on page
		return;
	}
	output_bussedElements.push(output_verbSelector(pars));
	output_bussedElements.push(output_tdEncSelector(pars));

	let itemSelector = output_itemSelector(pars);
	if (itemSelector) {
		output_bussedElements.push(itemSelector);
	}
	if (format==undefined) {
		format = "HTML";
	}
	output_broadcast(format);
	outputInited = true;
}

function output_broadcast(newFormat) {
	var visibleForThis = "visibleFor_"+newFormat;

	output_setFormat(newFormat);
	for (var ind in output_bussedElements) {
		var el=output_bussedElements[ind];
		if (el[visibleForThis]) {
			output_show(el);
		} else {
			output_hide(el);
		}
	}
}

function init_preview_observer() {
	// create an observer for lazy image loading (so far used for
	// previews only.
	if (IntersectionObserver) {
		var previewObserver = new IntersectionObserver(
			function(entries, obs) {
				entries.forEach(function(item) {
					if (item.isIntersecting) {
						insertPreview(item.target);
						obs.unobserve(item.target);
					}
				},
				{'threshold': 2});
			});

		document.querySelectorAll(".productlink").forEach(
			function(node) {previewObserver.observe(node)});
	}
}


///////////////// UI extensions

function enableMLMenu(menuRoot, selectHandler) {
	// makes a hierarchical popup menu from a nested ul/li structure.
	// menuRoot is a piece of DOM with the ul/li-s.
	// Furnish clickable li-s with a data-value attribute.
	// The selectHandler receives that value if one has been clicked
	// (plus the underlying event for special effects).
	// menuRoot is hidden anyway.
	// The function returns a function that pops up the menu and arranges
	// for it to be popped down again (and of course to call selectHandler
	// as appropriate).
	// There's example code for this in wirr.

	// make ul-s that are children of li-s to pop out things
	for (const node of menuRoot.querySelectorAll("li")) {
		if (node.querySelector("ul")) {
			// there's a submenu here, amend the last text node if necessary
			let textNode = null;
			for (child of node.childNodes) {
				if (child.nodeType === Node.TEXT_NODE) {
					textNode = child;
				}
			}
			if (textNode && !textNode.textContent.endsWith("➤")) {
				textNode.textContent = textNode.textContent+" ➤";
			}
		}
	}

	let popDown = (ev) => {
		try {
			let selected = ev.target.closest("li[data-value]");
			if (selected) {
				selectHandler(selected.dataset.value, ev);
			}
		} finally {
			menuRoot.style.display = 'none';
		}

	}

	return () => {
		menuRoot.style.display = 'block';
		document.addEventListener("click",
			popDown,
			{capture: true, once: true});
	}
}


///////////////// jquery-dependent code


$(document).ready(
	function() {
		output_init();
		init_preview_observer();
		add_anchors_to_heads();
	});


function makeTabCallback(handlers) {
// see develNotes
	return function (ev) {
		$("#tabset_tabs > li").removeClass("selected");
		var curTab = $(ev.currentTarget);
		curTab.addClass("selected");
		var mainId = curTab.find("a").attr("name");
		handlers[mainId]($('#mainbox'));
	}
}

function popupInnerWindow(content, parent, onClose) {
// puts content into a thing that looks like a window and can be
// dragged and resized.  Parent is the element the container
// gets added to within the tree.
// The function returns a closer function destroying the subwindow.
// This is only used in output_popupAddSel, and it shouldn't be
// used any more.  Let's use some more modern thing (e.g., what's used
// in flotplot) in the future and port output_popupAddSel to that.
	var prevParent = content.parent()
	prevParent.css({"position": "relative"});

	var container = $("<div class='innerWin'/>");
	var titlebar = $(
		"<p class='innerTitle'><span class='closer'>x&nbsp;</span></p>");
	container.append(titlebar);
	container.append(content);
	content.addClass("innerBody");
	content.show();

	container.draggable({cancel: ".innerBody"});
	parent.append(container);
	// XXX TODO: Why doesn't container expand to content size in the first place?
	container.css({"width": content.width(),
		"height": content.height()+titlebar.height(),
		"position": "relative"});

	closer = function(ignored) {
		container.hide();
		container.detach();
		content.hide();
		onClose();
	}

	container.find(".closer").bind("click", closer);
	return closer;
}


function output_popupAddSel(additemsSelection) {
// pops up the dialog with the additional output items.  The popup
// receives a cleanup function.
// The trick here is that the selector (that has display:none outside
// or the popup) needs to be added back to the form so it gets evaluated;
// but it needs to be detached while shown in the window.
	var govButton = output_bussedElements[2];
	additemsSelection.detach();

	var closer = popupInnerWindow(
		additemsSelection,
		$("#genForm-_OUTPUT"),
		function() {
			govButton.unbind("click");
			govButton.bind("click", function() {
				output_popupAddSel(additemsSelection)});
			govButton.empty().append("More output fields");
			govButton.parent().append(additemsSelection);
		});
	govButton.unbind("click");
	govButton.bind("click", closer);
	govButton.empty().append("Pop down field selector");
	return false;
}


function output_itemSelector(pars) {
	// returns a Bussedelement to pop up the element Selector if
	// there are items to select.
	var itemSelector = $("#genForm-_ADDITEMS");
	if (! itemSelector) {
		return null;
	}
	itemSelector.detach();

	var opener = $(`<button type="button" id="op_additem" class="popButton"
		>More output fields</button>`);
	opener.bind("click", () => output_popupAddSel(itemSelector));
	res = output_BussedElement(opener, "op_additem", ["HTML"]);
	return res;
}


function output_show(el) {
	if (!document.getElementById(el.id)) {
		var dest = $("#genForm-_OUTPUT");
		dest.append(document.createTextNode(" "));
		dest.append(el);
	}
}

function output_hide(el) {
	if (document.getElementById(el.id)) {
		$(el).detach();
	}
}


function openFlotPlot(tableElement, options) {
// loads the flot plotting code; this stub will be overrwritten as
// flotplot.js is loaded.
	$.getScript("/static/js/flotplot.js",
		function() {openFlotPlot(tableElement, options)}
		).fail(function() {
			alert("Loading the plotting code failed; please complain to the operators")});
}


function insertPreview(node, width) {
// replaces the text content of node with a DC-generated preview
// image.  node has to have a href attribute pointing to a DC
// FITS product for this to work (width is ignored these days).
	if (!node.getAttribute("href")) {
		return;
	}

	if (width==undefined) {
		width = 200;
	}

	var oldHref = node.getAttribute("href");
	var newPars = "preview=True&width="+width;
	var joiner = "?";
	// TODO: do actual URL parsing here
	if (oldHref.indexOf("?")!=-1) { // assume we have a query
		joiner = "&";
	}

	// IE9 workaround: defuse onmouseover before removing it.
	node.setAttribute("onmouseover", "");
	node.removeAttribute("onmouseover");
	var jqNode = $(node);
	jqNode.addClass("busy");

	var previewURL = oldHref+joiner+newPars;
	var img = $("<img class='preview'/>").attr("src", previewURL
		).attr("alt", "[preview image]");

	img.bind("load", function() {
		jqNode.prepend($("<br/>"));
		jqNode.prepend(img[0], node.firstChild);
		jqNode.removeClass("busy");
	});
	// TODO: use "complete" here when we've updated jquery
	img.bind("error", function() {
		jqNode.removeClass("busy");
	});
}


