/**
 * The useInjectedTitleBar hook is responsible for DOM manipulation of web content in windows where the Finsemble
 * WindowTitleBar has been injected (as opposed to living in an encapsulating parent TitleBarWindow). This hook
 * executes only once, to adjust any fixed content downward to make room for the title bar. It also enables a
 * mutation observer that monitors the entire document.body for newly added nodes with "fixed" position, adjusting
 * their vertical position accordingly when they are discovered.
 */
import { useEffect, useState } from "react";
import { FEAGlobals } from "../../common/Globals";
// Values for possible zero
const possibleZeros = ["0", "0px", 0];
/** Utility function to retrieve all readable CSS rules, which are used to refine
 *  content height adjustments.
 */
const getCSSRules = () => {
    let temp = [];
    // eslint-disable-next-line sonarjs/no-empty-collection
    return temp.concat(...Array.from(document.styleSheets).map((s) => {
        try {
            return [...(s.cssRules || [])];
        }
        catch (e) {
            console.debug("ignoring a cross-domain CSS rule we can't read");
            return [];
        }
    }));
};
/** Utility function that filters a set of CSS rules to those that match
 *  a specific element.  */
const getMatchedRules = (el, rules) => rules.filter((r) => el.matches(r.selectorText));
/** Utility function that determines if an element has a height set via CSS
 *  as we do not want to apply adjustments to elements that grow with their
 *  content
 */
const getCSSHeight = (el, allRules) => {
    var _a, _b;
    if ((_a = el === null || el === void 0 ? void 0 : el.style) === null || _a === void 0 ? void 0 : _a.height)
        return el.style.height;
    let matchedRules = getMatchedRules(el, allRules);
    for (const x of matchedRules) {
        if ((_b = x.style) === null || _b === void 0 ? void 0 : _b.height)
            return x.style.height;
    }
    return false;
};
/**
 * Used in bumpFixedElements and bumpAbsoluteElements
 * Calculates value to bump element by. If 'all' return top style plus the header height, if '0Positioned' return just header height
 * @param type Whether all elements are being bumped by the header height and their top style or just header height
 * @param topStyle Element top value
 * @param bumpBy Value to bump element by
 * @returns Top style for element
 */
const calculateBumpValue = (type, topStyle, bumpBy) => {
    let topVal = topStyle;
    if (type == "all") {
        topVal = `calc(${topStyle} + ${bumpBy})`;
    }
    else if (type == "0Positioned" && possibleZeros.includes(topStyle)) {
        topVal = bumpBy;
    }
    return topVal;
};
/**
 * Used in bumpAbsoluteElements
 *
 * Recursively traverse through the HTML document and return an array of the top level elements
 * and elements with static parents
 * @param {object} element The HTML element
 * @param {array} acc accumulator
 * @returns {array} array top level elements and elements with static parents
 *
 */
const traverseHtml = (element, acc = []) => {
    const elements = element.children;
    const len = elements.length;
    if (!len) {
        acc.push(element);
    }
    else {
        for (let i = 0; i < len; i++) {
            const style = window.getComputedStyle(elements[i], null);
            if (style.getPropertyValue("position") == "static") {
                acc = traverseHtml(elements[i], acc);
            }
            else {
                acc.push(elements[i]);
            }
        }
    }
    return acc;
};
/**
 * Called from bumpElements
 *
 * Bumps all elements with fixed positioning, that are not in the header, down by the configured amount.
 * @private
 * @param {object} params bumpElements object
 */
const bumpFixedElements = (params) => {
    var _a, _b;
    let { fixed, bumpBy } = params;
    const allNodes = document.body.querySelectorAll("*:not(#FSBLHeader, .fsbl-header)");
    const getFixedElements = () => {
        const fixedElements = [];
        allNodes.forEach((node) => {
            const style = window.getComputedStyle(node, null);
            if (style.getPropertyValue("position") == "fixed")
                fixedElements.push(node);
        });
        return fixedElements;
    };
    /**
     * Filter out fixed positioned elements with parents containing the following properties:
     * transform, perspective, filter or will-change="transform"
     * as they put the fixed-positioned elements back into the document flow (so position
     * is based on where they are in the DOM as per normal elements)
     * This needs to be done because fixed positioned elements will be positioned relative
     * to parents with these properties instead of the usual viewport,
     * which means we do not need to bump the element since the parent will have been adjusted.
     */
    const filterFixedElements = (elements) => {
        const ignorableNodes = [];
        const filtered = [...elements];
        const len = elements.length;
        allNodes.forEach((node) => {
            const style = window.getComputedStyle(node, null);
            const properties = ["transform", "perspective", "filter"];
            if (style.getPropertyValue("will-change") === "transform")
                ignorableNodes.push(node);
            properties.forEach((prop) => style.getPropertyValue(prop) !== "none" && ignorableNodes.push(node));
        });
        for (let i = len - 1; i >= 0; i--) {
            for (let j = 0; j < ignorableNodes.length; j++) {
                if (ignorableNodes[j].contains(elements[i])) {
                    filtered.pop();
                    break;
                }
            }
        }
        return filtered;
    };
    // Get all fixed positioned elements
    const filteredElements = filterFixedElements(getFixedElements());
    for (let i = 0; i < filteredElements.length; i++) {
        const style = window.getComputedStyle(filteredElements[i], null);
        const topStyle = style.getPropertyValue("top");
        if (!((_b = (_a = filteredElements[i]) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.contains("bumped-element"))) {
            filteredElements[i].style.top = calculateBumpValue(fixed, topStyle, bumpBy);
            filteredElements[i].classList.add("bumped-element");
        }
    }
};
/**
 * Called from bumpElements
 *
 * Bumps all absolute positioned elements with static parents, that are not in the header, down by the configured amount.
 * @private
 * @param {object} params bumpElements object
 */
const bumpAbsoluteElements = (params) => {
    var _a, _b;
    let { absolute, bumpBy } = params;
    const selector = "*:not(#FSBLHeader, .fsbl-header)";
    // Get top level absolute positioned elements and elements who have static parents
    const elements = traverseHtml({ children: document.body.querySelectorAll(`:scope > ${selector}`) });
    const len = elements.length;
    for (let i = 0; i < len; i++) {
        const style = window.getComputedStyle(elements[i], null);
        const topStyle = style.getPropertyValue("top");
        // Target absolute positioned elements
        if (style.getPropertyValue("position") == "absolute" && !((_b = (_a = elements[i]) === null || _a === void 0 ? void 0 : _a.classList) === null || _b === void 0 ? void 0 : _b.contains("bumped-element"))) {
            elements[i].style.top = calculateBumpValue(absolute, topStyle, bumpBy);
            elements[i].classList.add("bumped-element");
        }
    }
};
/**
 * Toggle setting found in `foreign.components.["Window Manager"].FSBLHeader.bumpElements.[bumpBy|fixed|absolute]`
 * Called from beginAdjustment()
 *
 * Calls bumpFixedElements and bumpAbsoluteElements to bump fixed and/or absolute elements by the value of bumpBy if configured to do so.
 * Since absolute and fixed positioned elements are removed from the document flow their positions will not be adjusted when the titlebar
 * is injected and they have a set top style. However, if the element is an absolute positioned element and it's
 * parent container has a position that is not set to static, then it'll adjust relative to the parent. Fixed positioned elements, on the other hand,
 * won't adjust unless their top styles are changed.
 */
const bumpElements = (config) => {
    if (!config)
        return;
    if (config.bumpBy !== "0px") {
        if (config.fixed)
            bumpFixedElements(config);
        if (config.absolute)
            bumpAbsoluteElements(config);
    }
};
/**
 * This is used in adjustTopLevelHeight() and adjustContentHeight()
 *
 * Set the max height of each element (that has a defined height - as far as
 * we can tell) to be the height of the parent container subtracted by the
 * height of the previous element. This forces the element to either obey its
 * predefined height or stay within the bounds of the max height.
 *
 * @param {array} cssRules CSS Rules for the current document.
 * @param {object} elem HTML element
 * @param {number} prevHeight height of previous sibling
 * @param {string} maxElemHeight maximum height of the element
 * @param {number} headerHeight the height of the header
 */
const adjust = (cssRules, elem, prevHeight, headerHeight = 25) => {
    var _a;
    let hasHeight = getCSSHeight(elem, cssRules);
    if (
    // workaround to fix dnd scrim disappearing
    !((_a = elem.classList) === null || _a === void 0 ? void 0 : _a.contains("fsbl-share-scrim")) &&
        //only apply adjustments to elements that have a defined height
        // (as far as we can tell - we can't access cross-domain css)
        hasHeight) {
        const clientHeight = elem.clientHeight ? elem.clientHeight : 0;
        const offsetHeight = elem.offsetHeight ? elem.offsetHeight : 0;
        const offset = offsetHeight - clientHeight;
        let adjustPrev = prevHeight + offset;
        const elemPosition = window.getComputedStyle(elem, null).getPropertyValue("position");
        if (elemPosition == "fixed" || elemPosition == "absolute")
            adjustPrev = headerHeight + offset;
        elem.style.maxHeight = `calc(100% - ${adjustPrev}px)`;
        elem.classList.add("adjusted-element");
    }
};
/**
 * Adjusts the content view height to be maximum of the parent container (passed in by adjustTopLevelHeight()) height
 * subtracted by the total height of elements above it. The height of the previous elements are added to this calculation
 * if they are not fixed or absolute.
 *
 * @param {array} cssRules CSS Rules for the current document.
 * @param {object} parentElem parent element
 * @param {number} headerHeight the height of the header
 */
const adjustContentHeight = (cssRules, parentElem, headerHeight) => {
    const { children } = parentElem;
    const len = children.length;
    let prevHeight = 0;
    for (let i = 0; i < len; i++) {
        adjust(cssRules, children[i], prevHeight, headerHeight);
        // Fixed and absolute positioned elements are ignored since they are removed from the document flow.
        const elemPosition = window.getComputedStyle(children[i], null).getPropertyValue("position");
        if (elemPosition !== "fixed" && elemPosition !== "absolute")
            prevHeight = prevHeight + children[i].offsetHeight;
    }
};
/**
 * Toggle setting found in `foreign.components.["Window Manager"].FSBLHeader.adjustContentHeights`
 * Called from beginAdjustment()
 *
 * Adjusts the top level content max-height to be 100% subtracted by the total height of the previous elements.
 * The height of the previous elements are added to this calculation if they are not fixed or absolute.
 * These top level elements will be passed into adjustContentHeight where their descendants
 * will also have their heights adjusted (where they have a defined height - elements that grow with content
 * are left alone).
 *
 * Absolute and Fixed positions elements, however, will just have their max-heights set to 100% subtracted by the header height
 * since they are not part of the document flow.
 * Example:
 * In this below situation, the B element extends passed the view because the header is injected. B doesn't have a
 * set height so it'll extend as far as it needs.
 * --------------------
 * |******************|
 * |*     Titlebar   *|
 * |******************|
 * |******************|
 * |*       A        *|
 * |*                *|
 * |******************|
 * |******************|
 * |*                *|
 * |*                *|
 * |*       B        *|
 * |*                *|
 * |*                *|
 * |*                *|
 * --------------------
 * The chain of adjust functions will set the maxHeight of A and B to ensure they both occupy space that the above element doesn't
 * and keep it within the bounds of the view.
 * --------------------
 * |******************|
 * |*     Titlebar   *|
 * |******************|
 * |******************|
 * |*       A        *|
 * |*                *|
 * |******************|
 * |******************|
 * |*                *|
 * |*                *|
 * |*       B        *|
 * |*                *|
 * |*                *|
 * |******************|
 * --------------------
 * @param {number} headerHeight the height of the header
 */
const adjustTopLevelHeight = (headerHeight) => {
    const cssRules = getCSSRules();
    const elements = document.body.querySelectorAll(":scope > *:not(#FSBLHeader, .fsbl-header)");
    let prevHeight = 0;
    // Tags that don't accept max-heights
    const avoidTags = ["svg", "script"];
    elements.forEach((elem) => {
        if (!avoidTags.includes(String(elem.tagName).toLowerCase())) {
            adjust(cssRules, elem, prevHeight, headerHeight);
            // Heights of fixed and absolute positioned elements are not added to the total heights of previous elements
            // since they are removed from the document flow and can be positioned anywhere in the page.
            if (elem.style.position !== "fixed" && elem.style.position !== "absolute")
                prevHeight = prevHeight + elem.offsetHeight;
            adjustContentHeight(cssRules, elem, headerHeight);
        }
    });
};
/**
 * Toggle setting found in `foreign.components.["Window Manager"].FSBLHeader.bumpElements.[monitorFixedAdditions]`
 * Called from beginAdjustment()
 *
 * Monitors the DOM for mutations, if the added node has a fixed position with a top style of 0px, it will be bumped by the title bar height.
 * This ensures that if a fixed positioned element is dynamically added to the DOM, for example a pop-up modal, it'll have it's height adjusted
 * by the header height since the initial bumping of elements can happen before this element exists.
 *
 * Element heights are not monitored and adjusted since this would require that all pre-existing elements have their height recalculated each time
 * a new element is dynamically added onto the page. Absolute positioned elements, however, could be monitored and adjusted since all we would
 * need to check for is that it's parent has a static position or not, if it does then it can be adjusted.
 * @param headerHeight the height of the header
 */
const monitorFixedPositionedElements = (headerHeight) => {
    const options = {
        childList: true,
        subtree: true,
    };
    /**
     * MutationObserver interface provides the ability to watch for
     * changes to the DOM tree using the observe method
     */
    const mutationObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                var _a;
                if (!((_a = node === null || node === void 0 ? void 0 : node.classList) === null || _a === void 0 ? void 0 : _a.contains("bumped-element"))) {
                    try {
                        const nodeStyle = window.getComputedStyle(node);
                        const topStyle = nodeStyle.top;
                        // make sure node is fixed, has top style set to 0, and has not been bumped
                        if (nodeStyle.position === "fixed" && possibleZeros.includes(topStyle)) {
                            node.style.top = calculateBumpValue("0Positioned", topStyle, headerHeight);
                            node.classList.add("bumped-element");
                        }
                    }
                    catch (err) {
                        // Expected to error if the node is not an Element e.g text is not of type Element
                    }
                }
            });
        });
    });
    // We observe the body and do not disconnect because there may be other elements that are dynamically loaded, such as a dialogue menu.
    mutationObserver.observe(document.body, options);
};
// Hack the window's scrollbar so that it displays underneath the header. html.overflow: hidden body.overflow:auto
// This is turned on by default. Set "Window Manager.hackScrollbar: false" to turn it off
const hackScrollbar = () => {
    const header = document.getElementsByClassName("fsbl-header")[0];
    //Add a margin top to the body if there title bar is injected into the component.
    if (header) {
        document.body.style.margin = "0px";
        document.body.style.marginTop = window.getComputedStyle(header, null).getPropertyValue("height");
    }
    const htmlElement = document.querySelector("html");
    if (htmlElement)
        htmlElement.style.overflowY = "hidden";
    const bodyElement = document.querySelector("body");
    if (bodyElement)
        bodyElement.style.overflowY = "auto";
};
/**
 * Begin adjusting the body and top level elements and their direct descendants by the header height.
 */
const beginAdjustment = (bumpElementConfig) => {
    const header = document.getElementsByClassName("fsbl-header")[0];
    if (!header) {
        FEAGlobals.FSBL.Clients.Logger.system.error("WindowTitleBarShell.maybeAdjustWindow:could not find fsbl-header.");
        return;
    }
    const bodyElement = document.querySelector("body");
    const headerHeight = window.getComputedStyle(header, null).getPropertyValue("height");
    if (bodyElement) {
        bodyElement.style.height = `calc(100vh - ${headerHeight})`;
    }
    if (bumpElementConfig.hackScrollbar)
        hackScrollbar();
    if (bumpElementConfig.adjustContentHeights)
        adjustTopLevelHeight(Number.parseInt(headerHeight, 10));
    if (bumpElementConfig.bumpFixedElements) {
        bumpElements(bumpElementConfig);
        if (bumpElementConfig.monitorFixedAdditions)
            monitorFixedPositionedElements(headerHeight);
    }
};
/**
 * Once document is ready and header is injected into DOM, begin adjusting the height of elements and bump fixed/absolute elements
 */
const adjustWindow = (bumpElementConfig) => {
    // don't start pushing stuff down until page is ready. This is needed for preloaded title bars because they can happen before the page is ready.
    if (document.readyState === "complete") {
        beginAdjustment(bumpElementConfig);
    }
    else {
        document.addEventListener("readystatechange", () => {
            if (document.readyState === "complete")
                beginAdjustment(bumpElementConfig);
        });
    }
};
export const useInjectedTitleBar = (bumpConfig) => {
    const [bumped, setBumped] = useState(false);
    // Run the hook when a BumpConfig is available (e.g. loaded from store)
    useEffect(() => {
        var _a;
        // If this isn't an injected title bar window then do nothing
        if ((_a = FEAGlobals === null || FEAGlobals === void 0 ? void 0 : FEAGlobals.fin) === null || _a === void 0 ? void 0 : _a.isTitlebarWindow())
            return;
        // Only run once, when the bumpConfig becomes available
        if (bumpConfig && !bumped) {
            adjustWindow(bumpConfig);
            setBumped(true);
        }
    }, [bumpConfig]);
};
//# sourceMappingURL=useInjectedTitleBar.js.map