/* ===================== AUTOGENERATED SOURCE IMPORT ===================== */

import Template from "/emcJS/util/html/template/HTMLTemplate.js";

const TPL = new Template(`
    <div id="scroll-container">
        <table id="table">
            <thead id="head">
                <tr id="header">
                </tr>
            </thead>
            <tbody id="body">
            </tbody>
            <tfoot id="foot">
            </tfoot>
        </table>
    </div>
    <div id="nocolumns-container" class="hidden">
        <emc-i18n-label id="nocolumns" i18n-value="missing column definition"></emc-i18n-label>
    </div>
    <div id="empty-container" class="hidden">
        <emc-i18n-label id="empty" i18n-value="no entries"></emc-i18n-label>
    </div>
    <slot id="column-container"></slot>
    
`);

import GlobalStyle from "/emcJS/util/html/template/CSSTemplate.js";

const STYLE = new GlobalStyle(`
    :host {
        position: relative;
        display: flex;
        flex-direction: column;
        min-height: 200px;
        min-width: 300px;
        margin: 8px;
        color: var(--datagrid-text-color, #000000);
        background-color: var(--datagrid-back-color, #ffffff);
        background-image: var(--datagrid-back-image, none);
        border-style: solid;
        border-width: var(--datagrid-border-width, 2px);
        border-color: var(--datagrid-border-color, #000000);
        border-radius: var(--datagrid-border-radius, 6px);
        line-height: normal;
        overflow: hidden;
    
        --scrollbar-track-border-radius: 0px;
        --scrollbar-thumb-border-radius: 0px;
    }
    
    #scroll-container {
        position: relative;
        display: flex;
        flex-direction: column;
        flex: 1;
        height: 100%;
        width: 100%;
        overflow: auto;
        scroll-padding-top: 3em;
        scroll-padding-bottom: 0.5em;
    }
    
    :host([nohead]:not([nohead="false"])) {
        scroll-padding-top: 0.5em;
    
        #head {
            display: none;
        }
    }
    
    :host([disabled]:not([disabled="false"])) {
        pointer-events: none;
    
        color: var(--datagrid-disabled-text-color, #999999) !important;
        background-color: var(--datagrid-disabled-back-color, #757575) !important;
        background-image: var(--datagrid-disabled-back-image, none) !important;
    
        #head .cell {
            color: var(--datagrid-disabled-header-text-color, #999999) !important;
            background-color: var(--datagrid-disabled-header-back-color, #4d4d4d) !important;
            background-image: var(--datagrid-disabled-header-back-image, none) !important;
            border-left-color: var(--datagrid-disabled-header-seperator-color, #696969) !important;
            border-right-color: var(--datagrid-disabled-header-seperator-color, #696969) !important;
    
            &.select-cell input[type="checkbox"] {
                color: var(--datagrid-disabled-header-select-cell-text-color, #757575) !important;
                background-color: var(--datagrid-disabled-header-select-cell-back-color, #999999) !important;
                border-color: var(--datagrid-disabled-header-select-cell-border-color, #6a6a6a) !important;
                cursor: default !important;
            }
        }
    
        #foot .cell {
            color: var(--datagrid-disabled-footer-text-color, #999999) !important;
            background-color: var(--datagrid-disabled-footer-back-color, #757575) !important;
            background-image: var(--datagrid-disabled-footer-back-image, none) !important;
            border-left-color: var(--datagrid-disabled-footer-seperator-color, #696969) !important;
            border-right-color: var(--datagrid-disabled-footer-seperator-color, #696969) !important;
    
            &.select-cell input[type="checkbox"] {
                color: var(--datagrid-disabled-footer-select-cell-text-color, #757575) !important;
                background-color: var(--datagrid-disabled-footer-select-cell-back-color, #999999) !important;
                border-color: var(--datagrid-disabled-footer-select-cell-border-color, #6a6a6a) !important;
                cursor: default !important;
            }
        }
    
        #body .cell {
            color: var(--button-disabled-text-color, #757575) !important;
            background-color: var(--button-disabled-back-color, #999999) !important;
            background-image: var(--button-disabled-back-image, none) !important;
            border-left-color: var(--datagrid-disabled-column-seperator-color, #696969) !important;
            border-right-color: var(--datagrid-disabled-column-seperator-color, #696969) !important;
            border-bottom-color: var(--datagrid-disabled-row-seperator-color, #757575) !important;
    
            &.select-cell input[type="checkbox"] {
                color: var(--datagrid-disabled-select-cell-text-color, #757575) !important;
                background-color: var(--datagrid-disabled-select-cell-back-color, #999999) !important;
                border-color: var(--datagrid-disabled-select-cell-border-color, #6a6a6a) !important;
                cursor: default !important;
            }
        }
    }
    
    :host([readonly]:not([readonly="false"])) .cell.select-cell input[type="checkbox"] {
        pointer-events: none;
    }
    
    :host([stretch="last"]) .cell:last-child {
        width: 100%;
    }
    
    #table {
        position: relative;
        min-width: 100%;
        border-spacing: 0px;
    
        tr {
            position: relative;
            z-index: 0;
        }
    
        .cell {
            position: relative;
            box-sizing: content-box;
            min-width: 50px;
            padding: 0;
    
            &.select-cell {
                width: 36px;
                min-width: 36px;
                max-width: 36px;
            }
    
            &.sort-cell {
                width: 30px;
                min-width: 30px;
                max-width: 30px;
            }
    
            &.last-cell {
                padding: 0px;
                width: 100%;
                min-width: 0px;
            }
    
            &.fixed-cell {
                position: sticky;
    
                &.stuck {
                    z-index: 2;
                }
    
                &.fixed-cell-start {
                    left: 0px;
                }
    
                &.fixed-cell-end {
                    right: 0px;
                }
            }
        }
    
        #head,
        #body,
        #foot {
    
            .cell:not(:first-child):not(.last-cell) {
                border-left-style: solid;
                border-left-width: var(--datagrid-column-seperator-width, 2px);
            }
    
            .cell.fixed-cell-end:not(:nth-last-child(1 of .fixed-cell-end)):after {
                position: absolute;
                display: block;
                top: 0px;
                right: -2px;
                width: 4px;
                height: 100%;
                background-color: inherit;
                content: " ";
            }
    
            .cell.fixed-cell:nth-last-child(1 of .fixed-cell-start.stuck) {
                border-right-style: solid;
                border-right-width: var(--datagrid-fixed-column-seperator-width, 4px);
    
                & + .cell {
                    border-left: none;
    
                    &.last-cell {
                        box-shadow: none;
                    }
                }
            }
    
            .cell.fixed-cell:nth-child(1 of .fixed-cell-end.stuck) {
                border-left-style: solid;
                border-left-width: var(--datagrid-fixed-column-seperator-width, 4px);
            }
    
            &.last-cell {
                border-left: none;
            }
        }
    }
    
    #head,
    #foot {
        position: sticky;
        z-index: 4;
    }
    
    #head {
        top: 0px;
    
        .cell {
            color: var(--datagrid-header-text-color, #ffffff);
            background-color: var(--datagrid-header-back-color, #333333);
            background-image: var(--datagrid-header-back-image, none);
            border-left-color: var(--datagrid-header-seperator-color, #ffffff);
            border-right-color: var(--datagrid-header-seperator-color, #ffffff);
    
            &.fixed-cell:nth-last-child(1 of .fixed-cell-start.stuck) {
                border-right-color: var(--datagrid-fixed-header-seperator-color, #afafaf);
            }
    
            &.fixed-cell:nth-child(1 of .fixed-cell-end.stuck) {
                border-left-color: var(--datagrid-fixed-header-seperator-color, #afafaf);
            }
    
            &.last-cell:not(:first-child) {
                box-shadow: inset var(--datagrid-header-seperator-color, #ffffff) var(--datagrid-column-seperator-width, 2px) 0px 0px;
            }
        }
    }
    
    #foot {
        bottom: 0px;
    
        .cell {
            color: var(--datagrid-footer-text-color, #ffffff);
            background-color: var(--datagrid-footer-back-color, #666666);
            background-image: var(--datagrid-footer-back-image, none);
            border-left-color: var(--datagrid-footer-seperator-color, #ffffff);
            border-right-color: var(--datagrid-footer-seperator-color, #ffffff);
    
            &.fixed-cell:nth-last-child(1 of .fixed-cell-start.stuck) {
                border-right-color: var(--datagrid-fixed-footer-seperator-color, #afafaf);
            }
    
            &.fixed-cell:nth-child(1 of .fixed-cell-end.stuck) {
                border-left-color: var(--datagrid-fixed-footer-seperator-color, #afafaf);
            }
    
            &.last-cell:not(:first-child) {
                box-shadow: inset var(--datagrid-footer-seperator-color, #ffffff) var(--datagrid-column-seperator-width, 2px) 0px 0px;
            }
    
            #content {
                overflow: hidden;
                text-overflow: ellipsis;
            }
        }
    }
    
    #body {
        position: relative;
    
        .cell {
            padding: 0;
            color: var(--datagrid-text-color, #000000);
            background-color: var(--datagrid-back-color, #ffffff);
            background-image: var(--datagrid-back-image, none);
            border-left-color: var(--datagrid-column-seperator-color, #ffffff);
            border-right-color: var(--datagrid-column-seperator-color, #ffffff);
    
            &.sort-cell {
                cursor: grab;
    
                div {
                    background-image: radial-gradient(10px 10px at 50% 50%, var(--datagrid-sort-grabber-indicator-color, #bbbbbb) 0%, var(--datagrid-sort-grabber-indicator-color, #bbbbbb) 17%, #00000000 30%);
                }
            }
    
            &.fixed-cell:nth-last-child(1 of .fixed-cell-start.stuck) {
                border-right-color: var(--datagrid-fixed-column-seperator-color, #afafaf);
            }
    
            &.fixed-cell:nth-child(1 of .fixed-cell-end.stuck) {
                border-left-color: var(--datagrid-fixed-column-seperator-color, #afafaf);
            }
    
            &.last-cell:not(:first-child) {
                box-shadow: inset var(--datagrid-column-seperator-color, #ffffff) var(--datagrid-column-seperator-width, 2px) 0px 0px;
            }
        }
    
        tr {
            outline-style: solid;
            outline-width: var(--datagrid-row-seperator-width, 1px);
            outline-color: var(--datagrid-row-seperator-color, #696969);
    
            &.dragging {
                box-shadow: 0 3px 9px 2px #00000069;
                z-index: 1;
    
                & ~ tr {
                    top: var(--datagrid-row-seperator-width, 1px);
                }
    
                .cell {
                    color: var(--datagrid-row-hover-text-color, #000000);
                    background-color: var(--datagrid-row-hover-back-color, #d6f7fd);
                    background-image: var(--datagrid-row-hover-back-image, none);
                }
            }
    
            &:not(:last-child):not(.dragging) .cell {
                border-bottom-style: solid;
                border-bottom-width: var(--datagrid-row-seperator-width, 1px);
                border-bottom-color: var(--datagrid-row-seperator-color, #696969);
            }
        }
    
        &:not(:has(tr.dragging)) tr:hover .cell {
            color: var(--datagrid-row-hover-text-color, #000000);
            background-color: var(--datagrid-row-hover-back-color, #d6f7fd);
            background-image: var(--datagrid-row-hover-back-image, none);
    
            &.sort-cell div {
                background-image: radial-gradient(10px 10px at 50% 50%, var(--datagrid-row-hover-sort-grabber-indicator-color, #bbbbbb) 0%, var(--datagrid-row-hover-sort-grabber-indicator-color, #bbbbbb) 17%, #00000000 30%);
            }
        }
    }
    
    #empty-container,
    #nocolumns-container {
        position: absolute;
        display: flex;
        align-items: center;
        justify-content: center;
        flex: 1;
        left: 0px;
        width: 100%;
        height: 100%;
        max-height: 200px;
        min-width: calc(200px - var(--scrollbar-size, 0px) - 4px);
        /* --datagrid-border-width * 2 */
        min-height: 2rem;
        pointer-events: none;
        user-select: none;
        animation: popInLater 0ms ease-in 100ms forwards;
        animation-fill-mode: forwards;
        opacity: 0;
    }
    
    #empty-container #empty,
    #nocolumns-container #nocolumns {
        display: flex;
        align-items: center;
        justify-content: center;
        min-width: 100px;
        padding: 20px;
        height: 30px;
        color: var(--datagrid-empty-text-color, #666666);
        background-color: var(--datagrid-empty-back-color, #dadada);
        background-image: var(--datagrid-empty-back-image, none);
        border-style: solid;
        border-width: var(--datagrid-empty-border-width, 2px);
        border-color: var(--datagrid-empty-border-color, #666666);
        border-radius: var(--datagrid-empty-border-radius, 6px);
        font-size: 16px;
        font-style: italic;
        white-space: nowrap;
    }
    
    #empty-container.hidden,
    #nocolumns-container.hidden {
        display: none;
    }
    
    #nocolumns-container:not(.hidden) + #empty-container {
        display: none;
    }
    
    .sort-cell {
        display: none;
        width: 40px;
        min-width: 40px;
        max-width: 40px;
        --dot-size: 0.5em;
    
        div {
            position: relative;
            display: flex;
            align-items: center;
            justify-content: center;
            width: calc(2 * var(--dot-size));
            height: calc(3 * var(--dot-size));
            margin: auto;
            background-size: var(--dot-size) var(--dot-size);
            background-position: 0px 0px;
        }
    }
    
    .select-cell {
        padding: 0px;
        display: none;
        text-align: center;
    
        input[type="checkbox"] {
            position: relative;
            display: flex;
            box-sizing: border-box;
            justify-content: center;
            align-items: center;
            appearance: none;
            width: 22px;
            height: 22px;
            padding: 2px;
            margin: auto;
            color: var(--datagrid-select-cell-text-color, #505050);
            background-color: var(--datagrid-select-cell-back-color, #ffffff);
            border-style: solid;
            border-width: 2px;
            border-color: var(--datagrid-select-cell-border-color, #000000);
            border-radius: 0.3em;
            font-size: 16px;
            cursor: pointer;
    
            &:focus {
                box-shadow: 0 0 2px 3px var(--input-focus-color, #06b5ff);
                outline: none;
            }
    
            &:checked {
                color: var(--datagrid-select-cell-checked-text-color, #ffffff);
                background-color: var(--datagrid-select-cell-checked-back-color, #505050);
                border-color: var(--datagrid-select-cell-checked-border-color, #000000);
            }
    
            &:indeterminate {
                color: var(--datagrid-select-cell-indeterminate-text-color, #505050);
                background-color: var(--datagrid-select-cell-indeterminate-back-color, #ffffff);
                border-color: var(--datagrid-select-cell-indeterminate-border-color, #000000);
            }
    
            &::before {
                position: absolute;
                display: flex;
                justify-content: center;
                align-items: center;
            }
    
            &:checked::before {
                line-height: 1.2em;
                font-weight: bold;
                content: "✓";
            }
    
            &:indeterminate::before {
                width: 70%;
                height: 70%;
                background-color: var(--datagrid-select-cell-indeterminate-text-color, #000000);
                border-radius: 50%;
                content: "";
            }
    
            &:indeterminate::after {
                position: absolute;
                display: flex;
                justify-content: center;
                align-items: center;
                width: 40%;
                height: 40%;
                background-color: var(--datagrid-select-cell-indeterminate-back-color, #ffffff);
                border-radius: 50%;
                content: "";
            }
        }
    }
    
    #head .select-cell input[type="checkbox"] {
        display: none;
        color: var(--datagrid-header-select-cell-text-color, #000000);
        background-color: var(--datagrid-header-select-cell-back-color, #ffffff);
        border-color: var(--datagrid-header-select-cell-border-color, #000000);
    
        &:checked {
            color: var(--datagrid-header-select-cell-checked-text-color, #000000);
            background-color: var(--datagrid-header-select-cell-checked-back-color, #ffffff);
            border-color: var(--datagrid-header-select-cell-checked-border-color, #ffffff);
        }
    
        &:indeterminate {
            color: var(--datagrid-header-select-cell-indeterminate-text-color, #000000);
            background-color: var(--datagrid-header-select-cell-indeterminate-back-color, #ffffff);
            border-color: var(--datagrid-header-select-cell-indeterminate-border-color, #000000);
        }
    }
    
    #foot .select-cell input[type="checkbox"] {
        display: none;
        color: var(--datagrid-footer-select-cell-text-color, #000000);
        background-color: var(--datagrid-footer-select-cell-back-color, #ffffff);
        border-color: var(--datagrid-footer-select-cell-border-color, #ffffff);
    
        &:checked {
            color: var(--datagrid-footer-select-cell-checked-text-color, #000000);
            background-color: var(--datagrid-footer-select-cell-checked-back-color, #ffffff);
            border-color: var(--datagrid-footer-select-cell-checked-border-color, #ffffff);
        }
    
        &:indeterminate {
            color: var(--datagrid-footer-select-cell-indeterminate-text-color, #000000);
            background-color: var(--datagrid-footer-select-cell-indeterminate-back-color, #ffffff);
            border-color: var(--datagrid-footer-select-cell-indeterminate-border-color, #000000);
        }
    }
    
    :host([sortable]:not([sortable="false"])) #head .sort-cell,
    :host([sortable]:not([sortable="false"])) #body .sort-cell,
    :host([sortable]:not([sortable="false"])) #foot .sort-cell {
        display: table-cell;
    }
    
    :host([selectable]:not([selectable="false"])) #head .select-cell,
    :host([selectable]:not([selectable="false"])) #body .select-cell,
    :host([selectable]:not([selectable="false"])) #foot .select-cell {
        display: table-cell;
    }
    
    :host([multiple]:not([multiple="false"])) #head .select-cell input[type="checkbox"],
    :host([multiple]:not([multiple="false"])) #foot .select-cell input[type="checkbox"] {
        display: flex;
    }
    
    @keyframes popInLater {
        to {
            opacity: 1;
        }
    }
    
`);

/* ======================================================================= */

import CustomElement from "../../element/CustomElement.js";
import {isEqual} from "../../../util/helper/Comparator.js";
import {deepClone} from "../../../util/helper/DeepClone.js";
import {appUID} from "../../../util/helper/UniqueGenerator.js";
import {debounce} from "../../../util/Debouncer.js";
import {getAllAttributes} from "../../../util/helper/ui/NodeAttributes.js";
import {filterInPlace} from "../../../util/helper/collection/ArrayMutations.js";
import BusyIndicatorManager from "../../../util/BusyIndicatorManager.js";
import MutationObserverManager from "../../../util/observer/manager/MutationObserverManager.js";
import DataRecieverMixin from "../../../util/dataprovider/DataRecieverMixin.js";
import HeaderManager from "./manager/HeaderManager.js";
import RowManager from "./manager/RowManager.js";
import Column from "./Column.js";
import DataGridCell from "./components/cell/DataGridCell.js";
import CellCache from "./data/CellCache.js";
import BusyIndicator from "../../BusyIndicator.js";
import StickyObserver from "../../../util/observer/StickyObserver.js";
import "../../i18n/I18nLabel.js";
import "./components/CellTypeLoader.js";

const MUTATION_CONFIG = {attributes: true};

const PX_REGEXP = /^[0-9]+(?:\.[0-9]+)?$/;

function getStyleLengthValue(type, value) {
    const minValue = DataGridCell.getTypeMinWidth(type);
    if (value != null && PX_REGEXP.test(value)) {
        return Math.max(parseFloat(value), minValue, 50);
    }
    if (minValue != null) {
        return Math.max(minValue, 50);
    }
    return 200;
}

// TODO add "no match" label
/*
TODO add DataGrid context to handle selection
- store active keys
- handle select/deselect on active keys
- row checkboxes get value from context
- select(key)
- unselect(key)
- selectAll()
- unselectAll()
- setSelected(keyList)
- isSelected(key)
- hasSelected()
- hasUnselected()
*/
export default class DataGrid extends DataRecieverMixin(CustomElement) {

    #internalId = appUID("data-grid");

    #cellCache = new CellCache();

    #scrollContainerEl;

    #tableEl;

    #headEl;

    #headerEl;

    #bodyEl;

    #nocolumnsContainerEl;

    #emptyContainerEl;

    #columnContainerEl;

    #columnDefinition = [];

    #data = [];

    #headerManager;

    #rowManager;

    #headerSelectEl;

    #selected = new Set();

    #stretched = null;

    #mutationObserver = new MutationObserverManager(MUTATION_CONFIG, () => {
        this.#onSlotChange();
    });

    #leftFixes = [];

    #rightFixes = [];

    #busyIndicator = new BusyIndicator();

    constructor() {
        super();
        this.shadowRoot.append(TPL.generate());
        STYLE.apply(this.shadowRoot);
        /* --- */
        this.#busyIndicator.setTarget(this.shadowRoot);
        /* --- */
        this.#headerSelectEl = document.createElement("input");
        this.#headerSelectEl.type = "checkbox";
        this.#headerSelectEl.name = "multiselect";
        this.#headerSelectEl.addEventListener("change", (event) => {
            event.stopPropagation();
            const value = this.#headerSelectEl.checked;
            const changedKeys = this.#rowManager.setAllVisibleRowsSelected(value);
            for (const rowKey of changedKeys) {
                if (value) {
                    this.#selected.add(rowKey);
                } else {
                    this.#selected.delete(rowKey);
                }
            }
            const ev = new Event("selection");
            ev.data = [...this.#selected].sort();
            this.dispatchEvent(ev);
        }, {passive: true});
        /* --- */
        this.#scrollContainerEl = this.shadowRoot.getElementById("scroll-container");
        this.#tableEl = this.shadowRoot.getElementById("table");
        this.#headEl = this.shadowRoot.getElementById("head");
        this.#headerEl = this.shadowRoot.getElementById("header");
        this.#bodyEl = this.shadowRoot.getElementById("body");
        this.#nocolumnsContainerEl = this.shadowRoot.getElementById("nocolumns-container");
        this.#emptyContainerEl = this.shadowRoot.getElementById("empty-container");
        /* --- */
        this.#columnContainerEl = this.shadowRoot.getElementById("column-container");
        this.#columnContainerEl.addEventListener("slotchange", () => {
            this.#onSlotChange();
        });
        this.#onSlotChange();
        /* --- */
        const stickyObserver = new StickyObserver((entries) => {
            for (const entry of entries) {
                const observedEl = entry.target;
                observedEl.classList.toggle("stuck", entry.isStuck);
            }
        }, {root: this.#scrollContainerEl});
        this.#headerManager = new HeaderManager(this.#headerEl, stickyObserver, this.#headerSelectEl, this.#internalId);
        this.#headerManager.addEventListener("afterrender", debounce(() => {
            this.#calculateCellFixes(this.#headEl);
        }));
        this.#rowManager = new RowManager(this.#bodyEl, stickyObserver, this.#cellCache, this.#internalId);
        this.#rowManager.addEventListener("afterrender", debounce(() => {
            const isEmpty = this.#bodyEl.children.length === 0;
            this.#emptyContainerEl.classList.toggle("hidden", !isEmpty);
            this.#updateSelectionAfterRender();
            this.#applyCellFixes(this.#bodyEl);
        }));
        this.#rowManager.addEventListener("sort-change", (event) => {
            event.stopPropagation();
            const {
                newOrder, oldOrder
            } = event;
            const ev = new Event("sort-change");
            ev.newOrder = newOrder;
            ev.oldOrder = oldOrder;
            this.dispatchEvent(ev);
        });
        /* --- */
        this.#tableEl.addEventListener("menu", (event) => {
            event.stopPropagation();
            event.preventDefault();
            const {
                value, columnName, rowKey
            } = event.data;
            const ev = new PointerEvent("menu", event);
            ev.data = {
                value,
                columnName,
                rowKey,
                source: event.srcElement
            };
            this.dispatchEvent(ev);
        });
        this.#tableEl.addEventListener("action", (event) => {
            event.stopPropagation();
            event.preventDefault();
            const {
                action, columnName, rowKey
            } = event.data;
            if (action == null || typeof action !== "string" || action === "") {
                const ev = new Event("action");
                ev.data = {
                    columnName,
                    rowKey,
                    source: event.srcElement
                };
                this.dispatchEvent(ev);
            } else {
                const ev = new Event(`action::${action}`);
                ev.data = {
                    columnName,
                    rowKey,
                    source: event.srcElement
                };
                this.dispatchEvent(ev);
            }
        });
        this.#tableEl.addEventListener("edit", (event) => {
            event.stopPropagation();
            event.preventDefault();
            const {
                value, action, columnName, rowKey
            } = event.data;
            if (action == null || typeof action !== "string" || action === "") {
                const ev = new Event("edit");
                ev.data = {
                    value,
                    columnName,
                    rowKey,
                    source: event.srcElement
                };
                this.dispatchEvent(ev);
            } else {
                const ev = new Event(`edit::${action}`);
                ev.data = {
                    value,
                    columnName,
                    rowKey,
                    source: event.srcElement
                };
                this.dispatchEvent(ev);
            }
        });
        this.#tableEl.addEventListener("selection", (event) => {
            event.stopPropagation();
            event.preventDefault();
            const {
                value, rowKey
            } = event.data;
            if (!this.multiple) {
                if (value) {
                    const oldrowKey = [...this.#selected][0];
                    this.#rowManager.setRowSelected(oldrowKey, false);
                    this.#selected.clear();
                    this.#selected.add(rowKey);
                } else if (this.allowDeselect) {
                    this.#selected.clear();
                } else {
                    const oldrowKey = [...this.#selected][0];
                    this.#rowManager.setRowSelected(oldrowKey, true);
                }
            } else if (value) {
                this.#selected.add(rowKey);
            } else {
                this.#selected.delete(rowKey);
            }
            this.#updateSelectHeader();
            const ev = new Event("selection");
            ev.data = [...this.#selected].sort();
            this.dispatchEvent(ev);
        });
        /* --- */
        this.#tableEl.addEventListener("sort", (event) => {
            event.stopPropagation();
            const {columnName} = event.data;
            const ev = new Event("sort");
            ev.data = {columnName};
            this.dispatchEvent(ev);
        });
        this.#tableEl.addEventListener("unsort", (event) => {
            event.stopPropagation();
            const {columnName} = event.data;
            const ev = new Event("unsort");
            ev.data = {columnName};
            this.dispatchEvent(ev);
        });
        /* --- */
        this.addEventListener("dragover", (e) => {
            if (this.#rowManager.isDragging) {
                e.preventDefault();
                e.stopPropagation();
            }
        });
        this.addEventListener("dragenter", (e) => {
            if (this.#rowManager.isDragging) {
                e.preventDefault();
                e.stopPropagation();
            }
        });
    }

    connectedCallback() {
        super.connectedCallback();
        this.#stretched = this.#columnDefinition.find((definition) => definition.name === this.stretched);
    }

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

    set nohead(value) {
        this.setBooleanAttribute("nohead", value);
    }

    get nohead() {
        return this.getBooleanAttribute("nohead");
    }

    set sortable(value) {
        this.setBooleanAttribute("sortable", value);
    }

    get sortable() {
        return this.getBooleanAttribute("sortable");
    }

    set selectable(value) {
        this.setBooleanAttribute("selectable", value);
    }

    get selectable() {
        return this.getBooleanAttribute("selectable");
    }

    set multiple(value) {
        this.setBooleanAttribute("multiple", value);
    }

    get multiple() {
        return this.getBooleanAttribute("multiple");
    }

    set allowDeselect(value) {
        this.setBooleanAttribute("allowdeselect", value);
    }

    get allowDeselect() {
        return this.getBooleanAttribute("allowdeselect");
    }

    set selectEnd(value) {
        this.setBooleanAttribute("selectend", value);
    }

    get selectEnd() {
        return this.getBooleanAttribute("selectend");
    }

    set stretched(value) {
        this.setAttribute("stretched", value);
    }

    get stretched() {
        return this.getAttribute("stretched");
    }

    set disabled(val) {
        this.setBooleanAttribute("disabled", val);
    }

    get disabled() {
        return this.getBooleanAttribute("disabled");
    }

    set readonly(val) {
        this.setBooleanAttribute("readonly", val);
    }

    get readonly() {
        return this.getBooleanAttribute("readonly");
    }

    /**
     * Deactivate caching for row data and their html elements.
     * Setting this impacts render performance.
     */
    set noCache(val) {
        this.setBooleanAttribute("nocache", val);
    }

    get noCache() {
        return this.getBooleanAttribute("nocache");
    }

    static get observedAttributes() {
        return ["sortable", "selectable", "selectend", "multiple", "stretched", "readonly"];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue != newValue) {
            switch (name) {
                case "sortable": {
                    const sortable = this.sortable;
                    this.#headerManager.sortable = sortable;
                    this.#rowManager.sortable = sortable;
                } break;
                case "selectable": {
                    const selectable = this.selectable;
                    this.#headerManager.selectable = selectable;
                    this.#rowManager.selectable = selectable;
                    if (!this.selectable) {
                        this.clearSelected();
                    }
                } break;
                case "selectend": {
                    const selectEnd = this.selectEnd;
                    this.#headerManager.selectEnd = selectEnd;
                    this.#rowManager.selectEnd = selectEnd;
                } break;
                case "multiple": {
                    if (!this.multiple) {
                        this.clearSelected();
                    }
                } break;
                case "stretched": {
                    this.#setStreched(newValue);
                } break;
                case "disabled": {
                    for (const [,, cell] of this.#cellCache.getAllCells()) {
                        cell.disabled = this.disabled;
                    }
                } break;
                case "readonly": {
                    for (const [,, cell] of this.#cellCache.getAllCells()) {
                        cell.readonly = this.readonly;
                    }
                } break;
            }
        }
    }

    setSelected(selected) {
        if (selected != null && this.selectable) {
            if (!Array.isArray(selected)) {
                if (typeof selected === "string") {
                    selected = [selected];
                } else {
                    selected = [];
                }
            }
            this.#selected.clear();
            if (this.multiple) {
                for (const entry of selected) {
                    this.#selected.add(entry);
                    this.#rowManager.setRowSelected(entry, true);
                }
            } else if (selected.length > 0) {
                this.#selected.add(selected[0]);
                this.#rowManager.setRowSelected(selected[0], true);
            }
            this.#updateSelectHeader();
            const ev = new Event("selection");
            ev.data = [...this.#selected].sort();
            this.dispatchEvent(ev);
        }
    }

    getSelected() {
        return [...this.#selected].sort();
    }

    clearSelected() {
        for (const key of this.#selected) {
            this.#rowManager.setRowSelected(key, false);
        }
        this.#selected.clear();
        if (this.selectable) {
            const ev = new Event("selection");
            ev.data = [];
            this.dispatchEvent(ev);
        }
    }

    selectAll() {
        if (this.selectable) {
            if (this.multiple) {
                const changedKeys = this.#rowManager.setAllVisibleRowsSelected(true);
                for (const rowKey of changedKeys) {
                    this.#selected.add(rowKey);
                }
            } else {
                const selectEl = this.shadowRoot.querySelector(`td.select-cell input[type="checkbox"]`);
                const rowKey = selectEl.getAttribute("row-key");
                this.#rowManager.setRowSelected(true);
                this.#selected.add(rowKey);
            }
            this.#updateSelectHeader();
            const ev = new Event("selection");
            ev.data = [...this.#selected].sort();
            this.dispatchEvent(ev);
        }
    }

    setSortIndicators(columns = []) {
        const showSortOrder = columns.length > 1;
        for (const colDef of this.#columnDefinition) {
            const {name} = colDef;
            const headerCellEl = this.#headerManager.getCellByColumnName(name);
            const sortIndex = columns.findIndex((entry) => entry === name || entry === `!${name}`);
            if (sortIndex >= 0) {
                const sort = columns[sortIndex];
                headerCellEl.sortDirection = sort.startsWith("!") ? "dec" : "inc";
                if (showSortOrder) {
                    headerCellEl.sortOrder = sortIndex + 1;
                } else {
                    headerCellEl.sortOrder = null;
                }
            } else {
                headerCellEl.sortDirection = null;
                headerCellEl.sortOrder = null;
            }
        }
    }

    async setData(rows = []) {
        await BusyIndicatorManager.busy();
        if (rows != null && !Array.isArray(rows)) {
            throw new TypeError("Data must be an array or null");
        }
        if (!isEqual(this.#data, rows)) {
            if (rows == null) {
                this.#data = [];
                this.#rowManager.purge();
            } else {
                this.#data = deepClone(rows);
                this.#rowManager.manage(this.#data, this.#columnDefinition, this.#selected, this.noCache);
            }
            this.#notifyRowUpdate();
            /* --- */
        }
        this.#updateSelectHeader();
        await BusyIndicatorManager.unbusy();
    }

    #notifyRowUpdate = debounce(() => {
        if (this.readonly || this.disabled) {
            for (const [,, cell] of this.#cellCache.getAllCells()) {
                cell.readonly = this.readonly;
                cell.disabled = this.disabled;
            }
        }
        const ev = new Event("rows-updated");
        this.dispatchEvent(ev);
    });

    getAllCellsForColumn(colName) {
        return this.#cellCache.getAllCellsForColumn(colName);
    }

    getCell(rowKey, colName) {
        return this.#cellCache.getCell(rowKey, colName);
    }

    async #applyColumnDefinition() {
        await BusyIndicatorManager.busy();
        const columnNodeList = this.#columnContainerEl.assignedElements({flatten: true}).filter((el) => el instanceof Column);
        const newColumnDefinition = [];
        /* --- */
        const oldNodes = new Set(this.#mutationObserver.getObservedNodes());
        const newNodes = new Set();

        for (const columnEl of columnNodeList) {
            const columnData = getAllAttributes(columnEl);

            if (columnData.hidden && columnData.hidden !== "false") {
                continue;
            }

            filterInPlace(newColumnDefinition, (entry) => {
                if (columnData.name !== entry.name) {
                    return true;
                }
                console.warn(`duplicate column definition for "${columnData.name}" in DataGrid`, this);
                return false;
            });

            newColumnDefinition.push(columnData);
            if (oldNodes.has(columnEl)) {
                oldNodes.delete(columnEl);
            } else {
                newNodes.add(columnEl);
            }
        }
        for (const node of oldNodes) {
            this.#mutationObserver.unobserve(node);
        }
        for (const node of newNodes) {
            this.#mutationObserver.observe(node);
        }

        if (!isEqual(this.#columnDefinition, newColumnDefinition)) {
            this.#columnDefinition = newColumnDefinition;
            this.#headerManager.manage(newColumnDefinition);
            this.#rowManager.manage(this.#data, newColumnDefinition);
            /* --- */
            for (const definition of newColumnDefinition) {
                const {
                    name, type, width
                } = definition;
                this.style.setProperty(`--min-width-${name}`, `${width}px`);
                if (name !== this.stretched) {
                    const widthValue = width;
                    const styleWidth = getStyleLengthValue(type, widthValue);
                    this.style.setProperty(`--width-${name}`, `${styleWidth}px`);
                }
            }
            /* --- */
            const ev = new Event("rows-updated");
            this.dispatchEvent(ev);
        }
        /* --- */
        this.#nocolumnsContainerEl.classList.toggle("hidden", newColumnDefinition.length > 0);
        /* --- */
        await BusyIndicatorManager.unbusy();
    }

    #updateSelectHeader() {
        const {
            visibleCount, selectedCount
        } = this.#rowManager.getSelectionStatus();

        const ev = new Event("selection-header");
        if (selectedCount === 0) { // none selected
            this.#headerSelectEl.checked = false;
            this.#headerSelectEl.indeterminate = false;
            ev.checked = false;
            ev.indeterminate = false;
        } else if (selectedCount === visibleCount) { // all selected
            this.#headerSelectEl.checked = true;
            this.#headerSelectEl.indeterminate = false;
            ev.checked = true;
            ev.indeterminate = false;
        } else { // some selected
            this.#headerSelectEl.checked = true;
            this.#headerSelectEl.indeterminate = true;
            ev.checked = true;
            ev.indeterminate = true;
        }
        this.dispatchEvent(ev);
    }

    #onSlotChange = debounce(() => {
        this.#applyColumnDefinition();
    });

    busy() {
        return this.#busyIndicator.busy();
    }

    unbusy() {
        return this.#busyIndicator.unbusy();
    }

    reset() {
        return this.#busyIndicator.reset();
    }

    #setStreched(strechedName) {
        if (this.#stretched != null) {
            const name = this.#stretched.name;
            const widthValue = this.#stretched.width;
            if (widthValue != null) {
                const styleWidth = getStyleLengthValue(this.#stretched.type, widthValue);
                this.style.setProperty(`--width-${name}`, `${styleWidth}px`);
            }
        }
        this.#stretched = this.#columnDefinition.find((definition) => definition.name === strechedName);
        if (this.#stretched != null) {
            this.style.setProperty(`--width-${strechedName}`, "100%");
        }
    }

    #updateSelectionAfterRender() {
        this.#rowManager.setAllVisibleRowsSelected((key) => this.#selected.has(key));
    }

    #calculateCellFixes(containerEl) {
        const rowEl = containerEl.querySelector(":scope tr");
        containerEl.style.display = "table-header-group";
        this.#leftFixes = [];
        this.#rightFixes = [];
        // left cells
        {
            const fixedLeftCells = [...rowEl.querySelectorAll(":scope .fixed-cell.fixed-cell-start")];
            let leftOffset = 0;
            for (const cellEl of fixedLeftCells) {
                const stuck = cellEl.classList.contains("stuck");
                cellEl.classList.remove("stuck");
                this.#leftFixes.push(leftOffset);
                const cellWidth = cellEl.offsetWidth;
                leftOffset += cellWidth;
                cellEl.classList.toggle("stuck", stuck);
            }
        }
        // right cells
        {
            const fixedRightCells = [...rowEl.querySelectorAll(":scope .fixed-cell.fixed-cell-end")].reverse();
            let rightOffset = 0;
            for (const cellEl of fixedRightCells) {
                const stuck = cellEl.classList.contains("stuck");
                cellEl.classList.remove("stuck");
                this.#rightFixes.push(rightOffset);
                const cellWidth = cellEl.offsetWidth;
                rightOffset += cellWidth;
                cellEl.classList.toggle("stuck", stuck);
            }
        }
        // apply cell fixes
        containerEl.style.display = "";
        this.#applyCellFixes(containerEl);
    }

    #applyCellFixes(containerEl) {
        const rowEls = containerEl.querySelectorAll(":scope tr");
        for (const rowEl of rowEls) {
            // left cells
            {
                const fixedLeftCells = [...rowEl.querySelectorAll(":scope .fixed-cell.fixed-cell-start")];
                let index = 0;
                for (const cellEl of fixedLeftCells) {
                    const leftOffset = this.#leftFixes[index++];
                    cellEl.style.left = `${leftOffset}px`;
                }
            }
            // right cells
            {
                const fixedRightCells = [...rowEl.querySelectorAll(":scope .fixed-cell.fixed-cell-end")].reverse();
                let index = 0;
                for (const cellEl of fixedRightCells) {
                    const rightOffset = this.#rightFixes[index++];
                    cellEl.style.right = `${rightOffset}px`;
                }
            }
        }
    }

}

customElements.define("emc-datagrid", DataGrid);
