(function () {
    let lastUpdate = (new Date()).getTime();
    let reloadConfig = false;
    let needAdminPass = true;
    let lastSalt = "";
    let channelList = {};
    let minUser = 200;
    let listeners = [];
    let buttonHandlers={};
    let checkers={};
    let userFormatters={};
    function addEl(type, clazz, parent, text) {
        let el = document.createElement(type);
        if (clazz) {
            if (!(clazz instanceof Array)) {
                clazz = clazz.split(/  */);
            }
            clazz.forEach(function (ce) {
                el.classList.add(ce);
            });
        }
        if (text) el.textContent = text;
        if (parent) {
            if (typeof(parent) != 'object'){
                parent=document.querySelector(parent);
            }
            if (parent) parent.appendChild(el);
        }
        return el;
    }
    function forEl(query, callback, base) {
        if (!base) base = document;
        let all = base.querySelectorAll(query);
        for (let i = 0; i < all.length; i++) {
            callback(all[i]);
        }
    }
    function closestParent(element, clazz) {
        while (true) {
            let parent = element.parentElement;
            if (!parent) return;
            if (parent.classList.contains(clazz)) return parent;
            element = parent;
        }
    }
    function alertRestart() {
        reloadConfig = true;
        alert("Board reset triggered, reconnect WLAN if necessary");
    }
    function getJson(url) {
        return fetch(url)
            .then(function (r) { return r.json() });
    }
    function getText(url) {
        return fetch(url)
            .then(function (r) { return r.text() });
    }
    buttonHandlers.reset=function() {
        ensurePass()
            .then(function (hash) {
                fetch('/api/reset?_hash=' + encodeURIComponent(hash));
                alertRestart();
            })
            .catch(function (e) { });
    }
    function update() {
        let now = (new Date()).getTime();
        let ce = document.getElementById('connected');
        if (ce) {
            if ((lastUpdate + 3000) > now) {
                ce.classList.add('ok');
            }
            else {
                ce.classList.remove('ok');
            }
        }
        getJson('/api/status')
            .then(function (jsonData) {
                if (jsonData.salt !== undefined) {
                    lastSalt=jsonData.salt;
                    delete jsonData.salt;
                }
                if (jsonData.minUser !== undefined){
                    minUser=jsonData.minUser;
                    delete jsonData.minUser;
                }
                callListeners(api.EVENTS.status,jsonData);
                let statusPage = document.getElementById('statusPageContent');
                let even = true; //first counter
                if (statusPage){
                    for (let k in jsonData) {
                        if (typeof (jsonData[k]) === 'object') {
                            if (k.indexOf('count') == 0) {
                                createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even);
                                even = !even;
                                for (let sk in jsonData[k]) {
                                    let key = k + "." + sk;
                                    if (typeof (jsonData[k][sk]) === 'object') {
                                        //msg details
                                        updateMsgDetails(key, jsonData[k][sk]);
                                    }
                                    else {
                                        let el = document.getElementById(key);
                                        if (el) el.textContent = jsonData[k][sk];
                                    }
                                }
                            }
                            if (k.indexOf("ch") == 0) {
                                //channel def
                                let name = k.substring(2);
                                channelList[name] = jsonData[k];
                            }
                        }
                        else {
                            let el = document.getElementById(k);
                            if (el) el.textContent = jsonData[k];
                            forEl('.status-' + k, function (el) {
                                el.textContent = jsonData[k];
                            });
                        }
                    }
                }
                lastUpdate = (new Date()).getTime();
                if (reloadConfig) {
                    reloadConfig = false;
                    resetForm();
                }
            })
    }
    function resetForm(ev) {
        getJson("/api/config")
            .then(function (jsonData) {
                callListeners(api.EVENTS.config,jsonData);
                for (let k in jsonData) {
                    if (k == "useAdminPass") {
                        needAdminPass = jsonData[k] != 'false';
                    }
                    let el = document.querySelector("[name='" + k + "']");
                    if (el) {
                        let v = jsonData[k];
                        let def = getConfigDefition(k);
                        if (def.check == 'checkMinMax') {
                            //simple migration if the current value is outside the range
                            //we even "hide" this from the user
                            v = parseFloat(v);
                            if (!isNaN(v)) {
                                if (def.min !== undefined) {
                                    if (v < parseFloat(def.min)) v = parseFloat(def.min);
                                }
                                if (def.max !== undefined) {
                                    if (v > parseFloat(def.max)) v = parseFloat(def.max);
                                }
                            }
                        }
                        if (el.tagName === 'SELECT') {
                            setSelect(el, v);
                        }
                        else {
                            el.value = v;
                        }
                        el.setAttribute('data-loaded', v);
                        let changeEvent = new Event('change');
                        el.dispatchEvent(changeEvent);
                    }
                }
                let name = jsonData.systemName;
                if (name) {
                    let el = document.getElementById('headline');
                    if (el) el.textContent = name;
                    document.title = name;
                }
            });
    }
    buttonHandlers.resetForm=resetForm;
    checkers.checkMinMax=function(v, allValues, def) {
        let parsed = parseFloat(v);
        if (isNaN(parsed)) return "must be a number";
        if (def.min !== undefined) {
            if (parsed < parseFloat(def.min)) return "must be >= " + def.min;
        }
        if (def.max !== undefined) {
            if (parsed > parseFloat(def.max)) return "must be <= " + def.max;
        }
    }

    checkers.checkPort=function(v,allValues,def){
        let parsed=parseInt(v);
        if (isNaN(parsed)) return "must be a number";
        if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536"; 
    }

    checkers.checkSystemName=function(v) {
        //2...32 characters for ssid
        let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
        if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
        if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
    }
    function checkApPass(v) {
        //min 8 characters
        if (v.length < 8) {
            return "password must be at least 8 characters";
        }
    }
    checkers.checkAdminPass=function(v) {
        return checkApPass(v);
    }

    checkers.checkApIp=function(v, allValues) {
        if (!v) return "cannot be empty";
        let err1 = "must be in the form 192.168.x.x";
        if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1;
        let parts = v.split(".");
        if (parts.length != 4) return err1;
        for (let idx = 0; idx < 4; idx++) {
            let iv = parseInt(parts[idx]);
            if (iv < 0 || iv > 255) return err1;
        }
    }
    checkers.checkNetMask=function(v, allValues) {
        return checkers.checkApIp(v, allValues);
    }

    checkers.checkIpAddress=function(v, allValues, def) {
        if (!v) return "cannot be empty";
        if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
            && !v.match(/.*\.local/))
            return "must be either in the form 192.168.1.1 or xxx.local";
    }
    checkers.checkMCAddress=function(v, allValues, def) {
        if (!v) return "cannot be empty";
        if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/))
            return "must be in the form 224.0.0.1";
        let parts=v.split(".");
        let o1=parseInt(parts[0]);
        if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255"

    }
    checkers.checkXDR=function(v, allValues) {
        if (!v) return;
        let parts = v.split(',');
        if (parseInt(parts[1]) == 0) return;
        if (parseInt(parts[1]) != 0 && !parts[6]) return "missing transducer name";
        for (let k in allValues) {
            if (!k.match(/^XDR/)) continue;
            let cmp = allValues[k];
            if (cmp == v) {
                return "same mapping already defined in " + k;
            }
            let cmpParts = cmp.split(',');
            if (parseInt(cmpParts[1]) != 0 && parts[6] == cmpParts[6]) {
                return "transducer " + parts[6] + " already defined in " + k;
            }
            //check similar mappings
            if (parts[0] == cmpParts[0] && parts[2] == cmpParts[2] && parts[3] == cmpParts[3]) {
                if (parts[4] == cmpParts[4] && parts[5] == cmpParts[5]) {
                    return "mapping for the same entity already defined in " + k;
                }
                if ((parseInt(parts[4]) == 0 && parseInt(cmpParts[4]) == 1) ||
                    (parseInt(parts[4]) == 1 && parseInt(cmpParts[4]) == 0)
                ) {
                    //ignore and single for the same mapping
                    return "mapping for the same entity already defined in " + k;
                }
            }
        }
    }
    let loggedChecks={};
    function getAllConfigs(omitPass) {
        let values = document.querySelectorAll('.configForm select , .configForm input');
        let allValues = {};
        for (let i = 0; i < values.length; i++) {
            let v = values[i];
            let name = v.getAttribute('name');
            if (!name) continue;
            if (name.indexOf("_") >= 0) continue;
            if (v.getAttribute('disabled')) continue;
            let def = getConfigDefition(name);
            if (def.type === 'password' && (v.value == '' || omitPass)) {
                continue;
            }
            let check = v.getAttribute('data-check');
            if (check && conditionOk(name)) {
                let cfgDef=getConfigDefition(name);
                let checkFunction=checkers[check];
                if (typeof (checkFunction) === 'function') {
                    if (! loggedChecks[check]){
                        loggedChecks[check]=true;
                        //console.log("check:"+check);
                    }
                    let res = checkFunction(v.value, allValues, cfgDef);
                    if (res) {
                        let value = v.value;
                        if (v.type === 'password') value = "******";
                        let label = v.getAttribute('data-label');
                        if (!label) label = v.getAttribute('name');
                        v.classList.add("error");
                        alert("invalid config for "+cfgDef.category+":" + label + "(" + value + "):\n" + res);
                        return;
                    }
                }
                else{
                    console.log("check not found:",check);
                }
            }
            allValues[name] = v.value;
        }
        return allValues;
    }
    buttonHandlers.changeConfig=function() {
        ensurePass()
            .then(function (pass) {
                let newAdminPass;
                let url = "/api/setConfig"
                let body = "_hash=" + encodeURIComponent(pass) + "&";
                let allValues = getAllConfigs();
                if (!allValues) return;
                for (let name in allValues) {
                    if (name == 'adminPassword') {
                        newAdminPass = allValues[name];
                    }
                    body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&";
                }
                fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/octet-stream' //we must lie here
                    },
                    body: body
                })
                    .then((rs) => rs.json())
                    .then(function (status) {
                        if (status.status == 'OK') {
                            if (newAdminPass !== undefined) {
                                forEl('#adminPassInput', function (el) {
                                    el.valu = newAdminPass;
                                });
                                saveAdminPass(newAdminPass, true);
                            }
                            alertRestart();
                        }
                        else {
                            alert("unable to set config: " + status.status);
                        }
                    })
            })
            .catch(function (e) { alert(e); })
    }
    buttonHandlers.factoryReset=function() {
        ensurePass()
            .then(function (hash) {
                if (!confirm("Really delete all configuration?\n" +
                    "This will reset all your Wifi settings and disconnect you.")) {
                    return;
                }
                getJson("/api/resetConfig?_hash=" + encodeURIComponent(hash))
                    .then(function (status) {
                        alertRestart();
                    })
            })
            .catch(function (e) { });
    }
    function createCounterDisplay(parent, label, key, isEven) {
        if (parent.querySelector("#" + key)) {
            return;
        }
        let clazz = "row icon-row counter-row";
        if (isEven) clazz += " even";
        let row = addEl('div', clazz, parent);
        row.setAttribute("id", key);
        let icon = addEl('span', 'icon icon-more', row);
        addEl('span', 'label', row, label);
        let value = addEl('span', 'value', row, '---');
        value.setAttribute('id', key + ".sumOk");
        let display = addEl('div', clazz + " msgDetails hidden", parent);
        display.setAttribute('id', key + ".ok");
        row.addEventListener('click', function (ev) {
            let rs = display.classList.toggle('hidden');
            if (rs) {
                icon.classList.add('icon-more');
                icon.classList.remove('icon-less');
            }
            else {
                icon.classList.remove('icon-more');
                icon.classList.add('icon-less');
            }
        });
        callListeners(api.EVENTS.counterDisplayCreated,row);
    }
    function validKey(key) {
        if (!key) return;
        return key.replace(/[^a-z_:A-Z0-9-]/g, '');
    }
    function updateMsgDetails(key, details) {
        forEl('.msgDetails', function (frame) {
            if (frame.getAttribute('id') !== key) return;
            for (let k in details) {
                k = validKey(k);
                let el = frame.querySelector("[data-id=\"" + k + "\"] ");
                if (!el) {
                    el = addEl('div', 'row', frame);
                    let cv = addEl('span', 'label', el, k);
                    cv = addEl('span', 'value', el, details[k]);
                    cv.setAttribute('data-id', k);
                }
                else {
                    el.textContent = details[k];
                }
            }
            forEl('.value', function (el) {
                let k = el.getAttribute('data-id');
                if (k && !details[k]) {
                    el.parentElement.remove();
                }
            }, frame);
        });
    }

    function showOverlay(text, isHtml, buttons) {
        let el = document.getElementById('overlayContent');
        if (text instanceof Object) {
            el.textContent = '';
            el.appendChild(text);
        }
        else {
            if (isHtml) {
                el.innerHTML = text;
                el.classList.remove("text");
            }
            else {
                el.textContent = text;
                el.classList.add("text");
            }
        }
        buttons = (buttons ? buttons : []).concat([{ label: "Close", click: hideOverlay }]);
        let container = document.getElementById('overlayContainer');
        let btframe = container.querySelector('.overlayButtons');
        btframe.textContent = '';
        buttons.forEach((btconfig) => {
            let bt = addEl('button', '', btframe, btconfig.label);
            bt.addEventListener("click", btconfig.click);
        });
        container.classList.remove('hidden');
    }
    function hideOverlay() {
        let container = document.getElementById('overlayContainer');
        container.classList.add('hidden');
        let el = document.getElementById('overlayContent');
        el.textContent = '';
    }
    function checkChange(el, row, name) {
        el.classList.remove("error");
        let loaded = el.getAttribute('data-loaded');
        if (loaded !== undefined) {
            if (loaded != el.value) {
                row.classList.add('changed');
            }
            else {
                row.classList.remove("changed");
            }
        }
        let dependend = conditionRelations[name];
        if (dependend) {
            for (let el in dependend) {
                checkCondition(dependend[el]);
            }
        }
    }
    let configDefinitions = {};
    let xdrConfig = {};
    //a map between the name of a config item and a list of dependend items
    let conditionRelations = {};
    function getConfigDefition(name) {
        if (!name) return {};
        let def;
        for (let k in configDefinitions) {
            if (configDefinitions[k].name === name) {
                def = configDefinitions[k];
                break;
            }
        }
        if (!def) return {};
        return def;
    }
    function getConditions(name) {
        let def = getConfigDefition(name);
        if (!def) return;
        let condition = def.condition;
        if (!condition) return;
        if (!(condition instanceof Array)) condition = [condition];
        return condition;
    }

    function conditionOk(name){
        let condition = getConditions(name);
        if (!condition) return true;
        let visible = false;
        if (!condition instanceof Array) condition = [condition];
        condition.forEach(function (cel) {
            let lvis = true;
            for (let k in cel) {
                let item = document.querySelector('[name=' + k + ']');
                if (item) {
                    let compare = cel[k];
                    if (compare instanceof Array) {
                        if (compare.indexOf(item.value) < 0) lvis = false;
                    }
                    else {
                        if (item.value != cel[k]) lvis = false;
                    }
                }
            }
            if (lvis) visible = true;
        }); 
        return visible;   
    }

    function checkCondition(element) {
        let name = element.getAttribute('name');
        let visible=conditionOk(name);
        let row = closestParent(element, 'row');
        if (!row) return;
        if (visible) row.classList.remove('hidden');
        else row.classList.add('hidden');
    }
    let caliv = 0;
    function createCalSetInput(configItem, frame, clazz) {
        let el = addEl('input', clazz, frame);
        let cb = addEl('button', '', frame, 'C');
        //el.disabled=true;
        cb.addEventListener('click', (ev) => {
            let cs = document.getElementById("calset").cloneNode(true);
            cs.classList.remove("hidden");
            cs.querySelector(".heading").textContent = configItem.label || configItem.name;
            let vel = cs.querySelector(".val");
            if (caliv != 0) window.clearInterval(caliv);
            caliv = window.setInterval(() => {
                if (document.body.contains(cs)) {
                    fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name))
                        .then((r) => r.text())
                        .then((txt) => {
                            if (txt != vel.textContent) {
                                vel.textContent = txt;
                            }
                        })
                        .catch((e) => {
                            alert(e);
                            hideOverlay();
                            window.clearInterval(caliv);
                        })
                }
                else {
                    window.clearInterval(caliv);
                }
            }, 200);
            showOverlay(cs, false, [{
                label: 'Set', click: () => {
                    el.value = vel.textContent;
                    let cev = new Event('change');
                    el.dispatchEvent(cev);
                }
            }]);
        })
        el.setAttribute('name', configItem.name)
        return el;
    }
    function createCalValInput(configItem, frame, clazz) {
        let el = addEl('input', clazz, frame);
        let cb = addEl('button', '', frame, 'C');
        //el.disabled=true;
        cb.addEventListener('click', (ev) => {
            const sv = function (val, cfg) {
                if (configItem.eval) {
                    let v = parseFloat(val);
                    let c = parseFloat(cfg);
                    return (eval(configItem.eval));
                }
                return v;
            };
            let cs = document.getElementById("calval").cloneNode(true);
            cs.classList.remove("hidden");
            cs.querySelector(".heading").textContent = configItem.label || configItem.name;
            let vel = cs.querySelector(".val");
            let vinp = cs.querySelector("input");
            vinp.value = el.value;
            if (caliv != 0) window.clearInterval(caliv);
            caliv = window.setInterval(() => {
                if (document.body.contains(cs)) {
                    fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name))
                        .then((r) => r.text())
                        .then((txt) => {
                            txt = sv(txt, vinp.value);
                            if (txt != vel.textContent) {
                                vel.textContent = txt;
                            }
                        })
                        .catch((e) => {
                            alert(e);
                            hideOverlay();
                            window.clearInterval(caliv);
                        })
                }
                else {
                    window.clearInterval(caliv);
                }
            }, 200);
            showOverlay(cs, false, [{
                label: 'Set', click: () => {
                    el.value = vinp.value;
                    let cev = new Event('change');
                    el.dispatchEvent(cev);
                }
            }]);
        })
        el.setAttribute('name', configItem.name)
        return el;
    }
    function createInput(configItem, frame, clazz) {
        let el;
        if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') {
            el = addEl('select', clazz, frame);
            if (configItem.readOnly) el.setAttribute('disabled', true);
            el.setAttribute('name', configItem.name)
            let slist = [];
            if (configItem.list) {
                configItem.list.forEach(function (v) {
                    if (v instanceof Object) {
                        slist.push({ l: v.l, v: v.v });
                    }
                    else {
                        slist.push({ l: v, v: v });
                    }
                })
            }
            else if (configItem.type != 'boatData') {
                slist.push({ l: 'on', v: 'true' })
                slist.push({ l: 'off', v: 'false' })
            }
            slist.forEach(function (sitem) {
                let sitemEl = addEl('option', '', el, sitem.l);
                sitemEl.setAttribute('value', sitem.v);
            })
            if (configItem.type == 'boatData') {
                el.classList.add('boatDataSelect');
            }
            return el;
        }
        if (configItem.type === 'filter') {
            return createFilterInput(configItem, frame, clazz);
        }
        if (configItem.type === 'xdr') {
            return createXdrInput(configItem, frame, clazz);
        }
        if (configItem.type === "calset") {
            return createCalSetInput(configItem, frame, clazz);
        }
        if (configItem.type === "calval") {
            return createCalValInput(configItem, frame, clazz);
        }
        el = addEl('input', clazz, frame);
        if (configItem.readOnly) el.setAttribute('disabled', true);
        el.setAttribute('name', configItem.name)
        if (configItem.type === 'password') {
            el.setAttribute('type', 'password');
            let vis = addEl('span', 'icon-eye icon', frame);
            vis.addEventListener('click', function (v) {
                if (vis.classList.toggle('active')) {
                    el.setAttribute('type', 'text');
                }
                else {
                    el.setAttribute('type', 'password');
                }
            });
        }
        else if (configItem.type === 'number') {
            el.setAttribute('type', 'number');
        }
        else {
            el.setAttribute('type', 'text');
        }
        return el;
    }

    function setSelect(item, value) {
        if (!item) return;
        item.value = value;
        if (item.value !== value) {
            //missing option with his value
            let o = addEl('option', undefined, item, value);
            o.setAttribute('value', value);
            item.value = value;
        }
    }

    function updateSelectList(item, slist, opt_keepValue) {
        let idx = 0;
        let value;
        if (opt_keepValue) value = item.value;
        item.innerHTML = '';
        slist.forEach(function (sitem) {
            let sitemEl = addEl('option', '', item, sitem.l);
            sitemEl.setAttribute('value', sitem.v !== undefined ? sitem.v : idx);
            idx++;
        })
        if (value !== undefined) {
            setSelect(item, value);
        }
    }
    function getXdrCategories() {
        let rt = [];
        for (let c in xdrConfig) {
            if (xdrConfig[c].enabled !== false) {
                rt.push({ l: c, v: xdrConfig[c].id });
            }
        }
        return rt;
    }
    function getXdrCategoryName(cid) {
        category = parseInt(cid);
        for (let c in xdrConfig) {
            let base = xdrConfig[c];
            if (parseInt(base.id) == category) {
                return c;
            }
        }
    }
    function getXdrCategory(cid) {
        category = parseInt(cid);
        for (let c in xdrConfig) {
            let base = xdrConfig[c];
            if (parseInt(base.id) == category) {
                return base;
            }
        }
        return {};
    }
    function getXdrSelectors(category) {
        let base = getXdrCategory(category);
        return base.selector || [];
    }
    function getXdrFields(category) {
        let base = getXdrCategory(category);
        if (!base.fields) return [];
        let rt = [];
        base.fields.forEach(function (f) {
            if (f.t === undefined) return;
            if (parseInt(f.t) == 99) return; //unknown type
            rt.push(f);
        });
        return rt;
    }

    function createXdrLine(parent, label) {
        let d = addEl('div', 'xdrline', parent);
        addEl('span', 'xdrlabel', d, label);
        return d;
    }
    function showHideXdr(el, show, useParent) {
        if (useParent) el = el.parentElement;
        if (show) el.classList.remove('xdrunused');
        else el.classList.add('xdrunused');
    }

    function createXdrInput(configItem, frame) {
        let configCategory = configItem.category;
        let el = addEl('div', 'xdrinput', frame);
        let d = createXdrLine(el, 'Direction');
        let direction = createInput({
            type: 'list',
            name: configItem.name + "_dir",
            list: [
                //GwXDRMappingDef::Direction
                { l: 'off', v: 0 },
                { l: 'bidir', v: 1 },
                { l: 'to2K', v: 2 },
                { l: 'from2K', v: 3 }
            ],
            readOnly: configItem.readOnly
        }, d, 'xdrdir');
        d = createXdrLine(el, 'Category');
        let category = createInput({
            type: 'list',
            name: configItem.name + "_cat",
            list: getXdrCategories(),
            readOnly: configItem.readOnly
        }, d, 'xdrcat');
        d = createXdrLine(el, 'Source');
        let selector = createInput({
            type: 'list',
            name: configItem.name + "_sel",
            list: [],
            readOnly: configItem.readOnly
        }, d, 'xdrsel');
        d = createXdrLine(el, 'Field');
        let field = createInput({
            type: 'list',
            name: configItem.name + '_field',
            list: [],
            readOnly: configItem.readOnly
        }, d, 'xdrfield');
        d = createXdrLine(el, 'Instance');
        let imode = createInput({
            type: 'list',
            name: configItem.name + "_imode",
            list: [
                //GwXDRMappingDef::InstanceMode
                { l: 'single', v: 0 },
                { l: 'ignore', v: 1 },
                { l: 'auto', v: 2 }
            ],
            readOnly: configItem.readOnly
        }, d, 'xdrimode');
        let instance = createInput({
            type: 'number',
            name: configItem.name + "_instance",
            readOnly: configItem.readOnly
        }, d, 'xdrinstance');
        d = createXdrLine(el, 'Transducer');
        let xdrName = createInput({
            type: 'text',
            name: configItem.name + "_xdr",
            readOnly: configItem.readOnly
        }, d, 'xdrname');
        d = createXdrLine(el, 'Example');
        let example = addEl('div', 'xdrexample', d, '');
        let data = addEl('input', 'xdrvalue', el);
        data.setAttribute('type', 'hidden');
        data.setAttribute('name', configItem.name);
        if (configItem.readOnly) data.setAttribute('disabled', true);
        let changeFunction = function () {
            let parts = data.value.split(',');
            direction.value = parts[1] || 0;
            category.value = parts[0] || 0;
            let selectors = getXdrSelectors(category.value);
            updateSelectList(selector, selectors);
            showHideXdr(selector, selectors.length > 0);
            let fields = getXdrFields(category.value);
            updateSelectList(field, fields);
            showHideXdr(field, fields.length > 0);
            selector.value = parts[2] || 0;
            field.value = parts[3] || 0;
            imode.value = parts[4] || 0;
            instance.value = parts[5] || 0;
            showHideXdr(instance, imode.value == 0);
            xdrName.value = parts[6] || '';
            let used = isXdrUsed(data);
            let modified = data.value != data.getAttribute('data-loaded');
            forEl('[data-category=' + configCategory + ']', function (el) {
                if (used) {
                    el.classList.add('xdrcused');
                    el.classList.remove('xdrcunused');
                    forEl('.categoryAdd', function (add) {
                        add.textContent = xdrName.value;
                    }, el);
                }
                else {
                    el.classList.remove('xdrcused');
                    el.classList.add('xdrcunused');
                    forEl('.categoryAdd', function (add) {
                        add.textContent = '';
                    }, el);
                }
                if (modified) {
                    el.classList.add('changed');
                }
                else {
                    el.classList.remove('changed');
                }
            });
            if (used) {
                getText('/api/xdrExample?mapping=' + encodeURIComponent(data.value) + '&value=2.1')
                    .then(function (txt) {
                        example.textContent = txt;
                    })
            }
            else {
                example.textContent = '';
            }
        }
        let updateFunction = function (evt) {
            if (evt.target == category) {
                selector.value = 0;
                field.value = 0;
                instance.value = 0;
            }
            let txt = category.value + "," + direction.value + "," +
                selector.value + "," + field.value + "," + imode.value;
            let instanceValue = parseInt(instance.value || 0);
            if (isNaN(instanceValue)) instanceValue = 0;
            if (instanceValue < 0) instanceValue = 0;
            if (instanceValue > 255) instanceValue = 255;
            txt += "," + instanceValue;
            let xdr = xdrName.value.replace(/[^a-zA-Z0-9]/g, '');
            xdr = xdr.substr(0, 12);
            txt += "," + xdr;
            data.value = txt;
            let ev = new Event('change');
            data.dispatchEvent(ev);
        }
        category.addEventListener('change', updateFunction);
        direction.addEventListener('change', updateFunction);
        selector.addEventListener('change', updateFunction);
        field.addEventListener('change', updateFunction);
        imode.addEventListener('change', updateFunction);
        instance.addEventListener('change', updateFunction);
        xdrName.addEventListener('change', updateFunction);
        data.addEventListener('change', changeFunction);
        return data;
    }

    function isXdrUsed(element) {
        let parts = element.value.split(',');
        if (!parts[1]) return false;
        if (!parseInt(parts[1])) return false;
        if (!parts[6]) return false;
        return true;
    }

    function createFilterInput(configItem, frame) {
        let el = addEl('div', 'filter', frame);
        let ais = createInput({
            type: 'list',
            name: configItem.name + "_ais",
            list: ['aison', 'aisoff'],
            readOnly: configItem.readOnly
        }, el);
        let mode = createInput({
            type: 'list',
            name: configItem.name + "_mode",
            list: ['whitelist', 'blacklist'],
            readOnly: configItem.readOnly
        }, el);
        let sentences = createInput({
            type: 'text',
            name: configItem.name + "_sentences",
            readOnly: configItem.readOnly
        }, el);
        let data = addEl('input', undefined, el);
        data.setAttribute('type', 'hidden');
        let changeFunction = function () {
            let cv = data.value || "";
            let parts = cv.split(":");
            ais.value = (parts[0] == '0') ? "aisoff" : "aison";
            mode.value = (parts[1] == '0') ? "whitelist" : "blacklist";
            sentences.value = parts[2] || "";
        }
        let updateFunction = function () {
            let nv = (ais.value == 'aison') ? "1" : "0";
            nv += ":";
            nv += (mode.value == 'blacklist') ? "1" : "0";
            nv += ":";
            nv += sentences.value;
            data.value = nv;
            let chev = new Event('change');
            data.dispatchEvent(chev);
        }
        mode.addEventListener('change', updateFunction);
        ais.addEventListener("change", updateFunction);
        sentences.addEventListener("change", updateFunction);
        data.addEventListener('change', function (ev) {
            changeFunction();
        });
        data.setAttribute('name', configItem.name);
        if (configItem.readOnly) data.setAttribute('disabled', true);
        return data;
    }
    let moreicons = ['icon-more', 'icon-less'];

    function collapseCategories(parent, expand) {
        let doExpand = expand;
        forEl('.category', function (cat) {
            if (typeof (expand) === 'function') doExpand = expand(cat);
            forEl('.content', function (content) {
                if (doExpand) {
                    content.classList.remove('hidden');
                }
                else {
                    content.classList.add('hidden');
                }
            }, cat);
            forEl('.title .icon', function (icon) {
                toggleClass(icon, doExpand ? 1 : 0, moreicons);
            }, cat);
        }, parent);
    }

    function findFreeXdr(data) {
        let page = document.getElementById('xdrPage');
        let el = undefined;
        collapseCategories(page, function (cat) {
            if (el) return false;
            let vEl = cat.querySelector('.xdrvalue');
            if (!vEl) return false;
            if (isXdrUsed(vEl)) return false;
            el = vEl;
            if (data) {
                el.value = data;
                let ev = new Event('change');
                el.dispatchEvent(ev);
                window.setTimeout(function () {
                    cat.scrollIntoView();
                }, 50);
            }
            return true;
        });
    }

    function convertUnassigned(value) {
        let rt = {};
        value = parseInt(value);
        if (isNaN(value)) return;
        //see GwXDRMappings::addUnknown
        let instance = value & 0x1ff;
        value = value >> 9;
        let field = value & 0x7f;
        value = value >> 7;
        let selector = value & 0x7f;
        value = value >> 7;
        let cid = value & 0x7f;
        let category = getXdrCategory(cid);
        let cname = getXdrCategoryName(cid);
        if (!cname) return rt;
        let fieldName = "";
        let idx = 0;
        (category.fields || []).forEach(function (f) {
            if (f.v === undefined) {
                if (idx == field) fieldName = f.l;
            }
            else {
                if (parseInt(f.v) == field) fieldName = f.l;
            }
            idx++;
        });
        let selectorName = selector + "";
        (category.selector || []).forEach(function (s) {
            if (parseInt(s.v) == selector) selectorName = s.l;
        });
        rt.l = cname + "," + selectorName + "," + fieldName + "," + instance;
        rt.v = cid + ",1," + selector + "," + field + ",0," + instance + ",";
        return rt;
    }

    function unassignedAdd(ev) {
        let dv = ev.target.getAttribute('data-value');
        if (dv) {
            findFreeXdr(dv);
            hideOverlay();
        }
    }
    buttonHandlers.loadUnassigned=function() {
        getText("/api/xdrUnmapped")
            .then(function (txt) {
                let ot = "";
                txt.split('\n').forEach(function (line) {
                    let cv = convertUnassigned(line);
                    if (!cv || !cv.l) return;
                    ot += '<div class="xdrunassigned"><span>' +
                        cv.l + '</span>' +
                        '<button class="addunassigned" data-value="' +
                        cv.v +
                        '">+</button></div>';
                })
                showOverlay(ot, true);
                forEl('.addunassigned', function (bt) {
                    bt.onclick = unassignedAdd;
                });
            })
    }
    function showXdrHelp() {
        let helpContent = document.getElementById('xdrHelp');
        if (helpContent) {
            showOverlay(helpContent.innerHTML, true);
        }
    }
    function formatDateForFilename(usePrefix, d) {
        let rt = "";
        if (usePrefix) {
            let hdl= document.getElementById('headline');
            if (hdl){
                rt=hdl.textContent+"_";
            }
            let fwt = document.querySelector('.status-fwtype');
            if (fwt) rt += fwt.textContent;
            rt += "_";
        }
        if (!d) d = new Date();
        [d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()]
            .forEach(function (v) {
                if (v < 10) rt += "0" + v;
                else rt += "" + v;
            })
        return rt;
    }
    function downloadData(data, name) {
        let url = "data:application/octet-stream," + encodeURIComponent(JSON.stringify(data, undefined, 2));
        let target = document.getElementById('downloadXdr');
        if (!target) return;
        target.setAttribute('href', url);
        target.setAttribute('download', name);
        target.click();
    }
    buttonHandlers.exportConfig=function() {
        let data = getAllConfigs(true);
        if (!data) return;
        downloadData(data, formatDateForFilename(true) + ".json");
    }
    buttonHandlers.exportXdr=function() {
        let data = {};
        forEl('.xdrvalue', function (el) {
            let name = el.getAttribute('name');
            let value = el.value;
            let err = checkers.checkXDR(value, data);
            if (err) {
                alert("error in " + name + ": " + value + "\n" + err);
                return;
            }
            data[name] = value;
        })
        downloadData(data, "xdr" + formatDateForFilename(true) + ".json");
    }
    function importJson(opt_keyPattern) {
        let clazz = 'importJson';
        forEl('.' + clazz, function (ul) {
            ul.remove();
        });
        let ip = addEl('input', clazz, document.body);
        ip.setAttribute('type', 'file');
        ip.addEventListener('change', function (ev) {
            if (ip.files.length > 0) {
                let f = ip.files[0];
                let reader = new FileReader();
                reader.onloadend = function (status) {
                    try {
                        let idata = JSON.parse(reader.result);
                        let hasOverwrites = false;
                        for (let k in idata) {
                            if (opt_keyPattern && !k.match(opt_keyPattern)) {
                                alert("file contains invalid key " + k);
                                return;
                            }
                            let del = document.querySelector('[name=' + k + ']');
                            if (del) {
                                hasOverwrites = true;
                            }
                        }
                        if (hasOverwrites) {
                            if (!confirm("overwrite existing data?")) return;
                        }
                        for (let k in idata) {
                            let del = document.querySelector('[name=' + k + ']');
                            if (del) {
                                if (del.tagName === 'SELECT') {
                                    setSelect(del, idata[k]);
                                }
                                else {
                                    del.value = idata[k];
                                }
                                let ev = new Event('change');
                                del.dispatchEvent(ev);
                            }
                        }
                    } catch (error) {
                        alert("unable to parse upload: " + error);
                        return;
                    }
                };
                reader.readAsBinaryString(f);
            }
            ip.remove();
        });
        ip.click();
    }
    buttonHandlers.importXdr=function() {
        importJson(new RegExp(/^XDR[0-9][0-9]*/));
    }
    buttonHandlers.importConfig=function() {
        importJson();
    }
    function toggleClass(el, id, classList) {
        let nc = classList[id];
        let rt = false;
        if (nc && !el.classList.contains(nc)) rt = true;
        for (let i in classList) {
            if (i == id) continue;
            el.classList.remove(classList[i])
        }
        if (nc) el.classList.add(nc);
        return rt;
    }

    function createConfigDefinitions(parent, capabilities, defs, includeXdr) {
        let categories = {};
        let frame = parent.querySelector('.configFormRows');
        if (!frame) throw Error("no config form");
        frame.innerHTML = '';
        configDefinitions = defs;
        let currentCategoryPopulated = true;
        defs.forEach(function (item) {
            if (!item.type || item.category === undefined) return;
            if (item.category.match(/^xdr/)) {
                if (!includeXdr) return;
            }
            else {
                if (includeXdr) return;
            }
            let catEntry;
            if (categories[item.category] === undefined) {
                catEntry = {
                    populated: false,
                    frame: undefined,
                    element: undefined
                }
                categories[item.category] = catEntry
                catEntry.frame = addEl('div', 'category', frame);
                catEntry.frame.setAttribute('data-category', item.category)
                let categoryTitle = addEl('div', 'title', catEntry.frame);
                let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
                addEl('span', 'label', categoryTitle, item.category);
                addEl('span', 'categoryAdd', categoryTitle);
                catEntry.element = addEl('div', 'content', catEntry.frame);
                catEntry.element.classList.add('hidden');
                categoryTitle.addEventListener('click', function (ev) {
                    let rs = catEntry.element.classList.toggle('hidden');
                    if (rs) {
                        toggleClass(categoryButton, 0, moreicons);
                    }
                    else {
                        toggleClass(categoryButton, 1, moreicons);
                    }
                })
            }
            else {
                catEntry = categories[item.category];
            }
            let showItem = true;
            let itemCapabilities = item.capabilities || {};
            itemCapabilities['HIDE' + item.name] = null;
            for (let capability in itemCapabilities) {
                let values = itemCapabilities[capability];
                let found = false;
                if (!(values instanceof Array)) values = [values];
                values.forEach(function (v) {
                    if (v === null) {
                        if (capabilities[capability] === undefined) found = true;
                    }
                    else {
                        if (capabilities[capability] == v) found = true;
                    }
                });
                if (!found) showItem = false;
            }
            let readOnly = false;
            let mode = capabilities['CFGMODE' + item.name];
            if (mode == 1) {
                //hide
                showItem = false;
            }
            if (mode == 2) {
                readOnly = true;
            }
            if (showItem) {
                item.readOnly = readOnly;
                catEntry.populated = true;
                let row = addEl('div', 'row', catEntry.element);
                let label = item.label || item.name;
                addEl('span', 'label', row, label);
                let valueFrame = addEl('div', 'value', row);
                let valueEl = createInput(item, valueFrame);
                if (!valueEl) return;
                valueEl.setAttribute('data-default', item.default);
                if (!readOnly) valueEl.addEventListener('change', function (ev) {
                    let el = ev.target;
                    checkChange(el, row, item.name);
                })
                let condition = getConditions(item.name);
                if (condition) {
                    condition.forEach(function (cel) {
                        for (let c in cel) {
                            if (!conditionRelations[c]) {
                                conditionRelations[c] = [];
                            }
                            conditionRelations[c].push(valueEl);
                        }
                    })
                }
                if (item.check) valueEl.setAttribute('data-check', item.check);
                valueEl.setAttribute('data-label', label);
                let btContainer = addEl('div', 'buttonContainer', row);
                if (!readOnly) {
                    let bt = addEl('button', 'defaultButton', btContainer, 'X');
                    bt.setAttribute('data-default', item.default);
                    bt.addEventListener('click', function (ev) {
                        valueEl.value = valueEl.getAttribute('data-default');
                        let changeEvent = new Event('change');
                        valueEl.dispatchEvent(changeEvent);
                    })
                }
                bt = addEl('button', 'infoButton', btContainer, '?');
                bt.addEventListener('click', function (ev) {
                    if (item.description) {
                        showOverlay(item.description);
                    }
                    else {
                        if (item.category.match(/^xdr/)) {
                            showXdrHelp();
                        }
                    }
                });
            }
        });
        for (let cat in categories) {
            let catEntry = categories[cat];
            if (!catEntry.populated) {
                catEntry.frame.remove();
            }
        }
    }
    function loadConfigDefinitions() {
        getJson("api/capabilities")
            .then(function (capabilities) {
                if (capabilities.HELP_URL) {
                    let el = document.getElementById('helpButton');
                    if (el) el.setAttribute('data-url', capabilities.HELP_URL);
                }
                try{
                    Object.freeze(capabilities);
                    callListeners(api.EVENTS.init,capabilities);
                }catch (e){
                    console.log(e);
                }
                getJson("config.json")
                    .then(function (defs) {
                        getJson("xdrconfig.json")
                            .then(function (xdr) {
                                xdrConfig = xdr;
                                configDefinitions = defs;
                                let normalConfig = document.getElementById('configPage');
                                let xdrParent = document.getElementById('xdrPage');
                                if (normalConfig) createConfigDefinitions(normalConfig, capabilities, defs, false);
                                if (xdrParent) createConfigDefinitions(xdrParent, capabilities, defs, true);
                                resetForm();
                                getText('api/boatDataString').then(function (data) {
                                    updateDashboard(data.split('\n'));
                                });
                            })
                    })
            })
            .catch(function (err) { alert("unable to load config: " + err) })
    }
    function verifyPass(pass) {
        return new Promise(function (resolve, reject) {
            let hash = lastSalt + pass;
            let md5hash = MD5(hash);
            getJson('api/checkPass?hash=' + encodeURIComponent(md5hash))
                .then(function (jsonData) {
                    if (jsonData.status == 'OK') resolve(md5hash);
                    else reject(jsonData.status);
                    return;
                })
                .catch(function (error) { reject(error); })
        });
    }


    buttonHandlers.adminPassCancel=function() {
        forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
        forEl('#adminPassInput', function (el) { el.value = '' });
    }
    function saveAdminPass(pass, forceIfSet) {
        forEl('#adminPassKeep', function (el) {
            try {
                let old = localStorage.getItem('adminPass');
                if (el.value == 'true' || (forceIfSet && old !== undefined)) {
                    localStorage.setItem('adminPass', pass);
                }
                else {
                    localStorage.removeItem('adminPass');
                }
            } catch (e) { }
        });
    }
    buttonHandlers.forgetPass=function() {
        localStorage.removeItem('adminPass');
        forEl('#adminPassInput', function (el) {
            el.value = '';
        });
    }
    function ensurePass() {
        return new Promise(function (resolve, reject) {
            if (!needAdminPass) {
                resolve('');
                return;
            }
            let pe = document.getElementById('adminPassInput');
            let hint = document.getElementById('adminPassError');
            if (!pe) {
                reject('no input');
                return;
            }
            if (pe.value == '') {
                let ok = document.getElementById('adminPassOk');
                if (!ok) {
                    reject('no button');
                    return;
                }
                ok.onclick = function () {
                    verifyPass(pe.value)
                        .then(function (pass) {
                            forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
                            saveAdminPass(pe.value);
                            resolve(pass);
                        })
                        .catch(function (err) {
                            if (hint) {
                                hint.textContent = "invalid password";
                            }
                        });
                };
                if (hint) hint.textContent = '';
                forEl('#adminPassOverlay', function (el) { el.classList.remove('hidden') });
            }
            else {
                verifyPass(pe.value)
                    .then(function (np) { resolve(np); })
                    .catch(function (err) {
                        pe.value = '';
                        ensurePass()
                            .then(function (p) { resolve(p); })
                            .catch(function (e) { reject(e); });
                    });
                return;
            }
        });
    }
    buttonHandlers.converterInfo=function() {
        getJson("api/converterInfo").then(function (json) {
            let text = "<h3>Converted entities</h3>";
            text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
            text += "   " + (json.nmea0183 || "").replace(/,/g, ", ");
            text += "</p>";
            text += "<p><b>NMEA2000 to NMEA0183:</b><br/>";
            text += "   " + (json.nmea2000 || "").replace(/,/g, ", ");
            text += "</p>";
            showOverlay(text, true);
        });
    }
    function handleTab(el) {
        let activeName = el.getAttribute('data-page');
        if (!activeName) {
            let extUrl = el.getAttribute('data-url');
            if (!extUrl) return;
            window.open(extUrl, el.getAttribute('data-window') || '_');
        }
        let activeTab = document.getElementById(activeName);
        if (!activeTab) return;
        forEl('.tabPage',function(pel){
            pel.classList.add('hidden');
        });
        forEl('.tab',function(tel){
            tel.classList.remove('active');
        });
        el.classList.add('active');
        activeTab.classList.remove('hidden');
        callListeners(api.EVENTS.tab,activeName);
    }
    /**
     *
     * @param {number} coordinate
     * @param axis
     * @returns {string}
     */
    function formatLonLatsDecimal(coordinate, axis) {
        coordinate = (coordinate + 540) % 360 - 180; // normalize for sphere being round

        let abscoordinate = Math.abs(coordinate);
        let coordinatedegrees = Math.floor(abscoordinate);

        let coordinateminutes = (abscoordinate - coordinatedegrees) / (1 / 60);
        let numdecimal = 2;
        //correctly handle the toFixed(x) - will do math rounding
        if (coordinateminutes.toFixed(numdecimal) == 60) {
            coordinatedegrees += 1;
            coordinateminutes = 0;
        }
        if (coordinatedegrees < 10) {
            coordinatedegrees = "0" + coordinatedegrees;
        }
        if (coordinatedegrees < 100 && axis == 'lon') {
            coordinatedegrees = "0" + coordinatedegrees;
        }
        let str = coordinatedegrees + "\u00B0";

        if (coordinateminutes < 10) {
            str += "0";
        }
        str += coordinateminutes.toFixed(numdecimal) + "'";
        if (axis == "lon") {
            str += coordinate < 0 ? "W" : "E";
        } else {
            str += coordinate < 0 ? "S" : "N";
        }
        return str;
    };
    function formatFixed(v, dig, fract) {
        v = parseFloat(v);
        if (dig === undefined) return v.toFixed(fract);
        let s = v < 0 ? "-" : "";
        v = Math.abs(v);
        let rv = v.toFixed(fract);
        let parts = rv.split('.');
        parts[0] = "0000000000" + parts[0];
        if (dig >= 10) dig = 10;
        if (fract > 0) {
            return s + parts[0].substr(parts[0].length - dig) + "." + parts[1];
        }
        return s + parts[0].substr(parts[0].length - dig);
    }
    let valueFormatters = {
        formatCourse: {
            f: function (v) {
                let x = parseFloat(v);
                let rt = x * 180.0 / Math.PI;
                if (rt > 360) rt -= 360;
                if (rt < 0) rt += 360;
                return rt.toFixed(0);
            },
            u: '°'
        },
        formatKnots: {
            f: function (v) {
                let x = parseFloat(v);
                x = x * 3600.0 / 1852.0;
                return x.toFixed(2);
            },
            u: 'kn'
        },
        formatWind: {
            f: function (v) {
                let x = parseFloat(v);
                x = x * 180.0 / Math.PI;
                if (x > 180) x = -1 * (360 - x);
                return x.toFixed(0);
            },
            u: '°'
        },
        mtr2nm: {
            f: function (v) {
                let x = parseFloat(v);
                x = x / 1852.0;
                return x.toFixed(2);
            },
            u: 'nm'
        },
        kelvinToC: {
            f: function (v) {
                let x = parseFloat(v);
                x = x - 273.15;
                return x.toFixed(0);
            },
            u: '°'
        },
        formatFixed0: {
            f: function (v) {
                let x = parseFloat(v);
                return x.toFixed(0);
            },
            u: ''
        },
        formatDepth: {
            f: function (v) {
                let x = parseFloat(v);
                return x.toFixed(1);
            },
            u: 'm'
        },
        formatLatitude: {
            f: function (v) {
                let x = parseFloat(v);
                if (isNaN(x)) return '-----';
                return formatLonLatsDecimal(x, 'lat');
            },
            u: ''
        },
        formatLongitude: {
            f: function (v) {
                let x = parseFloat(v);
                if (isNaN(x)) return '-----';
                return formatLonLatsDecimal(x, 'lon');
            },
            u: ''
        },
        formatRot: {
            f: function (v) {
                let x = parseFloat(v);
                if (isNaN(x)) return '---';
                x = x * 180.0 / Math.PI;
                return x.toFixed(2);
            },
            u: '°/s'
        },
        formatXte: {
            f: function (v) {
                let x = parseFloat(v);
                if (isNaN(x)) return '---';
                return x.toFixed(0);
            },
            u: 'm'
        },
        formatDate: {
            f: function (v) {
                v = parseFloat(v);
                if (isNaN(v)) return "----/--/--";
                //strange day offset from NMEA0183 lib
                let d = new Date("2010/01/01");
                let days = 14610 - d.getTime() / 1000 / 86400;
                let tbase = (v - days) * 1000 * 86400;
                let od = new Date(tbase);
                return formatFixed(od.getFullYear(), 4, 0) +
                    "/" + formatFixed(od.getMonth() + 1, 2, 0) +
                    "/" + formatFixed(od.getDate(), 2, 0);
            },
            u: ''
        },
        formatTime: {
            f: function (v) {
                v = parseFloat(v);
                if (isNaN(v)) return "--:--:--";
                let hr = Math.floor(v / 3600.0);
                let min = Math.floor((v - hr * 3600.0) / 60);
                let s = Math.floor((v - hr * 3600.0 - min * 60.0));
                return formatFixed(hr, 2, 0) + ':' +
                    formatFixed(min, 2, 0) + ':' +
                    formatFixed(s, 2, 0);
            },
            u: ''
        }


    }
    Object.freeze(valueFormatters);
    for (let k in valueFormatters){
        Object.freeze(valueFormatters[k]);
    }
    function resizeFont(el, reset, maxIt) {
        if (maxIt === undefined) maxIt = 10;
        if (!el) return;
        if (reset) el.style.fontSize = '';
        while (el.scrollWidth > el.clientWidth && maxIt) {
            let next = parseFloat(window.getComputedStyle(el).fontSize) * 0.9;
            el.style.fontSize = next + "px";
        }
    }
    function getUnit(def,useUser){
        let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ;
        let u = fmt ? fmt.u : ' ';
        if (!fmt && def.format && def.format.match(/formatXdr/)) {
            u = def.format.replace(/formatXdr:[^:]*:/, '');
        }
        return u;
    }
    /**
     * create a dashboard item if it does not exist
     * @param {*} def 
     * @param {*} show 
     * @param {*} parent 
     * @returns the value div of the dashboard item
     */
    function createOrHideDashboardItem(def,show, parent) {
        if (!def.name) return;
        let frame=document.getElementById('frame_'+def.name);
        let build=false;
        if (frame){
            if (frame.classList.contains('invalid') && show){
                build=true;
                frame.classList.remove('invalid');
                frame.innerHTML='';
            }
        }
        else{
            if (! parent) return;
            frame = addEl('div', 'dash', parent);
            frame.setAttribute('id','frame_'+def.name);
            build=true;
        }
        if (! show){
            if (!frame.classList.contains('invalid')){
                frame.classList.add('invalid');
                frame.innerHTML='';
            }
            return;    
        }
        if (build) {
            let title = addEl('span', 'dashTitle', frame, def.name);
            let value = addEl('span', 'dashValue', frame);
            value.setAttribute('id', 'data_' + def.name);
            if (def.format) value.classList.add(def.format);
            let footer = addEl('div', 'footer', frame);
            let src = addEl('span', 'source', footer);
            src.setAttribute('id', 'source_' + def.name);
            let u = getUnit(def, true)
            addEl('span', 'unit', footer, u);
            callListeners(api.EVENTS.dataItemCreated,{name:def.name,element:frame});
        }
        let de = document.getElementById('data_' + def.name);
        return de;
    }
    function hideDashboardItem(name){
        createOrHideDashboardItem({name:name},false);
    }
    function parseBoatDataLine(line) {
        let rt = {};
        let parts = line.split(',');
        rt.name = parts[0];
        rt.valid = parts[2] === '1';
        rt.update = parseInt(parts[3]);
        rt.source = parseInt(parts[4]);
        rt.format = parts[1];
        rt.value = parts[5];
        return rt;
    }
    function createDashboard() {
        let frame = document.getElementById('dashboardPage');
        if (!frame) return;
        frame.innerHTML = '';
    }
    function sourceName(v) {
        if (v == 0) return "N2K";
        for (let n in channelList) {
            if (v == channelList[n].id) return n;
            if (v >= channelList[n].id && v <= channelList[n].max) {
                return n;
            }
        }
        if (v < minUser) return "---";
        return "USER[" + v + "]";
    }
    let lastSelectList = [];
    function updateDashboard(data) {
        callListeners(api.EVENTS.boatData,data);
        let frame = document.getElementById('dashboardPage');
        let showInvalid = true;
        forEl('select[name=showInvalidData]', function (el) {
            if (el.value == 'false') showInvalid = false;
        })
        let names = {};
        for (let n in data) {
            let current = parseBoatDataLine(data[n]);
            if (!current.name) continue;
            names[current.name] = true;
            let show = current.valid||showInvalid;
            let de = createOrHideDashboardItem(current, show, frame);
            let newContent = '---';
            if (current.valid) {
                let formatter;
                if (current.format && current.format != "NULL") {
                    let key = current.format.replace(/^\&/, '');
                    formatter = userFormatters[current.name] || valueFormatters[key];
                }
                if (formatter && formatter.f) {
                    newContent = formatter.f(current.value,true);
                    if (newContent === undefined) newContent = "";
                }
                else {
                    let v = parseFloat(current.value);
                    if (!isNaN(v)) {
                        v = v.toFixed(3)
                        newContent = v;
                    }
                    else {
                        newContent = current.value;
                    }
                }
            }
            else {
                let uf=userFormatters[current.name];
                if (uf && uf.f){
                    //call the user formatter
                    //so that it can detect the invalid state
                    newContent=uf.f(undefined,false);
                }
                if (newContent === undefined)newContent = "---";
            }
            if (de) {
                if (newContent !== de.textContent) {
                    de.textContent = newContent;
                    resizeFont(de, true);
                }
            }
            let src = document.getElementById('source_' + current.name);
            if (src) {
                src.textContent = sourceName(current.source);
            }
        }
        //console.log("update");
        //remove all items that are not send any more
        //this can only happen if the device restarted
        //otherwise data items will not go away - they will become invalid
        forEl('.dash', function (el) {
            let id = el.getAttribute('id');
            if (id) {
                if (!names[id.replace(/^frame_/, '')]) {
                    el.remove();
                }
            }
        });
        let selectList = [];
        for (let n in names) {
            selectList.push({ l: n, v: n });
        }
        let selectChanged = false;
        if (lastSelectList.length == selectList.length) {
            for (let i = 0; i < lastSelectList.length; i++) {
                if (selectList[i] != lastSelectList[i]) {
                    selectChanged = true;
                    break;
                }
            }
        }
        else {
            selectChanged = true;
        }
        if (selectChanged) {
            forEl('.boatDataSelect', function (el) {
                updateSelectList(el, selectList, true);
            });
        }
    }
    buttonHandlers.uploadBin=function(ev) {
        let el = document.getElementById("uploadFile");
        let progressEl = document.getElementById("uploadDone");
        if (!el) return;
        if (el.files.length < 1) return;
        ev.target.disabled = true;
        let file = el.files[0];
        checkImageFile(file)
            .then(function (result) {
                let currentType;
                let currentVersion;
                let chipid;
                forEl('.status-version', function (el) { currentVersion = el.textContent });
                forEl('.status-fwtype', function (el) { currentType = el.textContent });
                forEl('.status-chipid', function (el) { chipid = el.textContent });
                let confirmText = 'Ready to update firmware?\n';
                if (result.chipId != chipid) {
                    confirmText += "WARNING: the chipid in the image (" + result.chipId;
                    confirmText += ") does not match the current chip id (" + chipid + ").\n";
                }
                if (currentType != result.fwtype) {
                    confirmText += "WARNING: image has different type: " + result.fwtype + "\n";
                    confirmText += "** Really update anyway? - device can become unusable **";
                }
                else {
                    if (currentVersion == result.version) {
                        confirmText += "WARNING: image has the same version as current " + result.version;
                    }
                    else {
                        confirmText += "version in image: " + result.version;
                    }
                }
                if (!confirm(confirmText)) {
                    ev.target.disabled = false;
                    return;
                }
                ensurePass()
                    .then(function (hash) {
                        let len = file.size;
                        let req = new XMLHttpRequest();
                        req.onloadend = function () {
                            ev.target.disabled = false;
                            let result = "unknown error";
                            try {
                                let jresult = JSON.parse(req.responseText);
                                if (jresult.status == 'OK') {
                                    result = '';
                                }
                                else {
                                    if (jresult.status) {
                                        result = jresult.status;
                                    }
                                }
                            } catch (e) {
                                result = "Error " + req.status;
                            }
                            if (progressEl) {
                                progressEl.style.width = 0;
                            }
                            if (!result) {
                                alertRestart();
                            }
                            else {
                                alert("update error: " + result);
                            }
                        }
                        req.onerror = function (e) {
                            ev.target.disabled = false;
                            alert("unable to upload: " + e);
                        }
                        if (progressEl) {
                            progressEl.style.width = 0;
                            req.upload.onprogress = function (ev) {
                                if (ev.lengthComputable) {
                                    let percent = 100 * ev.loaded / ev.total;
                                    progressEl.style.width = percent + "%";
                                }
                            }
                        }
                        let formData = new FormData();
                        formData.append("file1", el.files[0]);
                        req.open("POST", '/api/update?_hash=' + encodeURIComponent(hash));
                        req.send(formData);
                    })
                    .catch(function (e) {
                        ev.target.disabled = false;
                    });
            })
            .catch(function (e) {
                alert("This file is an invalid image file:\n" + e);
                ev.target.disabled = false;
            })
    }
    let HDROFFSET = 288;
    let VERSIONOFFSET = 16;
    let NAMEOFFSET = 48;
    let MINSIZE = HDROFFSET + NAMEOFFSET + 32;
    let CHIPIDOFFSET = 12; //2 bytes chip id here
    let imageCheckBytes = {
        0: 0xe9, //image magic
        288: 0x32, //app header magic
        289: 0x54,
        290: 0xcd,
        291: 0xab
    };
    function decodeFromBuffer(buffer, start, length) {
        while (length > 0 && buffer[start + length - 1] == 0) {
            length--;
        }
        if (length <= 0) return "";
        let decoder = new TextDecoder();
        let rt = decoder.decode(buffer.slice(
            start,
            start + length));
        return rt;
    }
    function getChipId(buffer) {
        if (buffer.length < CHIPIDOFFSET + 2) return -1;
        return buffer[CHIPIDOFFSET] + 256 * buffer[CHIPIDOFFSET + 1];
    }
    function checkImageFile(file) {
        return new Promise(function (resolve, reject) {
            if (!file) reject("no file");
            if (file.size < MINSIZE) reject("file is too small");
            let slice = file.slice(0, MINSIZE);
            let reader = new FileReader();
            reader.addEventListener('load', function (e) {
                let content = new Uint8Array(e.target.result);
                for (let idx in imageCheckBytes) {
                    if (content[idx] != imageCheckBytes[idx]) {
                        reject("missing magic byte at position " + idx + ", expected " +
                            imageCheckBytes[idx] + ", got " + content[idx]);
                    }
                }
                let version = decodeFromBuffer(content, HDROFFSET + VERSIONOFFSET, 32);
                let fwtype = decodeFromBuffer(content, HDROFFSET + NAMEOFFSET, 32);
                let chipId = getChipId(content);
                let rt = {
                    fwtype: fwtype,
                    version: version,
                    chipId: chipId
                };
                resolve(rt);
            });
            reader.readAsArrayBuffer(slice);
        });
    }
    function addTabPage(name,label,url){
        if (label === undefined) label=name;
        let tab=addEl('div','tab','#tabs',label);
        tab.addEventListener('click',function(ev){
            handleTab(ev.target);
        })
        if (url !== undefined){
            tab.setAttribute('data-url',url);
            return;
        }
        tab.setAttribute('data-page',name);
        let page=addEl('div','tabPage hidden','#tabPages');
        page.setAttribute('id',name);
        return page;
    }
    function addUserFormatter(name,unit,formatter){
        if (unit !== undefined && formatter !== undefined){
            userFormatters[name]={
                u:unit,
                f:formatter
            }
        }
        else{
            delete userFormatters[name];
        }
        hideDashboardItem(name); //will recreate it on next data receive
    }
    const api= {
        registerListener: function (callback,opt_event) {
            if (opt_event === undefined){
                listeners.push(callback);
            }
            else{
                listeners.push({
                    event:opt_event,
                    callback:callback
                })
            }
        },
        /**
         * helper for creating dom elements
         * parameters:
         *   type: the element type (e.g. div)
         *   class: a list of classes separated by space
         *   parent (opt): a parent element (either a dom element vor a query selector)
         *   text (opt): the text to be set as textContent 
         * returns: the newly created element
         */
        addEl: addEl,
        /**
         * iterator helper for a query selector
         * parameters:
         *  query: the query selector
         *  callback: the callback function (will be called with the element as param)
         *  base (opt): a dome element to be used as the root (defaults to document)
         */
        forEl: forEl,
        /**
         * find the closest parent that has a particular class
         * parameters:
         *  element: the element to start with
         *  class: the class to be searched for
         * returns: the element or undefined/null
         */
        closestParent: closestParent,
        /**
         * add a new tab
         * parameters: 
         *   name - the name of the page
         *   label (opt): the label for the new page 
         * returns: the newly created element
         */
        addTabPage: addTabPage,
        /**
         * add a user defined formatter for a boat data item
         * parameters:
         *   name : the boat data item name
         *   unit: the unit to be displayed
         *   formatter: the formatter function (must return a string)
         */
        addUserFormatter: addUserFormatter,
        removeUserFormatter: function(name){
            addUserFormatter(name);
        },
        /**
         * a dict of formatters
         * each one has 2 members:
         *   u: the unit
         *   f: the formatter function
         */
        formatters: valueFormatters,
        /**
         * parse a line of boat data
         * the line has name,format,valid,update,source,value
         */
        parseBoatDataLine: parseBoatDataLine,
        EVENTS: {
            init: 0, //called when capabilities are loaded, data is capabilities
            tab: 1, //tab page activated, data is the id of the tab page
            config: 2, //called when the config data is loaded,data is the config object
            boatData: 3, //called when boatData is received, data is the list of boat Data items
            dataItemCreated: 4, //data is an object with
                                // name: the item name, element: the frame item of the boat data display
            status: 5, //status received, data is the status object
            counterDisplayCreated: 6 //data is the row for the display
        }
    };
    function callListeners(event,data){
        listeners.forEach((listener)=>{
            if (typeof(listener) === 'function'){
                listener(event,data);
            }
            else if (typeof(listener) === 'object'){
                if (listener.event === event){
                    if (typeof(listener.callback) === 'function'){
                        listener.callback(event,data);
                    }
                }
            }
        })
    }
    window.esp32nmea2k = api;
    window.setInterval(update, 1000);
    window.setInterval(function () {
        let dp = document.getElementById('dashboardPage');
        if (dp.classList.contains('hidden')) return;
        getText('api/boatDataString').then(function (data) {
            updateDashboard(data.split('\n'));
        });
    }, 1000);
    window.addEventListener('load', function () {
        let buttons = document.querySelectorAll('button');
        for (let i = 0; i < buttons.length; i++) {
            let be = buttons[i];
            let buttonFunction=buttonHandlers[be.id];
            if (typeof(buttonFunction) === 'function'){
                be.onclick = buttonFunction; //assume a function with the button id
                //console.log("button: "+be.id);
            }
            else{
                console.log("no handler for button "+be.id);
            }
        }
        forEl('.showMsgDetails', function (cd) {
            cd.addEventListener('change', function (ev) {
                let key = ev.target.getAttribute('data-key');
                if (!key) return;
                let el = document.getElementById(key);
                if (!el) return;
                if (ev.target.checked) el.classList.remove('hidden');
                else (el.classList).add('hidden');
            });
        });
        let tabs = document.querySelectorAll('.tab');
        for (let i = 0; i < tabs.length; i++) {
            tabs[i].addEventListener('click', function (ev) {
                handleTab(ev.target);
            });
        }
        createDashboard();
        loadConfigDefinitions();
        try {
            let storedPass = localStorage.getItem('adminPass');
            if (storedPass) {
                forEl('#adminPassInput', function (el) {
                    el.value = storedPass;
                });
            }
        } catch (e) { }
        forEl('#uploadFile', function (el) {
            el.addEventListener('change', function (ev) {
                if (ev.target.files.length < 1) return;
                let file = ev.target.files[0];
                checkImageFile(file)
                    .then(function (res) {
                        forEl('#imageProperties', function (iel) {
                            let txt = "[" + res.chipId + "] ";
                            txt += res.fwtype + ", " + res.version;
                            iel.textContent = txt;
                            iel.classList.remove("error");
                        })
                    })
                    .catch(function (e) {
                        forEl('#imageProperties', function (iel) {
                            iel.textContent = e;
                            iel.classList.add("error");
                        })
                    })
            })
        })
    });
}());