// #region ==================== MISC

/**
 * Swaps two classes.
 * 
 * @link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList
 */
DOMTokenList.prototype.swap = function (classA, classB) {
    if (this.contains(classA)) {
        this.replace(classA, classB);
    }
    else if (this.contains(classB)) {
        this.replace(classB, classA);
    }
};

Array.prototype.removeItem = function (item) {
    var itemIndex = this.indexOf(item);
    if (itemIndex > -1) {
        this.splice(itemIndex, 1);
    }
    return itemIndex;
};

/**
 * Breakable forEach loop.
 * 
 * If the callback returns true, loop breaks.
 * 
 * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
 */
Array.prototype.forEachBreakable = function (callback) {
    for (let i = 0; i < this.length; i++) {
        let done = callback(this[i], i);
        if (done) {
            break;
        }
    }
};

Array.merge = function mergeArrays() {
    let arrays = Array.from(arguments);
    let result = arrays.reduce((mainArray, subArray) => mainArray.concat(subArray));
    return result;
};

Math.random = function (MathRandom) {
    return function (min = null, max = null, round = null) {
        switch (arguments.length) {
            case 1:
                if (typeof min == "boolean") {
                    round = min;
                    max = null;
                    min = null;
                } else {
                    max = min;
                    min = 0;
                }
                break;
            case 2: 
                if (typeof max == "boolean") {
                    round = max;
                    max = min;
                    min = 0;
                }
                break;
        }
        let random = MathRandom();
        if (min !== null && max !== null) {
            random = min + MathRandom() * (max - min);
        }
        if (round === true) {
            random = Math.round(random);
        }
        return random;
    }
}(Math.random);

//#endregion


// #region ==================== (X)PATH

function getElementIndex(element) {
    return Array.from(element.parentElement.children).indexOf(element);
}

function getElementPath(element, topParent = null) {
    let path = [];
    if (topParent && typeof topParent == "string") {
        topParent = document.querySelector(topParent);
    }
    while (element) {
        path.unshift(element);
        if (topParent && topParent == element) {
            break;
        }
        element = element.parentElement;
    }
    return path;
}

function getElementXPathIdentifier(element) {
    let id = element.tagName.toLowerCase();
    if (element.parentElement) {
        let tagSiblings = element.parentElement.querySelectorAll(":scope > " + element.tagName);
        if (tagSiblings.length > 1) {
            let index = Array.from(tagSiblings).indexOf(element);
            id += "[" + (index + 1) + "]";
        }
    }
    return id;
}

function getElementXPath(element, topParent = null) {
    let xpath = [];
    let parents = getElementPath(element, topParent);
    if (topParent) {
        parents.shift();
    }
    parents.forEach(parent => {
        xpath.push(getElementXPathIdentifier(parent));
    });
    xpath = xpath.join("/");
    if (!topParent) {
        xpath = "/" + xpath;
    }
    return xpath;
}

function getElementByXPath(xpath, contextNode = null) {
    contextNode ??= document.documentElement;
    let xpEvaluator = new XPathEvaluator();
    let xpathResult = xpEvaluator.evaluate(
        xpath,
        contextNode,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
    );
    return xpathResult.singleNodeValue;
}

// #endregion


// #region ==================== ELEMENT VISIBILITY

function isElementVisible(element, checkParents = false, topParent = null) {
    if (checkParents) {
        let parents = getElementPath(element, topParent);
        return !parents.some(parent => !isElementVisible(parent));
    } else {
        return !element.hidden
               && element.style.display != "none"
               && getComputedStyle(element).display != "none";
    }
}

function findInvisibleParentElement(element) {
    let path = getElementPath(element);
    let parent = path.find(parent => !isElementVisible(parent));
    return parent ?? null;
}

function createVisibleClone(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 ==================== TABLE WIDTH SYNCING

function getColumnWidths(tableOrBody) {
    let columns = Array.from(tableOrBody.querySelectorAll("tr:first-child :is(td, th)"));
    let widths = columns.map(col => parseFloat( getComputedStyle(col).width ));
    return widths;
}

// https://en.wikipedia.org/wiki/Transpose
// https://stackoverflow.com/questions/17428587/transposing-a-2d-array-in-javascript
function transpose(array) {
    return array[0].map((_, colIndex) => array.map(row => row[colIndex]));
};

function findMaxColumnWidthsFromArrays(columnsWidths2DArray) {
    let columnsWidths = transpose(columnsWidths2DArray);
    let columnWidths = columnsWidths.map(columnWidths => {
        return Math.max(...columnWidths);
    });
    return columnWidths;
}

function findMaxColumnWidthsFromTables(...tables) {
    let columnsWidths = [];
    tables.forEach(table => {
        columnsWidths.push(getColumnWidths(table));
    });
    let maxColumnWidths = findMaxColumnWidthsFromArrays(columnsWidths);
    return maxColumnWidths;
}

function syncTableColumns(...tables) {
    let maxWidths = findMaxColumnWidthsFromTables(...tables);
    tables.forEach(table => {
        let cells = Array.from(table.querySelectorAll("tr:first-child :is(td, th)"));
        cells.forEach((cell, index) => {
            cell.style.width = maxWidths[index] + "px";
        });
    });
}

//#endregion


function isNumericString(value, isFloat = true) {
    // let isNumeric = /^-?(\d+|\d+.\d+|.\d+)([eE][-+]?\d+)?$/.test(value.toString());
    let isNumeric = /^\d+$/.test(value.toString());
    return isNumeric;
}


// #region ==================== CALLBACK DELAY

function throttle(callback, delay = 1000) {
    var timeoutHandle = null;
    let throttleInner = function throttleInner() {
        let passedArgs = Array.from(arguments);
        if (timeoutHandle == null) {
            // NOTE: should the first call wait for the delay?
            timeoutHandle = setTimeout(function () {
                callback.apply(this, passedArgs);
                timeoutHandle = null;
            }.bind(this), delay || 1000);
        }
    };
    if (!(this instanceof Window)) {
        throttleInner = throttleInner.bind(this);
    }
    return throttleInner;
}

// function debounce(callback, delay = 1000) {
//     var timeoutHandler = null;
//     return function debounceInner() {
//         let passedArgs = Array.from(arguments);
//         clearTimeout(timeoutHandler);
//         timeoutHandler = setTimeout(function () {
//             callback.apply(this, passedArgs);
//         }.bind(this), delay || 1000);
//     }.bind(this);
// }

function debounce(callback, delay = 1000) {
    var timeoutHandle = null;
    let debounceInner = function debounceInner() {
        let passedArgs = Array.from(arguments);
        clearTimeout(timeoutHandle);
        timeoutHandle = setTimeout(function () {
            callback.apply(this, passedArgs);
        }.bind(this), delay || 1000);
    };
    if (!(this instanceof Window)) {
        debounceInner = debounceInner.bind(this);
    }
    return debounceInner;
}

//#endregion