// #region Imports
import { ElevationWeight } from '../../../Types/ElevationWeight';
import { slideInFromLeft, slideInFromRight, slideOutToLeft, slideOutToRight } 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';
import { DRAWER_SERVICE_DEFAULT_CONFIG } from './IDrawerServiceConfig';
// #endregion
/**
 * CSS class name applied to drawers that are stacked (have another drawer on top).
 *
 * @public
 */
export const DRAWER_STACKED_CLASS = 'stacked';
/**
 * Represents the `DRAWER_DEFAULT_OPTIONS` constant.
 *
 * @private
 */
const DRAWER_DEFAULT_OPTIONS = {
    hasBackdrop: 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.
 * Supports extensible behavior pattern for customizing drawer interactions.
 *
 * @example
 * ```typescript
 * // Configure service globally before use
 * DrawerService.configure({
 *     behaviors: [withDrawerStackBehavior({ baseWidth: '80%' })],
 *     closeOnNavigation: true
 * });
 *
 * // Use the singleton instance
 * const result = await DrawerService.instance.open('myDrawer', MyDrawerWrapper, options);
 * ```
 *
 * @public
 */
export class DrawerService {
    // #region Fields
    static _instance = null;
    static _config = { ...DRAWER_SERVICE_DEFAULT_CONFIG };
    static _isConfigured = false;
    _drawerRefMap;
    _drawerStack;
    _behaviors;
    _portalElement;
    // #endregion
    // #region Ctor
    /**
     * Constructs a new instance of the `DrawerService` class.
     * Use {@link DrawerService.instance} to access the singleton instance.
     *
     * @private
     */
    constructor() {
        this._drawerRefMap = new Map();
        this._drawerStack = new Array();
        this._behaviors = DrawerService._config.behaviors ?? [];
        // Configure all behaviors
        for (const behavior of this._behaviors) {
            behavior.configure({});
        }
        // Setup navigation listener if enabled
        if (DrawerService._config.closeOnNavigation) {
            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 Properties
    /**
     * Returns the singleton instance of the DrawerService.
     * Creates the instance on first access using the current configuration.
     *
     * @public
     * @readonly
     */
    static get instance() {
        DrawerService._instance ??= new DrawerService();
        return DrawerService._instance;
    }
    /**
     * Returns the current number of open drawers in the stack.
     *
     * @public
     * @readonly
     */
    get stackSize() {
        return this._drawerStack.length;
    }
    /**
     * Returns an array of all drawer references in stack order (oldest first).
     *
     * @public
     * @readonly
     */
    get stack() {
        return this._drawerStack
            .map((id) => this._drawerRefMap.get(id))
            .filter((ref) => ref !== undefined);
    }
    /**
     * Returns the registered behaviors for this service instance.
     *
     * @public
     * @readonly
     */
    get behaviors() {
        return this._behaviors;
    }
    // #endregion
    // #region Methods
    /**
     * Configures the drawer service globally.
     * Must be called before accessing the singleton instance.
     *
     * @public
     * @static
     * @param config - Configuration options for the drawer service.
     * @returns The DrawerService class for chaining.
     * @throws Error if called after the instance has already been created.
     *
     * @example
     * ```typescript
     * DrawerService.configure({
     *     behaviors: [withDrawerStackBehavior({ baseWidth: '80%' })],
     *     closeOnNavigation: true
     * });
     * ```
     */
    static configure(config) {
        if (DrawerService._instance !== null) {
            throw new Error('DrawerService.configure() must be called before accessing DrawerService.instance. ' +
                'The service has already been instantiated.');
        }
        DrawerService._config = {
            ...DRAWER_SERVICE_DEFAULT_CONFIG,
            ...config
        };
        DrawerService._isConfigured = true;
        return DrawerService;
    }
    /**
     * 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 drawerId - Unique identifier for the drawer instance.
     * @param wrapperClass - Class of the wrapper component containing the drawer.
     * @param options - Properties for portal and drawer.
     * @param data - Optional data passed to the wrapper component.
     * @returns A promise resolving with the drawer result.
     */
    async open(drawerId, wrapperClass, options, data) {
        // Let behaviors configure options before creating the ref
        for (const behavior of this._behaviors) {
            behavior.configure(options ?? {});
        }
        const existingRef = this._drawerRefMap.get(drawerId);
        const drawerRef = existingRef ?? await this.createDrawerRef(drawerId, wrapperClass, options, data);
        // Determine the correct position, resolving 'auto' to the last drawer's position
        const position = this.resolvePosition(options?.position);
        const enterAnimation = position === DrawerPosition.Right ? slideInFromRight : slideInFromLeft;
        const exitAnimation = position === DrawerPosition.Right ? slideOutToRight : slideOutToLeft;
        // Destructure to exclude isOpen from options - we control open state via drawerElement.open()
        // This ensures pressEscapeToClose and other properties are set before the escape listener is attached
        const { isOpen: _ignored, ...safeOptions } = options ?? {};
        Object.assign(drawerRef.drawerElement, {
            ...DRAWER_DEFAULT_OPTIONS,
            ...safeOptions,
            enter: safeOptions.enter ?? enterAnimation,
            exit: safeOptions.exit ?? exitAnimation
        });
        // Let behaviors attach to the drawer
        for (const behavior of this._behaviors) {
            behavior.attach(drawerRef);
        }
        return new Promise((resolve) => {
            drawerRef.resolve = resolve;
            void drawerRef.drawerElement.open();
        });
    }
    /**
     * 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) {
        const ref = this._drawerRefMap.get(drawerId);
        if (!ref) {
            console.error(`Wrapper with ID "${drawerId}" not found.`);
            return;
        }
        // Let behaviors detach from the drawer
        for (const behavior of this._behaviors) {
            behavior.detach(ref);
        }
        // Remove from stack if it was a stacked drawer
        const stackIndex = this._drawerStack.indexOf(drawerId);
        if (stackIndex !== -1) {
            this._drawerStack.splice(stackIndex, 1);
        }
        // Delete from map before closing to prevent duplicate close calls
        this._drawerRefMap.delete(drawerId);
        // Set up the event listener BEFORE calling close() to ensure we catch the event.
        // DrawerElement.close() plays the exit animation, then calls super.close() which
        // sets isOpen = false and fires the 'closed' event. By waiting for this event,
        // we ensure the entire close sequence (including animation) has completed.
        const closedPromise = waitForEvent(ref.drawerElement, 'closed');
        // Trigger the close (this starts the animation)
        void ref.drawerElement.close();
        // Wait for the 'closed' event which fires after the animation completes
        await closedPromise;
        // Remove the wrapper element (not the portal projection, as it's shared across all drawers)
        ref.wrapperElement.remove();
        // Resolve the promise from open() with the result
        if (ref.resolve) {
            ref.resolve(result);
        }
    }
    /**
     * Closes the topmost drawer in the stack.
     *
     * @public
     * @template TResult - Type of result returned when the drawer is closed.
     * @param {TResult} [result] - Optional result returned when closing the drawer.
     * @returns {Promise<void>} A promise resolving once the drawer is fully closed.
     */
    async closeTop(result) {
        if (this._drawerStack.length === 0) {
            return Promise.resolve();
        }
        const topDrawerId = this._drawerStack[this._drawerStack.length - 1];
        return this.close(topDrawerId, result);
    }
    /**
     * Closes all open drawers in the stack, from top to bottom.
     *
     * @public
     * @returns {Promise<void>} A promise resolving once all drawers are closed.
     */
    async closeAll() {
        const drawerIds = [...this._drawerStack].reverse();
        for (const drawerId of drawerIds) {
            await this.close(drawerId);
        }
    }
    /**
     * 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);
    }
    /**
     * Creates a new drawer reference.
     *
     * @private
     */
    async createDrawerRef(drawerId, wrapperClass, options, data) {
        // 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}".`);
        }
        // Calculate stack level
        const isStackEnabled = options?.enableStack;
        const stackLevel = isStackEnabled ? this._drawerStack.length : 0;
        // Use default width or options width
        const widthOption = options?.width;
        const defaultWidth = DRAWER_DEFAULT_OPTIONS.width;
        const baseWidthRaw = widthOption ?? defaultWidth;
        const computedWidth = typeof baseWidthRaw === 'number' ? `${baseWidthRaw}px` : baseWidthRaw;
        const drawerRef = {
            drawerId: drawerId,
            drawerElement: drawerElement,
            portalProjectionElement: portalProjection,
            wrapperElement: wrapperElement,
            stackLevel: stackLevel,
            computedWidth: computedWidth,
            options: options
        };
        this._drawerRefMap.set(drawerId, drawerRef);
        // Add to stack if stack mode is enabled
        if (isStackEnabled) {
            this._drawerStack.push(drawerId);
        }
        return drawerRef;
    }
    /**
     * 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);
                RendererServiceLocator.current.appendChild(document.body, this._portalElement);
            }
            // Register event listener BEFORE appending the element to catch the 'portalAttached' event
            this._portalElement.addEventListener('portalAttached', (e) => {
                resolve(e.detail.projection);
            }, { once: true });
            RendererServiceLocator.current.appendChild(this._portalElement, element);
        });
    }
    /**
     * Resolves the drawer position, handling 'auto' by returning the last drawer's position.
     *
     * @private
     * @param requestedPosition - The requested position, which may be 'auto'.
     * @returns The resolved position ('left', 'right', 'top', or 'bottom').
     */
    resolvePosition(requestedPosition) {
        // If not 'auto', return the requested position or default
        if (requestedPosition !== 'auto') {
            return requestedPosition ?? DRAWER_DEFAULT_OPTIONS.position;
        }
        // 'auto' - find the last drawer's position
        if (this._drawerStack.length > 0) {
            const lastDrawerId = this._drawerStack[this._drawerStack.length - 1];
            const lastDrawerRef = this._drawerRefMap.get(lastDrawerId);
            if (lastDrawerRef) {
                return lastDrawerRef.drawerElement.position;
            }
        }
        // Fallback to default position if no drawer is open
        return DRAWER_DEFAULT_OPTIONS.position;
    }
}
/**
 * @internal
 */
export const DRAWER_REGISTRY = new Map();
//# sourceMappingURL=DrawerService.js.map