/**
 * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import {Datasource} from "./datasource.mjs";
import {
    assembleMethodSymbol,
    CustomElement,
    registerCustomElement,
    getSlottedElements,
} from "../../dom/customelement.mjs";
import {
    findTargetElementFromEvent,
    fireCustomEvent,
} from "../../dom/events.mjs";
import {clone} from "../../util/clone.mjs";
import {
    isString,
    isFunction,
    isInstance,
    isObject,
    isArray,
} from "../../types/is.mjs";
import {
    validateArray,
    validateInteger,
    validateObject,
} from "../../types/validate.mjs";
import {Observer} from "../../types/observer.mjs";
import {
    ATTRIBUTE_DATATABLE_HEAD,
    ATTRIBUTE_DATATABLE_GRID_TEMPLATE,
    ATTRIBUTE_DATASOURCE_SELECTOR,
    ATTRIBUTE_DATATABLE_ALIGN,
    ATTRIBUTE_DATATABLE_SORTABLE,
    ATTRIBUTE_DATATABLE_MODE,
    ATTRIBUTE_DATATABLE_INDEX,
    ATTRIBUTE_DATATABLE_MODE_HIDDEN,
    ATTRIBUTE_DATATABLE_FEATURES,
    ATTRIBUTE_DATATABLE_MODE_VISIBLE,
    ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
    ATTRIBUTE_DATATABLE_MODE_FIXED,
} from "./constants.mjs";
import {instanceSymbol} from "../../constants.mjs";
import {
    Header,
    createOrderStatement,
    DIRECTION_ASC,
    DIRECTION_DESC,
    DIRECTION_NONE,
} from "./datatable/header.mjs";
import {DatatableStyleSheet} from "./stylesheet/datatable.mjs";
import {
    handleDataSourceChanges,
    datasourceLinkedElementSymbol,
} from "./util.mjs";
import "./columnbar.mjs";
import "./filter-button.mjs";
import {
    findElementWithSelectorUpwards,
    getDocument,
    getWindow,
} from "../../dom/util.mjs";
import {addAttributeToken} from "../../dom/attributes.mjs";
import {ATTRIBUTE_ERRORMESSAGE} from "../../dom/constants.mjs";
import {getDocumentTranslations} from "../../i18n/translations.mjs";
import "../state/state.mjs";
import "../host/collapse.mjs";
import {generateUniqueConfigKey} from "../host/util.mjs";

import "./datasource/dom.mjs";
import "./datasource/rest.mjs";

import "../form/context-help.mjs";
import {getLocaleOfDocument} from "../../dom/locale.mjs";


export {DataTable};

/**
 * @private
 * @type {symbol}
 */
const gridElementSymbol = Symbol("gridElement");

/**
 * @private
 * @type {symbol}
 */
const dataControlElementSymbol = Symbol("dataControlElement");

/**
 * @private
 * @type {symbol}
 */
const gridHeadersElementSymbol = Symbol("gridHeadersElement");

/**
 * @private
 * @type {symbol}
 */
const columnBarElementSymbol = Symbol("columnBarElement");

/**
 * @private
 * @type {symbol}
 */
const copyAllElementSymbol = Symbol("copyAllElement");

/**
 * @private
 * @type {symbol}
 */
const resizeObserverSymbol = Symbol("resizeObserver");

/**
 * The DataTable component is used to show the data from a data source.
 *
 * @copyright schukai GmbH
 * @summary A data table

 */

/**
 * A DataTable
 *
 * @fragments /fragments/components/datatable/datatable/
 *
 * @example /examples/components/datatable/empty The empty state
 * @example /examples/components/datatable/data-using-javascript The data using javascript
 * @example /examples/components/datatable/alignment The alignment
 * @example /examples/components/datatable/row-mode The row mode
 * @example /examples/components/datatable/grid-template The grid template
 * @example /examples/components/datatable/overview-class The overview class
 * @example /examples/components/datatable/datasource Use a datasource
 * @example /examples/components/datatable/pagination Use pagination
 * @example /examples/components/datatable/filter Filer the data
 * @example /examples/components/datatable/ Select rows
 *
 * @copyright schukai GmbH
 * @summary A beautiful and highly customizable data table. It can be used to display data from a data source.
 * @fires monster-datatable-row-copied
 * @fires monster-datatable-row-removed
 * @fires monster-datatable-row-added
 * @fires monster-datatable-row-selected
 * @fires monster-datatable-row-deselected
 * @fires monster-datatable-all-rows-selected
 * @fires monster-datatable-all-rows-deselected
 * @fires monster-datatable-selection-changed
 **/
class DataTable extends CustomElement {
    /**
     * This method is called by the `instanceof` operator.
     * @return {symbol}
     */
    static get [instanceSymbol]() {
        return Symbol.for("@schukai/monster/components/datatable@@instance");
    }

    /**
     * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
     *
     * The individual configuration values can be found in the table.
     *
     * @property {Object} templates Template definitions
     * @property {string} templates.main Main template
     * @property {Object} datasource Datasource configuration
     * @property {string} datasource.selector Selector for the datasource
     * @property {Object} mapping Mapping configuration
     * @property {string} mapping.data Data mapping
     * @property {Array} data Data
     * @property {Array} headers Headers
     * @property {Object} responsive Responsive configuration
     * @property {number} responsive.breakpoint Breakpoint for responsive mode
     * @property {Object} labels Labels
     * @property {string} labels.theListContainsNoEntries Label for empty state
     * @property {Object} classes Classes
     * @property {string} classes.container Container class
     * @property {Object} features Features
     * @property {boolean} features.settings Settings feature
     * @property {boolean} features.footer Footer feature
     * @property {boolean} features.autoInit Auto init feature (init datasource automatically)
     * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature
     * @property {boolean} features.copyAll Copy all feature
     * @property {boolean} features.help Help feature
     * @property {Object} templateMapping Template mapping
     * @property {string} templateMapping.row-key Row key
     * @property {string} templateMapping.filter-id Filter id
     **/
    get defaults() {
        return Object.assign(
            {},
            super.defaults,
            {
                templates: {
                    main: getTemplate(),
                    emptyState: getEmptyTemplate(),
                },

                datasource: {
                    selector: null,
                },

                mapping: {
                    data: "dataset",
                },

                data: [],
                headers: [],

                responsive: {
                    breakpoint: 900,
                },

                labels: getTranslations(),

                classes: {
                    control: "monster-theme-control-container-1",
                    container: "",
                    row: "monster-theme-control-row-1",
                },

                features: {
                    settings: true,
                    footer: true,
                    autoInit: true,
                    doubleClickCopyToClipboard: true,
                    copyAll: true,
                    help: true,
                },

                copy: {
                    delimiter: ";",
                    quoteOpen: '"',
                    quoteClose: '"',
                    rowBreak: "\n",
                },

                templateMapping: {
                    "row-key": null,
                    "filter-id": null,
                },
            },
            initOptionsFromArguments.call(this),
        );
    }

    /**
     *
     * @param {string} selector
     * @return {NodeListOf<*>}
     */
    getGridElements(selector) {
        return this[gridElementSymbol].querySelectorAll(selector);
    }

    /**
     *
     * @return {string}
     */
    static getTag() {
        return "monster-datatable";
    }

    /**
     * @return {void}
     */
    disconnectedCallback() {
        super.disconnectedCallback();
        if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
            this[resizeObserverSymbol].disconnect();
        }
    }

    /**
     * @return {void}
     */
    connectedCallback() {
        const self = this;
        super.connectedCallback();

        this[resizeObserverSymbol] = new ResizeObserver((entries) => {
            updateGrid.call(self);
        });

        this[resizeObserverSymbol].observe(this.parentNode);
    }

    /**
     * Get the row number of the selected rows as an array
     *
     * @returns {number[]}
     */
    getSelectedRows() {
        const rows = this.getGridElements(`[data-monster-role="select-row"]`);
        const selectedRows = [];
        rows.forEach((row) => {
            if (row.checked) {
                const key = row.parentNode.getAttribute("data-monster-insert-reference");
                const index = key.split("-").pop();
                selectedRows.push(parseInt(index, 10));
            }
        });

        return selectedRows;
    }

    /**
     * @return void
     */
    [assembleMethodSymbol]() {
        const rawKey = this.getOption("templateMapping.row-key");

        if (rawKey === null) {
            if (this.id !== null && this.id !== "") {
                const rawKey = this.getOption("templateMapping.row-key");
                if (rawKey === null) {
                    this.setOption("templateMapping.row-key", this.id + "-row");
                }
            } else {
                this.setOption("templateMapping.row-key", "row");
            }
        }

        if (this.id !== null && this.id !== "") {
            this.setOption("templateMapping.filter-id", "" + this.id + "-filter");
        } else {
            this.setOption("templateMapping.filter-id", "filter");
        }

        super[assembleMethodSymbol]();

        initControlReferences.call(this);
        initEventHandler.call(this);

        const selector = this.getOption("datasource.selector");

        if (isString(selector)) {
            const element = findElementWithSelectorUpwards(this, selector);
            if (element === null) {
                throw new Error("the selector must match exactly one element");
            }

            if (!isInstance(element, Datasource)) {
                throw new TypeError("the element must be a datasource");
            }

            this[datasourceLinkedElementSymbol] = element;

            queueMicrotask(() => {
                handleDataSourceChanges.call(this);
                element.datasource.attachObserver(
                    new Observer(handleDataSourceChanges.bind(this)),
                );
            });
        }

        getHostConfig
            .call(this, getColumnVisibilityConfigKey)
            .then((config) => {
                const headerOrderMap = new Map();

                getHostConfig
                    .call(this, getStoredOrderConfigKey)
                    .then((orderConfig) => {
                        if (isArray(orderConfig) || orderConfig.length > 0) {
                            for (let i = 0; i < orderConfig.length; i++) {
                                const item = orderConfig[i];
                                const parts = item.split(" ");
                                const field = parts[0];
                                const direction = parts[1] || DIRECTION_ASC;
                                headerOrderMap.set(field, direction);
                            }
                        }
                    })
                    .then(() => {
                        try {
                            initGridAndStructs.call(this, config, headerOrderMap);
                        } catch (error) {
                            addAttributeToken(
                                this,
                                ATTRIBUTE_ERRORMESSAGE,
                                error?.message || error.toString(),
                            );
                        }

                        updateColumnBar.call(this);
                    })
                    .catch((error) => {
                        addAttributeToken(
                            this,
                            ATTRIBUTE_ERRORMESSAGE,
                            error?.message || error.toString(),
                        );
                    });
            })
            .catch((error) => {
                addAttributeToken(
                    this,
                    ATTRIBUTE_ERRORMESSAGE,
                    error?.message || error.toString(),
                );
            });
    }

    /**
     * @return {CSSStyleSheet[]}
     */
    static getCSSStyleSheet() {
        return [DatatableStyleSheet];
    }

    /**
     * Copy a row from the datatable
     *
     * @param {number|string} fromIndex
     * @param {number|string} toIndex
     * @return {DataTable}
     * @fires monster-datatable-row-copied
     */
    copyRow(fromIndex, toIndex) {
        const datasource = this[datasourceLinkedElementSymbol];
        if (!datasource) {
            return this;
        }
        let d = datasource.data;
        let c = clone(d);

        let rows = c;
        const mapping = this.getOption("mapping.data");

        if (mapping) {
            rows = c?.[mapping];
        }

        if (rows === undefined || rows === null) {
            rows = [];
        }

        if (toIndex === undefined) {
            toIndex = rows.length;
        }

        if (isString(fromIndex)) {
            fromIndex = parseInt(fromIndex);
        }
        if (isString(toIndex)) {
            toIndex = parseInt(toIndex);
        }

        if (toIndex < 0 || toIndex > rows.length) {
            throw new RangeError("index out of bounds");
        }

        validateArray(rows);
        validateInteger(fromIndex);
        validateInteger(toIndex);

        if (fromIndex < 0 || fromIndex >= rows.length) {
            throw new RangeError("index out of bounds");
        }

        rows.splice(toIndex, 0, clone(rows[fromIndex]));
        datasource.data = c;

        fireCustomEvent(this, "monster-datatable-row-copied", {
            index: toIndex,
        });

        return this;
    }

    /**
     * Remove a row from the datatable
     *
     * @param {number|string} index
     * @return {DataTable}
     * @fires monster-datatable-row-removed
     */
    removeRow(index) {
        const datasource = this[datasourceLinkedElementSymbol];
        if (!datasource) {
            return this;
        }
        let d = datasource.data;
        let c = clone(d);

        let rows = c;
        const mapping = this.getOption("mapping.data");

        if (mapping) {
            rows = c?.[mapping];
        }

        if (rows === undefined || rows === null) {
            rows = [];
        }

        if (isString(index)) {
            index = parseInt(index);
        }

        validateArray(rows);
        validateInteger(index);

        if (index < 0 || index >= rows.length) {
            throw new RangeError("index out of bounds");
        }
        if (mapping) {
            rows = c?.[mapping];
        }

        rows.splice(index, 1);
        datasource.data = c;

        fireCustomEvent(this, "monster-datatable-row-removed", {
            index: index,
        });

        return this;
    }

    /**
     * Add a row to the datatable
     *
     * @param {Object} data
     * @return {DataTable}
     *
     * @fires monster-datatable-row-added
     **/
    addRow(data) {
        const datasource = this[datasourceLinkedElementSymbol];
        if (!datasource) {
            return this;
        }
        let d = datasource.data;
        let c = clone(d);

        let rows = c;

        const mapping = this.getOption("mapping.data");
        if (mapping) {
            rows = c?.[mapping];
        }

        if (rows === undefined || rows === null) {
            rows = [];
        }

        validateArray(rows);
        validateObject(data);

        rows.push(data);
        datasource.data = c;

        fireCustomEvent(this, "monster-datatable-row-added", {
            index: rows.length - 1,
        });

        return this;
    }
}

/**
 * @private
 * @return {string}
 */
function getColumnVisibilityConfigKey() {
    return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
}

/**
 * @private
 * @return {string}
 */
function getFilterConfigKey() {
    return generateUniqueConfigKey("datatable", this?.id, "filter");
}

/**
 * @private
 * @return {Promise}
 */
function getHostConfig(callback) {
    const host = findElementWithSelectorUpwards(this, "monster-host");

    if (!host) {
        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "no host found");
        return Promise.resolve({});
    }

    if (!this.id) {
        addAttributeToken(
            this,
            ATTRIBUTE_ERRORMESSAGE,
            "no id found; id is required for config",
        );
        return Promise.resolve({});
    }

    if (!host || !isFunction(host?.getConfig)) {
        throw new TypeError("the host must be a monster-host");
    }

    const configKey = callback.call(this);
    return host.hasConfig(configKey).then((hasConfig) => {
        if (hasConfig) {
            return host.getConfig(configKey);
        } else {
            return {};
        }
    });
}

/**
 * @private
 */
function updateColumnBar() {
    if (!this[columnBarElementSymbol]) {
        return;
    }

    const columns = [];
    for (const header of this.getOption("headers")) {
        const mode = header.getInternal("mode");

        if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
            continue;
        }

        columns.push({
            visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
            name: header.label,
            index: header.index,
        });
    }

    this[columnBarElementSymbol].setOption("columns", columns);
}

/**
 * @private
 */
function updateHeaderFromColumnBar() {
    if (!this[columnBarElementSymbol]) {
        return;
    }

    const options = this[columnBarElementSymbol].getOption("columns");
    if (!isArray(options)) return;

    const invisibleMap = {};

    for (let i = 0; i < options.length; i++) {
        const option = options[i];
        invisibleMap[option.index] = option.visible;
    }

    for (const header of this.getOption("headers")) {
        const mode = header.getInternal("mode");

        if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
            continue;
        }

        if (invisibleMap[header.index] === false) {
            header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN);
        } else {
            header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE);
        }
    }
}

/**
 * @private
 */
function updateConfigColumnBar() {
    if (!this[columnBarElementSymbol]) {
        return;
    }

    const options = this[columnBarElementSymbol].getOption("columns");
    if (!isArray(options)) return;

    const map = {};
    for (let i = 0; i < options.length; i++) {
        const option = options[i];
        map[option.name] = option.visible;
    }

    const host = findElementWithSelectorUpwards(this, "monster-host");
    if (!(host && this.id)) {
        return;
    }
    const configKey = getColumnVisibilityConfigKey.call(this);

    try {
        host.setConfig(configKey, map);
    } catch (error) {
        addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
    }
}

/**
 * @private
 */
function initEventHandler() {
    const self = this;

    const quoteOpenChar = this.getOption("copy.quoteOpen");
    const quoteCloseChar = this.getOption("copy.quoteClose");
    const delimiterChar = this.getOption("copy.delimiter");
    const rowBreak = this.getOption("copy.rowBreak");

    self[columnBarElementSymbol].attachObserver(
        new Observer((e) => {
            updateHeaderFromColumnBar.call(self);
            updateGrid.call(self);
            updateConfigColumnBar.call(self);
        }),
    );

    self[gridHeadersElementSymbol].addEventListener("click", function (event) {
        let element = null;
        const datasource = self[datasourceLinkedElementSymbol];
        if (!datasource) {
            return;
        }

        element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE);
        if (element) {
            const index = element.parentNode.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
            const headers = self.getOption("headers");

            event.preventDefault();

            headers[index].changeDirection();

            queueMicrotask(function () {
                /** hotfix, normally this should be done via the updater, no idea why this is not possible. */
                element.setAttribute(
                    ATTRIBUTE_DATATABLE_SORTABLE,
                    `${headers[index].field} ${headers[index].direction}`,
                );

                storeOrderStatement.call(self, true);
            });
        }
    });

    const eventHandlerDoubleClickCopyToClipboard = (event) => {
        const element = findTargetElementFromEvent(event, "data-monster-head");
        if (element) {
            let text = "";

            if (event.shiftKey) {
                const index = element.getAttribute("data-monster-insert-reference");
                if (index) {
                    const cols = self.getGridElements(
                        `[data-monster-insert-reference="${index}"]`,
                    );

                    const colTexts = [];
                    for (let i = 0; i < cols.length; i++) {
                        const col = cols[i];

                        if (
                            col.querySelector("monster-button-bar") ||
                            col.querySelector("monster-button")
                        ) {
                            continue;
                        }

                        if (col.textContent) {
                            colTexts.push(
                                quoteOpenChar + col.textContent.trim() + quoteCloseChar,
                            );
                        }
                    }

                    text = colTexts.join(delimiterChar);
                }
            } else {
                if (
                    element.querySelector("monster-button-bar") ||
                    element.querySelector("monster-button")
                ) {
                    return;
                }

                text = element.textContent.trim();
            }

            if (getWindow().navigator.clipboard && text) {
                getWindow()
                    .navigator.clipboard.writeText(text)
                    .then(
                        () => {
                        },
                        (err) => {
                        },
                    );
            }
        }
    };

    if (self.getOption("features.doubleClickCopyToClipboard")) {
        self[gridElementSymbol].addEventListener(
            "dblclick",
            eventHandlerDoubleClickCopyToClipboard,
        );
    }

    if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) {
        this[copyAllElementSymbol].addEventListener("click", (event) => {
            event.preventDefault();

            const table = [];
            let currentRow = [];
            let currentIndex = null;

            const cols = self.getGridElements(`[data-monster-insert-reference]`);
            const rowIndexes = new Map();
            cols.forEach((col) => {
                const index = col.getAttribute("data-monster-insert-reference");
                rowIndexes.set(index, true);
            });

            rowIndexes.forEach((value, key) => {
                const cols = self.getGridElements(
                    `[data-monster-insert-reference="${key}"]`,
                );

                for (let i = 0; i < cols.length; i++) {
                    const col = cols[i];

                    if (
                        col.querySelector("monster-button-bar") ||
                        col.querySelector("monster-button")
                    ) {
                        continue;
                    }

                    if (col.textContent) {
                        currentRow.push(
                            quoteOpenChar + col.textContent.trim() + quoteCloseChar,
                        );
                    }
                }

                if (currentRow.length > 0) {
                    table.push(currentRow);
                }
                currentRow = [];
            });

            if (table.length > 0) {
                const text = table.map((row) => row.join(delimiterChar)).join(rowBreak);
                if (getWindow().navigator.clipboard && text) {
                    getWindow()
                        .navigator.clipboard.writeText(text)
                        .then(
                            () => {
                            },
                            (err) => {
                            },
                        );
                }
            }
        });
    }

    const selectRowCallback = (event) => {
        const element = findTargetElementFromEvent(event, "data-monster-role", "select-row");
        if (element) {
            const key = element.parentNode.getAttribute("data-monster-insert-reference");
            const row = self.getGridElements(
                `[data-monster-insert-reference="${key}"]`,
            );

            const index = key.split("-").pop();

            if (element.checked) {
                row.forEach((col) => {
                    col.classList.add("selected");
                });

                fireCustomEvent(self, "monster-datatable-row-selected", {
                    index: index
                })

            } else {
                row.forEach((col) => {
                    col.classList.remove("selected");
                });

                fireCustomEvent(self, "monster-datatable-row-deselected", {
                    index: index
                })
            }

            fireCustomEvent(this, "monster-datatable-selection-changed", {})
        }

        const rows = self.getGridElements(`[data-monster-role="select-row"]`);
        const allSelected = Array.from(rows).every((row) => row.checked);
        const selectAll = this[gridHeadersElementSymbol].querySelector(`[data-monster-role="select-all"]`);
        selectAll.checked = allSelected;


    }

    this[gridElementSymbol].addEventListener("click", selectRowCallback);
    this[gridElementSymbol].addEventListener("touch", selectRowCallback);

    const selectAllCallback = (event) => {
        const element = findTargetElementFromEvent(event, "data-monster-role", "select-all");
        if (element) {
            const mode = element.checked

            const rows = this.getGridElements(`[data-monster-role="select-row"]`);
            rows.forEach((row) => {
                row.checked = mode;
            });

            if (mode) {
                fireCustomEvent(this, "monster-datatable-all-rows-selected", {})
            } else {
                fireCustomEvent(this, "monster-datatable-all-rows-deselected", {})
            }

            fireCustomEvent(this, "monster-datatable-selection-changed", {})

        }
    }

    this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback)
    this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback)


}

/**
 * @private
 */
function initGridAndStructs(hostConfig, headerOrderMap) {
    const rowID = this.getOption("templateMapping.row-key");

    if (!this[gridElementSymbol]) {
        throw new Error("no grid element is defined");
    }

    let template;
    getSlottedElements.call(this).forEach((e) => {

        if (e instanceof HTMLTemplateElement && e.id === rowID) {
            template = e;
        }
    });

    if (!template) {
        throw new Error("no template is defined");
    }

    const rowCount = template.content.children.length;

    const headers = [];

    for (let i = 0; i < rowCount; i++) {
        let hClass = "";
        const row = template.content.children[i];

        let mode = "";
        if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) {
            mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE);
        }

        let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE);
        if (!grid || grid === "" || grid === "auto") {
            grid = "minmax(0, 1fr)";
        }

        let label = "";
        let labelKey = "";

        if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) {
            label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD);
            labelKey = label;

            try {
                if (label.startsWith("i18n:")) {
                    label = label.substring(5, label.length);
                    label = getDocumentTranslations().getText(label, label);
                }
            } catch (e) {
                label = "i18n error " + label;
            }
        }

        if (!label) {
            label = i + 1 + "";
            mode = ATTRIBUTE_DATATABLE_MODE_FIXED;
            labelKey = label;
        }

        if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) {
            if (hostConfig[label] === false) {
                mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN;
            } else {
                mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE;
            }
        }

        let align = "";
        if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) {
            align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN);
        }

        switch (align) {
            case "center":
                hClass = "flex-center";
                break;
            case "end":
                hClass = "flex-end";
                break;
            case "start":
                hClass = "flex-start";
                break;
            default:
                hClass = "flex-start";
        }

        let field = "";
        let direction = DIRECTION_NONE;
        if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) {
            field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim();
            const parts = field.split(" ").map((item) => item.trim());
            field = parts[0];

            if (headerOrderMap.has(field)) {
                direction = headerOrderMap.get(field);
            } else if (
                parts.length === 2 &&
                [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1
            ) {
                direction = parts[1];
            }
        }

        if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
            hClass += " hidden";
        }

        const features = [];
        if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) {
            const features = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" ");
            features.forEach((feature) => {
                features.push(feature.trim());

                if (feature === "select") {
                    label = "<input type='checkbox' data-monster-role='select-all' />";

                    while (row.firstChild) {
                        row.removeChild(row.firstChild);
                    }

                    const checkbox = document.createElement("input");
                    checkbox.type = "checkbox";
                    checkbox.setAttribute("data-monster-role", "select-row");
                    row.appendChild(checkbox);

                }

            });
        }

        const header = new Header();
        header.setInternals({
            field: field,
            label: label,
            classes: hClass,
            index: i,
            mode: mode,
            grid: grid,
            labelKey: labelKey,
            direction: direction,
            features: features,
        });

        headers.push(header);
    }

    this.setOption("headers", headers);
    queueMicrotask(() => {
        storeOrderStatement.call(this, this.getOption("features.autoInit"));
    });
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
    const locale = getLocaleOfDocument();
    switch (locale.language) {
        case 'de':
            return {
                theListContainsNoEntries: "Die Liste enthält keine Einträge",
                copyAll: "Alles kopieren",
                helpText:
                    "<p>Sie können die Werte aus einzelnen Zeilen<br>" +
                    "in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" +
                    "<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" +
                    "Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>",
            };
        case 'fr':
            return {
                theListContainsNoEntries: "La liste ne contient aucune entrée",
                copyAll: "Copier tout",
                helpText:
                    "<p>Vous pouvez copier les valeurs des rangées individuelles<br>" +
                    "dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" +
                    "<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" +
                    "Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>",
            };
        case 'sp':
            return {
                theListContainsNoEntries: "La lista no contiene entradas",
                copyAll: "Copiar todo",
                helpText:
                    "<p>Puedes copiar los valores de filas individuales<br>" +
                    "al portapapeles haciendo doble clic en la columna correspondiente.</p>" +
                    "<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" +
                    "Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>",
            };
        case 'it':
            return {
                theListContainsNoEntries: "L'elenco non contiene voci",
                copyAll: "Copia tutto",
                helpText:
                    "<p>Puoi copiare i valori dalle singole righe<br>" +
                    "negli appunti facendo doppio clic sulla colonna relativa.</p>" +
                    "<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" +
                    "Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>",
            };
        case 'pl':
            return {
                theListContainsNoEntries: "Lista nie zawiera wpisów",
                copyAll: "Kopiuj wszystko",
                helpText:
                    "<p>Możesz skopiować wartości z poszczególnych wierszy<br>" +
                    "do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" +
                    "<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" +
                    "Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>",
            };
        case 'no':
            return {
                theListContainsNoEntries: "Listen inneholder ingen oppføringer",
                copyAll: "Kopier alt",
                helpText:
                    "<p>Du kan kopiere verdier fra enkeltrader<br>" +
                    "til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" +
                    "<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" +
                    "Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>",
            };
        case 'dk':
            return {
                theListContainsNoEntries: "Listen indeholder ingen poster",
                copyAll: "Kopiér alt",
                helpText:
                    "<p>Du kan kopiere værdier fra enkelte rækker<br>" +
                    "til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" +
                    "<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" +
                    "Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>",
            };
        case 'sw':
            return {
                theListContainsNoEntries: "Listan innehåller inga poster",
                copyAll: "Kopiera allt",
                helpText:
                    "<p>Du kan kopiera värden från enskilda rader<br>" +
                    "till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" +
                    "<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" +
                    "Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>",
            };


        case 'en':
        default:
            return {
                theListContainsNoEntries: "The list contains no entries",
                copyAll: "Copy all",
                helpText:
                    "<p>You can copy the values from individual rows<br>" +
                    "to the clipboard by double-clicking on the relevant column.</p>" +
                    "<p>To copy an entire row, hold down the Shift key while clicking.<br>" +
                    "If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>",
            };
    }
}

/**
 * @private
 * @return {string}
 */
export function getStoredOrderConfigKey() {
    return generateUniqueConfigKey("datatable", this?.id, "stored-order");
}

/**
 * @private
 */
function storeOrderStatement(doFetch) {
    const headers = this.getOption("headers");
    const statement = createOrderStatement(headers);
    setDataSource.call(this, {orderBy: statement}, doFetch);

    const host = findElementWithSelectorUpwards(this, "monster-host");
    if (!(host && this.id)) {
        return;
    }

    const configKey = getStoredOrderConfigKey.call(this);

    // statement explode with , and remove all empty
    const list = statement.split(",").filter((item) => item.trim() !== "");
    if (list.length === 0) {
        return;
    }

    host.setConfig(configKey, list);
}

/**
 * @private
 */
function updateGrid() {
    if (!this[gridElementSymbol]) {
        throw new Error("no grid element is defined");
    }

    let gridTemplateColumns = "";

    const headers = this.getOption("headers");

    let styles = "";

    for (let i = 0; i < headers.length; i++) {
        const header = headers[i];

        if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
            styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`;
            styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`;
        } else {
            gridTemplateColumns += `${header.grid} `;
        }
    }

    const sheet = new CSSStyleSheet();
    if (styles !== "") sheet.replaceSync(styles);
    this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet];

    const bodyWidth = this.parentNode.clientWidth;

    const breakpoint = this.getOption("responsive.breakpoint");
    this[dataControlElementSymbol].classList.toggle(
        "small",
        bodyWidth <= breakpoint,
    );

    if (bodyWidth > breakpoint) {
        this[gridElementSymbol].style.gridTemplateColumns =
            `${gridTemplateColumns}`;
        this[gridHeadersElementSymbol].style.gridTemplateColumns =
            `${gridTemplateColumns}`;
    } else {
        this[gridElementSymbol].style.gridTemplateColumns = "auto";
        this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto";
    }
}

/**
 * @private
 * @param {Header[]} headers
 * @param {bool} doFetch
 */
function setDataSource({orderBy}, doFetch) {
    const datasource = this[datasourceLinkedElementSymbol];

    if (!datasource) {
        return;
    }

    if (isFunction(datasource?.setParameters)) {
        datasource.setParameters({orderBy});
    }

    if (doFetch !== false && isFunction(datasource?.fetch)) {
        datasource.fetch();
    }
}

/**
 * @private
 * @return {DataTable}
 */
function initControlReferences() {
    if (!this.shadowRoot) {
        throw new Error("no shadow-root is defined");
    }

    this[dataControlElementSymbol] = this.shadowRoot.querySelector(
        "[data-monster-role=control]",
    );

    this[gridElementSymbol] = this.shadowRoot.querySelector(
        "[data-monster-role=datatable]",
    );

    this[gridHeadersElementSymbol] = this.shadowRoot.querySelector(
        "[data-monster-role=datatable-headers]",
    );

    this[columnBarElementSymbol] =
        this.shadowRoot.querySelector("monster-column-bar");

    this[copyAllElementSymbol] = this.shadowRoot.querySelector(
        "[data-monster-role=copy-all]",
    );

    return this;
}

/**
 * @private
 * @return {object}
 * @throws {TypeError} incorrect arguments passed for the datasource
 * @throws {Error} the datasource could not be initialized
 */
function initOptionsFromArguments() {
    const options = {};
    const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);

    if (selector) {
        options.datasource = {selector: selector};
    }

    const breakpoint = this.getAttribute(
        ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
    );
    if (breakpoint) {
        options.responsive = {};
        options.responsive.breakpoint = parseInt(breakpoint);
    }

    return options;
}

/**
 * @private
 * @return {string}
 */
function getEmptyTemplate() {
    return `<monster-state data-monster-role="empty-without-action">
    <div part="visual">
        <svg width="4rem" height="4rem" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
            <path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/>
            <path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/>
        </svg>
    </div>
    <div part="content" data-monster-replace="path:labels.theListContainsNoEntries">
        The list contains no entries.
    </div>
</monster-state>`;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
    // language=HTML
    return `
        <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control">
            <template id="headers-row">
                <div data-monster-attributes="class path:headers-row.classes,
                                              data-monster-index path:headers-row.index"
                     data-monster-replace="path:headers-row.html"></div>
            </template>
            <slot></slot>
            <div data-monster-attributes="class path:classes.container"
                 data-monster-role="table-container" part="table-container">
                <div class="filter">
                    <slot name="filter"></slot>
                </div>
                <div class="bar">
                    <monster-context-help
                            data-monster-attributes="class path:features.help | ?::hidden"
                            data-monster-replace="path:labels.helpText"
                    ></monster-context-help>
                    <a href="#" data-monster-attributes="class path:features.copyAll | ?::hidden"
                       data-monster-role="copy-all" data-monster-replace="path:labels.copyAll">Copy all</a>
                    <monster-column-bar
                            data-monster-attributes="class path:features.settings | ?::hidden"></monster-column-bar>
                    <slot name="bar"></slot>

                </div>
                <div data-monster-role="datatable-headers" data-monster-insert="headers-row path:headers"></div>
                <div data-monster-replace="path:templates.emptyState"
                     data-monster-attributes="class path:data | has-entries | ?:hidden:empty-state-container"></div>
                <div data-monster-role="datatable" data-monster-insert="\${row-key} path:data">
                </div>
            </div>
            <div data-monster-role="footer" data-monster-select-this="true"
                 data-monster-attributes="class path:data | has-entries | ?::hidden">
                <slot name="footer" data-monster-attributes="class path:features.footer | ?::hidden"></slot>
            </div>

        </div>
    `;
}

registerCustomElement(DataTable);