/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { computePosition, flip as flipMiddleware, hide as hideMiddleware, shift as shiftMiddleware, offset as offsetMiddleware, arrow as arrowMiddleware } from '@floating-ui/dom';
import { getContainingBlock, isContainingBlock, getWindow } from '@floating-ui/utils/dom';
import { roundByDPR } from './utils';
export const DEFAULT_FALLBACK_PLACEMENTS = ['top-start', 'top', 'top-end', 'left-start', 'left', 'left-end', 'right-start', 'right', 'right-end'];
export class VirtualElement {
    constructor(x, y, height = 0, width = 0) {
        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;
    }
    getBoundingClientRect() {
        return {
            x: this.x,
            y: this.y,
            top: this.y,
            left: this.x,
            bottom: this.y,
            right: this.x,
            width: this.height,
            height: this.width,
            toJSON() {
                return;
            }
        };
    }
    static fromElement(element) {
        const rect = element.getBoundingClientRect();
        return new VirtualElement(rect.left, rect.top, rect.height, rect.width);
    }
    static fromEvent(event) {
        if (event instanceof MouseEvent) {
            return new VirtualElement(event.clientX, event.clientY);
        }
        if (event instanceof TouchEvent) {
            return new VirtualElement(event.touches[0].clientX, event.touches[0].clientY);
        }
        throw new Error('Unsupported event type');
    }
}
/**
 * Calculates an elements position relative to another element.
 * @param {IPositionElementConfig} config Configuration to provide when positioning the element.
 * @returns {IPositionElementResult} The result of the positioning logic.
 */
export async function positionElementAsync({ element, anchorElement, placement = 'bottom', offset = false, offsetOptions, strategy = 'fixed', apply = true, flip = true, flipOptions = {
    fallbackPlacements: DEFAULT_FALLBACK_PLACEMENTS,
    fallbackStrategy: 'initialPlacement'
}, shift = true, shiftOptions, hide = false, hideOptions, arrowElement, arrowOptions = {}, topLayer = false }) {
    var _a, _b, _c;
    const middleware = [];
    //
    // Order of the following middleware is **important**
    //
    if (offset) {
        middleware.push(offsetMiddleware(offsetOptions));
    }
    if (shift) {
        middleware.push(shiftMiddleware(shiftOptions));
    }
    if (flip) {
        middleware.push(flipMiddleware(flipOptions));
    }
    if (hide) {
        middleware.push(hideMiddleware(hideOptions));
    }
    if (arrowElement) { // Must come before the topLayer middleware
        middleware.push(arrowMiddleware(Object.assign(Object.assign({}, arrowOptions), { element: arrowElement })));
    }
    if (topLayer) {
        middleware.push(topLayerMiddleware());
    }
    const { x, y, placement: finalPlacement, middlewareData } = await computePosition(anchorElement, element, { strategy, placement, middleware });
    // Should we apply the position information to the element?
    if (apply) {
        const styles = {
            left: '0',
            top: '0',
            translate: `${roundByDPR(x)}px ${roundByDPR(y)}px`
        };
        Object.assign(element.style, styles);
        // We use `display` here to ensure that any child overlays are also hidden
        if ((_a = middlewareData.hide) === null || _a === void 0 ? void 0 : _a.referenceHidden) {
            element.style.display = 'none';
        }
        else {
            element.style.removeProperty('display');
        }
    }
    return {
        x,
        y,
        hidden: (_c = (_b = middlewareData.hide) === null || _b === void 0 ? void 0 : _b.referenceHidden) !== null && _c !== void 0 ? _c : false,
        placement: finalPlacement,
        arrow: middlewareData.arrow
    };
}
/**
 * Custom middleware to handle positioning when the element is on the top layer AND within a containing block.
 */
export const topLayerMiddleware = () => ({
    name: 'topLayer',
    async fn({ x, y, elements: { reference, floating } }) {
        let onTopLayer = false;
        let topLayerIsFloating = false;
        let withinReference = false;
        const diffCoords = { x: 0, y: 0 };
        try {
            onTopLayer = onTopLayer || floating.matches(':popover-open');
        }
        catch (_a) { }
        try {
            onTopLayer = onTopLayer || floating.matches(':open');
        }
        catch (_b) { }
        try {
            onTopLayer = onTopLayer || floating.matches(':modal');
        }
        catch (_c) { }
        topLayerIsFloating = onTopLayer;
        const dialogAncestorQueryEvent = new Event('floating-ui-dialog-test', { composed: true, bubbles: true });
        floating.addEventListener('floating-ui-dialog-test', (event) => {
            event.composedPath().forEach((el) => {
                withinReference = withinReference || el === reference;
                if (el === floating || el.localName !== 'dialog') {
                    return;
                }
                try {
                    onTopLayer = onTopLayer || el.matches(':modal');
                }
                catch (_a) { }
            });
        }, { once: true });
        floating.dispatchEvent(dialogAncestorQueryEvent);
        let overTransforms = false;
        if (!(reference instanceof VirtualElement)) {
            const root = (withinReference ? reference : floating);
            const containingBlock = isContainingBlock(root) ? root : getContainingBlock(root);
            let css = {};
            if (containingBlock !== null && getWindow(containingBlock) !== containingBlock) {
                css = getComputedStyle(containingBlock);
                // The overlay is "over transforms" when the containing block uses specific CSS...
                overTransforms =
                    css.transform !== 'none' ||
                        css.translate !== 'none' ||
                        (css.containerType ? css.containerType !== 'normal' : false) ||
                        (css.backdropFilter ? css.backdropFilter !== 'none' : false) ||
                        (css.filter ? css.filter !== 'none' : false) ||
                        css.willChange.search('transform') > -1 ||
                        css.willChange.search('translate') > -1 ||
                        ['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value));
            }
            if (onTopLayer && overTransforms && containingBlock) {
                const rect = containingBlock.getBoundingClientRect();
                // Margins and borders are not included in the bounding client rect and need to be handled separately
                const { marginInlineStart = '0', marginBlockStart = '0', borderInlineWidth = '0', borderBlockWidth = '0' } = css;
                const inlineBoxAdjust = parseFloat(marginInlineStart) + parseFloat(borderInlineWidth);
                const blockBoxAdjust = parseFloat(marginBlockStart) + parseFloat(borderBlockWidth);
                diffCoords.x = rect.x + inlineBoxAdjust - containingBlock.scrollLeft;
                diffCoords.y = rect.y + blockBoxAdjust - containingBlock.scrollTop;
            }
        }
        if (onTopLayer && topLayerIsFloating) {
            return {
                x: x + diffCoords.x,
                y: y + diffCoords.y,
                data: diffCoords
            };
        }
        if (onTopLayer) {
            return {
                x,
                y,
                data: diffCoords
            };
        }
        return {
            x: x - diffCoords.x,
            y: y - diffCoords.y,
            data: diffCoords
        };
    }
});
