/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { Deferred } from './deferred';
import { task } from './event-utils';
// The symbol to use for the tryDismiss method on dismissible elements
export const tryDismiss = Symbol('tryDismiss');
// The key to use for storing the dismissible stack on the window object
const DISMISSIBLE_STACK_INSTANCE_KEY = Symbol('forgeDismissibleStack');
export class DismissibleStack {
    /**
     * A promise that resolves when all elements in the stack have finished dismissing.
     *
     * Resolves immediately if there are no elements in the stack.
     */
    get dismissing() {
        var _a, _b;
        return (_b = (_a = this._deferredDismiss) === null || _a === void 0 ? void 0 : _a.promise) !== null && _b !== void 0 ? _b : Promise.resolve();
    }
    // eslint-disable-next-line @tylertech-eslint/require-private-modifier
    constructor() {
        /**
         * A set of all elements that are currently eligible for dismissal.
         *
         * Typically used when a user is force dismissing an element, such as when they click
         * an element that triggered it to open.
         */
        this._dismissibleElements = new Set();
        /**
         * A map of all elements that are requesting to be dismissed, and the state that they
         * are requesting to dismiss with.
         *
         * This is used to queue up dismiss requests so that we can wait for all other elements
         * to dispatch their dismiss events, typically used with light dismiss.
         */
        this._elementsRequestingDismiss = new Map();
    }
    static get instance() {
        // We store our instance on the window object so that it can be shared across multiple
        // instances of Forge components
        if (!window[DISMISSIBLE_STACK_INSTANCE_KEY]) {
            window[DISMISSIBLE_STACK_INSTANCE_KEY] = new DismissibleStack();
        }
        return window[DISMISSIBLE_STACK_INSTANCE_KEY];
    }
    /**
     * Dismisses all elements in the stack that have been presented after the provided element.
     * @param el The element to dismiss
     * @param state The state to pass to the dismiss method of each element
     */
    async dismiss(el, state) {
        const elements = Array.from(this._dismissibleElements);
        const elementsAfter = elements.slice(elements.indexOf(el)).reverse();
        for (const element of elementsAfter) {
            if (!element[tryDismiss](state)) {
                break;
            }
        }
    }
    /**
     * Queues an element to be dismissed. This allows for us to asynchronously wait for all
     * other elements to dispatch their dismiss events so that consumers can cancel the dismiss
     * if they want to, without closing their parent dismissible elements.
     * @param el
     * @param dismissState
     * @returns
     */
    async requestDismiss(el, dismissState) {
        var _a;
        if (!this._deferredDismiss) {
            this._deferredDismiss = new Deferred();
        }
        this._elementsRequestingDismiss.set(el, dismissState);
        // Wait for all other element to finish queuing their dismiss events
        await task();
        // The first element to call this method will handle attempting to dismiss the rest of
        // elements, and will clear the queue when it is done. All other subsequent elements will
        // just return here if called within the same task.
        if (!this.isRequestingLightDismiss(el)) {
            return;
        }
        // Get all elements that are requesting to be dismissed, and reverse the order so that
        // we can attempt to dismiss them in a top-down order (most recently opened first)
        const elements = Array.from(this._elementsRequestingDismiss).reverse();
        // Clear all elements from the stack since we are don't need references to them anymore
        this._elementsRequestingDismiss.clear();
        // Attempt to dismiss each element in the queue (most recently opened first), if any of
        // them are prevented, stop trying to dismiss the rest
        try {
            for (const [element, state] of elements) {
                if (!element[tryDismiss](state)) {
                    break;
                }
            }
        }
        finally {
            (_a = this._deferredDismiss) === null || _a === void 0 ? void 0 : _a.resolve();
            this._deferredDismiss = undefined;
        }
    }
    /**
     * Adds an element to the dismissible queue.
     *
     * Use this when an element is opened and should be eligible for dismissal in a stack of other
     * dismissible elements.
     */
    add(el) {
        this._dismissibleElements.add(el);
    }
    /**
     * Removes an element from the dismissible queue.
     *
     * Use this when an element is closed and should no longer be eligible for dismissal in a stack.
     */
    remove(el) {
        this._dismissibleElements.delete(el);
        this._elementsRequestingDismiss.delete(el);
    }
    /**
     * Checks if an element is in the dismissible queue.
     */
    has(el) {
        return this._dismissibleElements.has(el);
    }
    /**
     * Checks if an element is requesting to be dismissed.
     */
    isRequestingLightDismiss(el) {
        return this._elementsRequestingDismiss.has(el);
    }
    /**
     * Checks if an element is the most recent element in the dismissible queue.
     * @param el The element to check.
     * @returns
     */
    isMostRecent(el) {
        const elements = Array.from(this._dismissibleElements);
        return elements[elements.length - 1] === el;
    }
}
