/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { OverlayAwareFoundation } from '../overlay/base/overlay-aware-foundation';
import { WithLongpressListener } from '../core/mixins/interactions/longpress/with-longpress-listener';
import { POPOVER_CONSTANTS, POPOVER_HOVER_TIMEOUT } from './popover-constants';
import { DismissibleStack } from '../core/utils/dismissible-stack';
import { VirtualElement } from '../core/utils/position-utils';
export class PopoverFoundation extends WithLongpressListener((OverlayAwareFoundation)) {
    constructor(adapter) {
        super(adapter);
        this._anchor = null;
        this._arrow = false;
        this._animationType = 'zoom';
        this._triggerTypes = [POPOVER_CONSTANTS.defaults.TRIGGER_TYPE];
        this._persistentHover = false;
        this._hoverDismissDelay = POPOVER_HOVER_TIMEOUT;
        this._hoverDelay = POPOVER_CONSTANTS.defaults.HOVER_DELAY;
        this._preset = POPOVER_CONSTANTS.defaults.PRESET;
        this._previouslyFocusedElement = null;
        // Click trigger listeners
        this._anchorClickListener = this._onAnchorClick.bind(this);
        // Double click trigger listeners
        this._anchorDoubleClickListener = this._onAnchorDoubleClick.bind(this);
        // Hover trigger listeners
        this._anchorMouseenterListener = this._onAnchorMouseenter.bind(this);
        this._anchorMouseleaveListener = this._onAnchorMouseleave.bind(this);
        this._popoverMouseenterListener = this._onPopoverMouseenter.bind(this);
        this._popoverMouseleaveListener = this._onPopoverMouseleave.bind(this);
        this._mousemoveListener = this._onMousemove.bind(this);
        // Focus trigger listeners
        this._anchorFocusListener = this._onAnchorFocus.bind(this);
        this._anchorBlurListener = this._onAnchorBlur.bind(this);
        this._popoverBlurListener = this._onPopoverBlur.bind(this);
        // Contextmenu listener
        this._contextmenuListener = this._onContextmenu.bind(this);
    }
    initialize() {
        super.initialize();
        this._adapter.tryApplyGlobalConfiguration([
            'placement',
            'animationType',
            'positionStrategy',
            'shift',
            'hide',
            'flip',
            'boundaryElement',
            'fallbackPlacements',
            'persistent',
            'arrow'
        ]);
        if (!this.anchorElement) {
            this._adapter.tryLocateAnchorElement(this._anchor);
        }
        this._initializeTriggerListeners();
        this._adapter.initializeAnchorElement();
    }
    destroy() {
        super.destroy();
        this._adapter.destroy();
        window.clearTimeout(this._hoverTimeout);
        window.clearTimeout(this._hoverAnchorLeaveTimeout);
        window.clearTimeout(this._popoverMouseleaveTimeout);
        this._previouslyFocusedElement = null;
        if (this.open) {
            this._closePopover();
        }
        this._removeTriggerListeners();
    }
    hideAsync() {
        return this._closePopover();
    }
    async _onOverlayLightDismiss(evt) {
        evt.preventDefault();
        this._requestDismiss(evt.detail.reason);
    }
    dispatchBeforeToggleEvent({ reason }) {
        const evt = this._dispatchBeforetoggleEvent();
        if (evt.defaultPrevented) {
            return false;
        }
        const previousFocusedEl = this._previouslyFocusedElement;
        this._closePopover().then(() => {
            this._dispatchToggleEvent();
            if (reason === 'escape' && previousFocusedEl && this._adapter.hasFocus()) {
                previousFocusedEl.focus();
            }
        });
        return true;
    }
    _openPopover({ dispatchEvents = true, fromKeyboard = false } = {}) {
        if (dispatchEvents) {
            const evt = this._dispatchBeforetoggleEvent();
            if (evt.defaultPrevented) {
                return;
            }
        }
        this._previouslyFocusedElement = this._adapter.captureFocusedElement();
        this._adapter.setOverlayOpen(true);
        if (!this.overlayElement.persistent) {
            DismissibleStack.instance.add(this._adapter.hostElement);
        }
        this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.OPEN, this.open);
        // We only attempt to set initial focus if the event was triggered by a keyboard interaction
        if (fromKeyboard) {
            this._adapter.tryAutofocus();
        }
        if (dispatchEvents) {
            this._dispatchToggleEvent();
        }
    }
    async _closePopover() {
        this._previouslyFocusedElement = null;
        DismissibleStack.instance.remove(this._adapter.hostElement);
        if (this._animationType === 'none') {
            this._adapter.setOverlayOpen(false);
        }
        else {
            await this._adapter.hide();
        }
        this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.OPEN, this.open);
    }
    _dispatchBeforetoggleEvent() {
        const evt = new CustomEvent(POPOVER_CONSTANTS.events.BEFORETOGGLE, {
            detail: {
                oldState: this.open ? 'open' : 'closed',
                newState: this.open ? 'closed' : 'open'
            },
            bubbles: false,
            cancelable: true
        });
        this._adapter.dispatchHostEvent(evt);
        return evt;
    }
    _dispatchToggleEvent() {
        this._adapter.dispatchHostEvent(new CustomEvent(POPOVER_CONSTANTS.events.TOGGLE, {
            detail: {
                oldState: this.open ? 'closed' : 'open',
                newState: this.open ? 'open' : 'closed'
            },
            bubbles: false
        }));
    }
    _initializeTriggerListeners() {
        if (this._triggerTypes.includes('manual')) {
            return;
        }
        let types = [...this._triggerTypes];
        // When contextmenu is used, we ignore all other trigger types
        if (types.includes('contextmenu')) {
            types = ['contextmenu'];
        }
        else {
            // Hover triggers already listen to focus by default
            if (types.includes('hover') && types.includes('focus')) {
                types.splice(types.indexOf('focus'), 1);
            }
            // We don't support both click and doubleclick together; click takes precedence
            if (types.includes('click') && types.includes('doubleclick')) {
                types.splice(types.indexOf('doubleclick'), 1);
            }
        }
        const triggerInitializers = {
            click: () => this._adapter.addAnchorListener('click', this._anchorClickListener),
            hover: () => {
                this._adapter.addAnchorListener('mouseenter', this._anchorMouseenterListener);
                this._adapter.addAnchorListener('focusin', this._anchorFocusListener);
            },
            focus: () => this._adapter.addAnchorListener('focusin', this._anchorFocusListener),
            longpress: () => {
                if (this._adapter.overlayElement.anchorElement && !(this._adapter.overlayElement.anchorElement instanceof VirtualElement)) {
                    this._startLongpressListener(this._adapter.overlayElement.anchorElement);
                }
            },
            doubleclick: () => this._adapter.addAnchorListener('dblclick', this._anchorDoubleClickListener),
            contextmenu: () => this._adapter.addDocumentListener('contextmenu', this._contextmenuListener)
        };
        types.forEach(triggerType => { var _a; return (_a = triggerInitializers[triggerType]) === null || _a === void 0 ? void 0 : _a.call(triggerInitializers); });
    }
    _removeTriggerListeners() {
        const triggerRemovers = {
            click: () => this._adapter.removeAnchorListener('click', this._anchorClickListener),
            hover: () => {
                this._adapter.removeAnchorListener('mouseenter', this._anchorMouseenterListener);
                this._adapter.removeAnchorListener('mouseleave', this._anchorMouseleaveListener);
                this._adapter.removeAnchorListener('focusin', this._anchorFocusListener);
                this._adapter.removeAnchorListener('focusout', this._anchorBlurListener);
                this._adapter.removeHostListener('focusout', this._popoverBlurListener);
                this._tryRemoveHoverListeners();
            },
            focus: () => {
                this._adapter.removeAnchorListener('focusin', this._anchorFocusListener);
                this._adapter.removeAnchorListener('focusout', this._anchorBlurListener);
                this._adapter.removeHostListener('focusout', this._popoverBlurListener);
            },
            longpress: () => {
                if (this._adapter.overlayElement.anchorElement && !(this._adapter.overlayElement.anchorElement instanceof VirtualElement)) {
                    this._stopLongpressListener(this._adapter.overlayElement.anchorElement);
                }
            },
            doubleclick: () => this._adapter.removeAnchorListener('dblclick', this._anchorDoubleClickListener),
            contextmenu: () => this._adapter.removeDocumentListener('contextmenu', this._contextmenuListener)
        };
        this._triggerTypes.forEach(triggerType => { var _a; return (_a = triggerRemovers[triggerType]) === null || _a === void 0 ? void 0 : _a.call(triggerRemovers); });
    }
    _startHoverListeners() {
        this._adapter.addDocumentListener('mousemove', this._mousemoveListener);
        this._adapter.addSurfaceListener('mouseenter', this._popoverMouseenterListener);
        this._adapter.addSurfaceListener('mouseleave', this._popoverMouseleaveListener);
    }
    _tryRemoveHoverListeners() {
        this._adapter.removeDocumentListener('mousemove', this._mousemoveListener);
        this._adapter.removeSurfaceListener('mouseenter', this._popoverMouseenterListener);
        this._adapter.removeSurfaceListener('mouseleave', this._popoverMouseleaveListener);
    }
    _requestDismiss(reason) {
        DismissibleStack.instance.requestDismiss(this._adapter.hostElement, { reason });
    }
    _requestClose(reason) {
        DismissibleStack.instance.dismiss(this._adapter.hostElement, { reason });
    }
    _onHoverClose() {
        var _a;
        /* c8 ignore next 3 */
        if (!this.open) {
            return;
        }
        if (this._currentHoverCoords) {
            const mouseElement = document.elementFromPoint(this._currentHoverCoords.x, this._currentHoverCoords.y);
            const isOwnElement = mouseElement &&
                (this._adapter.isChildElement(mouseElement) ||
                    (!(this._adapter.overlayElement.anchorElement instanceof VirtualElement) && ((_a = this._adapter.overlayElement.anchorElement) === null || _a === void 0 ? void 0 : _a.contains(mouseElement))));
            /* c8 ignore next 3 */
            if (isOwnElement) {
                return;
            }
        }
        window.clearTimeout(this._hoverTimeout);
        this._tryRemoveHoverListeners();
        this._requestClose('hover');
    }
    /**
     * Handles `click` events on the anchor element.
     *
     * Only called when using the "click" (default) trigger type.
     */
    _onAnchorClick(evt) {
        if (!this.open) {
            const fromKeyboard = evt.detail === 0 && !evt.pointerType;
            this._openPopover({ fromKeyboard });
        }
        else {
            this._requestClose('click');
        }
    }
    /**
     * Handles `dblclick` events on the anchor element.
     */
    _onAnchorDoubleClick() {
        if (!this.open) {
            this._openPopover();
        }
        else {
            this._requestClose('doubleclick');
        }
    }
    /**
     * Handles `mouseenter` events on the anchor element. This is used to determine if the popover should be opened.
     *
     * Only called when using the "hover" trigger type.
     */
    _onAnchorMouseenter() {
        window.clearTimeout(this._hoverAnchorLeaveTimeout);
        if (!this._adapter.overlayElement.open) {
            if (!this._persistentHover) {
                this._adapter.addAnchorListener('mouseleave', this._anchorMouseleaveListener);
            }
            if (this._hoverDelay) {
                this._hoverTimeout = window.setTimeout(() => {
                    this._openPopover();
                }, this._hoverDelay);
            }
            else {
                this._openPopover();
            }
        }
    }
    /**
     * Handles `mouseleave` events on the anchor element. This is used to determine if the popover should be closed.
     *
     * Only called when using the "hover" trigger type.
     *
     * We use a timeout here to allow for the user to take an indirect path toward the popover.
     */
    _onAnchorMouseleave() {
        this._startHoverListeners();
        window.clearTimeout(this._hoverTimeout);
        this._hoverAnchorLeaveTimeout = window.setTimeout(() => {
            this._hoverAnchorLeaveTimeout = undefined;
            this._onHoverClose();
        }, this._hoverDismissDelay);
    }
    /**
     * Handles `mouseenter` events on the popover element. This is used to determine if the mouse has entered the popover element,
     * only after the mouse leaves the anchor element..
     */
    _onPopoverMouseenter() {
        window.clearTimeout(this._hoverAnchorLeaveTimeout);
        window.clearTimeout(this._popoverMouseleaveTimeout);
    }
    /**
     * Handles `mouseleave` events on the popover element. This is used to determine if the mouse has moved outside of the popover.
     *
     * Only called when using the "hover" trigger type.
     *
     * We use a timeout here to allow for the user to take an indirect path toward an open child menu.
     * This allows for the popup to stay open while the user is moving their mouse to it to avoid closing immediately.
     */
    _onPopoverMouseleave() {
        this._popoverMouseleaveTimeout = window.setTimeout(() => {
            this._popoverMouseleaveTimeout = undefined;
            this._onHoverClose();
        }, this._hoverDismissDelay);
    }
    /**
     * Handles `mousemove` events on the document. This is used to determine if the mouse has moved outside of the popover.
     *
     * This listener is only initialized once the mouse leaves the anchor element.
     *
     * Only called when using the "hover" trigger type.
     */
    _onMousemove(evt) {
        this._currentHoverCoords = { x: evt.pageX, y: evt.pageY };
    }
    /**
     * Handles `focusin` events on the anchor element. This is used to determine if focus has been received on the anchor element when using the "focus" trigger type.
     */
    _onAnchorFocus(_evt) {
        if (!this._adapter.overlayElement.open) {
            this._adapter.addAnchorListener('focusout', this._anchorBlurListener);
            this._openPopover({ fromKeyboard: true });
        }
    }
    /**
     * Handles `focusout` events on the anchor element. This is used to determine if focus has been moved outside of the anchor element when using the "focus" trigger type.
     */
    _onAnchorBlur(evt) {
        if (!this._adapter.isChildElement(evt.relatedTarget)) {
            // Focus was moved outside of the popover element, so let's assume we need to close
            this._requestClose('focus');
        }
        else {
            // Focus was moved to within the popover element, now we must listen for focus to move outside of the popover
            this._adapter.addHostListener('focusout', this._popoverBlurListener);
        }
    }
    _onPopoverBlur({ relatedTarget }) {
        var _a, _b;
        const popoverHasFocus = this._adapter.hasFocus();
        const anchorHasFocus = !(this._adapter.overlayElement.anchorElement instanceof VirtualElement) &&
            (((_a = this._adapter.overlayElement.anchorElement) === null || _a === void 0 ? void 0 : _a.matches(':focus-within')) ||
                ((_b = this._adapter.overlayElement.anchorElement) === null || _b === void 0 ? void 0 : _b.contains(relatedTarget)));
        if (!popoverHasFocus && !anchorHasFocus) {
            this._requestClose('focus');
        }
    }
    _onLongpress() {
        if (!this.open) {
            this._openPopover();
        }
    }
    _onContextmenu(evt) {
        evt.preventDefault();
        this.anchorElement = VirtualElement.fromEvent(evt);
        if (!this.open) {
            this._openPopover();
        }
    }
    /**
     * Public API
     */
    get open() {
        return this._adapter.overlayElement.open;
    }
    set open(value) {
        if (this._adapter.overlayElement.open !== value) {
            if (value) {
                this._openPopover({ dispatchEvents: false });
            }
            else {
                this._closePopover();
            }
        }
    }
    get anchorElement() {
        return this._adapter.overlayElement.anchorElement;
    }
    set anchorElement(value) {
        if (this._adapter.overlayElement.anchorElement !== value) {
            if (this._adapter.isConnected) {
                this._removeTriggerListeners();
                this._adapter.cleanupAnchorElement();
            }
            this._adapter.overlayElement.anchorElement = value;
            if (this._adapter.isConnected) {
                this._initializeTriggerListeners();
                this._adapter.initializeAnchorElement();
            }
        }
    }
    get anchor() {
        return this._anchor;
    }
    set anchor(value) {
        if (this._anchor !== value) {
            this._anchor = value;
            if (this._adapter.isConnected) {
                this._removeTriggerListeners();
                this._adapter.cleanupAnchorElement();
                this._adapter.tryLocateAnchorElement(this._anchor);
                this._initializeTriggerListeners();
                this._adapter.initializeAnchorElement();
            }
        }
    }
    get arrow() {
        return this._arrow;
    }
    set arrow(value) {
        value = Boolean(value);
        if (this._arrow !== value) {
            this._arrow = value;
            this._adapter.toggleArrow(value);
            this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.ARROW, value);
        }
    }
    get animationType() {
        return this._animationType;
    }
    set animationType(value) {
        if (this._animationType !== value) {
            this._animationType = value;
            this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.ANIMATION_TYPE, !!this._animationType, this._animationType);
        }
    }
    get triggerType() {
        return this._triggerTypes.length === 1 ? this._triggerTypes[0] : this._triggerTypes;
    }
    set triggerType(value) {
        if (this._triggerTypes !== value) {
            if (this._adapter.isConnected) {
                this._removeTriggerListeners();
            }
            this._triggerTypes = Array.isArray(value) ? value : [value];
            this._triggerTypes = this._triggerTypes.filter(type => !!type);
            if (!this._triggerTypes.length) {
                this._triggerTypes = [POPOVER_CONSTANTS.defaults.TRIGGER_TYPE];
            }
            if (this._adapter.isConnected) {
                this._initializeTriggerListeners();
            }
        }
    }
    get longpressDelay() {
        return this._longpressDelay;
    }
    set longpressDelay(value) {
        if (this._longpressDelay !== value) {
            this._longpressDelay = value;
            this._adapter.setHostAttribute(POPOVER_CONSTANTS.attributes.LONGPRESS_DELAY, String(this._longpressDelay));
        }
    }
    get persistentHover() {
        return this._persistentHover;
    }
    set persistentHover(value) {
        value = Boolean(value);
        if (this._persistentHover !== value) {
            this._persistentHover = value;
            if (this._persistentHover && this._triggerTypes.includes('hover') && this._adapter.isConnected) {
                this._removeTriggerListeners();
                this._initializeTriggerListeners();
            }
            this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.PERSISTENT_HOVER, value);
        }
    }
    get hoverDelay() {
        return this._hoverDelay;
    }
    set hoverDelay(value) {
        if (isNaN(value) || value < 0) {
            value = POPOVER_CONSTANTS.defaults.HOVER_DELAY;
        }
        if (this._hoverDelay !== value) {
            this._hoverDelay = value;
            this._adapter.setHostAttribute(POPOVER_CONSTANTS.attributes.HOVER_DELAY, String(this._hoverDelay));
        }
    }
    get hoverDismissDelay() {
        return this._hoverDismissDelay;
    }
    set hoverDismissDelay(value) {
        if (this._hoverDismissDelay !== value) {
            this._hoverDismissDelay = value;
            this._adapter.setHostAttribute(POPOVER_CONSTANTS.attributes.HOVER_DISMISS_DELAY, String(this._hoverDismissDelay));
        }
    }
    get preset() {
        var _a;
        return (_a = this._preset) !== null && _a !== void 0 ? _a : POPOVER_CONSTANTS.defaults.PRESET;
    }
    set preset(value) {
        value = value !== null && value !== void 0 ? value : POPOVER_CONSTANTS.defaults.PRESET;
        if (this._preset !== value) {
            this._preset = value;
            const hasPreset = value !== POPOVER_CONSTANTS.defaults.PRESET;
            this._adapter.toggleHostAttribute(POPOVER_CONSTANTS.attributes.PRESET, hasPreset, this._preset);
        }
    }
}
