'use strict';  // all variables must be defined

let bCtrlPressed = false;  // true=ctrl key pressed
let bShiftPressed = false; // true=shift key pressed
let bAltPressed = false;   // true=alt key pressed
let iRowPrevious = -1;     // used for selection of table rows
let iRowClickCount = 0     // counts clicks on actual row to enable multi line selection with mouse only
let sortImg = [];
let filterProcess = null;  // to kill, previous filter process
let sInfo = ""             // info to display
let sWsGuid = ""           // guid of project
let sWsProject = ""        // name of project
let arFound_gb = []        // matches in model
let sLoginUser = ""        // system user for login
let sLoginPassword = ""    // password for login
let sLoginServer = "192.168.1.1" // IP of WebIQ server
let sVersion = "vX.Y"      // version of addOn
let sUnitTooltip = "No Unit class JSON available"
const SPACE = "&nbsp;"

const MAX_SIZE_LOG_FILE = 4000000 // 4 MByte

// const sItemNotFound = "no PLC variable"

// const sWebIQServer = "ws://127.0.0.1:10123/"
let sWebIQServer = "ws://192.168.1.1:10123/"
const sProtocol = "smarthmi-connect";
const FS_OPT = "\n"; // field seperator between options
const FS_VAL = "\t"; // field seperator betwenn key & value
const colTitle = "lightblue";

const FILE_VERSION = "json/version.txt"
const DIR_ROOT = "../"
const DIR_DESIGNER = DIR_ROOT + ".designer/"
const FILE_APP_MODEL = DIR_DESIGNER + "app-model.json"
const FILE_APP_INFO = DIR_DESIGNER + "app-info.json"
const FILE_WEBIQ = DIR_ROOT + "webiq.json"
const SRC_INTERNAL = "@Internal"
const DIR_JSON = DIR_ROOT + "json/"
const FILE_UNITS = DIR_JSON + "unit-classes.json"
const FILE_UIACTIONS = DIR_JSON + "scripts/ui-actions.json"
const FILE_LOCALSCRIPTS = DIR_JSON + "scripts/local-scripts.json"
const FILE_STYLESHEETS = DIR_JSON + "stylesheets.json"
const FILE_GROUPS = DIR_JSON + "/groups/config.json"

const DIR_LOCAL = DIR_JSON + "locale/"
const DIR_LANG = DIR_LOCAL + "lang/"
const FILE_LANGUAGES = DIR_LOCAL + "index.json"
// can only acces subdirs of url root directory
const DIR_PACKAGE = DIR_ROOT + "packages/"
const FILE_REPOSITORY = DIR_PACKAGE + "webiq-repository.json"
const DIR_ADDON_JSON = "json/"
// const FS_PATH = ">"
const FS_PATH = "<x>&gt;</x> "
const FS_FOUND = "<x>:</x> "
const FS_VAR = " : "

/*************************** TOOLS  ************************************/
function validateIp4(sIp4) {
  if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(sIp4)) {
    return (true)
  }
  return (false)
}
function utf16To8(s) {
  return unescape(encodeURIComponent(s));
  //return encodeURIComponent(s);
}
function utf8To16(s) {
  return decodeURIComponent(escape(s));
  //return decodeURIComponent(s);
}
/**
 * waits iMSec milli seconds
 */
function delay(iMSec) {
  return new Promise(
    resolve => setTimeout(resolve, iMSec)
  )
};
/**
 * returns value of browsers local storage
 * @param {string} sName 
 * @returns value of cookie
 */
function getLStorage(sName) {
  let sValue = localStorage.getItem(sName)
  if (sValue === null)
    return ""
  else
    return sValue
}
/**
 * set value of cookie with name "sName"
 * @param {string} sName  name of local storage item
 * @param {string} sValue value of local storage item
 * @returns value of cookie
 */
function setLStorage(sName, sValue) {
  // check if cookie name & value valid (no FS chars allowed)
  // if (sName.match(/^[a-zA-Z-]+$/) === null) {
  //   abortJS("Invalid cookie name '" + sName + "'")
  //   return false
  // }
  localStorage.setItem(sName, sValue)

  return true
}
/****************************  TOOLS.FILE ******************************/
/**
 * Retrieves content of requested text file
 * @param {string} sFileName 
 * @returns jResp.ok = true, jResp.content, jResp.ok=false jResp.errMsg
 */
async function readTextFile(sFileName) {
  let jCmd = null // json command
  let jResp = null // json response
  let iFileHandle = 0 // file handle
  let sContent = ""  // content of file
  let sDecoded = ""  // decoded block

  // set name of project to connect to
  sTarget = sWsGuid;

  // open file
  jCmd = cmdFsOpen;
  jCmd.data.path = sFileName;
  jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.message); });
  iFileHandle = jResp.data;

  do {
    // read file in loop, because content is transfered in blocks
    jCmd = cmdFsRead;
    jCmd.data.file_id = iFileHandle;
    jCmd.data.count = 4096;

    jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); });
    sDecoded = atob(jResp.data);

    sContent += sDecoded;
  } while (sDecoded.length === jCmd.data.count);
  // Server sends 2 telegram, when size last request < requested_size 
  // Server sends 1 telegram, when size last request === requested_size
  if (sDecoded.length > 0) {
    await wsRecv(jResp);
  }

  // close file
  jCmd = cmdFsClose;
  jCmd.data = iFileHandle;
  jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); });
  return sContent;
}
/**
 * Reads in a text file
 * @param {string} sFile name of text file to read
 * @returns content of text file
 */
async function fetchTextFile(sFile) {
  let resp = null
  resp = await fetch(sFile)
  if (resp.status !== 200) {
    abortJS(resp.statusText + "\n" + resp.url)
    //abortJS("")
  }
  const myText = await resp.text()
  return myText;
}
/****************************  TOOLS.TIME ******************************/
/**
 * formats unix time stamp into human readable format
 * @param {int} iUnixTS 
 * @param {string} sFormat "de"=>DD.MM.YYYY HH:MM:SS (24h) else=>YYYY-MM-DD HH:MM:SS (24h)
 * @returns 
 */
function formatDateTime(iUnixTS, sFormat) {
  let FS_TIME = ":"
  let FS_DATE = "."
  let oDate = new Date(iUnixTS * 1000);
  let sTime, sDate
  if (sFormat === "de") {
    sTime = ("" + oDate.getHours()).padStart(2, "0") + FS_TIME + ("" + oDate.getMinutes()).padStart(2, "0") + FS_TIME + ("" + oDate.getSeconds()).padStart(2, "0")
    sDate = ("" + oDate.getDate()).padStart(2, "0") + FS_DATE + ("" + (oDate.getMonth() + 1)).padStart(2, "0") + FS_DATE + ("" + oDate.getFullYear())
  } else {
    FS_DATE = "-"
    sTime = ("" + oDate.getHours()).padStart(2, "0") + FS_TIME + ("" + oDate.getMinutes()).padStart(2, "0") + FS_TIME + ("" + oDate.getSeconds()).padStart(2, "0")
    sDate = ("" + oDate.getFullYear() + FS_DATE + ("" + (oDate.getMonth() + 1)).padStart(2, "0") + FS_DATE + ("" + oDate.getDate()).padStart(2, "0"))
  }
  return sDate + " " + sTime
}
/**
 * Generates 24h time string
 * @returns time string hh:mm:ss.ms
 */
function getTime() {
  let now = new Date();
  let hh = now.getHours() + "";
  let mm = now.getMinutes() + "";
  let ss = now.getSeconds() + "";
  let ms = now.getMilliseconds() + "";
  return hh.padStart(2, "0") + ":" + mm.padStart(2, "0") + ":" + ss.padStart(2, "0") + "." + ms.padStart(3, "0");
}
/****************************  TOOLS.GUI ******************************/
/**
 * 
 * @returns {DOM object} overlay element
 */
function getOverlay() {
  const colContent = "white";
  const colOverlay = "rgba(0,0,0, 0.4)";
  let overlay = null;
  // div for overlay the complete screen
  overlay = document.createElement("div");
  overlay.style.backgroundColor = colOverlay;
  overlay.style.top = "0";
  overlay.style.left = "0";
  overlay.style.right = "0";
  overlay.style.bottom = "0";

  overlay.style.width = "100%";
  overlay.style.height = "100%";
  overlay.style.position = "fixed";
  overlay.style.zIndex = 10;
  overlay.style.display = "flex";
  overlay.style.alignItems = "center";
  overlay.style.justifyContent = "center";

  // box with header and content
  let box = document.createElement("div");
  box.style.backgroundColor = colContent
  box.style.padding = "0px"
  box.style.paddingRight = "8px"
  //box.style.minHeight = "300px"
  //box.style.position = "absolute";
  box.style.position = "relative";
  overlay.appendChild(box)
  document.body.appendChild(overlay);
  return overlay
}
let iPercent_gb = 0
// to test if progress bar is working
async function startLoadingSimulation() {
  await progress("on", 0, "Check if progress window is working")
  let iVal = 0
  let progressInterval = setInterval(() => {
    iVal += 20;
    progress("", iVal)
    if (iVal >= 100) {
      clearInterval(progressInterval)
      setTimeout(() => {
        progress("off")
      }, 1000);
    }
  }, 1000);
}
// open help file in target=help
function showHelp(sUrl) {
  window.open("doc/" + sUrl, 'help');
}
/**
 * show progress information 
 * @param {string} sAction "on":open progress "off":close progress
 * @param {*} rPercent progress value in percent 0-100
 */
async function progress(sAction, rPercent = 0, sMsg = "") {
  rPercent = Math.round(rPercent)
  if (sAction === "on") {
    objProgress.classList.add('active');
    document.body.style.cursor = "progress"
    await delay(400);
  } else if (sAction === "off") {
    document.body.style.cursor = "auto"
    objProgress.classList.remove('active');
    setTimeout(function () {
      objProgress.value = 0;
      objProgress.hidden = true;
      rPercent = 0
    }, 1400);
  }
  if (sMsg != "") countdownMessage.textContent = sMsg
  progressBar.style.width = `${rPercent}%`;
  progressBar.textContent = `${rPercent}%`;
}
/**
 * Dialog box, for input of user and password
 */
async function dialogLogin(sMsg, bCancel) {
  let myDiv = null
  let myLabel = null
  let overlayLogin = getOverlay();
  let box = overlayLogin.firstChild;
  let sDivHeight = "30px"

  box.style.padding = "0px"
  box.style.width = "400px"
  box.style.height = "180px"

  myDiv = document.createElement("div")
  myDiv.style.height = sDivHeight
  myDiv.style.backgroundColor = colTitle
  myDiv.style.paddingLeft = "8px"
  myDiv.style.paddingTop = "8px"
  myDiv.style.marginBottom = "10px"
  myDiv.innerHTML = sMsg
  // myDiv.style.backgroundColor = "red"
  box.appendChild(myDiv)

  // IP
  myDiv = document.createElement("div")
  myDiv.style.paddingLeft = "8px"
  myDiv.style.height = sDivHeight
  myLabel = document.createElement("label")
  myLabel.innerHTML = "WebIQ Server IP: "
  myDiv.appendChild(myLabel)

  let txtIP = document.createElement("input");
  txtIP.type = "text"
  txtIP.setAttribute("value", sLoginServer)
  myDiv.appendChild(txtIP)
  box.appendChild(myDiv)

  let btnTmp = document.createElement("input");
  btnTmp.type = "button"
  btnTmp.value = "Ip1"
  btnTmp.onclick = function () {
    // txtIP.value = eval(l.ip1)
    txtIP.value = "IP1"
  }
  myDiv.appendChild(btnTmp)
  btnTmp = document.createElement("input");
  btnTmp.type = "button"
  btnTmp.value = "Ip2"
  btnTmp.onclick = function () {
    // txtIP.value = eval(l.ip2)
    txtIP.value = "IP2"
  }
  myDiv.appendChild(btnTmp)

  // user
  myDiv = document.createElement("div")
  myDiv.style.paddingLeft = "8px"
  myDiv.style.height = sDivHeight
  myLabel = document.createElement("label")
  // myLabel.innerHTML = eval(l.systemUser) + " : "
  myLabel.innerHTML = "System user : "
  myDiv.appendChild(myLabel)

  let txtUser = document.createElement("input");
  txtUser.type = "text"
  txtUser.setAttribute("value", sLoginUser)
  myDiv.appendChild(txtUser)
  box.appendChild(myDiv)

  // password
  myDiv = document.createElement("div")
  myDiv.style.paddingLeft = "8px"
  myDiv.style.height = sDivHeight
  myLabel = document.createElement("label");
  // myLabel.innerHTML = eval(l.password) + " : "
  myLabel.innerHTML = "Password : "
  myDiv.appendChild(myLabel)

  let txtPassword = document.createElement("input");
  txtPassword.type = "password"
  txtPassword.setAttribute("value", sLoginPassword)
  myDiv.appendChild(txtPassword)

  let chkShow = document.createElement("img")
  chkShow.style.margin = "0px"
  chkShow.style.padding = "0px"
  chkShow.src = "img/eye.png"
  chkShow.style.height = "25px"
  chkShow.onclick = function () {
    if (txtPassword.type === "password") {
      txtPassword.type = "text"
    } else {
      txtPassword.type = "password"
    }
  }
  myDiv.appendChild(chkShow)
  box.appendChild(myDiv)


  // Login button
  myDiv = document.createElement("div")
  myDiv.style.marginTop = "10pt";
  myDiv.style.width = "100%"
  myDiv.style.display = "flex"
  myDiv.style.justifyContent = "space-around"

  let btnLogin = document.createElement("input");
  btnLogin.type = "button"
  // btnLogin.value = eval(l.login)
  btnLogin.value = "Login"
  btnLogin.style.width = "80px"
  btnLogin.onclick = function () {
    if (validateIp4(txtIP.value)) {
      setLStorage("wiqao-serverIP", txtIP.value)
      setLStorage("wiqao-loginUser", txtUser.value)
      setLStorage("wiqao-loginPassword", txtPassword.value)
      overlayLogin.remove()
      location.reload()
    } else {
      alert("invalid IP")
    }
  }
  myDiv.appendChild(btnLogin)
  if (bCancel) {
    let btnCancel = document.createElement("input");
    btnCancel.type = "button"
    // btnCancel.value = eval(l.cancel)
    btnCancel.value = "Cancel"
    btnCancel.style.width = "80px"
    btnCancel.onclick = function () {
      overlayLogin.remove()
    }
    myDiv.appendChild(btnCancel)
  }
  box.appendChild(myDiv)
}
/**
 * Displays a message box, with provided option for selection of any option
 * @param {string} sType type of msgBox (info,warning,error,confirm,select)
 * @param {string} sMsg message to display
 * @param {string} sOptions CSV string with options e.g. "option1=123|option2=234"
 * @param {string} sFsOpt field separator options e.g "|". Default "\n"
 * @param {string} sFsVal field separator label:value e.g. "=". Default "\t"
 * @returns 
 */

function msgBox(sType, sMsg, sOK, sOptions, sFsOpt, sFsVal) {
  let sValue = "";
  let sHeader = "";
  let colHeader = colTitle;
  let bCancel = false
  let bShowList = false

  sMsg += ""
  sMsg = sMsg.replaceAll("\n", "<br>").trim()
  sType = sType.toLowerCase();

  if (sOK === undefined) {
    sOK = eval(l.ok)
  }
  if (sType === "info") {
    colHeader = "#B3E572";
    sHeader = eval(l.info);
  } else if (sType === "warning") {
    colHeader = "orange"
    sHeader = eval(l.warning)
  } else if (sType === "error") {
    colHeader = "red";
    sHeader = eval(l.error);
  } else if (sType === "confirm") {
    colHeader = "#98CBF9";
    sHeader = eval(l.confirm);
    bCancel = true
  } else if (sType === "select") {
    colHeader = "#98CBF9";
    sHeader = eval(l.select);
    bCancel = true
    bShowList = true
  } else {
    alert("msgBox: Unknown type ${sType}")
  }
  sFsOpt = (sFsOpt === undefined) ? "\n" : FS_OPT;
  sFsVal = (sFsVal === undefined) ? "\t" : FS_VAL;
  let arW = []
  if (sOptions !== undefined) {
    arW = sOptions.split(sFsOpt); // split options into words
  }
  let arSelect = []

  let overlay = getOverlay();
  let box = overlay.firstChild;
  box.style.width = "400px"
  box.style.minHeight = "400px"

  // header/title of window
  let header = document.createElement("div");
  header.style.backgroundColor = colHeader;
  header.style.minWidth = "200px"
  header.style.width = "100%";
  header.style.height = "20px";
  header.style.paddingLeft = "8px";
  header.style.paddingTop = "14px";
  header.style.paddingBottom = "9px";
  header.innerHTML = sHeader;
  box.appendChild(header);

  // content box, with message & buttons
  let content = document.createElement("div");
  content.style.padding = "10pt";
  // message
  let boxMsg = document.createElement("div");
  boxMsg.style.minHeight = "280px"
  boxMsg.innerHTML = sMsg;

  let boxList
  if (bShowList) {
    boxMsg.style.minHeight = "30px"
    boxList = document.createElement("div")
    boxList.style.marginTop = "10pt"
    boxList.style.minWidth = "200px"
    boxList.style.width = "100%"
    boxList.style.maxHeight = "400px"
    boxList.style.display = "flex"
    boxList.style.flexDirection = "column"
    //boxList.style.border = "solid"
    boxList.style.overflowY = "auto"

    let filter = document.createElement("input");
    filter.placeholder = eval(l.filter)
    filter.style.width = "96%"
    filter.style.marginBottom = "4px"
    filter.onkeyup = function (e) {
      let arFilter = filter.value.toLowerCase().split(/[ ]+/)
      for (let i = 0; i < arSelect.length; i++) {
        if (filterMatch(arSelect[i].value.toLowerCase(), arFilter)) {
          arSelect[i].style.display = ""
        } else {
          arSelect[i].style.display = "none"
        }
      }
    }
    boxList.appendChild(filter)

    let boxScroll = document.createElement("div")
    boxScroll.style.width = "100%"
    boxScroll.style.height = "100%"
    boxScroll.style.overflowY = "auto"
    //boxScroll.style.backgroundColor = "blue"
    boxList.appendChild(boxScroll)

    // show all options
    for (let i = 0; i < arW.length; i++) {
      if (arW[i] !== "") {
        let arCol = arW[i].split(sFsVal);

        let btn = document.createElement("input");
        btn.type = "button";
        btn.value = arCol[0];
        btn.name = arCol[1];
        btn.style.height = "20pt";
        btn.style.margin = "1pt";
        btn.style.width = "90%";
        // define callback
        btn.onclick = function () {
          sValue = this.name;
          overlay.dispatchEvent(new Event('closeOverlay'));
        }
        arSelect.push(btn)
        boxScroll.appendChild(btn);
      }
    }
  }
  // buttons of selection list
  let boxButtons = document.createElement("div");
  boxButtons.style.marginTop = "10pt";
  boxButtons.style.width = "100%"
  boxButtons.style.display = "flex"
  boxButtons.style.flexDirection = "row"
  boxButtons.style.justifyContent = "space-around"
  //boxButtons.style.backgroundColor = "yellow"

  if (sType !== "select") {
    let btnOK = document.createElement("input")
    btnOK.type = "button"
    btnOK.value = sOK
    btnOK.style.width = "40%"
    btnOK.style.height = "40px"
    btnOK.name = "ok"
    btnOK.onclick = function () {
      sValue = this.name;
      overlay.dispatchEvent(new Event('closeOverlay'));
    }
    boxButtons.appendChild(btnOK)
  }

  if (bCancel) {
    let btnCancel = document.createElement("input")
    btnCancel.type = "button"
    btnCancel.value = eval(l.cancel)
    btnCancel.style.width = "40%"
    btnCancel.style.height = "40px"
    btnCancel.name = "cancel"
    btnCancel.onclick = function () {
      sValue = null //this.name;
      overlay.dispatchEvent(new Event('closeOverlay'));
    }
    boxButtons.appendChild(btnCancel)
  }
  content.appendChild(boxMsg);
  if (bShowList)
    content.appendChild(boxList);
  content.appendChild(boxButtons);
  box.appendChild(content);

  //overlay.appendChild(box);
  //document.body.appendChild(overlay);

  return new Promise((resolve, reject) => {
    overlay.addEventListener('closeOverlay', function (e) {
      overlay.remove();
      resolve(sValue);
    }, { once: false });
  });
}
// Displays error message and stops java script execution
function abortJS(sMsg, bHideStack) {
  // close web server connection if still open
  //if (myWsServer !== null) {
  //wsClose();
  //}
  let myErr = new Error();
  const sStack = myErr.stack
    .split('\n')
    .slice(2)  // remove row 1+2 containing non relevant info
    // .map((line) => line.replace(/[ ]+at[ ]+/, ''))          
    // .map((line) => line.replace(/\s+at\s+/, ''))
    .map(function x(i) { return i.replace(/^[ ]+at[ ]+/, '- ') })
    .join('\n') // concat to string

  if (bHideStack === true) {
    sMsg += "\n\n";
  } else {
    sMsg += "\n\n" + sStack
  }
  msgBox("error", sMsg.replaceAll("\n", "<br>"))
  // exit JS program
  throw new Error(`\n*** abortJS("${sMsg}") ***\nDo not catch error. It is thrown to abort the script.`);
}
// creates a table cell
function mkElem(sElement, sText, fctClick) {
  let td = document.createElement(sElement);
  td.innerHTML = sText;
  if (fctClick) {
    td.onclick = fctClick;
  }
  return td;
}
function keyDn(e) {
  if (e.code.startsWith("Control")) {
    bCtrlPressed = true;
    getElemById("idCtrl").style.backgroundColor = "lightgreen"
  } else if (e.code.startsWith("Shift")) {
    bShiftPressed = true;
    getElemById("idShift").style.backgroundColor = "lightgreen"
  } else if (e.code.startsWith("Alt")) {
    bAltPressed = true;
    getElemById("idAlt").style.backgroundColor = "lightgreen"
  }

  if (bCtrlPressed && bAltPressed) {
    if (e.code === "KeyA") {
      tableSetSelection('all')
    } else if (e.code === "KeyI") {
      tableSetSelection('invert')
    } else if (e.code === "KeyC") {
      varCheckOpcUa()
    } else if (e.code === "KeyO") {
      projectClose()
      openWorkspaceProject(-1)
    }
  }
}
function keyUp(e) {
  if (e.code.startsWith("Control")) {
    bCtrlPressed = false;
    getElemById("idCtrl").style.backgroundColor = ""
  } else if (e.code.startsWith("Shift")) {
    bShiftPressed = false;
    getElemById("idShift").style.backgroundColor = ""
  } else if (e.code.startsWith("Alt")) {
    bAltPressed = false;
    getElemById("idAlt").style.backgroundColor = ""
  }
  // console.log("shift=" + bCtrlPressed + " shift=" + bShiftPressed + " " + e.code)
}
/**
 * Calls getElementById, when not found user is notified with dialog, then program abort
 * 
 * @param {string} sId id of DOM element to return
 * @returns DOM element with id
 */
function getElemById(sId) {
  let oElem = document.getElementById(sId);
  if (!oElem) {
    abortJS(`Element not found.\nid='${sId}'`);
  } else {
    return oElem;
  }
}
async function attributeHighlight(sName) {
  document.getElementById('idValue').value = ''
  for (let i = 0; i < arTableCol.length; i++) {
    if (arTableCol[i].name === sName) {
      tableHeader.rows[1].cells[i].style.backgroundColor = "lightblue"
    } else {
      tableHeader.rows[1].cells[i].style.backgroundColor = ""
    }
  }
}
/**
 * Display dialog with options of selected attribute
 * attributes are defined in arTableCol
 */
async function attributeSelectOption() {
  let sAttribute = document.getElementById("idAttribute").value
  let txtValue = document.getElementById("idValue")
  let arOptions = arTableCol.find(x => x.name === sAttribute).edit
  let sUnit = arTableCol[COL_UNIT].name
  let sOptions = ""
  for (let i = 0; i < arOptions.length; i++) {
    if (arOptions[i] !== sINPUT) {
      sOptions += arOptions[i] + FS_VAL + arOptions[i] + FS_OPT
    }
  }
  let sSelect = await msgBox("select", "please select", "", sOptions)
  if (sSelect === null) return
  if (sSelect === sINPUT) {
    sSelect = ""
  } else if (sAttribute === sUnit) {
    // extract number of unit class
    sSelect = sSelect.split(" ")[0]
  }
  txtValue.value = sSelect.trim()
  varSetAttr()
}
/**
 * activate a language
 * @param {string} sLang en=English de=German
 * @param {bool} bReload true=reload document false=no reload
 * @returns null
 */
async function setupLanguage(sLang, bReload = true) {
  let sIn = "" // for reading text file

  //*** get language from cookie & activate it ***
  if (sLang === "") {
    sLang = getLStorage("wiqao-language")
    sLang = (sLang === "") ? "en" : sLang
  }
  sIn = await fetchTextFile(DIR_ADDON_JSON + sLang + ".json")
  l = JSON.parse(sIn)
  sIn = ""

  table = null
  tableHeader = null

  setLStorage("wiqao-language", sLang)
  if (bReload) {
    location.reload()
    return
  }
  for (const key in l) {
    l[key] = "`" + l[key] + "`";
  }
  // update table column names
  for (let i = 0; i < arTableCol.length; i++) {
    let sTest = eval("`" + arTableCol[i].lang + "`")
    arTableCol[i].label = sTest.replaceAll("`", "")
  }
  return l
}
/**
 * updates the x-scroll position of header, when x-scroll of content changed
 * and vice versa
 */
function getScrollC(me) {
  if (me = divTabData) {
    divTabHeader.scrollLeft = me.scrollLeft;
  } else {
    divTabData.scrollLeft = me.scrollLeft;
  }
}
/*************************** SORT  ************************************/
/**
 * Create a item name for correct sorting of indices and numbers
 * Converts to lower case for case insensitive sorting
 * e.g.  test.1 => test.0001, test.10 => test.0010
 *       test.0001
 *       test.0002
 *       test.1000
 * 
 * @param {string} sItem name of item
 */
function parseName(sItem) {
  let sNumber = "";
  let sNew = "";
  let iDigits = 4;
  sItem = String(sItem).toLowerCase();

  for (let i = 0; i < sItem.length; i++) {
    let sChar = sItem.slice(i, i + 1);
    // collect all digits of a number
    if ((sChar >= "0") && (sChar <= "9")) {
      sNumber += sChar;
    } else {
      // if number found, pad it and append it to new string
      if (sNumber !== "") {
        sNumber = sNumber.padStart(iDigits, "0");
        sNew += sNumber;
        sNumber = "";
      }
      sNew += sChar;
    }
  }
  // if number found, pad it and append it to new string
  if (sNumber !== "") {
    sNumber = sNumber.padStart(iDigits, "0");
    sNew += sNumber;
    sNumber = "";
  }
  //console.log(sNew.padStart(30) + " : " + sItem);
  return sNew;
}
/**
 * Compares key property of array of json objects
 * @param {string} sKey1 name of 1st key used for sorting (mandatory)
 * @param {string} sKey2 name of 2nd key used for sorting (optional)
 * @returns 1=a>b -1:a<b 0:a=b
 */
function sortByKey(bReverse, sKey1, sKey2, fctParse1, fctParse2) {
  const iRet = bReverse ? -1 : 1;

  return function (a, b) {
    let aa = a[sKey1];
    let bb = b[sKey1];

    if (fctParse1) {
      aa = fctParse1(aa);
      bb = fctParse1(bb);
    }
    if (aa > bb) {
      return iRet;
    } else if (aa < bb) {
      return -iRet;
    } else {
      // use optional sKey2
      if (sKey2) {
        let aa = a[sKey2];
        let bb = b[sKey2];

        if (fctParse2) {
          aa = fctParse2(aa);
          bb = fctParse2(bb);
        }
        if (aa > bb) {
          return iRet;
        } else if (aa < bb) {
          return -iRet;
        } else {
          return 0;
        }
      }
    }
  }
}
function parseToLower(sIn) {
  return sIn.toLowerCase();
}
/**
 * Set sort column, order and start sorting
 * @param {integer} iCol >=0 number of selected sort column <0 don't change sort key/order 
 */
function startSort(iCol) {
  if (iCol < 0) {
    iCol = tableData.iColSort;
  } else {
    if (iCol === tableData.iColSort) {
      tableData.bReverse = !tableData.bReverse;
    } else {
      tableData.bReverse = false;
      tableData.iColSort = iCol;
    }
  }
  console.log(`StartSort(${iCol}) Reverse=${tableData.bReverse}`);

  if (iCol === 0) {
    // sort by item name (unique)
    arList = arList.sort(sortByKey(tableData.bReverse, arTableCol[iCol].name, null, arTableCol[iCol].fctParse, null));
  } else {
    // by any other key, use item name as 2nd sort key
    arList = arList.sort(sortByKey(tableData.bReverse, arTableCol[iCol].name, arTableCol[0].name, arTableCol[iCol].fctParse, arTableCol[0].fctParse));
  }
  tableUpdate();
}
/*************************** VARIABLE  ************************************/
/**
 * Copies item attributes from jResp into arList[]
 * @param {json} jResp resonse of experimental.structure_store.variable.list
 */
function varGetAttr(jResp) {
  arList = [];  // clear global array
  progress("on")
  // create array with items for sorting
  let iMax = jResp.data.length
  let jLabel = {} // differnet path or var/array

  for (let i = 0; i < iMax; i++) {
    progress("", 100 * i / iMax)
    let e = jResp.data[i];
    let jItem = {};
    jItem.check = "&nbsp;"
    jItem.name = e.name;
    //jItem.sortName = parseName(jItem.name);

    jItem.interval = (e.interval === null) ? sNONE : e.interval;
    if (jItem.interval === -1) {
      jItem.interval = sAT_START
    }
    jItem.source = ((e.source === null) ? SRC_INTERNAL : e.source);
    jItem.unit = ((e.unit === null) ? sNONE : e.unit);
    jItem.type = e.type.name;
    jItem.digits = ((e.digits === null) ? sNONE : e.digits);

    jItem.arSize = (e.array_config) ? e.array_config.size : sNONE;
    if (e.limits === null) {
      jItem.limitsMin = sNONE //-1
      jItem.limitsMax = sNONE //-1
    } else {
      jItem.limitsMin = (e.limits.min === null) ? sNONE : e.limits.min
      jItem.limitsMax = (e.limits.max === null) ? sNONE : e.limits.max
    }
    // different nodes for array and single variable
    if (e.array_config) {
      jLabel = e.array_config.element_config.label
    } else {
      jLabel = e.label
    }
    if (jLabel === null) {
      // nothing defined
      jItem.label = sNONE;
    } else {
      if (jLabel.rules[0].mode === "identifier") {
        jItem.label = sIDENTIFIER; //e.name;// use item name as label
      } else if (jLabel.rules[0].mode === "append") {
        jItem.label = jLabel.rules[0].value; // use provided string as label
      } else {
        jItem.label = sINVALID;
      }
    }

    arList.push(jItem);  // add new item to array
  }
  progress("off")
}
/**
* async set item property
*/
async function varSetAttr() {
  txtValue.value = txtValue.value.trim() // trim white spaces
  // console.clear();
  if (table === null) {
    abortJS(tr(l.noProjectLoaded));
  }
  if (tableData.iRowsSelected === 0) {
    await msgBox("warning", eval(l.nothingSelected))
    return;
  }
  if ((tableData.iRowsSelected !== 1)) {
    let sRes = await msgBox("confirm", eval(l.confirmSetAttributes))
    if (sRes !== sOK)
      return;
  }
  sTarget = "";

  // set name of project to connect to
  sTarget = sWsGuid;
  await progress("on")

  // loop over all items of table
  const iROWS = table.rows.length;
  // jTabSelect = {}
  for (let iRow = ROW_DATA_FIRST; iRow < iROWS; iRow++) {
    if (tableRowSelected(table.rows[iRow]) === true) {
      let sItemName = table.rows[iRow].cells[COL_ITEM].outerText
      // jTabSelect[sItemName] = 0
      let sAttrVal = txtValue.value
      let jCmd = cmdVariableGet
      jCmd.data.name = sItemName
      // get all item properties
      let jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); })

      // change command
      jResp.cmd = "experimental.structure_store.variable.replace";
      //change property value

      if (selAttribute.value === "interval") {
        if (sAttrVal === sNONE) {
          jResp.data.interval = null
        } else {
          if (sAttrVal === sAT_START) {
            sAttrVal = "-1"
          } else {
            sAttrVal = sAttrVal
          }
          jResp.data.interval = parseInt(sAttrVal)
        }
      } else if (selAttribute.value === "arSize") {
        if (table.rows[iRow].cells[COL_ARSIZE].outerText !== sNONE) {
          jResp.data.array_config.size = parseInt("0" + txtValue.value);
        }
      } else if (selAttribute.value === "unit") {
        jResp.data.unit = sAttrVal;
      } else if (selAttribute.value === "digits") {
        jResp.data.digits = parseInt("0" + sAttrVal);
      } else if (selAttribute.value === "label") {
        let jData = null;
        if (sAttrVal === "") {
          sAttrVal = sNONE
        }
        if (sAttrVal === sIDENTIFIER) {
          jData = {
            "propagate": true,
            "rules": [{ "mode": "identifier" }]
          }
        } else if (sAttrVal === sNONE) {
          jData = null;
        } else {
          jData = {
            "propagate": true,
            "rules": [{ "mode": "append", "value": "" }]
          }
          const sFS = "."
          let arX = table.rows[iRow].cells[COL_ITEM].outerText.split(sFS)
          if (sAttrVal.startsWith(s1STWORD)) {
            let iStart = 0
            let sTmp = ""
            let sItem = ""
            let arW = sAttrVal.split(sFS)
            const iWord = parseInt(arW[1])
            if (iWord > 0) {
              iStart = iWord - 1
            } else if (iWord < 0) {
              iStart = arX.length + iWord
            }
            iStart = (iStart < 0) ? 0 : iStart

            for (let i = iStart; i < arX.length; i++) {
              sItem += sTmp + arX[i]
              sTmp = sFS
            }
            if (sItem === "") sItem = sNONE
            jData.rules[0].value = sItem
          } else if (sAttrVal.startsWith(sREPLACE)) {
            let arW = sAttrVal.split(",")
            jData.rules[0].value = table.rows[iRow].cells[COL_LABEL].outerText.replace(arW[1], arW[2])
          } else {
            jData.rules[0].value = sAttrVal;
          }
        }

        if (jResp.data.array_config) {
          jResp.data.array_config.element_config.label = jData;
        } else {
          jResp.data.label = jData;
        }
      } else if ((selAttribute.value === "limitsMin") || (selAttribute.value === "limitsMax")) {
        let sMin = table.rows[iRow].cells[COL_LIMITS_MIN].outerText
        let sMax = table.rows[iRow].cells[COL_LIMITS_MAX].outerText
        if (selAttribute.value === "limitsMin") {
          sMin = sAttrVal
        } else {
          sMax = sAttrVal
        }
        if ((sMin === sNONE) && (sMax = sNONE)) {
          jResp.data.limits = null
        } else {
          let limits = {}
          limits.min = (sMin === sNONE) ? null : parseFloat(sMin)
          limits.max = (sMax === sNONE) ? null : parseFloat(sMax)
          jResp.data.limits = limits
        }
      }

      //sCmd = JSON.stringify(oResp1, null, 2);
      await wsSendRecv(jResp).catch((err) => { abortJS(err.error.message); });
    }
    progress("", 100 * (iRow + 1) / iROWS);
  }
  //await wsClose();
  // Reset progress bar
  await progress("off")
  openWorkspaceProject(iProjectLoadId);
}
/*************************** FILTER  ************************************/
/**
 * Start function filterStart() in background, this means:
 * Instead of wait that the previous filter is done, it is killed. Therefore the new filter must not wait
 * until the previous one is finished.
 */
function filterDelay() {
  if (filterProcess !== null)
    clearTimeout(filterProcess);
  filterProcess = setTimeout(filterStart, 1);
}
/**
 * Clears filter text string
 */
function filterClear() {
  this.value = "";
  filterDelay();
}
/**
 * Checks if all words of arFillter are part of sText
 * @param {string} sText text to be checked
 * @param {string array} arFilter array with searched strings
 * @returns true=sText matches arFilter false=sText NOT matches arFilter
 */
function filterMatch(sText, arFilter) {
  if (false) {
    for (let i = 0; i < arFilter.length; i++) {
      let regEx = new RegExp(arFilter[i], "ig");
      return sText.match(regEx)
    }
  } else {
    for (let i = 0; i < arFilter.length; i++) {
      if (sText.indexOf(arFilter[i]) === -1) {
        return false
      }
    }
    return true
  }
}
/**
 * Rows which do not match the filter text, are hidden.
 * Loops over all columns, all filter text must match to display the row.
 * Multiple words in one text input, seperated by " ", must all match (AND)
 * Case (upper/lower) is ignored
 * e.g. filter text: "ab cd" matches "ABer CDxf", "abcd", "cdab" and "cd...ab"
 */
function filterStart() {
  const iROWS = table.rows.length;
  const iCOLS = arTableCol.length;

  // get all user inputs of filter text and split it
  let arColFilter = [];
  for (let iCol = 0; iCol < iCOLS; iCol++) {
    // trim white spaces, convert to lower case // e.g. "  But  This    is  " => "but  this    is"
    let sFilter = getElemById("id" + arTableCol[iCol].name).value.trim().toLowerCase();
    arColFilter.push(sFilter.split(/[ ]+/));  // use reg. expr. to handle multiple spaces as one field separator
  }

  // hide all not matching rows
  for (let iRow = ROW_DATA_FIRST; iRow < iROWS; iRow++) {
    // loop over all table columns
    let bHide = false;
    for (let iCol = 0; iCol < iCOLS; iCol++) {
      if (arTableCol[iCol].show) {
        let sValue = table.rows[iRow].cells[iCol].outerText.toLowerCase();
        let arW = arColFilter[iCol]; // get splitted words of column filter text
        if (!filterMatch(sValue, arW)) {
          bHide = true;
          iCol = iCOLS;
        }
      }
    }
    table.rows[iRow].hidden = bHide;
  }
  tableCountRows();

  filterProcess = null;
}
/*************************** TABLE  ************************************/
/**
 * Checks if Table row is selected
 * @param {DOM} tr table row eleemnt
 * @returns true=selected false=not selected
 */
function tableRowSelected(tr) {
  return (tr.style.backgroundColor !== sColorOff);
}
/**
 * Clear column "check" of table
 */
async function tableCheckClear() {
  divTabDetail.innerHTML = ""
  const iROWS = table.rows.length;
  for (let iRow = ROW_DATA_FIRST; iRow < iROWS; iRow++) {
    table.rows[iRow].cells[COL_CHECK].innerHTML = SPACE
    table.rows[iRow].cells[COL_CHECK].style.backgroundColor = ""
  }
}
function selectTableRow(tr, bOn = false) {
  tr.style.backgroundColor = (bOn) ? sColorOn : sColorOff
}
/**
 * Update table content
 */
function tableUpdate() {
  let arMarked = []
  // remove previously created table
  if (table === null) {
    // create table with header
    tableHeader = document.createElement("table");
    tableHeader.border = 1;
    tableHeader.borderColor = "black";
    tableHeader.cellPadding = 2;
    tableHeader.cellSpacing = 0;
    tableHeader.style.width = tableSumColWidth() + "px";
    let tr = null;

    for (let j = 0; j < 2; j++) {
      //let arOrder = [1,0];
      let arOrder = [0, 1];
      if (arOrder[j] === 0) {
        // table header: filter text input
        tr = document.createElement("tr");
        tr.style.backgroundColor = sColorTableHeader;
        for (let i = 0; i < arTableCol.length; i++) {
          let td = document.createElement("td");
          let input = document.createElement("input");
          input.style.width = String(parseInt(arTableCol[i].width) - 8) + "px"
          td.style.width = arTableCol[i].width;
          input.id = "id" + arTableCol[i].name;
          input.placeholder = eval(l.filter);
          input.onkeyup = filterDelay;
          input.ondblclick = filterClear;
          td.appendChild(input);
          tr.appendChild(td);
        }
        tableHeader.appendChild(tr);
      } else {
        // show table header: column label
        tr = document.createElement("tr");
        tr.style.backgroundColor = sColorTableHeader;
        for (let i = 0; i < arTableCol.length; i++) {
          let td = mkElem("td", "", function () {
            startSort(i);
          });
          td.style.width = arTableCol[i].width
          if (i === COL_UNIT) td.setAttribute('title', sUnitTooltip);
          if (arTableCol[i].show) {
            td.style.display = ""
          } else {
            td.style.display = "none"
          }
          let img = document.createElement("img");
          img.src = "img/arrow_up.svg"
          img.style.width = "14px"
          img.style.height = "14px"
          img.style.visibility = "hidden"

          sortImg.push(img);
          td.appendChild(img);
          td.appendChild(document.createTextNode(arTableCol[i].label));
          //td.style.width = "2%";
          tr.appendChild(td);
        }
        tableHeader.appendChild(tr);
      }
    }
    // create new table, define settings
    table = document.createElement("table");
    table.classList.add("prevent-select");
    table.style.width = tableHeader.style.width;
    table.border = tableHeader.border;
    table.borderColor = tableHeader.borderColor;
    table.style.backgroundColor = sColorTableContent;
    table.cellPadding = tableHeader.cellPadding;
    table.cellSpacing = tableHeader.cellSpacing;
  } else {
    // Store marked items
    for (let iRow = ROW_DATA_FIRST; iRow < table.rows.length; iRow++) {
      if (tableRowSelected(table.rows[iRow])) {
        arMarked.push(table.rows[iRow].cells[COL_ITEM].outerText)
      }
    }
    console.log(arMarked)
    // delete existing data rows
    const iROWS = table.rows.length;
    for (let iRow = ROW_DATA_FIRST; iRow < iROWS; iRow++) {
      table.deleteRow(ROW_DATA_FIRST);
    }
  }
  for (let iR = 0; iR < 2; iR++) {
    for (let iC = 0; iC < arTableCol.length; iC++) {
      tableHeader.rows[iR].cells[iC].style.display = arTableCol[iC].show ? "" : "none"
    }
  }
  // update table header with sort column, direction & width
  tableHeader.style.width = tableSumColWidth() + "px";
  for (let j = 0; j < tableHeader.rows.length; j++)
    for (let i = 0; i < tableHeader.rows[j].cells.length; i++)
      tableHeader.rows[j].cells[i].style.width = arTableCol[i].width

  table.style.width = tableHeader.style.width
  for (let i = 0; i < arTableCol.length; i++) {
    let img = sortImg[i];
    if (tableData.iColSort === i) {
      img.style.visibility = "visible";
      if (tableData.bReverse) {
        img.style.transform = `rotate(180deg)`;
      } else {
        img.style.transform = `rotate(0deg)`;
      }
    } else {
      img.style.visibility = "hidden";
    }

  }
  // print out all variables
  for (let i = 0; i < arList.length; i++) {
    let tr = document.createElement("tr");

    tr.ondblclick = function () { wiqVar.crossRef() }
    tr.onclick = function () {
      let iRow = tr.rowIndex;
      if (bCtrlPressed) {
        if (tableRowSelected(tr)) {
          // tr.style.backgroundColor = sColorOff;
          selectTableRow(tr, false)
        } else {
          selectTableRow(tr, true)
          // tr.style.backgroundColor = sColorOn;
        }
      } else if (bShiftPressed || (iRowClickCount > 1)) {
        if (iRowPrevious >= 0) {
          if (iRow < iRowPrevious) {
            for (let i = iRow; i <= iRowPrevious; i++) {
              selectTableRow(table.rows[i], true)
              // table.rows[i].style.backgroundColor = sColorOn;
            }
          } else {
            for (let i = iRowPrevious; i <= iRow; i++) {
              selectTableRow(table.rows[i], true)
              // table.rows[i].style.backgroundColor = sColorOn;
            }
          }
        }
      } else {
        for (let i = ROW_DATA_FIRST; i < table.rows.length; i++) {
          if (i === iRow) {
            selectTableRow(table.rows[i], true)
            // table.rows[i].style.backgroundColor = sColorOn;
          } else {
            selectTableRow(table.rows[i], false)
            // table.rows[i].style.backgroundColor = sColorOff;
          }
        }
      }
      if (iRow === iRowPrevious) {
        iRowClickCount++
      } else {
        iRowClickCount = 1
      }
      iRowPrevious = iRow
      tableCountRows();
    }
    table.appendChild(tr);
    // highlight row
    let e = arList[i];
    for (let i = 0; i < arTableCol.length; i++) {
      let td = mkElem("td", e[arTableCol[i].name]);
      if (arTableCol[i].show) {
        td.style.width = arTableCol[i].width;
        td.style.display = ""
      } else {
        td.style.display = "none"
      }
      if (arTableCol[i].align === "R") {
        td.style.textAlign = "right";
      }
      tr.appendChild(td);
    }
    // show info on HTML page
    divTabHeader.appendChild(tableHeader);
    divTabData.appendChild(table);
    if (arMarked.includes(e.name))
      selectTableRow(table.rows[i], true)
    // tr.style.backgroundColor = sColorOn;
  }
  filterStart();  // filter table items
}
/**
 * counts rows: all, visible, selected
 */
function tableCountRows() {
  let iRowsAll = 0
  if (table)
    iRowsAll = table.rows.length;
  let iRowsVisible = 0;
  let iRowsSelected = 0;
  let iRowFirst = -1
  let iRowLast = -1

  for (let i = ROW_DATA_FIRST; i < iRowsAll; i++) {
    if (table.rows[i].hidden === false) {
      iRowsVisible++;
      if (tableRowSelected(table.rows[i])) {
        if (iRowFirst === -1)
          iRowFirst = i
        iRowLast = i
        iRowsSelected++;
      }
    }
  }
  iRowsAll -= ROW_DATA_FIRST;
  tableData.iRowsAll = iRowsAll;
  tableData.iRowsSelected = iRowsSelected;
  tableData.iRowsVisible = iRowsVisible;
  tableData.iRowFirst = iRowFirst
  tableData.iRowLast = iRowLast

  let sInfo = `${iRowsSelected}/${iRowsVisible}/${iRowsAll}/${sWsProject}`;
  txtInfo.value = sInfo;
}
/**
 * Select rows of table
 * sAction "invert" inverts selection
 *         "all"    selects all visible
 *         else     deselects all visible
 */
function tableSetSelection(sAction) {
  for (let i = ROW_DATA_FIRST; i < table.rows.length; i++) {
    if (table.rows[i].hidden === false) {
      if (sAction === "invert") {
        if (tableRowSelected(table.rows[i])) {
          // table.rows[i].style.backgroundColor = sColorOff
          selectTableRow(table.rows[i], false)
        } else {
          // table.rows[i].style.backgroundColor = sColorOn
          selectTableRow(table.rows[i], true)
        }
      } else {
        // table.rows[i].style.backgroundColor = (sAction === "all") ? sColorOn : sColorOff
        if (sAction === "all")
          selectTableRow(table.rows[i], true)
        else
          selectTableRow(table.rows[i], false)

      }
    }
  }
  tableCountRows();
}
/**
 * Sumarizes the width of all columns
 * @returns sum of all column width
 */
function tableSumColWidth() {
  const iMax = arTableCol.length;
  let iSum = 0;
  for (let i = 0; i < iMax; i++) {
    if (arTableCol[i].show) {
      iSum += parseInt(arTableCol[i].width) + 7;
    }
  }
  return iSum;
}
/**
 * Opens a dialog to configure table (show/hide columns)
 */
function tableConfigure() {
  const colHeader = "lightblue"
  let sValue = ""
  let overlay = getOverlay();
  let box = overlay.firstChild;

  let header = document.createElement("div");
  header.style.backgroundColor = colHeader;
  header.style.minWidth = "200px"
  header.style.width = "100%";
  header.style.height = "20px";
  header.style.paddingLeft = "8px";
  header.style.paddingTop = "14px";
  header.style.paddingBottom = "9px";
  header.innerHTML = eval(l.selectColumns);
  box.appendChild(header);

  for (let i = 0; i < arTableCol.length; i++) {
    //{ col: 0, name: "name", label: "Item", fctParse: parseName, width: "400px", align: "L", show: true },
    // col width
    let inp = document.createElement("input")
    inp.type = "text"
    inp.value = arTableCol[i].width
    inp.style.width = "50px"
    box.appendChild(inp)
    inp.onchange = function () {
      this.value = parseInt(this.value) + "px"
      arTableCol[i].width = this.value
      console.log("value=" + this.value);
    }

    // ***** checkbox show/hide *****
    let e = document.createElement("input")
    e.type = "checkbox"
    e.value = arTableCol[i].name;
    e.checked = arTableCol[i].show;
    e.id = "idTableConfigCol" + i

    e.onclick = function () {
      console.log("value=" + this.value + " show:" + this.checked);
      arTableCol[i].show = this.checked
    }

    box.appendChild(e)
    // ***** label of checkbox *****
    let lbl = document.createElement("label")
    lbl.setAttribute("for", e.id)
    lbl.style.width = "200px"
    lbl.appendChild(document.createTextNode(arTableCol[i].label))
    box.appendChild(lbl)

    // ***** <br> *****
    box.appendChild(document.createElement("br"))
  }
  let btn = null

  // button OK
  btn = document.createElement("input");
  btn.type = "button";
  btn.value = eval(l.ok);
  btn.name = "OK";
  btn.style.height = "20pt";
  btn.style.margin = "2pt";
  btn.style.width = "180pt";
  // define callback
  btn.onclick = function () {
    sValue = eval(l.setColumns);
    let sCols = ""
    let sWidth = ""
    // collect all columns with option show
    for (let i = 0; i < arTableCol.length; i++) {
      sWidth += arTableCol[i].width + ":"
      if (arTableCol[i].show) {
        sCols += arTableCol[i].name + ":"
      }
    }
    setLStorage("wiqao-columns", sCols)
    setLStorage("wiqao-columns-width", sWidth)
    overlay.dispatchEvent(new Event('closeOverlay'));
  }
  box.appendChild(document.createElement("br"))
  box.appendChild(btn)

  return new Promise((resolve, reject) => {
    overlay.addEventListener('closeOverlay', function (e) {
      overlay.remove()
      resolve(sValue)
      // delete existing tables
      /*
      if (tableHeader) {
        tableHeader.remove()
        tableHeader = null
      }
      if (table) {
        table.remove()
        table = null
      }
      */
      tableUpdate()
    }, { once: false });
  });
}

// check if html page is called via a server, otherwise the WebIQ API is not working
function checkUrl() {
  if (!["http:", "https"].includes(window.location.protocol)) {
    alert("This page must be opened via the WebIQ Designer WebServer\ne.g. http://localhost:10130/addon/")
  }
}
/*************************** PROOJECT  ************************************/
/**
* async load project
* @param {string} iProjectID -1=open dialog to select project >=0:open project with provided ID
*/
async function openWorkspaceProject(iProjectID, bVarList = true) {

  // console.clear();
  sInfo = "";
  sTarget = "";
  let jCmd = {}
  let jResp = {}
  let bConnected = true

  // establish connection to webIQ server
  if (myWsServer === null) {
    sWebIQServer = `ws://${sLoginServer}:10123/`
    try {
      await wsConnect(sWebIQServer, sProtocol)
    } catch {
      dialogLogin(eval(l.loginFailed), false)
    }
    // login with system user & password
    let cmdLoginX = cmdLogin
    cmdLoginX.data.username = sLoginUser
    cmdLoginX.data.password = sLoginPassword
    await wsSendRecv(cmdLoginX).catch((err) => {
      bConnected = false
      if (err.error.errc === 3) {
        myWsServer = null
        dialogLogin(eval(l.loginFailed), false)
      } else {
        sMsg = err.error.message
        abortJS(sMsg);
      }
    })
  }
  // check log file size
  jResp = await wsSendRecv(cmdConnectLogGet).catch((err) => {
    sMsg = err.error.message
    abortJS(sMsg);
  })
  if (jResp.data.file_size_limit > MAX_SIZE_LOG_FILE) {
    let sRes = await msgBox("confirm", eval(l.hugeLogFileSize))
    if (sRes !== null) {
      // retrieve list with all variables/items
      jCmd = cmdConnectLogSet
      jCmd.data = jResp.data
      jCmd.data.file_size_limit = MAX_SIZE_LOG_FILE
      jResp = await wsSendRecv(jCmd).catch((err) => {
        abortJS(err.error.message);
      })
    }
  }


  // true: loaded via localhost:10124 from workspace in %appdata%
  // false: loaded via file://...
  if (true) {
    if (bConnected) {
      await projectWorkspace()
    } else {
      return
    }
  } else {
    // get workspace projects with project names
    let iProjects = 0;
    let sProjectOpts = "";
    jCmd = cmdFsLs;
    jCmd.data = [".workspace"];
    jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); });
    let arProjects = [];
    for (let i = 0; i < jResp.data[0].listing.files.length; i++) {
      if (jResp.data[0].listing.files[i].type === "json") {
        let oProject = {};

        oProject.guid = jResp.data[0].listing.files[i].name.replace(".json", "");
        jCmd = cmdWorkspaceInfo;
        jCmd.data = oProject.guid;
        let oResp1 = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); });
        // build options string for msgBox()
        oProject.name = oResp1.data.app_name;
        sProjectOpts += oProject.name + FS_VAL + iProjects + FS_OPT;

        // store project in array
        arProjects.push(oProject);
        iProjects++;
      }
    }
    if (iProjects === 0) {
      abortJS(l.noWorkspaceProject);
      return
    } else if (iProjects > 1) {
      if (iProjectID === -1) {
        //      sProjectOpts += "*** CANCEL ***" + FS_VAL + 9999 + FS_OPT;
        iProjectID = await msgBox("select", eval(l.multipleProjectsInWorkspace), "", sProjectOpts, FS_OPT, FS_VAL);
        if (iProjectID === "cancel") {
          return;
        }
      }
    } else {
      iProjectID = 0
    }
    iProjectLoadId = iProjectID;

    //console.log(`Open ${arProjects[iProjectID].name} ${arProjects[iProjectID].guid}`);

    // update window title
    document.title = arProjects[iProjectID].name;
    sWsGuid = arProjects[iProjectID].guid;
    sWsProject = arProjects[iProjectID].name;

    // start project on server, can not check if it is yet running
    jCmd = cmdWorkspaceRecover;
    jCmd.data = sWsGuid;
    jResp = await wsSendRecv(jCmd).catch((err) => { abortJS(err.error.message); });
  }

  // set name of project to connect to
  sTarget = sWsGuid;
  try {
    document.getElementById("idGuid").value = sWsGuid
  } catch (e) {
    // alert("idGuid = " + sWsGuid)
  }
  if (bVarList) {
    // retrieve list with all variables/items
    jResp = await wsSendRecv(cmdVariableList).catch((err) => {
      if (err.error.errc === 17) {
        abortJS(eval(l.connectFailed))
        // msgBox("error", eval(l.connectFailed))
      } else {
        abortJS(err.error.message);
      }
    })

    await varGetAttr(jResp);

    // update elements for editing variable attributes
    let oAttribute = document.getElementById("idAttribute");
    if (oAttribute)
      if (oAttribute.options.length === 0) {
        for (let i = 0; i < arTableCol.length; i++) {
          if (arTableCol[i].edit) {
            let opt = new Option(arTableCol[i].label, arTableCol[i].name)
            oAttribute.add(opt, undefined)
          }
        }
      }
    startSort(-1);
    // sTarget = "";
  }
  return sWsGuid
}
/**
 * Close project
 */
async function projectClose() {
  if (myWsServer !== null) {
    await wsClose();
  }
  sWsProject = "";
  sWsGuid = "";
  // delete existing tables
  if (tableHeader) {
    tableHeader.remove()
    tableHeader = null
  }
  if (table) {
    table.remove()
    table = null
  }

  tableCountRows();
}
async function projectWorkspace() {
  let sContent = ""
  let jContent = {}
  let sFile = ""

  // get project guid
  sFile = FILE_APP_INFO
  sContent = await fetchTextFile(sFile)
  jContent = JSON.parse(sContent)

  sWsGuid = jContent.guid

  // get project title
  sFile = FILE_WEBIQ
  sContent = await fetchTextFile(sFile)
  jContent = JSON.parse(sContent)
  sWsProject = jContent.title
  document.title = "[+] " + sWsProject // display title in tab of browser

  // read units
  sFile = FILE_UNITS
  sContent = await fetchTextFile(sFile)
  jContent = JSON.parse(sContent)
  console.log("*** Units ***")
  let iClassBak = -1
  arTableCol[COL_UNIT].edit = [sNONE] // clear array
  if (jContent.unitClasses.length>0) sUnitTooltip = ".\n"
  for (let i = 0; i < jContent.unitClasses.length; i++) {
    if (iClassBak !== jContent.unitClasses[i].unitClass) {
      console.log("class=" + jContent.unitClasses[i].unitClass + " " + jContent.unitClasses[i].name + " " + jContent.unitClasses[i].unitText)
      iClassBak = jContent.unitClasses[i].unitClass
      arTableCol[COL_UNIT].edit.push(jContent.unitClasses[i].unitClass + " " + jContent.unitClasses[i].name)
      sUnitTooltip += jContent.unitClasses[i].unitClass + " " + jContent.unitClasses[i].name + "\n"
    }
  }

  // read list with existing languages
  sFile = FILE_LANGUAGES
  sContent = await fetchTextFile(sFile)
  jContent = JSON.parse(sContent)
  console.log("*** Languages ***")
  for (let lang in jContent.locales) {
    // console.log("lang=" + lang + " " + jContent.locales[lang].label)
    // read translation file
    sFile = DIR_LANG + lang + ".json"
    sContent = await fetchTextFile(sFile)
    // let jLang = JSON.parse(sContent)
  }
  console.log("*** ***")
}
// get all webIQ variables, even struct in structs
// returns an string array with all variable names
async function getAllVars() {
  let arTmp = []
  const arBaseTyp = ["bool", "int", "float", "string"]

  async function recursiveList(sName, sType) {
    let jCmd = cmdStructGet
    jCmd.data.name = sType
    let jMembers = await wsSendRecv(cmdStructGet).catch((err) => { abortJS(err.error.message); })
    for (const key in jMembers.data.members) {
      let sType2 = jMembers.data.members[key].type.name
      let sName2 = sName + "." + key
      if (arBaseTyp.includes(sType2)) {
        arTmp.push(sName2);
      } else {
        await recursiveList(sName2, sType2)
      }
    }
  }
  // add items to list
  let jTmp = await wsSendRecv(cmdVariableList).catch((err) => { abortJS(err.error.message); })
  for (let i = 0; i < jTmp.data.length; i++) {
    if (arBaseTyp.includes(jTmp.data[i].type.name))
      arTmp.push(jTmp.data[i].name);
    else {
      await recursiveList(jTmp.data[i].name, jTmp.data[i].type.name)
    }
  }
  return arTmp
}
// get all webIQ items, even struct in structs
// returns an string array with all item names
async function getAllItems() {
  let arTmp = []
  // add items to list
  let jTmp = await wsSendRecv(cmdItemList).catch((err) => { abortJS(err.error.message); })
  for (let i = 0; i < jTmp.data.items.length; i++) {
    arTmp.push(jTmp.data.items[i].alias);
  }
  return arTmp
}
