/*___________________________________________________________________________*/
class LimSignal {
   #callbacks

   constructor(cb) {
      this.#callbacks = Array.isArray(cb) ? cb : [];
   }

   connect(cb) {
      if (typeof cb !== "function") {
         console.debug(`callback is ${typeof cb} not a function`);
      }

      if (this.#callbacks.includes(cb)) {
         console.debug(`callback is already present`);
      }

      this.#callbacks.push(cb);
   }

   disconnect(cb) {
      const index = this.#callbacks.indexOf(cb);
      if (0 <= index)
         this.#callbacks.splice(index, 1);
   }

   push(cb) {
      this.#callbacks.push(cb);
   }

   emit(that) {
      for (let cb of this.#callbacks)
         cb(that);
   }
}

/*___________________________________________________________________________*/
class LimClassFactory {

   static ctors = {};

   static registerConstructor(className, ctor) {
      LimClassFactory.ctors[className] = ctor;
   }

   static createClass(className, state, ...moreArgs) {
      return LimClassFactory.ctors[className](state, ...moreArgs);
   }
}

/*___________________________________________________________________________*/
class LimNdDocument {
   #docId
   #currentLoopIndexes
   #currentLoopIndexesChanged
   #blockSetCurrentLoopIndexes
   #blockUpdateCurrentLoopIndexes
   #currentObjectSelection
   #currentObjectSelectionChanged
   #blockSetCurrentObjectSelection
   #blockUpdateCurrentObjectSelection
   #tableChanged
   #selectedRowVisibility
   #selectedRowVisibilityChanged

   constructor(docId) {
      this.#docId = docId;
      this.#currentLoopIndexes = null;
      this.#currentLoopIndexesChanged = new LimSignal();
      this.#currentObjectSelection = null;
      this.#currentObjectSelectionChanged = new LimSignal();
      this.#blockSetCurrentLoopIndexes = false;
      this.#blockUpdateCurrentLoopIndexes = false;
      this.#blockSetCurrentObjectSelection = false;
      this.#blockUpdateCurrentObjectSelection = false;
      this.#tableChanged = {};
      this.#selectedRowVisibility = null;
      this.#selectedRowVisibilityChanged = new LimSignal();
   }

   get docId() {
      return this.#docId;
   }

   set docId(value) {
      this.#docId = value;
   }

   get currentLoopIndexes() {
      return { ...this.#currentLoopIndexes };
   }

   set currentLoopIndexes(val) {
      if (this.#blockSetCurrentLoopIndexes || JSON.stringify(this.#currentLoopIndexes) === JSON.stringify(val) || typeof val === "undefined")
         return;
      this.#currentLoopIndexes = { ...val };
      this.#blockSetCurrentLoopIndexes = true;
      this.#currentLoopIndexesChanged.emit(this);
      this.#blockSetCurrentLoopIndexes = false;
      if (!this.#blockUpdateCurrentLoopIndexes)
         this.updateCurrentLoopIndexes();
   }

   get currentLoopIndexesChanged() {
      return this.#currentLoopIndexesChanged;
   }

   onCurrentLoopIndexesChanged(params) {
      this.#blockUpdateCurrentLoopIndexes = true;
      this.currentLoopIndexes = params?.loopIndexes
      this.#blockUpdateCurrentLoopIndexes = false;
   }

   updateCurrentLoopIndexes() {
      if (typeof this.#currentLoopIndexes === "object") {
         const requestUrl = new URL(`/api/v1/current_loop_indexes`, window.location.origin);
         requestUrl.searchParams.append("doc_id", this.#docId);
         for (let k of Object.getOwnPropertyNames(this.#currentLoopIndexes))
            requestUrl.searchParams.append(k, new String(this.#currentLoopIndexes[k]));
         fetch(requestUrl, { method: "PUT" });
      }
   }

   get currentObjectSelection() {
      return { ...this.#currentObjectSelection };
   }

   set currentObjectSelection(val) {
      if (this.#blockSetCurrentObjectSelection || JSON.stringify(this.#currentObjectSelection) === JSON.stringify(val) || typeof val === "undefined")
         return;
      this.#currentObjectSelection = { ...val };
      this.#blockSetCurrentObjectSelection = true;
      this.#currentObjectSelectionChanged.emit(this);
      this.#blockSetCurrentObjectSelection = false;
      if (!this.#blockUpdateCurrentObjectSelection)
         this.updateCurrentObjectSelection();
   }

   get currentObjectSelectionChanged() {
      return this.#currentObjectSelectionChanged;
   }

   onCurrentObjectSelectionChanged(params) {
      this.#blockUpdateCurrentObjectSelection = true;
      this.currentObjectSelection = params?.objectSelection;
      this.#blockUpdateCurrentObjectSelection = false;
   }

   updateCurrentObjectSelection() {
      const requestUrl = new URL(`/api/v1/current_object_selection`, window.location.origin);
      requestUrl.searchParams.append("doc_id", this.#docId);
      for (let k of Object.getOwnPropertyNames(this.#currentObjectSelection))
         requestUrl.searchParams.append(k, JSON.stringify(this.#currentObjectSelection[k]));
      fetch(requestUrl, { method: "PUT" });
   }

   highlightObject(binLayerName, binObjId) {
      const requestUrl = new URL(`/api/v1/highlight_object`, window.location.origin);
      requestUrl.searchParams.append("doc_id", this.#docId);
      requestUrl.searchParams.append('bin_layer_name', binLayerName);
      requestUrl.searchParams.append('bin_obj_id', `${binObjId}`);
      fetch(requestUrl, { method: "PUT" });
   }

   tableChangedConnect(name, callback) {
      if (this.#tableChanged?.[name])
         this.#tableChanged?.[name].connect(callback);
      else
         this.#tableChanged[name] = new LimSignal([callback]);
   }

   tableChangedDisconnect(name, callback) {
      return this.#tableChanged?.[name]?.disconnect?.(callback);
   }

   onTableChanged(params) {
      if (typeof params?.name === "string" && this.#tableChanged.hasOwnProperty(params.name))
         this.#tableChanged[params.name].emit(this);
   }

   get selectedRowVisibility() {
      return this.#selectedRowVisibility;
   }

   set selectedRowVisibility(val) {
      if (this.#selectedRowVisibility === val)
         return;
      this.#selectedRowVisibility = val;
      this.#selectedRowVisibilityChanged.emit(this);
   }

   get selectedRowVisibilityChanged() {
      return this.#selectedRowVisibilityChanged;
   }
}

/*___________________________________________________________________________*/
class LimWebClientDocument {
   #currentLoopIndexes
   #currentLoopIndexesChanged
   #blockSetCurrentLoopIndexes
   #blockUpdateCurrentLoopIndexes
   #currentObjectSelection
   #currentObjectSelectionChanged
   #blockSetCurrentObjectSelection
   #blockUpdateCurrentObjectSelection
   #tableChanged

   constructor() {
      this.#currentLoopIndexes = { w: 0, m: 0 };
      this.#currentLoopIndexesChanged = new LimSignal();
      this.#currentObjectSelection = [];
      this.#currentObjectSelectionChanged = new LimSignal();
      this.#blockSetCurrentLoopIndexes = false;
      this.#blockUpdateCurrentLoopIndexes = false;
      this.#blockSetCurrentObjectSelection = false;
      this.#blockUpdateCurrentObjectSelection = false;
      this.#tableChanged = {};
   }

   get currentLoopIndexes() {
      return { ...this.#currentLoopIndexes };
   }

   set currentLoopIndexes(val) {
      if (this.#blockSetCurrentLoopIndexes || JSON.stringify(this.#currentLoopIndexes) === JSON.stringify(val) || typeof val === "undefined")
         return;
      this.#currentLoopIndexes = { ...val };
      this.#blockSetCurrentLoopIndexes = true;
      this.#currentLoopIndexesChanged.emit(this);
      this.#blockSetCurrentLoopIndexes = false;
      if (!this.#blockUpdateCurrentLoopIndexes)
         this.updateCurrentLoopIndexes();
   }

   get currentLoopIndexesChanged() {
      return this.#currentLoopIndexesChanged;
   }

   onCurrentLoopIndexesChanged(params) {
      this.#blockUpdateCurrentLoopIndexes = true;
      this.currentLoopIndexes = params?.loopIndexes
      this.#blockUpdateCurrentLoopIndexes = false;
   }

   updateCurrentLoopIndexes() {
      window.top.postMessage(JSON.stringify({
          object: "currentDocument",
          signal: "currentLoopIndexesChanged",
          sender: document.URL,
          params: {
            loopIndexes: this.currentLoopIndexes,
          }
      }), '*');
   }

   get currentObjectSelection() {
      return { ...this.#currentObjectSelection };
   }

   set currentObjectSelection(val) {
      if (this.#blockSetCurrentObjectSelection || JSON.stringify(this.#currentObjectSelection) === JSON.stringify(val) || typeof val === "undefined")
         return;
      this.#currentObjectSelection = { ...val };
      this.#blockSetCurrentObjectSelection = true;
      this.#currentObjectSelectionChanged.emit(this);
      this.#blockSetCurrentObjectSelection = false;
      if (!this.#blockUpdateCurrentObjectSelection)
         this.updateCurrentObjectSelection();
   }

   get currentObjectSelectionChanged() {
      return this.#currentObjectSelectionChanged;
   }

   onCurrentObjectSelectionChanged(params) {
      this.#blockUpdateCurrentObjectSelection = true;
      this.currentObjectSelection = params?.objectSelection;
      this.#blockUpdateCurrentObjectSelection = false;
   }

   updateCurrentObjectSelection() {
   }

   highlightObject(binLayerName, binObjId) {
   }

   tableChangedConnect(name, callback) {
      if (this.#tableChanged?.[name])
         this.#tableChanged?.[name].connect(callback);
      else
         this.#tableChanged[name] = new LimSignal([callback]);
   }

   tableChangedDisconnect(name, callback) {
      return this.#tableChanged?.[name]?.disconnect?.(callback);
   }

   onTableChanged(params) {
      if (typeof params?.name === "string" && this.#tableChanged.hasOwnProperty(params.name))
         this.#tableChanged[params.name].emit(this);
   }
}

/*___________________________________________________________________________*/
class LimTableDataClient {
   #docId
   #paneId
   #tableName
   #tableData
   #currentLoopIndexes
   #onCurrentLoopIndexesChanged
   #currentObjectSelection
   #onCurrentObjectSelectionChanged
   #onTableChanged

   #tableRowVisibility
   #selectedRowVisibilityOptions
   #selectedRowVisibilityOptionsChanged
   #selectedRowVisibility
   #selectedRowVisibilityChanged
   #onSelectedRowVisibilityChanged
   #syncSelectedRowVisibility

   constructor(...args) {
      const [pars, docId, paneId, ...remainingArgs] = args;
      this.#docId = docId;
      this.#paneId = paneId;
      this.#tableData = null;
      this.#tableName = pars?._tableName ?? ""
      this.#onCurrentLoopIndexesChanged = that => {
         const loopPosChanged = {}
         for (let k of Object.getOwnPropertyNames(that.currentLoopIndexes))
            loopPosChanged[k] = that.currentLoopIndexes[k] != this.#currentLoopIndexes?.[k] ?? -1;
         this.#currentLoopIndexes = that.currentLoopIndexes;
         this?.onCurrentLoopIndexesChanged?.(this.#currentLoopIndexes, Object.entries(loopPosChanged).map(item => item[1] ? item[0] : null).filter(item => item));
      }
      this.#onCurrentObjectSelectionChanged = that => {
         this.currentObjectSelection = that.currentObjectSelection;
         this?.onCurrentObjectSelectionChanged?.(this.#currentObjectSelection);
      }
      this.#onTableChanged = that => this?.onTableChanged?.();

      this.#tableRowVisibility = pars?.tableRowVisibility ?? "all";
      const rowVisibility = [
         [ "currentFrame", "Current Frame" ],
         [ "currentPlate", "Current Plate" ],
         [ "currentWell", "Current Well" ],
         [ "currentPoint", "Current Point" ],
         [ "currentTime", "Current Time" ],
         [ "currentZ", "Current Z Slice" ],
         [ "currentFile", "Current File" ],
         [ "all", "All" ]
      ];
      this.#selectedRowVisibilityOptions = 0 < (pars?.tableRowSelectVisibility?.length ?? 0) ? new Map(rowVisibility.filter(item => pars.tableRowSelectVisibility.includes(item[0]))) : new Map();
      this.#selectedRowVisibility = pars?.tableRowVisibility ?? (this.#selectedRowVisibilityOptions.size ? this.#selectedRowVisibilityOptions.keys().next().value : null)
      this.#selectedRowVisibilityOptionsChanged = new LimSignal();
      this.#selectedRowVisibilityChanged = new LimSignal();
      this.#onSelectedRowVisibilityChanged = that => {
         if (typeof that.selectedRowVisibility === "string")
            this.selectedRowVisibility = that.selectedRowVisibility;
      }
      this.#syncSelectedRowVisibility = pars?.tableRowSyncVisibility ?? false;
   }

   connectToDocument() {
      this.#currentLoopIndexes = limCurrentDocument.currentLoopIndexes;
      limCurrentDocument.currentLoopIndexesChanged.connect(this.#onCurrentLoopIndexesChanged);
      this.#currentObjectSelection = limCurrentDocument.currentObjectSelection;
      limCurrentDocument.currentObjectSelectionChanged.connect(this.#onCurrentObjectSelectionChanged);
      if (this.#syncSelectedRowVisibility) {
         if (limCurrentDocument.selectedRowVisibility !== null)
            this.#selectedRowVisibility = limCurrentDocument.selectedRowVisibility;
         else if (this.#selectedRowVisibility !== null)
            limCurrentDocument.selectedRowVisibility = this.#selectedRowVisibility;
         limCurrentDocument.selectedRowVisibilityChanged.connect(this.#onSelectedRowVisibilityChanged);
      }
      limCurrentDocument.tableChangedConnect(this.#tableName, this.#onTableChanged);
   }

   disconnectFromDocument() {
      limCurrentDocument.currentLoopIndexesChanged.disconnect(this.#onCurrentLoopIndexesChanged);
      limCurrentDocument.currentObjectSelectionChanged.disconnect(this.#onCurrentObjectSelectionChanged);
      if (this.#syncSelectedRowVisibility) {
         limCurrentDocument.selectedRowVisibilityChanged.disconnect(this.#onSelectedRowVisibilityChanged);
      }
      limCurrentDocument.tableChangedDisconnect(this.#tableName, this.#onTableChanged);
   }

   get tableData() {
      return this.#tableData;
   }

   get tableRowVisiblity() {
      return this.#tableRowVisibility;
   }

   get selectedRowVisibility() {
      return this.#selectedRowVisibility;
   }

   set selectedRowVisibility(val) {
      if (this.#selectedRowVisibility === val)
         return;
      const oldVal = this.#selectedRowVisibility;
      this.#selectedRowVisibility = val;
      this?.onSelectedRowVisibilityChanged?.(this.#selectedRowVisibility, oldVal);
      this.#selectedRowVisibilityChanged.emit(this);
      limCurrentDocument.selectedRowVisibility = this.#selectedRowVisibility;
   }

   get selctedRowVisibilityCurrentLoops() {
      const loopsLookup = {
         "currentFrame": [ "f", "p", "w", "m", "t", "z" ],
         "currentPlate": [ "p" ],
         "currentWell": [ "w" ],
         "currentPoint": [ "m" ],
         "currentTime": [ "t" ],
         "currentZ": [ "z" ],
         "currentFile": [ "f" ],
         "all": []
      };

      const currentLoopList = loopsLookup[this.tableRowVisiblity];
      if (typeof this.selectedRowVisibility === "string") {
         for (let loop of loopsLookup[this.selectedRowVisibility]) {
            if (!currentLoopList.includes(loop))
               currentLoopList.push(loop);
         }
      }

      return currentLoopList;
   }

   isSelectedRowVisiblityIncludingAnyOfLoops(loops) {
      const sel = this.selctedRowVisibilityCurrentLoops;
      for (let loop of loops) {
         if (sel.includes(loop))
            return true;
      }
      return false;
   }

   async fetchTableMetadata(callback) {
      const requestUrl = new URL(`/api/v1/table_metadata`, window.location.origin);
      requestUrl.searchParams.append("pane_id", this.#paneId);
      requestUrl.searchParams.append("doc_id", this.#docId);
      requestUrl.searchParams.append("table_name", this.#tableName);
      const response = await fetch(requestUrl);
      const retJson = await response.json();
      this.#tableData = new LimTableData(retJson);
      callback?.();
   }

   async fetchTableData(params, callback) {
      const requestUrl = new URL(`/api/v1/table_data`, window.location.origin);
      requestUrl.searchParams.append("pane_id", this.#paneId);
      requestUrl.searchParams.append("doc_id", this.#docId);
      requestUrl.searchParams.append("table_name", this.#tableName);
      if (typeof params === "object" && params) {
         for (let key of Object.getOwnPropertyNames(params))
            requestUrl.searchParams.append(key, params[key]);
      }
      const response = await fetch(requestUrl);
      const retJson = await response.json();
      this.#tableData.setData(retJson);
      callback?.();
   }

   exportTableUrl(fmt, filename, cols, filter) {
      const requestUrl = new URL(`/api/v1/export_table`, window.location.origin);
      requestUrl.searchParams.append("pane_id", this.#paneId);
      requestUrl.searchParams.append("doc_id", this.#docId);
      requestUrl.searchParams.append("table_name", this.#tableName);
      requestUrl.searchParams.append("file_name", filename);
      requestUrl.searchParams.append("format", fmt);
      if (Array.isArray(filter) && filter.length)
         requestUrl.searchParams.append("filter", JSON.stringify({ op: "all", filters: filter }));
      if (Array.isArray(cols) && cols.length)
         requestUrl.searchParams.append("cols", JSON.stringify(cols));
      return requestUrl;
   }

   get currentLoopIndexes() {
      return { ... this.#currentLoopIndexes };
   }

   set currentLoopIndexes(val) {
      if (JSON.stringify(this.#currentLoopIndexes) === JSON.stringify(val))
         return;
      this.#currentLoopIndexes = { ...val };
      limCurrentDocument.currentLoopIndexes = this.#currentLoopIndexes;
   }

   get currentObjectSelection() {
      return { ...this.#currentObjectSelection };
   }

   set currentObjectSelection(val) {
      if (JSON.stringify(this.#currentObjectSelection) === JSON.stringify(val))
         return;
      this.#currentObjectSelection = { ...val };
      limCurrentDocument.currentObjectSelection = this.#currentObjectSelection;
   }

   makeDefaultRowFilterList() {
      // "all" = skip filtering
      // "currentFile" = only file is fixed to current
      // "currentTime" = file and time is fixed to current
      // "currentFrame" = everything is fixed to current
      let rowFilterList = [];
      const currentLoopList = this.selctedRowVisibilityCurrentLoops;
      if (0 < (currentLoopList?.length ?? 0) && this?.tableData) {
         const lookup = this.tableData.loopTypeToCloIdMap;
         for (let loopType of Object.getOwnPropertyNames(this.currentLoopIndexes)) {
            if (lookup.hasOwnProperty(loopType) && currentLoopList.includes(loopType)) {
               rowFilterList.push({ op: "eq", cola: lookup[loopType], valb: this.currentLoopIndexes[loopType] + 1 });
            }
         }
      }
      return rowFilterList;
   }

   get selectedRowVisibilityOptionValues() {
      return [...this.#selectedRowVisibilityOptions.entries()];
   }

   get selectedRowVisibilityOptionValuesChanged() {
      return this.#selectedRowVisibilityOptionsChanged;
   }

   get selectedRowVisibilityOptionValue() {
      return this.selectedRowVisibility;
   }

   set selectedRowVisibilityOptionValue(val) {
      this.selectedRowVisibility = val;
   }

   get selectedRowVisibilityOptionValueChanged() {
      return this.#selectedRowVisibilityChanged;
   }
}

/*___________________________________________________________________________*/
const LimTableDataClientExtender = (BASE) => class extends BASE {
   #tdc
   constructor() {
      super();
   }

   initialize(...args) {
      this.#tdc = new LimTableDataClient(...args);
      this.#tdc.onCurrentLoopIndexesChanged = (val, change) => {
         this?.onCurrentLoopIndexesChanged?.(val, change);
      }
      this.#tdc.onCurrentObjectSelectionChanged = (val) => {
         this?.onCurrentObjectSelectionChanged?.(val);
      }
      this.#tdc.onTableChanged = () => {
         this?.onTableChanged?.();
      }
      this.#tdc.onSelectedRowVisibilityChanged = (newVal, oldVal) => {
         this?.onSelectedRowVisibilityChanged?.(newVal, oldVal);
      }
   }

   connectToDocument() {
      this.#tdc.connectToDocument();
   }

   disconnectFromDocument() {
      this.#tdc.disconnectFromDocument();
   }

   get tableData() {
      return this.#tdc.tableData;
   }

   get tableRowVisiblity() {
      return this.#tdc.tableRowVisibility;
   }

   get selectedRowVisibility() {
      return this.#tdc.selectedRowVisibility;
   }

   set selectedRowVisibility(val) {
      this.#tdc.selectedRowVisibility = val;
   }

   get selctedRowVisibilityCurrentLoops() {
      return this.#tdc.selctedRowVisibilityCurrentLoops;
   }

   isSelectedRowVisiblityIncludingAnyOfLoops(loops) {
      return this.#tdc.isSelectedRowVisiblityIncludingAnyOfLoops(loops);
   }

   async fetchTableMetadata(callback) {
      this.#tdc.fetchTableMetadata(callback);
   }

   async fetchTableData(params, callback) {
      this.#tdc.fetchTableData(params, callback);
   }

   exportTableUrl(fmt, filename, cols, filter) {
      return this.#tdc.exportTableUrl(fmt, filename, cols, filter);
   }

   get currentLoopIndexes() {
      return this.#tdc.currentLoopIndexes;
   }

   set currentLoopIndexes(val) {
      this.#tdc.currentLoopIndexes = val;
   }

   get currentObjectSelection() {
      return this.#tdc.currentObjectSelection;
   }

   set currentObjectSelection(val) {
      this.#tdc.currentObjectSelection = val;
   }

   makeDefaultRowFilterList() {
      return this.#tdc.makeDefaultRowFilterList();
   }

   get selectedRowVisibilityOptionValues() {
      return this.#tdc.selectedRowVisibilityOptionValues;
   }

   get selectedRowVisibilityOptionValuesChanged() {
      return this.#tdc.selectedRowVisibilityOptionValuesChanged;
   }

   get selectedRowVisibilityOptionValue() {
      return this.#tdc.selectedRowVisibilityOptionValue;
   }

   set selectedRowVisibilityOptionValue(val) {
      this.#tdc.selectedRowVisibilityOptionValue = val;
   }

   get selectedRowVisibilityOptionValueChanged() {
      return this.#tdc.selectedRowVisibilityOptionValueChanged;
   }
};

function LimGetTextSize(txt, font) {
   if (typeof this._canvas_element === "undefined") {
      this._canvas_element = document.createElement('canvas');
      this._canvas_context = this._canvas_element.getContext("2d");
   }

   if (typeof font !== "undefined" && this._canvas_context.font != font) {
      this._canvas_context.font = font;
      this._font_height = parseInt(this._canvas_context.font);
   }

   return { 'width': this._canvas_context.measureText(txt).width, 'height': this._font_height };
}


/*___________________________________________________________________________*/
class LimToolbar extends HTMLElement {

   constructor() {
      super();
   }

   appendItem(val) {
      this.appendChild(val);
   }

   removeAllItems() {
      while (this.children.length)
         this.removeChild(this.children[0]);
   }

   removeItems(items) {
      for (let item of items)
         this.removeChild(item);
   }

   item(index) {
      return this.children.item(index);
   }

   connectedCallback() {
   }

   disconnectedCallback() {
   }

   adoptedCallback() {
   }

   static fixSvg(svg, w, h) {
      const domparser = new DOMParser();
      let doc = domparser.parseFromString(svg, "image/svg+xml");
      if (typeof w !== "undefined")
         doc.documentElement.setAttribute("width", w);
      if (typeof h !== "undefined")
         doc.documentElement.setAttribute("height", h);
      for (let e of doc.documentElement.getElementsByTagName("*")) {
         if (e.attributes.fill && e.attributes.fill.value != "none") {
            e.removeAttribute("fill");
            e.classList.add("filled");
         }
         if (e.attributes.stroke && e.attributes.stroke.value != "none") {
            e.removeAttribute("stroke");
            e.classList.add("outlined");
         }
      }
      return doc.documentElement;
   };
}

customElements.define('lim-toolbar', LimToolbar);

/*___________________________________________________________________________*/
const LimAddDropdownMenu = (element) => {

   element.createDropDownMenu = function() {
      const thisBox = this.getBoundingClientRect();
      this.dropDownMenu = document.createElement("div");
      this.dropDownMenu.id = this.id ? (this.id + "-dropdown-menu") : undefined;
      this.dropDownMenu.className = "lim-dropdown-menu";
      this.dropDownMenu.style.top = `${thisBox.bottom + 2}px`;
      this.dropDownMenu.style.left = `${Math.max(2, thisBox.left)}px`;

      this.classList.add("lim-hovered");
      this.dropdownAbortController = new AbortController();

      addEventListener("focusout", (e) => {
         if (!this.contains(e.relatedTarget)) {
             e.stopImmediatePropagation();
             e.preventDefault();
             this.destroyDropDownMenu();
         }
      }, { signal: this.dropdownAbortController.signal } );

      addEventListener("mousedown", (e) => {
         if (!this.contains(e.target)) {
             e.stopImmediatePropagation();
             e.preventDefault();
             this.destroyDropDownMenu();
         }
      }, { capture: true, signal: this.dropdownAbortController.signal } );

      addEventListener("keydown", (e) => {
         if (e.key == "Escape") {
             e.stopImmediatePropagation();
             e.preventDefault();
             this.destroyDropDownMenu();
         }
      }, { capture: true, signal: this.dropdownAbortController.signal } );

      const items = typeof this.items === "function" ? this.items() : this.items;
      if (!Array.isArray(items))
         return;

      let focusElement = null;
      for (let i = 0; i < items.length; i++) {
         let el = null;
         const item = items[i];
         if (item?.hidden)
            continue;
         if (item?.type === "separator") {
            if (0 === this.dropDownMenu.childElementCount || this.dropDownMenu.lastElementChild.tagName === "HR")
               continue;
            el = document.createElement("hr");
         }
         else {
            el = document.createElement("button");
            if (!(item?.enabled ?? true))
               el.disabled = true;
            el.innerHTML = item.text;
            el.onclick = (e) => {
               item.trigger();
               e.stopImmediatePropagation();
               this.destroyDropDownMenu();
            }
            el.onmouseover = (e) => {
               e.target.focus();
            }
            if (!focusElement && !el.disabled)
               focusElement = el;
         }
         this.dropDownMenu.appendChild(el);
      }

      if (this.dropDownMenu.lastElementChild.tagName === "HR")
         this.dropDownMenu.removeChild(this.dropDownMenu.lastElementChild);

      this.appendChild(this.dropDownMenu);
      focusElement?.focus?.();
   }
   element.createDropDownMenu.bind(element);

   element.destroyDropDownMenu = function() {
      if (!this.dropDownMenu)
         return;
      this.dropdownAbortController.abort();
      this.removeChild(this.dropDownMenu);
      this.classList.remove("lim-hovered");
      this.dropDownMenu = null;
      this.focus();
   }
   element.destroyDropDownMenu.bind(element);

   element.onclick = (e) => {
      if (!element.dropDownMenu) {
         element.createDropDownMenu();
      }
      else {
         const thisBox = element.getBoundingClientRect();
         if (e.target == element || (thisBox.left < e.clientX && e.clientX <thisBox.right && thisBox.top < e.clientY && e.clientY < thisBox.bottom))
            element.destroyDropDownMenu();
      }
   }
}

/*___________________________________________________________________________*/
const LimCreateContextMenu = (parent, x, y, items) => {
   if (!Array.isArray(items) || !items.length)
       return;

   const context = document.createElement("div");
   context.className = "lim-dropdown-menu";
   context.style.position = "fixed";

   context.dropdownAbortController = new AbortController();

   addEventListener("focusout", (e) => {
       if (!context.contains(e.relatedTarget)) {
           e.stopImmediatePropagation();
           e.preventDefault();
           context?.destroyDropDownMenu?.();
       }
   }, { signal: context.dropdownAbortController.signal });

   addEventListener("mousedown", (e) => {
       if (!context.contains(e.target)) {
           e.stopImmediatePropagation();
           e.preventDefault();
           context?.destroyDropDownMenu?.();
       }
   }, { capture: true, signal: context.dropdownAbortController.signal });

   addEventListener("keydown", (e) => {
       if (e.key == "Escape") {
           e.stopImmediatePropagation();
           e.preventDefault();
           context?.destroyDropDownMenu?.();
       }
   }, { capture: true, signal: context.dropdownAbortController.signal });

   let focusElement = null;
   for (let i = 0; i < items.length; i++) {
       let el = null;
       const item = items[i];
       if (item?.hidden)
           continue;
       if (item?.type === "separator") {
           if (0 === context.childElementCount || context.lastElementChild.tagName === "HR")
               continue;
           el = document.createElement("hr");
       }
       else {
           el = document.createElement("button");
           if (!(item?.enabled ?? true))
               el.disabled = true;
           el.innerHTML = item.text;
           el.onclick = (e) => {
               item.trigger();
               e.stopImmediatePropagation();
               context?.destroyDropDownMenu?.();
           }
           el.onmouseover = (e) => {
               e.target.focus();
           }
           if (!focusElement && !el.disabled)
               focusElement = el;
       }
       context.appendChild(el);
   }
   if (context.lastElementChild.tagName === "HR")
       context.removeChild(context.lastElementChild);

   focusElement?.focus?.();

   context.destroyDropDownMenu = () => {
       context.destroyDropDownMenu = null;
       context.dropdownAbortController.abort();
       parent.removeChild(context);
   }

   const viewportWidth = window.innerWidth;
   const viewportHeight = window.innerHeight;

   parent.appendChild(context);

   const menuWidth = context.offsetWidth;
   const menuHeight = context.offsetHeight;

   if (x + menuWidth > viewportWidth) {
       x = viewportWidth - menuWidth;
   }
   if (y + menuHeight > viewportHeight) {
       y = viewportHeight - menuHeight;
   }

   context.style.left = `${x}px`;
   context.style.top = `${y}px`;
}

/*___________________________________________________________________________*/
const LimCreateMessageBox = (parent, text, caption, buttons, callback) => {

   const background = document.createElement("div");
   background.className = "lim-popup-background";

   const messageBox = document.createElement("div");
   messageBox.className = "lim-pane lim-messagebox";

   const titleBar = document.createElement("div");
   titleBar.className = "lim-fill-width lim-titlebar lim-titlebar-active";
   titleBar.innerText = caption;
   messageBox.appendChild(titleBar);

   const content = document.createElement("div");
   content.className = "lim-pane lim-fill-width lim-fill-height";
   messageBox.appendChild(content);

   const textarea = document.createElement("div");
   textarea.innerHTML = text;
   textarea.className = "lim-fill-width";
   textarea.style.padding = "6px 12px";
   content.appendChild(textarea);

   const onclick = function(button) {
       parent.removeChild(background);
       callback?.(button)
   };

   buttons = buttons.toLowerCase();

   const buttonbar = document.createElement("div");
   buttonbar.className = "lim-buttonbar";

   if (buttons.includes("ok")) {
       const button = document.createElement("button");
       button.innerText = "Ok";
       button.onclick = onclick.bind(null, "ok");
       buttonbar.appendChild(button);
   }

   if (buttons.includes("yes")) {
       const button = document.createElement("button");
       button.innerText = "Yes";
       button.onclick = onclick.bind(null, "yes");
       buttonbar.appendChild(button);
   }

   if (buttons.includes("no")) {
       const button = document.createElement("button");
       button.innerText = "No";
       button.onclick = onclick.bind(null, "no");
       buttonbar.appendChild(button);
   }

   if (buttons.includes("cancel")) {
       const button = document.createElement("button");
       button.innerText = "Cancel";
       button.onclick = onclick.bind(null, "cancel");
       buttonbar.appendChild(button);
   }

   if (0 === buttonbar.children.length) {
       const button = document.createElement("button");
       button.innerText = "Ok";
       button.onclick = onclick.bind(null, "ok");
       buttonbar.appendChild(button);
   }

   content.appendChild(buttonbar);

   background.appendChild(messageBox);
   parent.appendChild(background);
}

/*___________________________________________________________________________*/
class LimDropdown extends HTMLElement {
   #options
   #iconres
   #textValue
   #icon
   #text
   #arrow
   #dropdown
   #container

   constructor() {
      super();
   }

   static observedAttributes = [ "iconres", "items", "options", "pressed", "text", "type" ];

   get iconres() {
      return this.#iconres;
   }

   set iconres(val) {
      if (this.#iconres === val)
         return;

      this.#iconres = val;

      if (!this.#container)
         return;

      if (this.#iconres) {
         fetch(this.#iconres)
         .then((response) => response.text())
         .then((text) => {
            while (this.#icon.children.length)
               this.#icon.removeChild(this.children[0]);
            this.#icon.appendChild(LimToolbar.fixSvg(text, "24px", "24px"));
            this.classList.add("has-icon");
         });
      }
      else  {
         while (this.#icon.children.length)
            this.#icon.removeChild(this.children[0]);
         this.classList.remove("has-icon");
      }
   }

   connectedCallback() {
      this.classList.add("lim-dropdown");

      if (this.#container)
         return;

      this.#container = document.createElement("span");
      this.#container.className = "container";
      this.appendChild(this.#container);

      this.#icon = document.createElement("span");
      this.#icon.className = "icon";
      this.#container.appendChild(this.#icon);

      if (this.#iconres) {
         fetch(this.#iconres)
         .then((response) => response.text())
         .then((text) => {
            while (this.#icon.children.length)
               this.#icon.removeChild(this.children[0]);
            this.#icon.appendChild(LimToolbar.fixSvg(text, "24px", "24px"));
            this.classList.add("has-icon");
         });
      }

      this.#text = document.createElement("span");
      this.#text.className = "text";
      this.#text.innerHTML =  this.#textValue;
      this.#container.appendChild(this.#text);

      this.#arrow = document.createElement("span");
      this.#arrow.className = "arrow";
      this.#arrow.innerHTML = "&#9698";
      this.#container.appendChild(this.#arrow);

      this.#dropdown = null;

      this.onclick = (e) => {
         if (this.#dropdown === null) {
            this.createMenu();
         }
         else {
            this.deleteMenu();
         }
      }
      this.addEventListener("keydown", (e) => {
         if (e.key === " " && this.#dropdown === null) {
            this.createMenu();
            e.stopPropagation();
         }
      });
   }

   disconnectedCallback() {
      if (!this.#container)
         return;

      this.removeChild(this.#container);
      this.#container = null;
      this.onclick = null;
   }

   adoptedCallback() {
   }

   attributeChangedCallback(name, oldValue, newValue) {
      if (name === "type"){
         this.type = newValue;
      }
      if (name === "pressed"){
          this.pressed = newValue === "true";
      }
      if (name === "options"){
         this.options = newValue;
      }
      if (name === "items"){
         this.items = newValue;
      }
      if (name === "iconres"){
         this.iconres = newValue;
      }
   }

   createMenu() {
      const thisBox = this.getBoundingClientRect();
      let focusedIndex = Math.max(0, this.selectedIndex);
      this.#dropdown = document.createElement("div");
      this.#dropdown.className = "lim-dropdown-menu";
      this.#dropdown.style.top = `${thisBox.bottom}px`;
      if (thisBox.left < 3*visualViewport.width/4)
         this.#dropdown.style.left = `${thisBox.left}px`;
      else {
         this.#dropdown.style.left = "unset";
         this.#dropdown.style.right = `${visualViewport.width - thisBox.right}px`;
      }
      this.#dropdown.style["min-width"] = `${thisBox.right-thisBox.left}px`;
      if (this.type === 'select-multiple') {
         for (let i = 0; i < this.options.length; i++) {
            const opt = this.options[i];
            const el = document.createElement("label");
            const ch = document.createElement("input");
            ch.type = "checkbox";
            ch.checked = opt.selected;
            ch.tabIndex = -1;
            el.tabIndex = 0;
            el.className = "item";
            el.appendChild(ch);
            el.appendChild(document.createTextNode(opt?.text ?? opt?.value));
            el.onclick = (e) => {
               ch.checked = this.options[i].selected = !this.options[i].selected;
               this.updateText();
               this.onchange(e, this);
               e.stopPropagation();
            }
            el.addEventListener("mouseover", (e) => {
               el.focus();
            });
            el.addEventListener("blur", (e) => {
               if (this.#dropdown && !this.contains(e.relatedTarget)) {
                  this.deleteMenu();
               }
            });
            el.addEventListener("keydown", (e) => {
               if (e.key === " ") {
                  ch.checked = this.options[i].selected = !this.options[i].selected;
                  this.updateText();
                  this.onchange(e, this);
               }
               else if (e.key === "ArrowDown") {
                  focusedIndex = Math.max(0, Math.min(focusedIndex + 1, this.options.length - 1));
                  this.#dropdown.childNodes.item(focusedIndex).focus();
               }
               else if (e.key === "ArrowUp") {
                  focusedIndex = Math.max(0, Math.min(focusedIndex - 1, this.options.length - 1));
                  this.#dropdown.childNodes.item(focusedIndex).focus();
               }
               else if (e.key === "Escape") {
                  this.deleteMenu();
               }
               else if (e.key === "Enter") {
                  this.deleteMenu();
               }
               e.stopPropagation();
            });
            this.#dropdown.appendChild(el);
         }
      }
      else {
         for (let i = 0; i < this.options.length; i++) {
            const opt = this.options[i];
            const el = document.createElement("a");
            el.tabIndex = 0;
            el.className = "item";
            el.innerHTML = opt?.text ?? opt?.value;
            if (opt?.colorChip) {
               const chip = `<span class="color-chip" style="background-color: ${opt?.colorChip};"></span>`
               el.innerHTML = chip + (opt?.text ?? opt?.value);
            }
            else {
               el.innerHTML = opt?.text ?? opt?.value;
            }
            el.onclick = (e) => {
               this.selectedIndex = i;
               this.onchange(e, this);
               this.deleteMenu();
               e.stopPropagation();
            }
            el.addEventListener("mouseover", (e) => {
               e.target.focus();
            });
            el.addEventListener("blur", (e) => {
               if (this.#dropdown && !this.contains(e.relatedTarget)) {
                  this.deleteMenu();
               }
            });
            el.addEventListener("keydown", (e) => {
               if (e.key === "ArrowDown") {
                  focusedIndex = Math.max(0, Math.min(focusedIndex + 1, this.options.length - 1));
                  this.#dropdown.childNodes.item(focusedIndex).focus();
               }
               else if (e.key === "ArrowUp") {
                  focusedIndex = Math.max(0, Math.min(focusedIndex - 1, this.options.length - 1));
                  this.#dropdown.childNodes.item(focusedIndex).focus();
               }
               else if (e.key === "Escape") {
                  this.deleteMenu();
               }
               else if (e.key === "Enter") {
                  this.selectedIndex = focusedIndex;
                  this.onchange(e, this);
                  this.deleteMenu();
               }
               e.stopPropagation();
            });
            this.#dropdown.appendChild(el);
         }
      }
      this.appendChild(this.#dropdown);
      this.#dropdown.childNodes.item(focusedIndex)?.focus?.();
   }

   deleteMenu() {
      if (this.#dropdown) {
         const dd = this.#dropdown;
         this.#dropdown = null;
         dd.remove();
         this.focus();
      }
   }

   updateText() {
      if (this.type === 'select-multiple') {
         const sel = this.options.filter(item => item.selected).map(item => item.text)
         this.#textValue = 0 === sel.length ? "<i>(0) none</i>"
            : 1 === sel.length ? `(1) ${sel[0]}`
            : `(${sel.length}) ${sel.join(", ")}`;
      }
      else {
         const selectedText = this.options.find(item => item.selected === true);
         this.#textValue = selectedText?.text ?? selectedText?.value ?? "";
      }

      if (this.#text)
         this.#text.innerHTML = this.#textValue;
   }

   get options() {
      return this.#options;
   }

   set options(val) {
      if (JSON.stringify(this.#options) === JSON.stringify(val))
         return;

      this.#options = val;

      let maxWidth = 30;
      for (let i = 0; i < this.options.length; i++) {
         const opt = this.options[i];
         const tsize = LimGetTextSize(opt?.text ?? opt?.value, "11px Tahoma");
         if (maxWidth < tsize.width)
            maxWidth = tsize.width;
      }
      const mult = this.type === 'select-multiple' ? 1.6 : 1.1;
      this.style["max-width"] = `${mult * ((this.#iconres ? 24 : 0) + maxWidth + 10 + 20)}px`

      this.updateText();
   }

   get value() {
      return this.type === 'select-multiple'
         ? this.#options.filter(item => item.selected === true)?.map(item => item.value) ?? []
         : this.#options.find(item => item.selected === true)?.value;
   }

   set value(val) {
      if (!Array.isArray(this.options))
         return;

      this.options = this.type === 'select-multiple' && Array.isArray(val)
         ? this.options.map(item => ({ ...item, selected: val.includes(item.value) }))
         : this.options.map(item => ({ ...item, selected: item.value === val }));
   }

   get selectedIndex() {
      return this.#options.findIndex(item => item.selected === true);
   }

   set selectedIndex(val) {
      if (this.selectedIndex === val)
         return;

      this.options = this.options.map((item, i) => ({ ...item, selected: i===val }));
   }
}
customElements.define('lim-dropdown', LimDropdown);

/*___________________________________________________________________________*/
class LimToolbarSwitch extends HTMLElement {
   #checked
   #onclick
   #text
   #textElement
   #rootLabel
   #checkbox
   #span

   static observedAttributes = [ "checked", "onclick", "text" ];

   constructor() {
      super();
   }

   connectedCallback() {
      //this.classList.add("lim-toolbar-switch");
      if (!this.#rootLabel) {
         this.style.height = "40px";
         this.style.marginLeft = "4px";
         this.style.marginRight = "4px";

         this.#textElement = document.createElement("span");
         this.#textElement.innerText = this.#text
         this.#textElement.style.alignSelf = "center";
         this.#textElement.style.marginRight = "8px";
         this.#textElement.style.userSelect = "none";
         this.appendChild(this.#textElement);

         this.#rootLabel = document.createElement("label");
         this.#rootLabel.className = "lim-switch";
         this.#rootLabel.style.alignSelf = "center";

         this.#checkbox = document.createElement("input");
         this.#checkbox.id = (this?.id ?? Math.random().toString(16)) + "-input";
         this.#checkbox.type = "checkbox";
         this.#checkbox.checked = this.#checked;
         this.#checkbox.onclick = this.#onclick;
         this.#rootLabel.appendChild(this.#checkbox);

         this.#span = document.createElement("span");
         this.#span.className = "lim-slider round";
         this.#rootLabel.appendChild(this.#span);

         this.appendChild(this.#rootLabel);
      }
   }

   disconnectedCallback() {
      if (this.#rootLabel) {
          this.removeChild(this.#rootLabel);
          this.#rootLabel = null;
          this.#checkbox = null;
          this.#span = null;
      }
   }

   adoptedCallback() {
   }

   attributeChangedCallback(name, oldValue, newValue) {
      if (name === "checked") {
         this.checked = newValue === "true";
      }
      else if (name === "onclick") {
         this.onclick = newValue;
      }
      else if (name === "text") {
         this.text = newValue;
      }
   }


   get checked() {
      this.#checked = this.#checkbox?.checked;
      return this.#checked;
   }

   set checked(val) {
      this.#checked = val;
      if (this.#checkbox)
         this.#checkbox.checked = this.#checked;
   }

   get onclick() {
      return this.#onclick;
   }

   set onclick(val) {
      this.#onclick = val;
      if (this.#checkbox)
         this.#checkbox.onclick = this.#onclick;
   }

   get text() {
      return this.#text;
   }

   set text(val) {
      this.#text = val;
      if (this.#textElement)
         this.#textElement.innerText = this.#text;
   }
}
customElements.define('lim-toolbar-switch', LimToolbarSwitch);


/*___________________________________________________________________________*/
/*___________________________________________________________________________*/
/*___________________________________________________________________________*/

/**
 * @param {String} HTML representing a single node (which might be an Element,
                   a text node, or a comment).
 * @return {Node}
 */
function LimHtmlToNode(html) {
const template = document.createElement('template');
template.innerHTML = html;
const nNodes = template.content.childNodes.length;
if (nNodes !== 1) {
      throw new Error(
         `html parameter must represent a single node; got ${nNodes}. ` +
         'Note that leading or trailing spaces around an element in your ' +
         'HTML, like " <img/> ", get parsed as text nodes neighbouring ' +
         'the element; call .trim() on your input to avoid this.'
      );
}
return template.content.firstChild;
}

/**
* @param {String} HTML representing any number of sibling nodes
* @return {NodeList}
*/
function LimHtmlToNodes(html) {
   const template = document.createElement('template');
   template.innerHTML = html;
   return template.content.childNodes;
}

function LimParseHtmlRGBA(htmlColor) {
   if (htmlColor.startsWith("#")) {
      htmlColor = htmlColor.slice(1);
   }
   if (3 === htmlColor.length) {
      return [ parseInt(htmlColor[0], 16) * 255 / 15, parseInt(htmlColor[1], 16) * 255 / 15, parseInt(htmlColor[2], 16) * 255 / 15, 255 ];
   }
   else if (4 === htmlColor.length) {
      return [ parseInt(htmlColor[0], 16) * 255 / 15, parseInt(htmlColor[1], 16) * 255 / 15, parseInt(htmlColor[2], 16) * 255 / 15, , parseInt(htmlColor[3], 16) * 255 / 15 ];
   }
   else if (6 === htmlColor.length) {
      return [ parseInt(htmlColor.slice(0, 2), 16), parseInt(htmlColor.slice(2, 4), 16), parseInt(htmlColor.slice(4, 6), 16), 255 ];
   }
   else if (8 === htmlColor.length) {
      return [ parseInt(htmlColor.slice(0, 2), 16), parseInt(htmlColor.slice(2, 4), 16), parseInt(htmlColor.slice(4, 6), 16), parseInt(htmlColor.slice(6, 8), 16) ];
   }
}
