const PassiveListener = { passive: true };
const LocalGet        = { method: 'GET', mode: 'same-origin',
			  redirect: 'follow' };

// Get an element by id or an Element object
function getelem(id) {
    return (id instanceof Element) ? id : document.getElementById(id);
}

// Find a child with a specific tag
function chi(me,tag) { return me.getElementsByTagName(tag)[0]; }

// Find a sibling element with a specific tag
function sib(me,tag) { return chi(me.parentElement, tag); }

// Add/remove class entries in bulk; tags is an Array each containing
// an Array of arguments to toggle. On return the second element of
// each Array will be updated to the current value.
function classmod(elem,tags) {
    for (var i = 0; i < tags.length; i++)
	tags[i][1] = elem.classList.toggle(...tags[i]);
    return tags;
}

// Read a key=value text file and return it as a Promise of a Map
function fetchconfig(url) {
    return fetch(url, LocalGet)
	.then(res => {
	    if (!res.ok) {
		throw new Error('HTTP error '+response.status);
	    } else {
		return res.text();
	    }
	})
	.then(text => {
	    var map = new Map();
	    for (const c of text.split(/[\r\n]+/)) {
		var m = c.match(/^\s*([\;\/]?)((?:"[^"]*"|[^"])*?)\s*=(.*)$/);
		if (m && m[1] == "") {
		    var k = m[2].replaceAll(/(^"|"$|(")")/g, "$2");
		    map.set(k, m[3]);
		}
	    }
	    return map;
	});
}

// Get the value from an input field if valid, otherwise its default
function valval(ie) {
    return ie.checkValidity() ? ie.value : ie.defaultValue;
}

// Parse a string for a valid boolean truth value
function cfgbool(str) {
    return !!str && !str.match(/^([0fnd]|of)/i);
}

// Initialize a form from a map. Checkboxes take a cfgbool string;
// if in the HTML their value is set to a false cfgbool string then
// the checkbox is inverted logic.
function initform(form,map,ro = false) {
    form = getelem(form);

    for (var e of form.elements) {
	if (e.classList.contains('noinit') ||
	    e instanceof HTMLFieldSetElement)
	    continue;
	if (ro && e.disabled != undefined && !e.classList.contains('noro'))
	    e.disabled = true;
	if (e instanceof HTMLInputElement ||
	    e instanceof HTMLSelectElement) {
	    const val = map.get(e.name);
	    if (val == null)
		continue;
	    if (e.type == 'radio')
		e.checked = (val == e.value);
	    else
		e.value = val;
	} else if (e instanceof HTMLButtonElement) {
	    e.disabled = ro;
	}
    }
}

// Load form initialization data
function loadform(form,url,ro = false) {
    fetchconfig(url)
	.then((map) => {initform(form,map,ro); })
	.catch(() => {});
}

// Replace the contents of selected HTML elements based on a map with
// selectors.
function fillin(map,html = false,top = document) {
    for (const [key,val] of map) {
	try {
	    const m = key.match(/(.*?)(?:\;([\w.-]*)(\??))?$/);
	    for (var e of top.querySelectorAll(m[1])) {
		try {
		    if (!html) e.textContents = val;
		    else if (!m[2]) e.innerHTML = val;
		    else if (!m[3]) e.setAttribute(m[2],val);
		    else if (!cfgbool(val)) e.removeAttribute(m[2]);
		    else e.setAttribute(m[2],'');
		} catch(e) { };
	    }
	} catch(e) { };
    }
}

// Load status or HTML data
function load(url,html = false)
{
    fetchconfig(url)
	.then(map => fillin(map,html))
	.catch(() => {});
}

// POST upload of data from within a form, with (optional)
// progress and response text, and redirect after success
function upload(form,data) {
    var xhr = new XMLHttpRequest();

    var progress = chi(form,'progress');
    if (progress) {
	progress.value = 0;
	xhr.upload.addEventListener('progress', (e) => {
	    if (!e.lengthComputable)
		return;
	    progress.max   = e.total * 1.05;
	    progress.value = e.loaded;
	}, PassiveListener);
    }

    classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]);

    xhr.addEventListener('loadend', (e) => {
	const ok = xhr.status >= 200 && xhr.status < 400;
	if (progress && ok)
	    progress.value = progress.max;

	var result = chi(form,'output');
	if (result) {
	    var msg = xhr.responseText.trimEnd();
	    if (!msg)
		msg = xhr.status + ' ' + xhr.statusText;
	    result.textContent = msg;
	}

	classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);

	// Optionally redirect elsewhere after success
	const reft = parseFloat(form.dataset.ref) * 1000;
	if (ok && reft >= 0.0) {
	    const refh = form.dataset.refUrl || window.location.href;
	    setTimeout(() => { window.location.href = refh; }, reft);
	}
    }, PassiveListener);

    xhr.open(form.method, form.action);
    xhr.responseType = 'text';
    xhr.send(data);
    return xhr;
}

// key=value formatting of form data; including inverted checkboxes
function textformdata(form) {
    var data = '';
    for (var e of form.elements) {
	var val = e.value;
	if (val == undefined || !e.name || e instanceof HTMLButtonElement) {
	    continue;
	} else if (e instanceof HTMLInputElement) {
	    if (e.type == 'radio' && !e.checked)
		continue;
	}
	data += e.name + '=' + val + "\r\n";
    }
    return data;
}

// POST form contents upload with response text
function uploadform() {
    event.preventDefault();
    const form = event.target.form || event.target;
    var files = form.elements['file'];
    if (files == undefined)
	return upload(form,textformdata(form));
    else if (files.files.length == 1)
	return upload(form,files.files[0]);
    else
	return files.click();
}

// Flip the status of an INPUT element between text and password
function showpwd(me = event.currentTarget) {
    const now_visible = me.classList.toggle('hide');
    const new_type = now_visible ? 'text' : 'password';
    me.classList.toggle('show',!now_visible);
    sib(me,'input').setAttribute('type', new_type);
}

// Insert translations as needed
var translations = null;
var delay_translate = false;
function translate(top = document) {
    delay_translate = (!translations || document.readyState != 'complete');
    if (!delay_translate)
	fillin(translations, true, top);
}
document.addEventListener('load', (e) => translate(), PassiveListener);

var lang_styleobj = document.createElement('style');
document.head.append(lang_styleobj);
function setlang(l = null) {
    l = l || document.documentElement.lang;
    if (!l.match(/^\w+(?:-\w+)*$/)) return; // Invalid language tag
    var sty = lang_styleobj.sheet;
    while (sty.rules.length) sty.deleteRule(0);
    sty.insertRule('[lang]:not(:lang('+l+')):not(:last-child),[lang]:lang('+l+')~[lang] {display: none}', 0);
    document.documentElement.lang = l;
}
setlang();

fetchconfig('/sys/lang')
    .then((map) => {
	setlang(map.get('LANG'));
	translations = map;
	if (delay_translate)
	    translate();
    })
    .catch(() => {});

// HTML include hack
class IncHTML extends HTMLElement {
    constructor() { self = super(); }
    connectedCallback() {
	fetch(self.getAttribute('src'), LocalGet)
	    .then ((r) => r.text())
	    .then ((text) => {
		const p = self.parentElement;
		self.outerHTML = text;
		translate(p);
	    });
    }
}
customElements.define('x-inc', IncHTML);

// Smart checkbox INPUT element
class XBox extends HTMLInputElement {
    set negative(x) { return this.toggleAttribute('negative', x); }
    get negative() { return this.hasAttribute('negative'); }
    #truth;
    get truth() { return this.#truth; }
    update_truth() {
	const old_truth = this.#truth;
	this.#truth = this.checked != this.negative;
	if (this.#truth !== old_truth)
	    this.resolve_conflicts();
    }
    set truth(x) {
	const new_truth = !!x;
	if (new_truth !== this.#truth) {
	    this.checked = new_truth != this.negative;
	    this.update_truth();
	}
    }
    set_list(n,x) { this.setAttribute(n, x ? x.join(',') : ''); }
    get_list(n) { var a = this.getAttribute(n); return a ? a.split(/\s*;\s*/) : []; }
    get needs() { return this.get_list('needs'); }
    set conflicts(x) { this.set_list('conflicts',x); }
    get conflicts() { return this.get_list('conflicts'); }
    set value(x) { this.truth = cfgbool(x); }
    get value() { return this.truth ? '1' : '0'; }

    constructor() {
	super();
	this.setAttribute('type', 'checkbox');
	this.checked = cfgbool(this.getAttribute('value')) != this.negative;
	this.addEventListener('change', this.update_truth);
	this.update_truth();
    }

    connectedCallback() { this.update_truth(); }
    adoptedCallback() { this.update_truth(); }

    static get observedAttributes() {
	return ['value', 'negative', 'conflicts', 'needs'];
    }
    attributeChangedCallback(name, oldval, newval) {
	if (oldval === newval)
	    return;

	if (name == 'value') {
	    this.truth = cfgbool(newval);
	} else if (name == 'negative') {
	    this.checked = this.#truth != this.negative;
	} else if (name == 'conflicts' || name == 'needs') {
	    this.resolve_conflicts();
	}
    }

    form_objects(list) {
	if (!this.form)
	    return [];
	return list.map((n) => this.form.elements[n]);
    }
    do_resolve(which, sense) {
	for (const e of this.form_objects(which)) {
	    if (e instanceof XBox)
		e.truth = sense;
	}
    }
    #resolving;
    resolve_conflicts() {
	const t = this.#truth;
	if (this.#resolving || !this.form)
	    return;
	this.#resolving = true;
	if (t) {
	    this.do_resolve(this.needs, true);
	    this.do_resolve(this.conflicts, false);
	} else {
	    for (const e of this.form.elements) {
		if (e instanceof XBox && e.truth) {
		    if (e.form_objects(e.needs).some((x) => x === this))
			e.truth = false;
		}
	    }
	}
	this.#resolving = false;
    }
}
customElements.define('x-box', XBox, { extends: 'input' });
