const itemHeight = 20;
const extraItemCount = 2;
const rootGroupId = "4813494d137e1631bba301d5acab6e7bb7aa74ce1185d456565ef51d737677b2";
const tailColId = "0c62f876ef1dea830de9f32c2f4b46dd6d74d50d15896e09ef5a2fcd4ac7e1d7";

const _set_equality = (xs, ys) => xs.size === ys.size && [...xs].every((x) => ys.has(x));

class LimTreeViewBase extends HTMLElement {
    #headerContainer
    #treeContainer
    #sizeObserver

    #header
    #nesting
    #selectionMode
    #groupSelectionMode
    #src
    #stickyCols
    #tailCol

    #coldefs
    #colsizes
    #contentWidth
    #stickyColumnIndex

    #items
    #itemCount
    #itemGroupDepth
    #maxItemGroupDepth

    #groups
    #openGroups
    #openGroupsDefault

    #currentGroupId
    #currentItemId
    #selectedItemIds
    #selAnchorItemId

    #highlightedItemIds

    static SELECTION_NONE = 0;
    static SELECTION_SINGLE = 1;
    static SELECTION_MULTIPLE = 2;

    static observedAttributes = [ "group-selection-mode", "header", "nesting", "selection-mode", "src", "sticky-cols", "tail-col" ];

    constructor() {
        super();
    }

   get persistentState() {
      return JSON.stringify({ openGroups: Array.from(this.#openGroups.entries()).splice(0, 500), openGroupsDefault: this.#openGroupsDefault });
   }

   set persistentState(val) {
      const state = JSON.parse(val);
      this.#openGroups = new Map(state?.openGroups ?? []);
      this.#openGroupsDefault = state?.openGroupsDefault ?? true;
   }

    get header() {
        return (this.#header ?? true) ? "normal" : "none";
    }

    set header(val) {
        if (val === "normal" || val === 1 || val === true)
            val = true;
        else
            val = false;

        if (this.#header !== val) {
            this.#header = val;
            this.render();
        }
    }

    get isHeaderVisible() {
        return this.#header ?? true;
    }

    get nesting() {
        return `${this.nestingWidth}`;
    }

    set nesting(val) {
        if (typeof val === "string")
            val = parseInt(val);
        else if (typeof val === "number" && 0 <= val)
            ;
        else
            val = 0;

        if (this.#nesting !== val) {
            this.#nesting = val;
            this._resetColSizes();
            this.render();
        }
    }

    get nestingWidth() {
        return this.#nesting ?? 0;
    }

    get selectionMode() {
        return ["none", "single", "multiple"][this.#selectionMode ?? 0];
    }

    set selectionMode(val) {
        if (typeof val === "string") {
            val = val.toLowerCase();
            if (val === "multiple")
                val = LimTreeViewBase.SELECTION_MULTIPLE;
            else if (val === "single")
                val = LimTreeViewBase.SELECTION_SINGLE;
            else
                val = LimTreeViewBase.SELECTION_NONE;
        }
        else if (typeof val === "number" && 0 <= val && val < 3) {
        }
        else {
            val = LimTreeViewBase.SELECTION_NONE;
        }

        if (this.#selectionMode === val)
            return;

        this.#selectionMode = val;
        this.render();
    }

    get groupSelectionMode() {
        return ["none", "single", "multiple"][this.#groupSelectionMode ?? 0];
    }

    set groupSelectionMode(val) {
        if (typeof val === "string") {
            val = val.toLowerCase();
            if (val === "multiple")
                val = LimTreeViewBase.SELECTION_MULTIPLE;
            else if (val === "single")
                val = LimTreeViewBase.SELECTION_SINGLE;
            else
                val = LimTreeViewBase.SELECTION_NONE;
        }
        else if (typeof val === "number" && 0 <= val && val < 3) {
        }
        else {
            val = LimTreeViewBase.SELECTION_NONE;
        }

        if (this.#groupSelectionMode === val)
            return;

        this.#groupSelectionMode = val;
        this.render();
    }

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

    set src(val) {
        if (this.#src === val)
            return

        this.#src = val;
        this.update();
    }

    get stickyCols() {
        return `${this.#stickyCols ?? 0}`;
    }

    set stickyCols(val) {
        if (typeof val === "string")
            val = parseInt(val);
        else if (typeof val === "number" && 0 <= val)
            ;
        else
            val = 0;

        if (this.#stickyCols !== val) {
            this.#stickyCols = val;
            this._resetColSizes();
            this.render();
        }
    }

    get numberOfStickyCols() {
        return this.#stickyCols ?? 0;
    }

    get tailCol() {
        return (this.#tailCol ?? true) ? "show" : "hide";
    }

    set tailCol(val) {
        if (val === "show" || val === 1 || val === true)
            val = true;
        else
            val = false;

        if (this.#tailCol !== val) {
            this.#tailCol = val;
            if (0 < this.#coldefs?.length && this.#coldefs[this.#coldefs.length-1].id === tailColId) {
                this._resetColSizes();
                this.#coldefs[this.#coldefs.length-1].visible = this.#tailCol;
                this.render();
            }
        }
    }

    get isTailColVisible() {
        return this.#tailCol ?? true;
    }

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

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

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

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

    findItem(id) {
        const ret = this.#items.filter(item => item.id === id);
        return ret.length ? ret[0] : null;
    }

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

    findGroup(id) {
        const ret = this.#groups.filter(group => group.id === id);
        return ret.length ? ret[0] : null;
    }

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

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

    get currentGroup() {
        const ret = this.#groups.filter(group => group.id === this.#currentGroupId);
        return ret.length ? ret[0] : null;
    }

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

    set currentGroupId(val) {
        if (this.#currentGroupId)
            this._remClassFromElements([this.#currentGroupId], "current");

        this.#currentGroupId = val;

        if (this.#currentGroupId) {
            this._addClassToElements([this.#currentGroupId], "current");
        }

        this?.currentGroupIdChanged?.();

        if (this.#currentGroupId) {
            this.selectedItemIds = [];
            this.currentItemId = null;
        }
    }

    get currentItem() {
        const ret = this.#items.filter(item => item.id === this.#currentItemId);
        return ret.length ? ret[0] : null;
    }

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

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

    set currentItemId(val) {
        if (this.#currentItemId)
            this._remClassFromElements([ this.#currentItemId ], "current");

        this.#currentItemId = val;

        if (this.#currentItemId) {
            this._addClassToElements([ this.#currentItemId ], "current");
        }

        this?.currentItemIdChanged?.();

        if (this.#currentItemId) {
            this.currentGroupId = null;
        }
    }

    get selectedItems() {
        if (this.#selectedItemIds instanceof Set && 0 < this.#selectedItemIds.size) {
            return this.#items.filter(item => this.#selectedItemIds.has(item.id));
        }
        else {
            return [];
        }
    }

    get selectedItemIds() {
        if (this.#selectedItemIds instanceof Set && 0 < this.#selectedItemIds.size) {
            return [...this.#selectedItemIds.values()];
        }
        else {
            return [];
        }
    }

    set selectedItemIds(val) {
        const newSet = Array.isArray(val) ? new Set(val) : new Set();
        if (this.#selectedItemIds instanceof Set && _set_equality(newSet, this.#selectedItemIds))
            return;

        if (this.#selectedItemIds instanceof Set)
            this._remClassFromElements(this.#selectedItemIds.values(), "selected");

        this.#selectedItemIds = newSet;
        this._addClassToElements(this.#selectedItemIds.values(), "selected");

        this?.selectedItemChanged?.();

        if (this.#selectedItemIds?.size) {
            this.currentGroupId = null;
        }
    }

    toggleSelectedItemId(val) {
        if (this.#selectedItemIds.has(val)) {
            this._remClassFromElements([ val ], "selected");
            this.#selectedItemIds.delete(val);
        }
        else {
            this._addClassToElements([ val ], "selected");
            this.#selectedItemIds.add(val);
        }

        this?.selectedItemChanged?.();

        if (this.#selectedItemIds?.size) {
            this.currentGroupId = null;
        }
    }

    selectRangeOfIds(val) {
        if (!this.#items)
            return;

        let i = 0;
        let endId = null;
        const anchor = this.#selAnchorItemId ?? this.#items[0]?.id;
        for (; i < this.#items.length; i++) {
            const id = this.#items[i].id;
            if (id === anchor)
                endId = val;
            if (id === val)
                endId = anchor;
            if (endId)
                break;
        }
        const newlyAddedIds = [];
        for (; i < this.#items.length; i++) {
            const id = this.#items[i].id;
            newlyAddedIds.push(id);
            if (id === endId)
                break;
        }
        this.selectedItemIds = newlyAddedIds;

        if (this.#selectedItemIds?.size) {
            this.currentGroupId = null;
        }
    }

    toggleRangeOfIds(val) {
        if (!this.#items)
            return;

        let i = 0;
        let endId = null;
        const anchor = this.#selAnchorItemId ?? this.#items[0]?.id;
        for (; i < this.#items.length; i++) {
            const id = this.#items[i].id;
            if (id === anchor)
                endId = val;
            if (id === val)
                endId = anchor;
            if (endId)
                break;
        }
        const ids = [];
        const select  = this.#selectedItemIds.has(anchor);
        for (; i < this.#items.length; i++) {
            const id = this.#items[i].id;
            const selected = this.#selectedItemIds.has(id);
            if (selected !== select)
                ids.push(id);
            if (id === endId)
                break;
        }
        if (select) {
            for (let id of ids)
                this.#selectedItemIds.add(id);
            this._addClassToElements(ids, "selected");
        }
        else {
            for (let id of ids)
                this.#selectedItemIds.delete(id);
            this._remClassFromElements(ids, "selected");
        }

        if (this.#selectedItemIds?.size) {
            this.currentGroupId = null;
        }
    }

    get highlightedItems() {
        if (this.#highlightedItemIds instanceof Set && 0 < this.#highlightedItemIds.size) {
            return this.#items.filter(item => this.#highlightedItemIds.has(item.id));
        }
        else {
            return [];
        }
    }

    get highlightedItemIds() {
        if (this.#highlightedItemIds instanceof Set && 0 < this.#highlightedItemIds.size) {
            return [...this.#highlightedItemIds.values()];
        }
        else {
            return [];
        }
    }

    set highlightedItemIds(val) {
        const newSet = Array.isArray(val) ? new Set(val) : new Set();
        if (this.#highlightedItemIds instanceof Set && _set_equality(newSet, this.#highlightedItemIds))
            return;

        if (this.#highlightedItemIds instanceof Set)
            this._remClassFromElements(this.#highlightedItemIds.values(), "highlighted");

        this.#highlightedItemIds = newSet;
        this._addClassToElements(this.#highlightedItemIds.values(), "highlighted");
    }

    setOpenGroup(group, state) {
        this.#openGroups.set(group, state);
    }

    get colBorder() {
        return 1;
    }

    get colPadding() {
        return 4;
    }

    get headerHeight() {
        return itemHeight + 2;
    }

    get calculatedWidth() {
        const style = window.getComputedStyle(this);
        return style.width.match(/\d+px/) ? parseInt(style.width) : -1;
    }

    get calculatedHeight() {
        const style = window.getComputedStyle(this);
        return style.height.match(/\d+px/) ? parseInt(style.height) : -1;
    }

    get contentWidth() {
        const scrollbar = this.calculatedHeight < this.contentHeight ? 17 : 0;
        return this.#colsizes === null ? this.calculatedWidth - scrollbar : this.#contentWidth;
    }

    get contentHeight() {
        return (this._countItemsInOpenGroups() + this._countVisibleGroups()) * itemHeight;
    }

    get visibleColRange() {
        let x = 0;
        let start = this.#colsizes.length;
        let end = 0;
        let count = 0;
        const left = Math.floor(this.scrollLeft);
        const right = Math.ceil(this.scrollLeft + this.clientWidth);
        for (let i = 0; i < this.#colsizes.length; i++) {
            if (this.#coldefs[i].visible) {
                const w = this.#colsizes[i].width;
                if (left < x + w && i < start) {
                    start = i;
                }
                if (x < right && end < i) {
                    end = i;
                }
                x += w;
                count++;
            }
        }
        return [ Math.max(0, start - extraItemCount), Math.min(end + extraItemCount, this.#colsizes.length) ];
    }

    get visibleColCount() {
        return this.#coldefs.filter(item => item.visible).length;
    }

    connectedCallback() {
        this.#coldefs = [];
        this.#items = [];
        this.#itemCount = 0;
        this.#itemGroupDepth = [];
        this.#maxItemGroupDepth = 0;
        this.#groups = [];
        this.#selectedItemIds = new Set();
        this._resetColSizes();
        this.style.display = "block";

        if (typeof this.#openGroups == "undefined")
           this.#openGroups = new Map();
        if (typeof this.#openGroupsDefault == "undefined")
           this.#openGroupsDefault = true;

        if (!this.#treeContainer) {
            const shadow = this.attachShadow({ mode: "open" });

            const sheet = new CSSStyleSheet();
            const color_mid = "var(--color-mid)";
            const color_window = "var(--color-window)";
            const color_base = "var(--color-base)";
            const color_accent = "var(--color-accent)";
            const color_altbase = "var(--color-altbase)";
            const color_highlight = "var(--color-highlight)";
            const color_text_disabled = "var(--color-text-disabled)";

            const border = `${this.colBorder}px solid ${color_mid}`;
            sheet.insertRule(`*, *::before, *::after { box-sizing: border-box; }`, sheet.cssRules.length);
            sheet.insertRule(`.header-container { position: sticky; top: 0; width: 100%; height: ${this.headerHeight}px; z-index: 2; }`, sheet.cssRules.length);
            sheet.insertRule(`.tree-container { position: relative; width: 100%; height: 100%; }`, sheet.cssRules.length);
            sheet.insertRule(`.row { position: absolute; background-color: ${color_base}; }`, sheet.cssRules.length);
            sheet.insertRule(`.group.current { background-color: ${color_highlight} !important; outline: 1px solid var(--tv-current-color) !important; outline-offset: -1px; }`, sheet.cssRules.length);
            sheet.insertRule(`span.cell { padding: ${this.colPadding}px; user-select: none; cursor: default; -webkit-user-select: none; position: absolute; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; -webkit-font-smoothing: subpixel-antialiased; }`, sheet.cssRules.length);
            sheet.insertRule(`input { padding: 2px; cursor: beamer; position: absolute; font-family: inherit; font-size: inherit; outline: none; background-color: ${color_base}; color: var(--color-text); }`, sheet.cssRules.length);
            sheet.insertRule(`input.group { border: 1px solid var(--tv-current-color); }`, sheet.cssRules.length);
            sheet.insertRule(`input.cell { border: none; }`, sheet.cssRules.length);
            sheet.insertRule(`.row.current { outline: 1px solid var(--tv-current-color) !important; outline-offset: -1px; }`, sheet.cssRules.length);
            sheet.insertRule(`.row.selected { background-color: ${color_highlight} !important; }`, sheet.cssRules.length);
            sheet.insertRule(`.row.dragging { filter: opacity(66%) !important; }`, sheet.cssRules.length);
            sheet.insertRule(`.row.highlighted { color: var(--tv-highlight-color); font-weight: var(--tv-highlight-weight) !important; }`, sheet.cssRules.length);
            sheet.insertRule(`.row.disabled .cell { color: ${color_text_disabled} !important; }`, sheet.cssRules.length);
            sheet.insertRule(`[visuals~="hover"] .row:not(.disabled):not(.selected):hover { filter: var(--tv-hover-filter); }`, sheet.cssRules.length);
            // simple theme
            // default theme
            sheet.insertRule(`[theme=""] .header { border-top: ${border}; border-bottom: ${border}; background-color: ${color_window}; }`, sheet.cssRules.length);
            sheet.insertRule(`[theme=""] .group { border-bottom: ${border}; background-color: ${color_window}; }`, sheet.cssRules.length);
            sheet.insertRule(`[theme=""] .data { background-color: ${color_base}; }`, sheet.cssRules.length);
            sheet.insertRule(`[theme=""] .odd { background-color: ${color_altbase}; }`, sheet.cssRules.length);
            sheet.insertRule(`[theme=""] .lborder { border-left: ${border}; }`, sheet.cssRules.length);
            sheet.insertRule(`[theme=""] .rborder { border-right: ${border}; }`, sheet.cssRules.length);
            shadow.adoptedStyleSheets = [sheet];


            this.#headerContainer = createHeaderContainer();
            this.#headerContainer.setAttribute("theme", this.getAttribute("theme") ?? "");
            shadow.appendChild(this.#headerContainer);

            this.#treeContainer = createTreeContainer();
            this.#treeContainer.setAttribute("visuals", this.getAttribute("visuals") ?? "");
            this.#treeContainer.setAttribute("theme", this.getAttribute("theme") ?? "");


            shadow.appendChild(this.#treeContainer);

            this.#headerContainer.ondragenter = this.#treeContainer.ondragenter
            = this.#headerContainer.ondragleave = this.#treeContainer.ondragleave
            = this.#headerContainer.ondragover = this.#treeContainer.ondragover
            = shadow.ondragenter = shadow.ondragleave = shadow.ondragover = event => {
                event.preventDefault();
                //event.stopPropagation();
                event.dataTransfer.dropEffect = "none";
            };
        }

        if (!this.#sizeObserver) {
            this.#sizeObserver = new ResizeObserver((entries) => {
                this._resetColSizes();
                this.render();
            });
            this.#sizeObserver.observe(this);
        }

        this.update();
    }

    disconnectedCallback() {
        if (this.#sizeObserver) {
            this.#sizeObserver.disconnect()
            this.#sizeObserver = null;
        }

        if (this.#treeContainer) {
            this.removeChild(this.#treeContainer);
            this.#treeContainer = null;
        }
    }

    adoptedCallback() {
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "group-selection-mode") {
            this.groupSelectionMode = newValue;
        }
        else if (name === "header"){
            this.header = newValue;
        }
        else if (name === "nesting"){
            this.nesting = newValue;
        }
        else if (name === "selection-mode") {
            this.selectionMode = newValue;
        }
        else if (name === "src") {
            this.src = newValue;
        }
        else if (name === "sticky-cols") {
            this.stickyCols = newValue;
        }
        else if (name === "tail-col") {
            this.tailCol = newValue;
        }
    }

    async update(src_url) {
        if (!document.body.contains(this))
            return;

        if (typeof src_url !== "undefined")
            this.#src = src_url;

        await this.fetchData();

        this?.onDataRecieved?.();

        if (this.#currentItemId && !this.#items.filter(item => item.id === this.#currentItemId).length) {
            this.#currentItemId = null;
            this?.currentItemIdChanged?.();
        }

        if (this.#selectedItemIds instanceof Set && 0 < this.#selectedItemIds.size) {
            this.#selectedItemIds = new Set(this.#items.filter(item => this.#selectedItemIds.has(item.id)).map(item => item.id));
            this?.selectedItemChanged?.();
        }

        if (this.#selAnchorItemId && !this.#items.filter(item => item.id === this.#selAnchorItemId).length) {
            this.#selAnchorItemId = null;
        }

        if (this.#highlightedItemIds instanceof Set && 0 < this.#highlightedItemIds.size) {
            this.#highlightedItemIds = new Set(this.#items.filter(item => this.#highlightedItemIds.has(item.id)).map(item => item.id));
        }

        if (this.#currentItemId || this.#selectedItemIds.size || (this.#currentGroupId && !this.#groups.filter(item => item.id === this.#currentGroupId).length)) {
            this.#currentGroupId = null;
        }

        this.render();
    }

    async fetchData() {
        if (typeof this?.src !== "string")
            return;

        const data_prefix = "data:application/json;base64,";
        if (this.src.startsWith(data_prefix)) {
            const data = JSON.parse(atob(this.src.substring(data_prefix.length)));
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    this._setData(data);
                    resolve();
                })
            })
        }
        else {
            const response = await fetch(this.src);
            const data = await response.json();
            this._setData(data);
        }
    }

    render() {
    }

    _addClassToElements(id_iterator, cls) {
        if (!id_iterator)
            return;
        const isSelDraggable = this?.isSelectionDraggable?.() ?? false;
        const isCurrDraggable = this?.isCurrentDraggable?.() ?? false;
        for (const id of id_iterator) {
            const selitem = this.shadowRoot.getElementById(id);
            if (selitem) {
                selitem?.classList?.add?.(cls);
                if (isSelDraggable && cls === "selected") {
                    selitem.setAttribute("draggable", "true");
                }
                else if (isCurrDraggable && cls === "current") {
                    selitem.setAttribute("draggable", "true");
                }
            }
        }
    }

    _remClassFromElements(id_iterator, cls) {
        if (!id_iterator)
            return;
        for (const id of id_iterator) {
            const selitem = this.shadowRoot.getElementById(id);
            if (selitem) {
                selitem?.classList?.remove?.(cls);
                if (cls === "selected" || cls === "current") {
                    selitem.removeAttribute("draggable");
                }
            }
        }
    }

    _resetColSizes() {
        this.#colsizes = null;
        this.#contentWidth = 0;
        this.#stickyColumnIndex = -1;
    }

    _setData(data) {
        this.#coldefs = data.coldefs;
        for (let i = 0; i < this.#coldefs.length; i++) {
            const coldef = this.#coldefs[i];
            coldef.visible = !(coldef?.hidden ?? false);
            if (coldef?.fmtfncode) {
                eval(coldef.fmtfncode)(coldef);
            }
            if (coldef?.stylefncode) {
                eval(coldef.stylefncode)(coldef);
            }
        }

        if (0 < this.#coldefs.length && this.#coldefs[this.#coldefs.length-1].id !== tailColId) {
            this.#coldefs.push({ id: tailColId, width:'1fr', style: { padding: "0" }, visible: this.isTailColVisible });
        }

        this.#items = data.rowdata;
        this.#itemCount = this.#items.length;
        this.#groups = data?.groups ?? [ { id: rootGroupId, depth: 0, groupcount: 0, rowcount: this.#itemCount, rows: [ 0, this.#itemCount ] } ];
        this.#groups.forEach(item => item.visible = item.id !== rootGroupId);
        this.#itemGroupDepth = this.#groups.map(group => (0 < group.rowcount ? Array(group.rowcount).fill(group.depth) : [])).reduce((p, c) => p.concat(c));
        this.#maxItemGroupDepth = this.#groups.map(group => (0 < group.rowcount ? group.depth : 0)).reduce((p, c) => Math.max(p, c))
        if (this.#itemGroupDepth.length !== this.#itemCount) {
            throw new Error("Sum group rowcounts is not equal to itemCout!");
        }

        this.#groups.forEach(item => item.open = (this.#openGroups.get(item.id) ?? (0 == item.depth || this.#openGroupsDefault)));

        const groupByIds = new Set(this.#groups.map(group => group?.colid).filter(item => !!item));
        this.#coldefs.forEach(item => item.visible = item.visible && !groupByIds.has(item.id));

        this._resetColSizes();
    }

    _countVisibleGroups() {
        let gcount = 0;
        let skipDeeper = Number.MAX_VALUE;
        for (let group of this.#groups) {
            if (!group.visible || skipDeeper < group.depth)
                continue;
            skipDeeper = group.open ? Number.MAX_VALUE : group.depth;
            gcount++;
        }
        return gcount;
    }

    _countItemsInOpenGroups() {
        let rcount = 0;
        let skipDeeper = Number.MAX_VALUE;
        for (let group of this.#groups) {
            if (skipDeeper < group.depth)
                continue;
            if (group.open) {
                rcount += group.rowcount;
                skipDeeper = Number.MAX_VALUE;
            }
            else {
                skipDeeper = group.depth;
            }
        }
        return rcount;
    }

    _updateColSizes() {
        if (!this.#coldefs?.length)
            return false;

        let total_width = this.contentWidth;
        if (total_width < 0)
            return false;

        if (this.#colsizes === null) {
            this.#colsizes = [];
            let viscount = 0;
            for (let j = 0; j < this.#coldefs.length; j++) {
                const colsize = {};
                const coldef = this.#coldefs[j];
                const min_width = parseInt(coldef?.minwidth ?? "0");
                const width_def = coldef?.width ?? "auto";
                const nesting_width = coldef.visible && 0 === viscount ? this.nestingWidth * this.#maxItemGroupDepth : 0;
                if (width_def.match(/\d+px/)) {
                    colsize.type = "px";
                    colsize.width = nesting_width + parseInt(width_def);
                }
                else if (width_def.match(/\d+%/)) {
                    colsize.type = "percent";
                    colsize.percent = parseInt(width_def);
                    colsize.width = Math.max(nesting_width + min_width, Math.floor(colsize.percent * total_width / 100));
                }
                else if (width_def.match(/\d+fr/)) {
                    colsize.type = "fr";
                    colsize.num = parseInt(width_def);
                }
                else {
                    colsize.type = "auto";
                    colsize.header_width = LimGetTextSize(coldef.title, "11px Tahoma").width;
                    colsize.items_max_width = 0;
                    for (let k = 0; k < this.#items.length; k++) {
                        const data = this.#items?.[k]?.[coldef.id];
                        if (typeof data !== "undefined") {
                            const itemText = coldef?.fmtfn?.(data) ?? data;
                            const w = LimGetTextSize(itemText).width;
                            if (colsize.items_max_width < w)
                                colsize.items_max_width = w;
                        }
                    }
                    colsize.items_max_width = nesting_width + colsize.items_max_width;
                    colsize.width = Math.max(min_width, Math.ceil(Math.max(colsize.header_width, colsize.items_max_width))) + 2*this.colBorder + 2*this.colPadding;
                }
                if (coldef.visible) {
                    viscount++;
                }
                this.#colsizes.push(colsize);
            }
        }

        let fr_denom = 0;
        let fr_width = total_width;
        for (let j = 0; j < this.#coldefs.length; j++) {
            const coldef = this.#coldefs[j];
            const colsize = this.#colsizes[j];
            if (coldef.visible) {
                if (colsize.type === "fr") {
                    fr_denom += colsize?.num ?? 0;
                }
                else {
                    fr_width -= colsize?.width ?? 0;
                }
            }
        }

        this.#contentWidth = 0;
        this.#stickyColumnIndex = -1;
        for (let i = 0; i < this.#coldefs.length; i++) {
            const coldef = this.#coldefs[i];
            const colsize = this.#colsizes[i];
            if (colsize.type === "fr") {
                const min_width = parseInt(coldef?.minwidth ?? "0");
                colsize.width = Math.max(min_width, Math.floor(fr_width * colsize.num / fr_denom));
            }
            if (coldef.visible) {
                this.#contentWidth += colsize.width;
                if (0 < this.numberOfStickyCols && this.#stickyColumnIndex < 0) {
                    this.#stickyColumnIndex = i;
                }
            }
        }

        return true;
    }

    _createHeaderRowItem(text, style, width, height) {
        const el =  createTreeRowItem(width, height);
        el.textContent = text;
        for (let s of Object.getOwnPropertyNames(style))
            el.style[s] = style[s];
        return el;
    }

    _createHeaderRow(start, end) {
        const el =  document.createElement("div");
        el.classList.add("header");
        el.style.flex = "initial";
        el.style.position = "absolute";
        el.style.height = `${this.headerHeight}px`;
        el.style.top = 0;

        let viscount = 0;
        let totalWidth = 0;
        for (let i = 0; i < this.#coldefs.length; i++) {
            const coldef = this.#coldefs[i];
            const itemWidth = this.#colsizes[i].width;
            if (coldef.visible && 0 < itemWidth) {
                if (start <= i && i < end && this.#stickyColumnIndex !== i) {
                    const itemStyle = coldef?.headerStyle ?? {};
                    const elItem = this._createHeaderRowItem(coldef?.title ?? "", itemStyle, itemWidth, itemHeight);
                    if (this.#stickyColumnIndex + 1 < i)
                        elItem.classList.add("lborder");
                    elItem.style.left = `${totalWidth}px`;
                    elItem.style.top = 0;
                    el.appendChild(elItem);
                }
                totalWidth += itemWidth;
                viscount++;
            }
        }
        if (0 <= this.#stickyColumnIndex) {
            const coldef = this.#coldefs[this.#stickyColumnIndex];
            const itemWidth = this.#colsizes[this.#stickyColumnIndex].width;
            const itemStyle = coldef?.headerStyle ?? {};
            const elItem = this._createHeaderRowItem(coldef?.title ?? "", itemStyle, itemWidth, itemHeight);
            if (1 < this.visibleColCount)
                elItem.classList.add("rborder");
            elItem.style.background = "inherit";
            elItem.style.left = `${this.scrollLeft}px`;
            elItem.style.top = 0;
            el.appendChild(elItem);
        }

        el.style.width = `${this.contentWidth}px`;
        return el;
    }

    _createDataRowItem(text, style, width, height) {
        const el =  createTreeRowItem(width, height);
        el.textContent = text;
        for (let s of Object.getOwnPropertyNames(style))
            el.style[s] = style[s];
        return el;
    }

    _createDataRow(y, itemIndex, start, end) {
        const item = this.#items[itemIndex];
        const elRow = createTreeRow(this.contentWidth, itemHeight);
        elRow.classList.add("data");
        elRow.classList.add(itemIndex % 2 === 0 ? "even" : "odd")
        if (typeof item?.id !== "undefined")
            elRow.id = `${item.id}`;
        elRow.style.left = 0;
        elRow.style.top = `${y}px`;

        if (typeof this?.onRowDoubleClicked === "function") {
            elRow.ondblclick = (event) => {
                this?.onRowDoubleClicked?.(event, item);
            }
        }

        if (typeof this.onItemContextMenu === "function") {
            elRow.oncontextmenu = (event) => {
                event.preventDefault()
                this?.onItemContextMenu?.(event, item);
            }
        }

        if (item?.disabled ?? false) {
            elRow.classList.add("disabled");
        }

        else if (this.#selectionMode) {

            if (this.#selectionMode === LimTreeViewBase.SELECTION_SINGLE) {
                elRow.onmouseup = elRow.onmousedown = (event) => {
                    const thisItemIsSelected = this.selectedItemIds.includes(item.id);
                    if (event.type === "mousedown" && thisItemIsSelected || 0 != event.button && thisItemIsSelected) {
                        return;
                    }
                    this.#selAnchorItemId = item.id;
                    this.selectedItemIds = [ item.id ];
                    this.currentItemId = item.id;
                };
            }

            else {
                elRow.onmouseup = elRow.onmousedown = (event) => {
                    const thisItemIsSelected = this.selectedItemIds.includes(item.id);
                    if (event.ctrlKey && event.type === "mousedown" || event.type === "mousedown" && thisItemIsSelected || 0 != event.button && thisItemIsSelected) {
                        if (event.type === "mousedown" && thisItemIsSelected)
                            this.currentItemId = item.id;
                        return;
                    }
                    if (event.shiftKey && !event.ctrlKey) {
                        this.selectRangeOfIds(item.id);
                    }
                    else if (!event.shiftKey && event.ctrlKey) {
                        this.#selAnchorItemId = item.id;
                        this.toggleSelectedItemId(item.id);
                    }
                    else if (event.shiftKey && event.ctrlKey) {
                        this.toggleRangeOfIds(item.id);
                    }
                    else {
                        this.#selAnchorItemId = item.id;
                        this.selectedItemIds = [ item.id ];
                    }

                    if (this.currentItemId === item.id && event.target.tagName === "SPAN") {
                        this?.onRowCurrentClicked?.(event, item);
                    }

                    this.currentItemId = item.id;
                };
            }

            const selDraggable = this?.isSelectionDraggable?.() ?? false;
            const currDraggable = this?.isCurrentDraggable?.() ?? false;
            if ((selDraggable || currDraggable) && typeof this?.onSelectionStartDrag === "function") {
                elRow.ondragstart = (event) => {
                    if (!selDraggable)
                        this.selectedItemIds = [this.currentItemId];
                    this._addClassToElements(this.selectedItemIds, "dragging");
                    this.onSelectionStartDrag(event);
                };
                elRow.ondragend = (event) => {
                    this._remClassFromElements(this.selectedItemIds, "dragging");
                };
            }


            if (this.#currentItemId === item.id) {
                elRow.classList.add("current");
            }

            if (this.#selectedItemIds.has(item.id)) {
                elRow.classList.add("selected");
                elRow.setAttribute("draggable", "true");
            }

            if (this.#highlightedItemIds?.has?.(item.id)) {
                elRow.classList.add("highlighted");
            }
        }

        let viscount = 0;
        let totalWidth = 0;
        for (let i = 0; i < this.#coldefs.length; i++) {
            const coldef = this.#coldefs[i];
            const itemWidth = this.#colsizes[i].width;
            if (coldef.visible && 0 < itemWidth) {
                if (start <= i && i < end && this.#stickyColumnIndex !== i) {
                    const data = item?.[coldef.id];
                    const itemText = typeof data === "undefined" ? "" : (coldef?.fmtfn?.(data) ?? data);
                    const itemStyle = { ...(coldef?.style ?? {}) };
                    const itemStyle2 = typeof data === "undefined" ? {} : (coldef?.stylefn?.(data) ?? {});
                    for (let s of Object.getOwnPropertyNames(itemStyle2))
                        itemStyle[s] = itemStyle2[s];
                    const elItem = this._createDataRowItem(itemText, itemStyle, itemWidth, itemHeight);
                    if (typeof item?.id !== "undefined" && typeof coldef?.id !== "undefined")
                        elItem.id = `${item.id}-${coldef.id}`;
                    if (this.#stickyColumnIndex + 1 < i)
                        elItem.classList.add("lborder");
                    if (0 === viscount && this.nestingWidth)
                        elItem.style.paddingLeft = `${this.nestingWidth * this.#itemGroupDepth[itemIndex] + this.colPadding}px`;
                    const tooltip = item?.[coldef?.tooltip];
                    if (tooltip)
                        elItem.title = tooltip;
                    elItem.style.left = `${totalWidth}px`;
                    elItem.style.top = 0;
                    elRow.appendChild(elItem);
                }
                totalWidth += itemWidth;
                viscount++;
            }
        }
        if (0 <= this.#stickyColumnIndex) {
            const coldef = this.#coldefs[this.#stickyColumnIndex];
            const itemWidth = this.#colsizes[this.#stickyColumnIndex].width;
            const data = item?.[coldef.id];
            const itemText = typeof data === "undefined" ? "" : (coldef?.fmtfn?.(data) ?? data);
            const itemStyle = { ...(coldef?.style ?? {}) };
            const itemStyle2 = typeof data === "undefined" ? {} : (coldef?.stylefn?.(data) ?? {});
            for (let s of Object.getOwnPropertyNames(itemStyle2))
                itemStyle[s] = itemStyle2[s];
            const elItem = this._createDataRowItem(itemText, itemStyle, itemWidth, itemHeight);
            if (typeof item?.id !== "undefined" && typeof coldef?.id !== "undefined")
                elItem.id = `${item.id}-${coldef.id}`;
            if (1 < this.visibleColCount)
                elItem.classList.add("rborder");
            if (this.nestingWidth)
                elItem.style.paddingLeft = `${this.nestingWidth * this.#itemGroupDepth[itemIndex] + this.colPadding}px`;
            elItem.style.background = "inherit";
            elItem.style.left = `${this.scrollLeft}px`;
            elItem.style.top = 0;
            elRow.appendChild(elItem);
        }

        return elRow;
    }

    _createGroupRow(y, groupIndex) {
        const elRow = createTreeRow(this.contentWidth, itemHeight);
        const group = this.#groups[groupIndex];
        elRow.classList.add("group");
        elRow.classList.add(`depth-${group.depth}`);
        if (typeof group?.id !== "undefined") {
            elRow.id = `${group.id}`;

            if (typeof this.onGroupContextMenu === "function") {
                elRow.oncontextmenu = (event) => {
                    event.preventDefault()
                    this?.onGroupContextMenu?.(event, group);
                }
            }

            elRow.onauxclick = elRow.onclick = (event) => {
                if (this.#groupSelectionMode) {
                  this.currentGroupId = group.id;
               }
               if (event.type == "click") {
                  if (event.shiftKey) {
                     const open = !group.open;
                     if (1 == group.depth) {
                        this.#openGroupsDefault = open;
                     }
                     for (let g of this.#groups) {
                        if (group.depth <= g.depth) {
                           g.open = open;
                           this.#openGroups.set(g.id, g.open);
                        }
                     }
                  }
                  else {
                     group.open = !group.open;
                     this.#openGroups.set(group.id, group.open);
                  }
                  this?.persistentStateChanged?.();
                  this.render();
               }
            };
            if ((this?.isGroupDropTarget?.(group) ?? false)
                && typeof this.onGroupDataDragOver === "function"
                && typeof this.onGroupDataDropped === "function") {
                elRow.ondragover = (event) => {
                    event.preventDefault();
                    this?.onGroupDataDragOver?.(event, group);
                }
                elRow.ondrop = (event) => {
                    event.preventDefault();
                    this?.onGroupDataDropped?.(event, group);
                }
            }
        }
        elRow.style.left = 0;
        elRow.style.top = `${y}px`;

        if (group.id == this.currentGroupId) {
            elRow.classList.add("current");
        }

        const title = `${group.rowcount + group.groupcount ? (group.open ? "▼" : "►") : "▬" } ${typeof this?.formatGroupTitle === "function" ? this?.formatGroupTitle(group) : (group?.title ?? "")}`
        const elItem = this._createDataRowItem(title, {}, this.clientWidth, itemHeight);
        elItem.style.paddingLeft = `${this.nestingWidth * (group.depth - 1) + this.colPadding}px`;
        elItem.style.left = `${this.scrollLeft}px`;
        elItem.style.top = 0;
        elRow.appendChild(elItem);
        return elRow;
    }

    _render_rows(colStart, colEnd) {
        let pxcount = 0;
        let skipDeeper = Number.MAX_VALUE;
        const view_top = Math.floor(this.scrollTop - extraItemCount * itemHeight);
        const view_bottom = Math.ceil(view_top + this.clientHeight + 2 * extraItemCount * itemHeight);
        for (let i = 0; i < this.#groups.length; i++) {
            const group = this.#groups[i];
            if (skipDeeper < group.depth)
                continue;
            skipDeeper = group.open ? Number.MAX_VALUE : group.depth;
            if (group.visible) {
                if (view_top <= pxcount + itemHeight) {
                    const row = this._createGroupRow(pxcount, i);
                    this.treeContainer.appendChild(row);
                }
                pxcount += itemHeight;
                if (view_bottom <= pxcount)
                    return;
            }
            if (0 == group.groupcount && group.depth < skipDeeper) {
                if (view_top <= pxcount + group.rowcount * itemHeight &&  pxcount < view_bottom) {
                    for (let j = 0; j < group.rowcount; j++) {
                        if (view_top <= pxcount + itemHeight) {
                            const row = this._createDataRow(pxcount, group.rows[0] + j, colStart, colEnd);
                            this.treeContainer.appendChild(row);
                        }
                        pxcount += itemHeight;
                        if (view_bottom <= pxcount)
                            return;
                    }
                }
                else {
                    pxcount += group.rowcount * itemHeight;
                }
            }
        }
    }

    makeCellEditable(cellId, onAccepted, editText) {
        const spanElement = this.shadowRoot.getElementById(cellId);
        const pl = Math.max(1, spanElement.style.paddingLeft ? parseInt(spanElement.style.paddingLeft) : 0);
        const pr = Math.max(1, spanElement.style.paddingRight ? parseInt(spanElement.style.paddingRight) : 0);
        const l = parseInt(spanElement.style.left);
        const t = parseInt(spanElement.style.top);
        const w = parseInt(spanElement.style.width);
        const h = parseInt(spanElement.style.height);
        const edit = document.createElement("input");
        edit.classList.add("cell");
        edit.style.width = `${w-pl-pr+2}px`;
        edit.style.height = `${h-2}px`;
        edit.style.left = `${l+pl-2}px`;
        edit.style.top = `${t+1}px`;
        edit.value = editText ?? spanElement.innerText;
        spanElement.parentElement.removeAttribute("draggable");
        spanElement.replaceWith(edit);
        edit.focus();
        edit.select();
        edit.onblur = (e) => {
            if (edit.parentElement.classList.contains("selected") && (this?.isSelectionDraggable?.() ?? false) || edit.parentElement.classList.contains("current") && (this?.isCurrentDraggable?.() ?? false))
                edit.parentElement.setAttribute("draggable", "true");
            edit.replaceWith(spanElement);
        }
        edit.onkeydown = (e) => {
            if (e.key === "Enter" || e.key === "Escape") {
                if (edit.parentElement.classList.contains("selected") && (this?.isSelectionDraggable?.() ?? false) || edit.parentElement.classList.contains("current") && (this?.isCurrentDraggable?.() ?? false))
                    edit.parentElement.setAttribute("draggable", "true");
                if (e.key === "Enter")
                    onAccepted(edit.value);
                edit.onblur = null;
                edit.replaceWith(spanElement);
            }
        }
        return edit;
    };

    makeGroupEditable(groupId, onAccepted, editText) {
        const spanElement = this.shadowRoot.getElementById(groupId);
        const pl = Math.max(1, spanElement.style.paddingLeft ? parseInt(spanElement.style.paddingLeft) : 0) + 16;
        const pr = Math.max(1, spanElement.style.paddingRight ? parseInt(spanElement.style.paddingRight) : 0);
        const l = parseInt(spanElement.style.left);
        const t = parseInt(spanElement.style.top);
        const w = parseInt(spanElement.style.width);
        const h = parseInt(spanElement.style.height);
        const edit = document.createElement("input");
        edit.classList.add("group");
        edit.style.width = `${w - pl - pr + 2}px`;
        edit.style.height = `${h - 2}px`;
        edit.style.left = `${l + pl - 2}px`;
        edit.style.top = `${t + 1}px`;
        edit.value = editText ?? spanElement.innerText;
        spanElement.replaceWith(edit);
        edit.focus();
        edit.select();
        edit.onblur = (e) => {
            edit.replaceWith(spanElement);
        }
        edit.onkeydown = (e) => {
            if (e.key === "Enter" || e.key === "Escape") {
                if (e.key === "Enter")
                    onAccepted(edit.value);
                edit.onblur = null;
                edit.replaceWith(spanElement);
            }
        }
        return edit;
    };

}

export class LimVirtualTreeView extends LimTreeViewBase {
    #onscroll

    constructor() {
        super();
    }

    connectedCallback() {
        super.connectedCallback();

        if (!this.#onscroll) {
            this.#onscroll = () => this.render();
            this.addEventListener('scroll', this.#onscroll);
        }
    }

    disconnectedCallback() {
        super.disconnectedCallback();

        this.removeEventListener('scroll', this.#onscroll);
        this.#onscroll = null;
    }

    render() {
        if (this.treeContainer)
            this.treeContainer.innerHTML = "";

        if (!this._updateColSizes())
            return;

        const [ colStart, colEnd ] = this.visibleColRange;

        this.treeContainer.style.width = `${this.contentWidth}px`;
        this.treeContainer.style.height = `${this.contentHeight}px`;
        this._render_rows(colStart, colEnd);

        this.headerContainer.innerHTML = "";
        if (this.isHeaderVisible) {
            this.headerContainer.style.display = "block";
            this.headerContainer.style.width = `${this.contentWidth}px`;
            this.headerContainer.appendChild(this._createHeaderRow(colStart, colEnd));
        }
        else {
            this.headerContainer.style.display = "none";
        }
    }
}

const createHeaderContainer = () => {
    const el = document.createElement("div");
    el.classList.add("header-container");
    return el;
};

const createTreeContainer = () => {
    const el = document.createElement("div");
    el.classList.add("tree-container")
    return el;
};

const createTreeRow = (w, h) => {
    const el = document.createElement("div");
    el.classList.add("row");
    el.style.width = `${w}px`;
    el.style.height = `${h}px`;
    return el;
};

const createTreeRowItem = (w, h) => {
    const el = document.createElement("span");
    el.classList.add("cell");
    el.style.width = `${w}px`;
    el.style.height = `${h}px`;
    return el;
};

customElements.define('lim-virtual-treeview', LimVirtualTreeView);