/* eslint-disable @typescript-eslint/no-non-null-assertion */
// #region Imports
import { ElevationWeight } from '../../../Types/ElevationWeight';
import { slideInFromLeft, slideOutToLeft } from '../../../../Animations/Presets/Slide';
import { RendererServiceLocator } from '../../../../Dom/Renderer';
import { on, waitForEvent } from '@breadstone/mosaik-elements';
import { EID } from '../../../../Utilities/EId';
import { PortalElement } from '../../Primitives/Portal/PortalElement';
import { DrawerElement } from './DrawerElement';
import { DrawerMode } from '../../../Types/DrawerMode';
import { DrawerPosition } from '../../../Types/DrawerPosition';
/**
 * Represents the `DRAWER_DEFAULT_OPTIONS` constant.
 *
 * @private
 */
const DRAWER_DEFAULT_OPTIONS = {
    hasBackdrop: true,
    isOpen: true,
    pressEscapeToClose: true,
    clickOutsideToClose: true,
    enter: slideInFromLeft,
    exit: slideOutToLeft,
    closeable: false,
    elevation: ElevationWeight.Regular,
    hasShadow: true,
    header: '',
    subHeader: '',
    mode: DrawerMode.Over,
    position: DrawerPosition.Right,
    width: 'auto',
    height: 'auto'
};
/**
 * Service for managing the display and behavior of drawer elements.
 * Provides methods to open and close drawer elements within a portal structure.
 *
 * @public
 */
export class DrawerService {
    // #region Fields
    _drawerRefMap;
    _portalElement;
    // #endregion
    // #region Ctor
    /**
     * Constructs a new instance of the `DrawerService` class.
     *
     * @public
     */
    constructor() {
        this._drawerRefMap = new Map();
        on(window, 'popstate', () => {
            if (this._drawerRefMap.size > 0) {
                for (const [key, value] of this._drawerRefMap) {
                    if (value.options?.navigateToClose) {
                        void this.close(key);
                    }
                }
            }
        });
    }
    // #endregion
    // #region Methods
    /**
     * Opens a custom wrapper component containing a drawer within a portal.
     *
     * @public
     * @template T - Type of the wrapper component extending HTMLElement.
     * @template TData - Type of data provided to the wrapper component.
     * @template TResult - Expected return type of the resolved drawer result.
     * @param {string} drawerId - Unique identifier for the drawer instance.
     * @param {new (args?: TData) => T} wrapperClass - Class of the wrapper component containing the drawer.
     * @param {IDrawerOptions} options - Properties for portal and drawer.
     * @param {TData} [data] - Optional data passed to the wrapper component.
     * @returns {Promise<TResult>} A promise resolving with the drawer result.
     */
    async open(drawerId, wrapperClass, options, data) {
        let ref = this._drawerRefMap.get(drawerId);
        if (!ref) {
            // Create the wrapper component containing the drawer
            const wrapperElement = new wrapperClass(data);
            RendererServiceLocator.current.setProperty(wrapperElement, 'id', drawerId);
            // Create and attach to portal
            const portalProjection = await this.extendToPortal(wrapperElement, options);
            const drawerElement = wrapperElement.shadowRoot?.querySelector(DrawerElement.is);
            if (!drawerElement) {
                throw new Error(`Drawer element not found within wrapper for ID "${drawerId}".`);
            }
            // eslint-disable-next-line require-atomic-updates
            ref = {
                drawerId: drawerId,
                drawerElement: drawerElement,
                portalProjectionElement: portalProjection,
                wrapperElement: wrapperElement,
                options: options
            };
            this._drawerRefMap.set(drawerId, ref);
        }
        Object.assign(ref.drawerElement, {
            ...DRAWER_DEFAULT_OPTIONS,
            ...options
        });
        // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
        return new Promise(async (resolve) => {
            ref.resolve = resolve; // Store the resolver for use in `close`
            await ref.drawerElement.open();
            void waitForEvent(ref.drawerElement, 'closed').then(async () => {
                this._drawerRefMap.delete(drawerId);
                await ref.portalProjectionElement.close();
                ref.portalProjectionElement.remove();
            });
        });
    }
    /**
     * Closes the drawer in the wrapper component and optionally removes the wrapper from the DOM.
     *
     * @public
     * @template TResult - Type of result returned when the drawer is closed.
     * @param {string} drawerId - Unique identifier for the drawer instance.
     * @param {TResult} [result] - Optional result returned when closing the drawer.
     * @returns {Promise<void>} A promise resolving once the drawer is fully closed.
     */
    async close(drawerId, result) {
        // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
        return new Promise(async (resolve) => {
            const ref = this._drawerRefMap.get(drawerId);
            if (ref) {
                this._drawerRefMap.delete(drawerId);
                void waitForEvent(ref.drawerElement, 'closed').then(async () => {
                    await ref.portalProjectionElement.close();
                    ref.portalProjectionElement.remove();
                    if (ref.resolve) {
                        ref.resolve(result);
                    }
                    resolve();
                });
                await ref.drawerElement.close();
            }
            else {
                console.error(`Wrapper with ID "${drawerId}" not found.`);
                resolve(); // Resolve to avoid hanging promise if drawer not found
            }
        });
    }
    /**
     * Returns the drawer element with the specified ID if it exists.
     * For example, all drawer elements will be automatically registered (by id) when its available in the DOM.
     * With this method, you can get the drawer element by its ID and call its methods directly.
     *
     * @public
     */
    tryGet(drawerId) {
        return DRAWER_REGISTRY.get(drawerId);
    }
    /**
     * Extends the drawer component by placing it within a portal structure.
     *
     * @private
     * @param portalProps - Properties to set on the portal.
     * @param element - The element to place within the portal.
     * @returns The created PortalElement containing the drawer.
     */
    extendToPortal(element, props) {
        return new Promise((resolve) => {
            if (!this._portalElement) {
                this._portalElement = RendererServiceLocator.current.createElement(PortalElement.is);
                RendererServiceLocator.current.setAttribute(this._portalElement, 'id', EID.next(PortalElement.is));
                Object.assign(this._portalElement, props);
                this._portalElement.addEventListener('portalAttached', (e) => {
                    resolve(e.detail.projection);
                });
                RendererServiceLocator.current.appendChild(document.body, this._portalElement);
            }
            RendererServiceLocator.current.appendChild(this._portalElement, element);
        });
    }
}
/**
 * @internal
 */
export const DRAWER_REGISTRY = new Map();
/**
 * @public
 */
export class DrawerServiceLocator {
    // #region Fields
    static _current = null;
    // #endregion
    // #region Properties
    static get current() {
        if (!DrawerServiceLocator.isSet()) {
            throw new Error('\'DrawerService\' not set.');
        }
        return DrawerServiceLocator._current;
    }
    // #endregion
    // #region Methods
    static isSet() {
        return DrawerServiceLocator._current !== null;
    }
    static set(current) {
        DrawerServiceLocator._current = current;
    }
}
/**
 * @public
 */
DrawerServiceLocator.set(new DrawerService());
//# sourceMappingURL=DrawerService.js.map