class WidthSyncer {
    
    
    // #region ==================== ELEMENT VISIBILITY
    
    /**
     * @requires XPath2
     * @returns bool
     */
    static isElementVisible(element, checkParents = false, topParent = null) {
        if (checkParents) {
            let parents = XPath2.getElementPath(element, topParent);
            return !parents.some(
                parent => !WidthSyncer.isElementVisible(parent)
            );
        } else {
            return !element.hidden
                && element.style.display != "none"
                && getComputedStyle(element).display != "none";
        }
    }
    
    /**
     * @requires XPath2
     * @returns HTMLElement | bool
     */
    static findInvisibleParentElement(element) {
        let path = XPath2.getElementPath(element);
        let parent = path.find(parent => !WidthSyncer.isElementVisible(parent));
        return parent ?? null;
    }
    
    /**
     * @returns HTMLElement
     */
    static createOffScreenClone(element) {
        let clone = element.cloneNode(true);
        if (clone.hidden) {
            clone.hidden = false;
        } else if (
            clone.style.display == "none"
            || getComputedStyle(element).display == "none"
        ) {
            clone.style.display = "";
        }
        document.body.append(clone);
        clone.style.position = "absolute";
        clone.style.left = -window.innerWidth + "px";
        clone.style.top = -window.innerHeight + "px";
        return clone;
    }
    
    //#endregion
    
    
    // #region ==================== SYNC ELEMENTS
    
    /**
     * Syncs/equalizes the widths of elements using the widest element.
     * @requires XPath2
     */
    static syncElementsWidths(elements, hardMaxWidth = null, callback = null) {
        if (typeof elements == "string") {
            elements = document.querySelectorAll(elements);
        }
        if (elements instanceof NodeList) {
            elements = Array.from(elements);
        }
        if (elements.length == 0) {
            return;
        }
        let parent = elements[0]?.closest(".sync-widths");
        if (parent) {
            let dataSyncWidthMax = parent.dataset.syncWidthMax ?? null;
            if (dataSyncWidthMax) {
                dataSyncWidthMax = parseFloat(dataSyncWidthMax);
            }
            hardMaxWidth = dataSyncWidthMax;
        }
        let widths = elements.map(el => {
            if (el.style.width != "") {
                el.style.width = "";
            }
            return WidthSyncer.getElementWidth(el);
        });
        widths = widths.filter(width => !isNaN(width));
        let maxWidth = Math.max(...widths);
        maxWidth = Math.min(maxWidth, hardMaxWidth ?? maxWidth);
        elements.forEach((el, i) => {
            el.style.width = maxWidth + "px";
            if (typeof callback == "function") {
                callback.call(el, widths[i], maxWidth);
            }
        });
    }
    
    static getElementWidth(element) {
        let width = 0;
        let invisibleParent = WidthSyncer.findInvisibleParentElement(element);
        if (invisibleParent) {
            let parentClone  = WidthSyncer.createOffScreenClone(invisibleParent);
            let elementXPath = XPath2.getElementXPath(element, invisibleParent);
            let elementClone = XPath2.getElementByXPath(elementXPath, parentClone);
            width = parseFloat( getComputedStyle(elementClone).width );
            parentClone.remove();
        } else {
            width = parseFloat( getComputedStyle(element).width );
        }
        return width;
    }
    
    //#endregion
    
    
    // #region ==================== SYNC GROUPS
    
    static findSyncWidthGroup(groupName) {
        return Array.from(
            document.querySelectorAll(`[class*=sync-width-${groupName}]`)
        );
    }
    
    static findSyncWidthGroups() {
        let groups = {};
        namedGroups: {
            let elements = Array.from(
                document.querySelectorAll("[class*=sync-width-]")
            );
            elements.forEach(el => {
                let match = el.className.match(/sync-width-([a-z]+)/);
                if (match) {
                    let groupName = match[1];
                    if (!groups.hasOwnProperty(groupName)) {
                        groups[groupName] = [];
                    }
                    groups[groupName].push(el);
                }
            });
        }
        unnamedGroups: {
            let containers = Array.from(document.querySelectorAll(".sync-widths"));
            containers.forEach((container, groupName) => {
                let elements = Array.from(container.querySelectorAll(".sync-width"));
                elements = elements.filter(
                    element => element.closest(".sync-widths") === container
                );
                if (elements.length) {
                    groups[groupName] = elements;
                }
            });
        }
        return groups;
    }
    
    //#endregion
    
    
    // #region ==================== TABLE
    
    static getHeaderTableWidthCells(table) {
        let widthCells = [];
        let container = table.tHead ?? table.tBodies[0] ?? [];
        if (container.length == 0) {
            return cells;
        }
        let colSpanWidths = [];
        let rowIndex = 0;
        let row = container.rows[rowIndex];
        let cells = Array.from(row.cells);
        cells.forEach(cell => {
            if (cell.colSpan == 1) {
                widthCells.push(cell);
            } else {
                let subRow = container.rows[rowIndex + 1] ?? null;
                if (subRow) {
                    let startIndex = 0;
                    if (colSpanWidths.length) {
                        startIndex = colSpanWidths.reduce((pv, cv) => pv + cv, 0);
                    }
                    let endIndex = startIndex + cell.colSpan;
                    let subCells = Array.from(subRow.cells).slice(startIndex, endIndex);
                    widthCells = widthCells.concat(subCells);
                }
                colSpanWidths.push(cell.colSpan);
            }
        });
        return widthCells;
    }
    
    static getBodyTableWidthCells(table) {
        let cells = [];
        let tbody = table.tBodies[0] ?? null;
        if (tbody) {
            cells = Array.from(tbody.querySelectorAll("tr:first-child td"));
        }
        return cells;
    }
    
    static getCellsWidths(cells) {
        let widths = [];
        cells.forEach(cell => {
            widths.push(
                parseFloat(getComputedStyle(cell).width)
            );
        });
        return widths;
    }
    
    static syncCellsWidths(
        headerCells,
        headerCellsWidths,
        bodyCells,
        bodyCellsWidths,
    ) {
        let appliedWidths = [];
        for (var i = 0; i < headerCellsWidths.length; i++) {
            if (headerCellsWidths[i] > bodyCellsWidths[i]) {
                bodyCells[i].style.width = headerCellsWidths[i] + "px";
                appliedWidths.push(headerCellsWidths[i]);
            } else {
                headerCells[i].style.width = bodyCellsWidths[i] + "px";
                appliedWidths.push(bodyCellsWidths[i]);
            }
        }
        return appliedWidths;
    }
    
    static syncTableHeaderBodyWidths(headerTable, bodyTable, asPercentage = true, lockWidths = false) {
        headerTable.style.width = "auto";
        let headerCells       = WidthSyncer.getHeaderTableWidthCells(headerTable);
        let headerCellsWidths = WidthSyncer.getCellsWidths(headerCells);
        bodyTable.style.width = "auto";
        let bodyCells         = WidthSyncer.getBodyTableWidthCells(bodyTable);
        let bodyCellsWidths   = WidthSyncer.getCellsWidths(bodyCells);
        let appliedWidths = WidthSyncer.syncCellsWidths(
            headerCells,
            headerCellsWidths,
            bodyCells,
            bodyCellsWidths,
        );
        if (asPercentage) {
            let tableWidth = parseFloat(getComputedStyle(headerTable).width);
            headerCells.forEach((cell, index) => {
                let width = appliedWidths[index];
                let percentage = width / tableWidth * 100;
                cell.style.width = percentage + "%";
                bodyCells[index].style.width = percentage + "%";
            });
        }
        headerTable.style.width = "";
        bodyTable.style.width = "";
        if (lockWidths) {
            headerTable.style.width = getComputedStyle(headerTable).width;
            bodyTable.style.width = getComputedStyle(bodyTable).width;
        }
    }
    
    //#endregion
    
    
}