const LimTableDataFormat = {
   Unknown: 0,
   FloatingPoint: 1,
   Scientific: 2,
   TimeSec: 3,
   TimeMinSec: 4,
   TimeHourMinSec: 5,
   TimeHourMin: 6,
   TimeSecMs: 7,
   TimeMinSecMs: 8,
   TimeHourMinSecMs: 9
};
Object.seal(LimTableDataFormat);

class LimTableData {
   #tabName
   #tabMetadata
   #colIds;
   #colMetadata;
   #dataColumns

   constructor(json) {
      const tableData = LimTableData.jsonParsedTableData(json);
      this.#tabName = tableData?.tabName ?? "";
      this.#tabMetadata = tableData?.tabMetadata ?? {};
      this.#colIds = tableData?.colIds ?? [];
      this.#colMetadata = tableData?.colMetadata ?? this.#colIds.map(item => {});
      this.#dataColumns = tableData?.dataColumns ?? this.#colIds.map(item => []);
   }

   static jsonParsedTableData(json) {
      if (typeof json === "string") {
         try {
            return JSON.parse(json);
         }
         catch (e) {
            console.log(e);
         }
      }
      else if (typeof json === "object") {
         return json;
      }

      return null;
   }

   static printCellImage(val) {
      try {
         const obj = val ? JSON.parse(val) : null;
         return obj ? `<img src="data:${obj.data.type},${obj.data.base64}"/>` : "";
      }
      catch (e) {
         console.log(e.message);
         return "";
      }
   }

   static printCellHtml(val) {
      try {
         const obj = val ? JSON.parse(val) : null;
         return obj ? obj.data : "";
      }
      catch (e) {
         console.log(e.message);
         return "";
      }
   }

   static printCellJsonObject(val) {
      try {
         const obj = val ? JSON.parse(val) : null;
         return obj ? obj.disp : "";
      }
      catch (e) {
         console.log(e.message);
         return "";
      }
   }

   static printCellInteger(val) {
      return Number.isInteger(val) ? val : (!Number.isNaN(val) && Number.isFinite(val) ? val.toFixed(1) : "");
   }

   static printCellDouble(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? val.toLocaleString() : "";
   }

   static printCellDoubleAuto(val, prec) {
      if(Number.isNaN(val) || !Number.isFinite(val))
         return "";
      let pow = Math.log(Math.abs(val));
      if(pow >= 0) {
         pow = Math.floor(pow) + 1;
         return val.toLocaleString(undefined, { notation: (pow > prec) ? "scientific" : "standard", maximumSignificantDigits: prec, maximumFractionDigits: prec });
      }
      else {
         pow = Math.floor(pow);
         return val.toLocaleString(undefined, { notation: (pow < -4) ? "scientific" : "standard", maximumSignificantDigits: prec, maximumFractionDigits: prec });
      }
   }

   static printCellDoubleFlt(val, prec) {
      return !Number.isNaN(val) && Number.isFinite(val) ? val.toLocaleString(undefined, { minimumFractionDigits: prec, maximumFractionDigits: prec }) : "";
   }

   static printCellDoubleSci(val, prec) {
      return !Number.isNaN(val) && Number.isFinite(val) ? val.toExponential(Number.isInteger(prec) ? prec : 3) : "";
   }

   static printCellTimeSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? val.toFixed(0) : "";
   }

   static printCellTimeMinSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? Math.floor(val / 60).toFixed(0) + ":" + Math.floor(val % 60).toFixed(0) : "";
   }

   static printCellTimeHourMinSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? Math.floor(val / 3600).toFixed(0) + ":" + Math.floor((val % 3600) / 60).toFixed(0) + ":" + Math.floor((val % 3600) % 60).toFixed(0) : "";
   }

   static printCellTimeHourMin(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? Math.floor(val / 3600).toFixed(0) + ":" + Math.floor((val % 3600) / 60).toFixed(0) : "";
   }

   static printCellTimeSecMSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? val.toFixed(3) : "";
   }

   static printCellTimeMinSecMSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? Math.floor(val / 60).toFixed(0) + ":" + Math.floor(val % 60).toFixed(0) + (val - Math.floor(val)).toFixed(3).substring(1) : "";
   }

   static printCellTimeHourMinSecMSec(val) {
      return !Number.isNaN(val) && Number.isFinite(val) ? Math.floor(val / 3600).toFixed(0) + ":" + Math.floor((val % 3600) / 60).toFixed(0) + ":" + Math.floor((val % 3600) % 60).toFixed(0) + (val - Math.floor(val)).toFixed(3).substring(1) : "";
   }

   static makePrintFunction(meta, dataFormat = undefined, precision = undefined) {
      if (meta.hasOwnProperty("jsonObject")) {
         if (meta.jsonObject === "image")
            return LimTableData.printCellImage;
         else if (meta.jsonObject === "svg" || meta.jsonObject === "html")
            return LimTableData.printCellHtml;
         else
            return LimTableData.printCellJsonObject;
      }
      if (meta.decltype === "int") {
         return LimTableData.printCellInteger;
      }
      else if (meta.decltype === 'double') {
         const fmt = meta?.dataFormat ?? dataFormat ?? 0;
         const prec = meta?.dataFormatPrecision ?? precision ?? -1;
         if (fmt === 0 && prec < 0) // default
            return LimTableData.printCellDouble;
         if (fmt === 0 && prec >= 0)
            return val => LimTableData.printCellDoubleAuto(val, prec);
         else if (fmt === 1) // float
            return val => LimTableData.printCellDoubleFlt(val, prec);
         else if (fmt === 2) // scientific
            return val => LimTableData.printCellDoubleSci(val, prec);
         else if (fmt === 3) // time sec
            return LimTableData.printCellTimeSec;
         else if (fmt === 4) // time min:sec
            return LimTableData.printCellTimeMinSec;
         else if (fmt === 5) // time hour:min:sec
            return LimTableData.printCellTimeHourMinSec;
         else if (fmt === 6) // time hour:min
            return LimTableData.printCellTimeHourMin;
         else if (fmt === 7) // time sec.ms
            return LimTableData.printCellTimeSecMSec;
         else if (fmt === 8) // time min:sec.ms
            return LimTableData.printCellTimeMinSecMSec;
         else if (fmt === 9) // time hour:min:sec.ms
            return LimTableData.printCellTimeHourMinSecMSec;
      }

      return val => `${val ?? ""}`;
   }

   setData(json) {
      const tableData = new LimTableData(json);
      if (Array.isArray(tableData?.colIdList) && Array.isArray(tableData?.dataColumnList) && tableData.colIdList.length == tableData.dataColumnList.length) {
         this.#dataColumns = this.#colIds.map(item => []);
         for (let i = 0; i != tableData.colIdList.length; i++) {
            const j = this.#colIds.indexOf(tableData.colIdList[i]);
            this.#dataColumns[j] = tableData.dataColumnList[i];
         }
      }
   }

   get json() {
      return JSON.stringify({
         tabName: this.#tabName,
         tabMetadata: this.#tabMetadata,
         colIds: this.#colIds,
         colMetadata: this.#colMetadata,
         dataColumns: this.#dataColumns
      });
   }

   get tableName() {
      return this.#tabName;
   }

   get isContainingRowsFromSingleFrame() {
      for (let loopColId of this.loopColIdList) {
         const s = new Set(this.colData(loopColId).filter(item => typeof item == "number"));
         if (1 !== s.size)
            return false;
      }
      return true;
   }

   get isFrameTable() {
      const rows = this.rowCount;
      if (rows) {
         return rows === [...new Set(this.rowsToObj(null, this.loopColIdList).map(item => JSON.stringify(item)).values())].length;
      }
      else
         return !this.isObjectTable && 0 < this.loopColIdList.length;
   }

   get isGroupedFrameTable() {
      if(!this.groupedBy.length || !this.rowCount)
         return this.isFrameTable;
      //TODO: should every group have same length?
      let groups = this.groups;
      let isGroupFrame = groups.map(rows => {
         return rows.length === [...new Set(this.rowsToObj(rows, this.loopColIdList).map(item => JSON.stringify(item)).values())].length;
      });
      return !isGroupFrame.includes(false);
   }

   get isObjectTable() {
      const rows = this.rowCount;
      if (rows) {
         return (this.hasColId("_ObjId") || this.hasColId("_ObjId3d"))
            && [...new Set(this.rowsToObj(null, this.loopColIdList).map(item => JSON.stringify(item)).values())].length < rows;
      }
      else
         return this.hasColId("_ObjId") || this.hasColId("_ObjId3d");
   }

   get tableMetadata() {
      return this.#tabMetadata;
   }

   get colIdList() {
      return this.#colIds;
   }

   get systemColIdList() {
      return this.#colIds.filter(item => item[0] === "_");
   }

   get colTitleList() {
      return this.#colMetadata.map(item => item.title);
   }

   get colMetadataList() {
      return this.#colMetadata;
   }

   get dataColumnList() {
      return this.#dataColumns;
   }

   get colCount() {
      return this.#colIds.length;
   }

   get rowCount() {
      return Math.max(...this.#dataColumns.map(item => item.length));
   }

   get rowEntity() {
      if (this.isFrameTable) {
         if (this.hasColId("_Well")) {
            const m = this.colMetadata("_Well");
            if (m?.globalRange?.count === m?.globalRange?.distincts)
               return "well";
         }
         return "frame";
      }
      else if (this.isObjectTable) {
         return "object";
      }
      return "row";
   }

   get rowTitleLoopColIdList() {
      let ret = [];
      const idToTypeMap = this.loopColIdToTypeMap;
      for (let loopId of this.loopColIdList) {
         const m = this.colMetadata(loopId);
         if (idToTypeMap[loopId] === "w" && this.hasColId("_Well"))
            ret.push("_Well");
         else if (m?.globalRange) {
            if (m.globalRange.min < m.globalRange.max)
               ret.push(loopId);
         }
         else 
            ret.push(loopId);
      }
      return ret;
   }

   get rowTitleColIdList() {
      let ret = this.rowTitleLoopColIdList;
      if (this.hasColId("_ObjId")) {
         if (this.hasColId("_Entity"))
            ret.push("_Entity");
         ret.push("_ObjId");
      }

      if (this.hasColId("_ObjId3d")) {
         if (this.hasColId("_Entity"))
            ret.push("_Entity");
         ret.push("_ObjId3d");
      }
   
      return ret;
   }

   get frameTitlesLong() {
      let colIds = this.rowTitleLoopColIdList;
      const loopTypeLookup = this.loopColIdToTypeMap
      const loopLabelLookup = { "p": "Plate", "w": "Well", "m": "Pos", "t": "Time", "z": "Z", "c": "Custom", "s": "Slide", "r": "Region" };
      let ret = Array.from(limRange(this.rowCount), item => []);
      while (colIds.length) {
         const colId = colIds.shift();
         if (colId === "_Well")
            this.colData("_Well").forEach((item, index) => ret[index].push(`${item}`));
         else if (colId === "_File")
            this.colData("_File").forEach((item, index) => ret[index].push(`File: #${item}`));
         else if (colId.startsWith("_loop")) {
            const label = loopLabelLookup[loopTypeLookup[colId]];
            this.colData(colId).forEach((item, index) => ret[index].push(`${label}: #${item}`));
         }
      }

      return ret.map(item => item.join(", "));
   }

   get rowTitlesLong() {
      const colIds = this.rowTitleColIdList;
      const loopTypeLookup = this.loopColIdToTypeMap
      const loopLabelLookup = { "p": "Plate", "w": "Well", "m": "Pos", "t": "Time", "z": "Z", "c": "Custom", "s": "Slide", "r": "Region" };
      let ret = Array.from(limRange(this.rowCount), item => []);
      while (colIds.length) {
         const colId = colIds.shift();
         if (colId === "_Well")
            this.colData("_Well").forEach((item, index) => ret[index].push(`${item}`));
            else if (colId === "_File")
            this.colData("_File").forEach((item, index) => ret[index].push(`File: #${item}`));         
         else if (colId.startsWith("_loop")) {
            const label = loopLabelLookup[loopTypeLookup[colId]];
            this.colData(colId).forEach((item, index) => ret[index].push(`${label}: #${item}`));
         }
         else if (colId === "_Entity") {
            const objIdIndex = colIds.indexOf("_ObjId");
            const objId3dIndex = colIds.indexOf("_ObjId3d");
            if (0 <= objIdIndex) {
               const entity = this.colData("_Entity")
               this.colData("_ObjId").forEach((item, index) => ret[index].push(`${entity[index]}: #${item}`));
               colIds.splice(objIdIndex, 1);
            }
            else if (0 <= objId3dIndex) {
               const entity = this.colData("_Entity")
               this.colData("_ObjId3d").forEach((item, index) => ret[index].push(`${entity[index]}: #${item}`));
               colIds.splice(objId3dIndex, 1);
            }
         }
         else if (colId === "_ObjId") {
            this.colData("_ObjId").forEach((item, index) => ret[index].push(`ObjId: #${item}`));
         }
         else if (colId === "_ObjId3d") {
            this.colData("_ObjId3d").forEach((item, index) => ret[index].push(`ObjId: #${item}`));
         }
      }

      return ret.map(item => item.join(", "));
   }

   // loops
   get loopColIdList() {
      return this.colIdList.filter(item => item.startsWith("_loop") || item.startsWith("_File"));
   }

   get loopColIdToTypeMap() {
      let ret = {}
      const lookup = ["", "p", "w", "m", "t", "z", "c", "s", "r", "x", "l"];
      const loopColIdList = this.loopColIdList;
      for (let i = 0; i < this.loopColIdList.length; i++) {
         const id = loopColIdList[i];
         if (id === "_File") {
            ret[id] = "f";
         }
         else {
            const m = this.colMetadata(id);
            ret[id] = lookup[m?.loopType ?? 0];
         }
      }
      return ret;
   }

   get loopTypeToCloIdMap() {
      let ret = {}
      const lookup = ["", "p", "w", "m", "t", "z", "c", "s", "r", "x", "l"];
      const loopColIdList = this.loopColIdList;
      for (let i = 0; i < this.loopColIdList.length; i++) {
         const id = loopColIdList[i];
         if (id === "_File") {
            ret["f"] = id;
         }
         else {
            const m = this.colMetadata(id);
            ret[lookup[m?.loopType ?? 0]] = id;
         }
      }
      return ret;
   }

   loopIndexesToRowIndex(loopIndexes) {
      let loopColIdList = {}
      const lookup = this.loopTypeToCloIdMap;
      for (let loopType of Object.getOwnPropertyNames(loopIndexes))
         if (lookup.hasOwnProperty(loopType))
            loopColIdList[lookup[loopType]] = loopIndexes[loopType] + 1;
      const operand = JSON.stringify(loopColIdList);
      return this.rowsToObj(null, Object.getOwnPropertyNames(loopColIdList)).map(item => JSON.stringify(item)).indexOf(operand);
   }

   rowIndexToLoopIndexes(index) {
      let ret = {};
      const lookup = this.loopColIdToTypeMap;
      for (let colId of this.loopColIdList) {
         ret[lookup[colId]] = this.colData(colId)[index] - 1;
      }
      return ret;
   }

   objectSelectionToRowIndexes(objectSelection) {
      let ret = [];
      const ent = this.colIndexById("_Entity");
      const id2d = this.colIndexById("_ObjId");
      const id3d = this.colIndexById("_ObjId3d");
      const obj = 0 <= id2d ? id2d : id3d;
      if (ent < 0 || obj < 0)
         return [];
      const entCol = this.colDataAt(ent);
      const objCol = this.colDataAt(obj);
      for (let k of Object.getOwnPropertyNames(objectSelection)) {
         const lookup = new Map(entCol.map((item, index) => item === k ? [objCol[index], index] : null).filter(item => !!item));
         for (let id of objectSelection[k]) {
            const index = lookup.get(id);
            if (typeof index === "number")
               ret.push(index);
         }
      }
      ret.sort()
      return ret;
   }

   rowIndexesToObjectSelection(indexes) {
      let ret = {};
      const ent = this.colIndexById("_Entity");
      const id2d = this.colIndexById("_ObjId");
      const id3d = this.colIndexById("_ObjId3d");
      const obj = 0 <= id2d ? id2d : id3d;
      if (ent < 0 || obj < 0)
         return {};
      const entCol = this.colDataAt(ent);
      const objCol = this.colDataAt(obj);
      for (let index of indexes) {
         const entity = entCol[index];
         if (ret.hasOwnProperty(entity))
            ret[entity].push(objCol[index]);
         else
            ret[entity] = [objCol[index]];
      }
      for (let k of Object.getOwnPropertyNames(ret))
         ret[k].sort();
      return ret;
   }

   // colIndex
   hasColId(id) {
      return 0 <= this.#colIds.indexOf(id);
   }

   colIndexById(id) {
      return this.#colIds.indexOf(id);
   }

   colIndexByTitle(title) {
      return this.#colMetadata.map(item => item.title).indexOf(title);
   }

   colIndexByFeature(feature) {
      return this.#colMetadata.findIndex(element => element.feature === feature);
   }

   colIndexByFullText(text) {
      let index = -1;
      return 0 <= (index = this.colIndexById(text)) ? index
         : this.colIndexByTitle(text);
   }

   matchColsFulltext(param) {
      let retval = [];
      if (typeof param === "string") {
         if (param === '*')
            return [...limRange(this.colCount)];
         const reparam = new RegExp(param, "i");
         for (let i = 0; i < this.colCount; i++) {
            if (this.colIdAt(i) == param)
               retval.push(i);
            else if (reparam.test(this.colTitleAt(i)))
               retval.push(i);
         }
      }
      else if (param instanceof RegExp) {
         for (let i = 0; i < this.colCount; i++) {
            const fullText = [this.colIdAt(i), this.colTitleAt(i)];
            for (let str of fullText) {
               if (param.test(str)) {
                  retval.push(i);
                  break;
               }
            }
         }
      }
      else if (Array.isArray(param)) {
         for (let i = 0; i < this.colCount; i++) {
            if (param.includes(this.colIdAt(i)))
               retval.push(i);
            else if (param.includes(this.colTitleAt(i)))
               retval.push(i);
         }
      }

      return retval;
   }

   get groupedBy() {
      return this.tableMetadata.groupedBy ?? [];
   }

   get groups() {
      if (!this.#dataColumns.length)// || !this.#dataColumns[0].length)
         return [];

      let c = 0;
      for (; c < this.#dataColumns.length; c++)
         if(this.#dataColumns[c].length)
            break;
      if(!this.#dataColumns[c]?.length)
         return [];

      const colIndexes = this.groupedBy.map(item => this.colIndexById(item)).filter(item => 0 <= item);
      const groupingRows = this.#dataColumns[c].map((_, rowIndex) => colIndexes.map(columnIndex => this.#dataColumns[columnIndex][rowIndex]));

      let g = 0;
      let groups = 0 < groupingRows.length ? [[0]] : [];
      for (let i = 1; i < groupingRows.length; i++) {
         if (JSON.stringify(groupingRows[i]) === JSON.stringify(groupingRows[i - 1])) {
            groups[g].push(i);
         }
         else {
            g++;
            groups.push([i]);
         }
      }

      return groups;
   }

   // column info

   colIdAt(index) {
      return this.#colIds[index];
   }

   colMetadata(id) {
      return this.colMetadataAt(this.colIndexById(id));
   }

   colMetadataAt(index) {
      return this.#colMetadata[index];
   }

   colTitle(id) {
      return this.colTitleAt(this.colIndexById(id));
   }

   colTitleAt(index) {
      return this.#colMetadata[index].title;
   }

   colUnit(id) {
      return this.colUnitAt(this.colIndexById(id));
   }

   colUnitAt(index) {
      return this.#colMetadata[index].units;
   }

   colTitleAndUnit(id, sep = " ") {
      return this.colTitleAndUnitAt(this.colIndexById(id), sep);
   }

   colTitleAndUnitAt(index, sep = " ") {
      return this.#colMetadata[index]?.units
         ? `${this.#colMetadata[index].title}${sep}[${this.#colMetadata[index].units}]`
         : this.#colMetadata[index].title;
   }

   colDecltype(id) {
      return this.colDecltypeAt(this.colIndexById(id));
   }

   colDecltypeAt(index) {
      return this.#colMetadata[index].decltype;
   }

   colIsNumeric(id) {
      return this.colIsNumericAt(this.colIndexById(id));
   }

   colIsNumericAt(index) {
      return ["int", "double"].includes(this.#colMetadata[index]?.decltype);
   }

   listColIds(filter) {
      if (Array.isArray(filter)) {
         const a = [...filter];
         filter = item => { return a.includes(item); };
      }
      return filter ? this.#colIds.filter(filter) : this.#colIds;
   }

   // column Data

   colData(id, selRows = undefined) {
      return this.colDataAt(this.colIndexById(id), selRows);
   }

   colDataAt(index, selRows = undefined) {
      if (typeof selRows === "undefined")
         return this.#dataColumns[index];
      else
         return selRows.map(rowIndex => this.#dataColumns[index][rowIndex]);
   }

   colDataStats(id, stats = ["mean"]) {
      return this.colDataStatsAt(this.colIndexById(id), stats);
   }

   colDataStatsAt(index, stats = ["mean"]) {
      let ret = stats.map(item => null);
      const all = this.colDataAt(index);
      const data = all.filter(item => item !== null);
      const isNumeric = this.colIsNumericAt(index);
      const statslc = stats.map(item => item.toLocaleLowerCase());
      let n, m1, m2, min, max;
      for (let i = 0; i < statslc.length; i++) {
         const stat = statslc[i];
         if (stat === "total") {
            ret[i] = all.length;
         }
         if (stat === "count") {
            ret[i] = data.length;
         }
         if (isNumeric && 0 < data.length && stat === "min") {
            min = Math.min(...data);
            ret[i] = min;
         }
         if (isNumeric && 0 < data.length && stat === "max") {
            max = Math.max(...data);
            ret[i] = max;
         }
         if (isNumeric && 0 < data.length && stat === "sum") {
            if (typeof n === "undefined")
               n = data.length;
            if (typeof m1 === "undefined")
               m1 = data.reduce((p, c) => p + c, 0);
            ret[i] = m1;
         }
         if (isNumeric && 0 < data.length && stat === "mean") {
            if (typeof n === "undefined")
               n = data.length;
            if (typeof m1 === "undefined")
               m1 = data.reduce((p, c) => p + c, 0);
            ret[i] = m1 / n;
         }
         if (isNumeric && 1 < data.length && stat === "stdev") {
            if (typeof n === "undefined")
               n = data.length;
            if (typeof m1 === "undefined")
               m1 = data.reduce((p, c) => p + c, 0);
            if (typeof m2 === "undefined")
               m2 = data.reduce((p, c) => p + c * c, 0);
            ret[i] = Math.sqrt(m2 / n - (m1 / n) ** 2) * (n / (n - 1));
         }
      }
      return ret;
   }

   columnHistogramEquidistantBins(id, bincnt, min = undefined, max = undefined, selRows = undefined) {
      return this.columnHistogramEquidistantBinsAt(this.colIndexById(id), bincnt, min, max, selRows);
   }

   columnHistogramEquidistantBinsAt(index, bincnt, min = undefined, max = undefined, selRows = undefined, cumulative = false, normalized = false) {
      const values = this.colDataAt(index);
      const selvalues = selRows ? this.colDataAt(index, selRows).filter(item => Number.isFinite(item)) : [];

      const finiteValues = values.filter(item => Number.isFinite(item));
      if (typeof min === "undefined")
         min = Math.min(...finiteValues);
      if (typeof max === "undefined")
         max = Math.max(...finiteValues);

      if (max === min) {
         const cnt = values.filter(x => isFinite(x)).length;
         const sel = selvalues.filter(x => isFinite(x)).length;
         return { cnt: [cnt], sel: [sel], los: [min], his: [max], rows: [[...limRange(values.length)]] };
      }
      else {
         const rng = max - min;
         const bin = rng / bincnt;
         const offset = 0;//bin * 0.05;
         const [sel, cnt, los, his, rows] = [[], [], [], [], []];

         for (let i = 0; i < bincnt; i++) {
            sel.push(0);
            cnt.push(0);
            rows.push([]);
            los.push(min + bin * i + offset);
            his.push(min + bin * (i + 1) - offset);
         }

         for (let i = 0; i < values.length; i++) {
            if (!Number.isFinite(values[i]))
               continue;
            const bin = 0 !== rng ? Math.floor((bincnt - 0.5) * (values[i] - min) / rng) : 0;
            if (bin < bincnt) {
               cnt[bin]++;
               rows[bin].push(i);
            }
         }
         for (let v of selvalues) {
            if (!Number.isFinite(v))
               continue;
            sel[0 !== rng ? Math.floor((bincnt - 0.5) * (v - min) / rng) : 0]++;
         }
         if(cumulative)
            for(let i=1; i<rows.length; i++)
               rows[i] = rows[i].concat(rows[i-1]); 

         let sum = 0;
         let sum_sel = 0;
         if(normalized && !cumulative)
            for(let i=0; i< cnt.length; i++) {
               sum += cnt[i];
               sum_sel += sel;[i];
            }
         if(cumulative)
            for(let i=0; i< cnt.length; i++) {
               sum += cnt[i];
               sum_sel += sel[i]; 
               cnt[i] = sum;
               sel[i] = sum_sel;
            }
         if(normalized)
            for(let i=0; i< cnt.length; i++) {
               cnt[i] /= sum;
               sel[i] /= sum;
            }

         return { cnt: cnt, sel: sel, los: los, his: his, rows: rows };
      }
   }

   columnHistogramEnumeration(id, labels = undefined, selRows = undefined) {
      return this.columnHistogramEnumerationAt(this.colIndexById(id), labels, selRows)
   }

   columnHistogramEnumerationAt(index, labels = undefined, selRows = undefined) {
      const values = this.colDataAt(index);
      const selvalues = selRows ? this.colDataAt(index, selRows) : [];

      if (typeof labels === "undefined") {
         labels = [...new Set(values)];
         labels.sort();
      }

      const [sel, cnt, rows] = [[], [], []];
      for (let i = 0; i < labels.length; i++) {
         sel.push(selvalues.filter(item => item === labels[i]).length);
         cnt.push(values.filter(item => item === labels[i]).length);
         rows.push(values.map((item, index) => item === labels[i] ? index : -1).filter(item => 0 <= item));
      }

      return { cnt: cnt, sel: sel, labels: labels.map(item => String(item)), rows: rows };
   }

   rowsToObj(rows, colids) {
      if (!rows)
         rows = [...limRange(this.rowCount)];
      if (!colids)
         colids = this.#colIds;
      const indices = colids.map(id => { return this.#colIds.indexOf(id); });
      return rows.map(i => {
         return Object.fromEntries(indices
            .map(j => { return 0 <= j ? [this.#colIds[j], this.#dataColumns[j][i]] : null; })
            .filter(item => { return !!item; }));
      });
   }

   selectionToRows(selrows) {
      let retSel = [];
      if (selrows.length === 0)
         return retSel;
      const refRows = this.rowsToObj([...limRange(this.rowCount)], this.listIdentColIds()).map(item => { return JSON.stringify(item); })
      for (let r of selrows) {
         retSel.push(refRows.indexOf(JSON.stringify(r)));
      }
      retSel.sort();
      return retSel.filter(item => { return 0 <= item; });
   }
}

function* limRange(a, b, c) {
   const start = typeof a === "number" && typeof b === "number" ? a : 0;
   const stop = typeof a === "number" && typeof b === "number" ? b : a;
   const step = typeof c === "number" ? c : 1;
   for (let curr = start; curr < stop; curr += step)
      yield curr;
}
