
/*___________________________________________________________________________*/
class LimSummaryTable extends LimTableDataClientExtender(HTMLElement) {
   #table
   #headerCommonSuffix
   #shownColumns
   #hiddenColumns
   #sections

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);
      if (pars?.title)
         this.title = pars.title;
      this.name = pars?.name || "Summary";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/info.svg";
      this.#headerCommonSuffix = pars?.headerCommonSuffix;
      this.#shownColumns = pars?.shownColumns;
      this.#hiddenColumns = pars?.hiddenColumns;
      this.#sections = pars?.sections ?? [];

      this.makeFeatureMap = (allowed, forbidden, filterFn = undefined) => {
         const td = this.tableData;
         const map = new Map();
         if (td) {
            for (let i = 0; i < td.colCount; i++) {
               const meta = td.colMetadataAt(i);
               if (filterFn && !filterFn(meta))
                  continue;

               if (allowed.includes(i) ? true : (forbidden.includes(i) ? false : !(meta?.hidden ?? false))) {
                  map.set(td.colIdAt(i), td.colTitleAt(i));
               }
            }
         }
         return map;
      }

      this.optionList = new Map();
      if (pars?.tableRowSelectVisibility?.length ?? false) {
         this.optionList.set("selectedRowVisibility", {
            type: "selection",
            title: "Display Data",
         });
      }
      this.optionList.set("copy", {
         type: "action",
         title: "Copy to clipboard",
         text: "Copy to clipboard",
         iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/clipboard.svg"
      });
      this.copyOptionTrigger = () => {
         this.copyToClipboard();
      }
   }

   connectedCallback() {
      this.#table = document.createElement("table");
      this.#table.className = "lim-table lim-summary-table lim-results-tableview";
      this.appendChild(this.#table);
      this.connectToDocument();
      this.fetchAndUpdateTable();
   }

   disconnectedCallback() {
      this.removeChild(this.#table);
      this.disconnectFromDocument();
   }

   onTableChanged() {
      this.fetchAndUpdateTable();
   }

   adoptedCallback() {
   }

   attributeChangedCallback(name, oldValue, newValue) {
   }

   get rowFilterList() {
      return this.makeDefaultRowFilterList();
   }

   fetchAndUpdateTable() {
      this.fetchTableMetadata(() => {
         let pars = {};
         const rowFilterList = this.rowFilterList;
         if (rowFilterList.length)
            pars.filter = JSON.stringify({ op: "all", filters: rowFilterList });
         this.fetchTableData(pars, () => {
            this.update();
         });
      });
   }

   onSelectedRowVisibilityChanged(val, oldVal) {
      if (!this?.tableData)
         return;
      this.fetchAndUpdateTable();
   }

   update() {
      if (!this.tableData)
         return;

      this.#table.innerText = "";
      const td = this.tableData;
      const columnIds = [...this.makeFeatureMap(
         this.#shownColumns ? td.matchColsFulltext(this.#shownColumns) : [],
         this.#hiddenColumns ? td.matchColsFulltext(this.#hiddenColumns) : []
      ).keys()];

      const dataColCount = Math.max(...columnIds.map(id => td.colData(id).filter(item => item !== null).length));
      const tbody = document.createElement("tbody");
      for (let id of columnIds) {
         const meta = td.colMetadata(id);
         const data = td.colData(id).filter(item => item !== null);
         const fmtValue = LimTableData.makePrintFunction(meta);

         if (0 < tbody.rows.length && (this.#sections.includes(id) || this.#sections.includes(meta.title))) {
            let row = tbody.insertRow();
            row.classList.add("separator");
            let h = row.insertCell(-1);
            let d = row.insertCell(-1);
            if (1 < dataColCount)
               d.colSpan = dataColCount;
         }

         let row = tbody.insertRow();
         let h = row.insertCell(-1);
         h.innerText = meta?.title + (this.#headerCommonSuffix ? this.#headerCommonSuffix : "");

         if (1 < data.length) {
            for (let i = 0; i < dataColCount;) {
               let j = i + 1;
               while (j < dataColCount && data[i] == data[j]) {
                  j++
               }
               let d = row.insertCell(-1);
               if (i < data.length) {
                  d.innerText = fmtValue(data[i]);
                  d.colSpan = j - i;
               }
               i = j;
            }
         }
         else {
            let d = row.insertCell(-1);
            d.innerText = fmtValue(data[0]);
            if (1 < dataColCount)
               d.colSpan = dataColCount;
         }
      }

      this.#table.appendChild(tbody);
   }

   copyToClipboard() {
      let out = "";
      for (let r of this.#table.rows) {
         const N = r.cells.length;
         for (let i = 0; i < N; i++) {
            out += `${r.cells[i].innerText}`;
            if (i < N - 1)
               out += '\t';
         }
         out += '\r\n';
      }
      window.navigator.clipboard.writeText(out);
   }
}

/*___________________________________________________________________________*/
class LimDataGridHtmlTooltip {
   init(params) {
      const eGui = (this.eGui = document.createElement('div'));
      const color = params.color || 'var(--color-base)';
      const size = Math.min(params?.imageSizeX ?? 100, params?.imageSizeY ?? 100);

      eGui.classList.add('custom-tooltip');
      eGui.style['background-color'] = color;
      eGui.style['border'] = `1px solid var(--color-text)`;

      try {
         const parser = new DOMParser();
         const obj = JSON.parse(params.value);
         if (obj) {
            const doc = parser.parseFromString(obj.data, "text/html");
            const errorNode = doc.querySelector('parsererror');
            if (errorNode) {
               cell.innerHTML = `<span class="lim-error">Error</span>`;
            }
            else {
               const overlay = doc.querySelector('.lim-image-overlay');
               if (overlay) {
                  overlay.classList.add('lim-position-relative')
                  overlay.style.width = `${size}px`;
                  overlay.style.height = `${size}px`;
                  for (let col of doc.querySelectorAll('.img-color')) {
                     col.style.width = `${size}px`;
                     col.style.height = `${size}px`;
                     col.style.objectFit = 'fill';
                  }
                  for (let bin of doc.querySelectorAll('.img-binary')) {
                     bin.classList.add('lim-position-absolute');
                     bin.classList.add('lim-position-top');
                     bin.classList.add('lim-position-left');
                  }
                  eGui.appendChild(overlay);
               }
            }
         }
      }
      catch (e) {
         console.log(e);
         eGui.innerHTML = `<span class="lim-error">Error</span>`;
      }
   }

   getGui() {
      return this.eGui;
   }
}

/*___________________________________________________________________________*/
class LimDataGridImageTooltip {
   init(params) {
      const eGui = (this.eGui = document.createElement('div'));
      const color = params.color || 'var(--color-base)';
      const { sizeX, sizeY } = params;

      eGui.classList.add('custom-tooltip');
      eGui.style['background-color'] = color;
      eGui.style['border'] = `1px solid var(--color-text)`;

      try {
         const obj = JSON.parse(params.value);
         if (obj) {
            eGui.innerHTML = cell.innerHTML = `<img style="width:${sizeX}px;height:${sizeY}px;" src="data:${obj.data.type},${obj.data.base64}"/>`;
         }
      }
      catch (e) {
         console.log(e);
         eGui.innerHTML = `<pre class="lim-error">Error</pre>`;
      }
   }

   getGui() {
      return this.eGui;
   }
}

/*___________________________________________________________________________*/
class LimDataGridMultiImageTooltip {
   init(params) {
      const eGui = (this.eGui = document.createElement('div'));
      const color = params.color || 'var(--color-base)';
      const { sizeX, sizeY, allChannels, binLayers } = params;

      eGui.classList.add('custom-tooltip');
      eGui.style['background-color'] = color;
      eGui.style['border'] = `1px solid var(--color-text)`;

      try {
         const obj = JSON.parse(params.value);
         if (obj && Array.isArray(obj.data) && 0 <= allChannels) {
            const color = `<img class="lim-image-color" style="width:${sizeX}px;height:${sizeY}px;" src="data:${obj.data[allChannels].data.type},${obj.data[allChannels].data.base64}"/>`;
            const binaries = binLayers.length ? binLayers.map(item => obj.data[item].data).reduce((p, c) => p + c) : "";
            eGui.innerHTML = `<div class="lim-multi-image selenabled" style="width:${sizeX}px;height:${sizeY}px;">${color}${binaries}</div>`;
         }
      }
      catch (e) {
         console.log(e);
         eGui.innerHTML = `<pre class="lim-error">Error</pre>`;
      }
   }

   getGui() {
      return this.eGui;
   }
}

/*___________________________________________________________________________*/
class LimDataGridFitTooltip {
   init(params) {
      const eGui = (this.eGui = document.createElement('div'));
      const color = params.color || 'var(--color-base)';

      eGui.classList.add('fit-tooltip');
      const fmt = (number, digits = 3) => {
         return typeof number !== "number" || isNaN(number) ? "n/a" :
            number < -10e21 ? "-inf" :
               10e21 < number ? "+inf" :
                  number.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits });
      }

      try {
         const parser = new DOMParser();
         const obj = JSON.parse(params.value);
         eGui.innerHTML = `<span class="lim-error">Error</span>`;
         if (obj) {
            const doc = parser.parseFromString(obj.data, "text/html");
            const errorNode = doc.querySelector('parsererror');
            if (!errorNode) {
               const rowN = [];
               const rowH = params.metaFit.paramNames.map(item => `<td class="text-right">${item}</td>`).join("");
               const row0 = obj.data.paramValues.map(item => item.toFixed(3)).map(item => `<td class="text-right">${item}</td>`).join("");
               for (let i = 0; i < params.metaFit.paramErrorNames.length; i++) {
                  const row = obj.data.paramErrorValues.map(column => column[i]).map(item => fmt(item, 3)).map(item => `<td class="text-right">${item}</td>`).join("");
                  rowN.push(`<tr><th class="text-left">${params.metaFit.paramErrorNames[i]}</th>${row}</tr>`);
               }

               const goodness = [];
               for (let i = 0; i < params.metaFit.fitGoodnessNames.length; i++) {
                  goodness.push(`<tr><th class="text-left">${params.metaFit.fitGoodnessNames[i]}</th><td class="text-right">${fmt(obj.data?.fitGoodnessValues?.[i], 4)}</td></tr>`);
               }

               eGui.innerHTML = `<div class="align-center">${params.metaFit.fnSvg}</div>
                  <table><tr class="bb"><th class="text-left">Fit parameters</th>${rowH}</tr><tr><th class="text-left">Best-fit value</th>${row0}</tr>${rowN.join("\n")}</table>
                  <table>${goodness.join("\n")} </table>`;
            }
         }
      }
      catch (e) {
         console.log(e);
      }
   }

   getGui() {
      return this.eGui;
   }
}

/*___________________________________________________________________________*/
class LimDataGrid extends LimTableDataClientExtender(HTMLElement) {
   #showRowIndexCol
   #enableColSorting
   #enableColResizing
   #enableColMoving
   #enableColFiltering
   #statistics
   #statisticsVisible
   #dataSelectionEnabled
   #shownColumns
   #hiddenColumns

   #grid
   #gridOptions

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);
      const r = document.querySelector(":root");
      const colorScheme = getComputedStyle(r).getPropertyValue("--color-scheme").trim();
      const schemes = { light: "ag-theme-alpine", dark: "ag-theme-alpine-dark" };

      this.className = schemes?.[colorScheme] ?? "ag-theme-alpine";

      this.#showRowIndexCol = pars?.showRowIndexCol ?? true;
      this.#enableColSorting = pars?.enableColSorting ?? true;
      this.#enableColResizing = pars?.enableColResizing ?? true;
      this.#enableColMoving = pars?.enableColMoving ?? false;
      this.#enableColFiltering = pars?.enableColFiltering ?? false;
      this.#statistics = pars?.statistics ?? ['min', 'max', 'mean'];
      this.#statisticsVisible = pars?.statisticsVisible ?? true;
      this.#dataSelectionEnabled = pars?.dataSelectionEnabled ?? true;

      this.#shownColumns = pars?.shownColumns;
      this.#hiddenColumns = pars?.hiddenColumns;

      this.#grid = null;
      this.#gridOptions = {
         defaultColDef: {
            resizable: true,
            sortable: this.#enableColSorting,
            resizable: this.#enableColResizing,
            suppressMovable: !this.#enableColMoving,
            suppressFiltersToolPanel: true,
            cellStyle: { 'border-right': '1px dashed var(--color-gray-4)' },
            filterParams: { buttons: ['apply', 'reset'], closeOnApply: true },
            wrapHeaderText: true,
            autoHeaderHeight: true
         }
      };

      this.iconres = pars?.iconres ?? "";
      if (!this.iconres)
         this.iconres = "/res/gnr_core_gui/CoreGUI/Icons/base/table.svg";

      this.name = pars?.name ?? "";
      if (!this.name)
         this.name = "Data table";

      this.optionList = new Map();
      if (0 < (pars?.tableRowSelectVisibility?.length ?? 0)) {
         this.optionList.set("selectedRowVisibility", {
            type: "selection",
            title: "Show data for"/*,
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/autosize_columns.svg"
            */
         });
      }
      if (pars?.autoSizeColumnsOptionVisible ?? true) {
         this.optionList.set("autoSize", {
            type: "action",
            title: "Auto-size all columns",
            text: "Auto-size",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/autosize_columns.svg"
         });
      }
      if (pars?.statisticsOptionVisible ?? true) {
         this.optionList.set("statistics", {
            type: "option",
            title: "Statistics",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/suma_file.svg"
         });
      }
      if (true) {
         this.optionList.set("export", {
            type: "action-popup",
            title: "Export",
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/export.svg"
         });
      }
      if (pars?.infoOptionVisible ?? true) {
         this.optionList.set("info", {
            type: "info",
            style: {
               flex: '1 1 40px',
               'text-align': 'right',
               'align-self': 'center',
               'padding-right': '0.6em'
            }
         });
      }

      this.autosizeSkipHeader = false;
      this.autoSizeOptionTrigger = () => {
         if (!this.#grid)
            return;

         const allColumnIds = [];
         this.#gridOptions.columnApi.getColumns().forEach((column) => {
            allColumnIds.push(column.getId());
         });
         this.#gridOptions.columnApi.autoSizeColumns(allColumnIds, this.autosizeSkipHeader);
         this.autosizeSkipHeader = !this.autosizeSkipHeader;
      };

      Object.defineProperties(this, {
         statisticsOptionValue: {
            get() { return this.#statisticsVisible; },
            set(val) {
               this.#statisticsVisible = val;
               if (this.#grid) {
                  this.#gridOptions.api.setPinnedBottomRowData(this.#statisticsVisible ? this.statisticsRows : null);
               }
            }
         },
         statisticsOptionValueChanged: {
            value: new LimSignal(pars?.statisticsOptionValueChanged ? [pars.statisticsOptionValueChanged] : []),
            writable: false
         },
         statisticsOptionEnabled: {
            get() { return Array.isArray(this.#statistics) && this.#statistics.length; }
         },
         statisticsOptionEnabledChanged: {
            value: new LimSignal(pars?.statisticsOptionEnabledChanged ? [pars.statisticsOptionEnabledChanged] : []),
            writable: false
         },
      });

      Object.defineProperties(this, {
         infoOptionValue: {
            get() {
               const sel = this.#gridOptions?.api?.getSelectedNodes?.().length ?? 0;
               const total = this?.tableData?.rowCount ?? 0;
               return 1 < sel ? `${total.toLocaleString()} rows, ${sel.toLocaleString()} sel` : `${total.toLocaleString()} rows`;
            },
         },
         infoOptionValueChanged: {
            value: new LimSignal(pars?.infoOptionValueChanged ? [pars.infoOptionValueChanged] : []),
            writable: false
         },
         infoOptionTrigger: {
            value: (e, that) => {
               this.currentObjectSelection = {};
            },
            writable: false
         },
      });
   }

   connectedCallback() {
      this.style.display = "block";
      this.connectToDocument();
      this.fetchAndUpdateTable();
   }

   disconnectedCallback() {
      this.disconnectFromDocument();
      this.#grid = null;
      this.innerHTML = "";
   }

   onTableChanged() {
      this.fetchAndUpdateTable();
   }

   adoptedCallback() {
   }

   attributeChangedCallback(name, oldValue, newValue) {
   }

   get colIdList() {
      let colIds = this.tableData.systemColIdList;
      const shown = this.#shownColumns ? this.tableData.matchColsFulltext(this.#shownColumns) : [];
      const hidden = this.#hiddenColumns ? this.tableData.matchColsFulltext(this.#hiddenColumns) : [];
      for (let i = 0; i < this.tableData.colCount; i++) {
         const meta = this.tableData.colMetadataAt(i);
         if (shown.includes(i) ? true : (hidden.includes(i) ? false : !(meta?.hidden ?? false))) {
            const colId = this.tableData.colIdAt(i);
            if (!colIds.includes(colId))
               colIds.push(colId);
         }
      }
      return colIds;
   }

   get rowFilterList() {
      return this.makeDefaultRowFilterList();
   }

   onTableChanged() {
      this.fetchAndUpdateTable();
   }

   fetchAndUpdateTable() {
      this.fetchTableMetadata(() => {
         this.fetchAndUpdateData();
      });
   }

   fetchAndUpdateData() {
      let pars = {};
      const colIdList = this.colIdList;
      if (0 < colIdList.length && colIdList.length < this.tableData.colCount)
         pars.cols = JSON.stringify(colIdList);

      const rowFilterList = this.rowFilterList;
      if (rowFilterList.length)
         pars.filter = JSON.stringify({ op: "all", filters: rowFilterList });

      this.fetchTableData(pars, () => {
         this.update()
         this.infoOptionValueChanged.emit(this)
      });
   }

   get exportOptionItems() {
      if (!this.tableData)
         return [{ text: "Not available", enabled: false }];

      if (this.tableRowVisibility === "all") {
         return [
            { text: "Copy to clipboard", trigger: () => this.copyToClipboard() },
            { text: "Save to Excel file...", trigger: () => this.saveAs('xlsx', 'current') },
            { text: "Save to CSV file...", trigger: () => this.saveAs('csv', 'current') },
         ];
      }
      else {
         const entity = this.tableData.rowEntity;
         return [
            { text: `Copy current ${entity}s to clipboard`, trigger: () => this.copyToClipboard() },
            { text: `Save current ${entity}s to Excel file...`, trigger: () => this.saveAs('xlsx', 'current') },
            { text: `Save current ${entity}s to CSV file...`, trigger: () => this.saveAs('csv', 'current') },
            { type: "separator" },
            { text: `Save all ${entity}s to Excel file...`, trigger: () => this.saveAs('xlsx', 'whole') },
            { text: `Save all ${entity}s to CSV file...`, trigger: () => this.saveAs('csv', 'whole') },
         ];
      }
   }

   onCurrentLoopIndexesChanged(val, change) {
      if (!this?.tableData)
         return;
      if (this.isSelectedRowVisiblityIncludingAnyOfLoops(change))
         this.fetchAndUpdateTable();
      else if (this.#dataSelectionEnabled && this.tableData.isFrameTable && val) {
         const rowIndex = this.tableData.loopIndexesToRowIndex(val);
         if (0 <= rowIndex) {
            this.updateGridRowSelection([rowIndex]);
            this.infoOptionValueChanged.emit(this);
         }
      }
   }

   onCurrentObjectSelectionChanged(val) {
      if (this.#dataSelectionEnabled && this?.tableData?.isObjectTable) {
         this.updateGridRowSelection(this.tableData.objectSelectionToRowIndexes(val));
         this.infoOptionValueChanged.emit(this);
      }
   }

   onSelectedRowVisibilityChanged(val, oldVal) {
      if (!this?.tableData)
         return;
      this.fetchAndUpdateData();
   }

   updateGridRowSelection(sel) {
      if (!this.#grid)
         return;
      if (0 < sel.length) {
         let selRowNodes = [];
         let firstNodeVisible = null;
         this.#gridOptions.api.forEachNode(rowNode => {
            const selected = sel.includes(rowNode.data.i);
            if (selected)
               selRowNodes.push(rowNode);
            if (selected && !firstNodeVisible)
               firstNodeVisible = rowNode;
         });
         this.#gridOptions.api.deselectAll();
         this.#gridOptions.api.setNodesSelected({ nodes: selRowNodes, newValue: true });
         if (firstNodeVisible)
            this.#gridOptions.api.ensureNodeVisible(firstNodeVisible, null);
      }
      else {
         this.#gridOptions.api.deselectAll();
      }
   }

   update() {
      if (!this.tableData || !document.body.contains(this))
         return;

      const td = this.tableData;

      let columnDefs = [];
      let fieldNames = [];

      this.statisticsRows = this.#statistics.map(item => ({}));

      if (this.#showRowIndexCol) {
         columnDefs.push({
            field: 'number',
            headerName: '#',
            hide: false,
            type: 'rightAligned',
            pinned: 'left',
            valueFormatter: (params) => { return params.value; }
         });

         this.#statistics.forEach((item, index) => this.statisticsRows[index]['number'] = item);
      }

      let visibleColIndex = 0;
      const shown = this.#shownColumns ? td.matchColsFulltext(this.#shownColumns) : [];
      const hidden = this.#hiddenColumns ? td.matchColsFulltext(this.#hiddenColumns) : [];
      for (let i = 0; i < td.colCount; i++) {
         const meta = td.colMetadataAt(i);
         const printFn = meta.hasOwnProperty("jsonObject") ? LimTableData.printCellJsonObject : LimTableData.makePrintFunction(meta);
         const isVisible =  shown.includes(i) ? true : (hidden.includes(i) ? false : !(meta?.hidden ?? false));
         const isNumeric = meta.decltype === 'int' || meta.decltype === 'double';
         const fieldName = `c${i}`;
         const title = td.colTitleAndUnitAt(i);
         columnDefs.push({
            field: fieldName,
            colId: td.colIdAt(i),
            headerName: title,
            hide: !isVisible,
            type: isNumeric ? 'numericColumn' : undefined,
            valueFormatter: (params) => { return printFn(params.value); }
         });

         const coldef = columnDefs[columnDefs.length - 1];
         if (this.#enableColFiltering && !meta?.jsonObject)
            coldef.filter = isNumeric ? 'agNumberColumnFilter' : 'agTextColumnFilter';

         if (meta?.jsonObject === "image") {
            coldef.tooltipComponentParams = {
               imageSizeX: meta?.imageSizeX ?? 100,
               imageSizeY: meta?.imageSizeY ?? 100,
            };
            coldef.tooltipComponent = LimDataGridImageTooltip;
            coldef.tooltipField = coldef.field;
         }
         else if (meta?.jsonObject === "multi-image") {
            coldef.tooltipComponentParams = {
               allChannels : meta?.images?.findIndex?.(item => item.tags.includes("AllChannels")) ?? -1,
               binLayers: meta?.images?.map?.((item, index) => item.tags.includes("BinaryLayer") ? index : -1)?.filter?.(item => 0 <= item) ?? [],
               imageSizeX: meta?.imageSizeX ?? 100,
               imageSizeY: meta?.imageSizeY ?? 100,
            };
            coldef.tooltipComponent = LimDataGridMultiImageTooltip;
            coldef.tooltipField = coldef.field;
         }
         else if (meta?.jsonObject === "fit") {
            coldef.tooltipComponentParams = {
               metaFit: meta?.fit
            };
            coldef.tooltipComponent = LimDataGridFitTooltip;
            coldef.tooltipField = coldef.field;
         }
         else if (meta?.jsonObject === "boxplot") {
            const data = td.colDataAt(i).map(item => item ? JSON.parse(item) : null).map(item => item?.type === "boxplot" && item?.data ? item.data : null);
            const filteredData = data.filter(item => !!item && Number.isFinite(item.minimum) && Number.isFinite(item.maximum));
            coldef.cellRendererParams = {
               min: Math.min(...filteredData.map(item => item.lower)),
               max: Math.max(...filteredData.map(item => item.upper)),
            }
            coldef.cellRenderer = params => {
               const size = Math.max(100, params.column.getActualWidth() - 40);
               const [min, rng] = [params.min, params.max - params.min];
               const [h0, h1, h2, height] = [4, 8, 12, 16];
               try {
                  const obj = JSON.parse(params.value);
                  if (!obj?.data)
                     return "";
                  const lo = size * (obj.data.lower - min) / rng;
                  const q1 = size * (obj.data.Q1 - min) / rng;
                  const q2 = size * (obj.data.median - min) / rng;
                  const q3 = size * (obj.data.Q3 - min) / rng;
                  const hi = size * (obj.data.upper - min) / rng;
                  return `<svg width="${size}" height="${height}">
                     <line x1="${lo}" y1="${h0}" x2="${lo}" y2="${h2}" class="limit" />
                     <line x1="${lo}" y1="${h1}" x2="${q1}" y2="${h1}" class="range" />
                     <rect x="${q1}" y="${h0}" width="${q3 - q1}" height="${h2 - h0}" class="iqr"/>
                     <line x1="${q2}" y1="${h0}" x2="${q2}" y2="${h2}" class="median" />
                     <line x1="${q3}" y1="${h1}" x2="${hi}" y2="${h1}" class="range" />
                     <line x1="${hi}" y1="${h0}" x2="${hi}" y2="${h2}" class="limit" />
                  </svg>`;
               }
               catch {
                  return "";
               }
            }
         }
         else if (meta?.jsonObject === "violinPlot") {
            coldef.cellRenderer = params => {
               const size = Math.max(100, params.column.getActualWidth() - 40);
               const [h, height] = [15, 16];
               try {
                  const obj = JSON.parse(params.value);
                  if (!obj?.data)
                     return "";

                  let points = [];
                  const [x, y] = [obj.data.x, obj.data.y];
                  const [xmin, xmax] = [x[0], x[x.length - 1]];
                  const len = Math.min(x.length, y.length);
                  const ymax = obj.data?.yMax ?? Math.max(...y);
                  for (let i = 0; i < size; i += 2) {
                     const j = Math.floor(i * len / size);
                     const _x = size * (x[j] - xmin) / xmax;
                     const _y = h * y[j] / ymax;
                     points.push(`L${_x} ${height - _y}`);
                  }
                  return `<svg width="${size}" height="${height}">
                     <path d="M0 ${height} ${points.join(' ')}" class="violin"  />
                  </svg>`;
               }
               catch {
                  return "";
               }
            }
         }
         fieldNames.push(fieldName);

         if (!this.#showRowIndexCol && isVisible && 0 === visibleColIndex) {
            this.#statistics.forEach((item, index) => this.statisticsRows[index][fieldName] = item);
            coldef.valueFormatter = (params) => { return params.node.rowPinned ? params.value : printFn(params.value); }
         }
         else {
            const stats = td.colDataStatsAt(i, this.#statistics);
            this.#statistics.forEach((item, index) => this.statisticsRows[index][fieldName] = stats[index]);
         }

         if (isVisible)
            visibleColIndex++;
      }

      this.#gridOptions.columnDefs = columnDefs;
      this.#gridOptions.rowData = td.dataColumnList[0].map((_, rowIndex) => ({
         i: rowIndex,
         number: rowIndex + 1,
         ...Object.fromEntries(td.dataColumnList.map((columnData, columnIndex) => [fieldNames[columnIndex], columnData[rowIndex]]))
      }));

      if (this.#dataSelectionEnabled && this.tableData.isFrameTable)
         this.#gridOptions.rowSelection = 'single';
      else if (this.#dataSelectionEnabled && this.tableData.isObjectTable && this.tableData.isContainingRowsFromSingleFrame)
         this.#gridOptions.rowSelection = 'multiple';
      else {
         this.#gridOptions.rowSelection = 'none';
         this.currentObjectSelection = {};
      }

      this.#gridOptions.pinnedBottomRowData = this.#statisticsVisible ? this.statisticsRows : null;
      this.#gridOptions.suppressCellFocus = true;
      this.#gridOptions.tooltipShowDelay = 500;
      this.#gridOptions.tooltipHideDelay = 60000;
      this.#gridOptions.suppressRowHoverHighlight = true;
      this.#gridOptions.suppressColumnVirtualisation = true;
      this.#gridOptions.getRowStyle = params => {
         if (params.node.rowPinned) {
            return { background: 'var(--color-gray-1)' };
         }
      };

      this.#gridOptions.onSelectionChanged = (event) => {
         if (!this.tableData || event.source !== "rowClicked")
            return;
         const sel = event.api.getSelectedRows().map(item => item.i).sort();
         if (this.tableData.isFrameTable) {
            if (sel.length) {
               const loopIndexes = td.rowIndexToLoopIndexes(sel[0]);
               setTimeout(() => { this.currentLoopIndexes = loopIndexes });
            }
         }
         else if (this.tableData.isObjectTable) {
            const objSelection = td.rowIndexesToObjectSelection(event.api.getSelectedRows().map(item => item.i).sort());
            setTimeout(() => { this.currentObjectSelection = objSelection; });
         }

      };

      this.#gridOptions.onRowDoubleClicked = (event) => {
         if (this.tableData && this.tableData.isObjectTable) {
            const selObj = td.rowIndexesToObjectSelection([event.rowIndex]);
            const binLayers = Object.getOwnPropertyNames(selObj);
            if (binLayers?.length && selObj?.[binLayers[0]]?.length)
               limCurrentDocument?.highlightObject?.(binLayers[0], selObj[binLayers[0]][0]);
         }
      }

      if (!this.#grid) {
         this.#grid = new agGrid.Grid(this, this.#gridOptions);
      }
      else {
         this.#gridOptions.api.setColumnDefs(this.#gridOptions.columnDefs);
         this.#gridOptions.api.setRowData(this.#gridOptions.rowData);
         this.#gridOptions.api.setPinnedBottomRowData(this.#gridOptions.pinnedBottomRowData);
      }

      if (this.#dataSelectionEnabled && this.tableData.isFrameTable)
         this.updateGridRowSelection(this.tableData.loopIndexesToRowIndex(this.currentLoopIndexes));
      else if (this.#dataSelectionEnabled && this.tableData.isObjectTable)
         this.updateGridRowSelection(this.tableData.objectSelectionToRowIndexes(this.currentObjectSelection));

      const allColumnIds = [];
      this.#gridOptions.columnApi.getColumns().forEach((column) => {
         allColumnIds.push(column.getId());
      });

      this.#gridOptions.columnApi.autoSizeColumns(allColumnIds, false);
      this.autosizeSkipHeader = true;
   }

   copyToClipboard() {
      const cols = this.#gridOptions.columnDefs.filter(item => !item.hide).map(item => item.colId).filter(item => !!item);
      const requestUrl = this.exportTableUrl("txt", "clipboard.txt", cols, this.rowFilterList);
      fetch(requestUrl)
         .then((response) => response.text())
         .then((text) => {
            window.navigator.clipboard.writeText(text);
         });
   }

   saveAs(ext, what) {
      const rowFilterList = what === "whole" ? [] : this.rowFilterList;
      const cols = what === 'whole'
         ? this.#gridOptions.columnDefs.map(item => item.colId).filter(item => !!item)
         : this.#gridOptions.columnDefs.filter(item => !item.hide).map(item => item.colId).filter(item => !!item);
      const filename = what === "whole" ? (this.name ? `All ${this.name}.${ext}` : `All Data table.${ext}`) : (this.name ? `${this.name}.${ext}` : `Data table.${ext}`);
      const requestUrl = this.exportTableUrl(ext, filename, cols, rowFilterList);
      const download = document.createElement('a');
      download.href = requestUrl;
      download.download = filename;
      download.click();
   }
}
customElements.define('lim-summary-table', LimSummaryTable);
LimClassFactory.registerConstructor("LimSummaryTable", (...pars) => {
   const el = new LimSummaryTable();
   el.initialize(...pars);
   return el;
});

customElements.define('lim-data-grid', LimDataGrid);
LimClassFactory.registerConstructor("LimDataGrid", (...pars) => {
   const el = new LimDataGrid();
   el.initialize(...pars);
   return el;
});
