// #region Imports
import { DomServiceLocator } from '../../../../Dom/Dom';
import { ControllerBase } from '../../../Controllers/Abstracts/ControllerBase';
import { SlotController } from '../../../Controllers/SlotController';
import { PortalElement } from './PortalElement';
/**
 * A controller that observes content projected into a slot in the host,
 * removes the DOM nodes from the host, and transfers them into a render target
 * inside a portal component.
 *
 * @public
 */
export class ProjectionController extends ControllerBase {
    // #region Fields
    _slotController;
    _slotName;
    _targetSelector;
    _observer;
    _elements;
    _projectingFn;
    _portalAttachedHandler;
    _slotListeners;
    _portal;
    _initialized;
    // #endregion
    // #region Ctor
    constructor(host, options) {
        super(host);
        this._slotController = new SlotController(host);
        this._slotName = options.slotName ?? '';
        this._targetSelector = options.targetSelector;
        this._projectingFn = options.projecting;
        this._observer = new MutationObserver(() => this.projectContent());
        this._elements = [];
        this._portal = null;
        this._initialized = false;
        this._slotListeners = new Map();
        this._portalAttachedHandler = () => this.projectContent();
    }
    // #endregion
    // #region Properties
    /**
     * Returns projected elements.
     *
     * @public
     */
    get elements() {
        return this._elements;
    }
    // #region Methods
    /**
     * @public
     * @override
     */
    hostConnected() {
        super.hostConnected();
        // When reconnecting after a previous initialization (e.g. element was
        // moved in the DOM by lit repeat/keyed updates), re-establish the
        // portal listener and mutation observer that were torn down in
        // hostDisconnected.
        if (this._initialized) {
            this.setupPortalListener();
            this.projectContent();
            this._observer.observe(this.host, {
                childList: true,
                subtree: true
            });
        }
    }
    /**
     * @public
     * @override
     */
    hostFirstUpdated() {
        super.hostFirstUpdated();
        this._initialized = true;
        this.setupPortalListener();
        this.projectContent();
        this._observer.observe(this.host, {
            childList: true,
            subtree: true
        });
    }
    /**
     * @public
     * @override
     */
    hostDisconnected() {
        this._observer.disconnect();
        this.cleanupPortalListener();
        this.cleanupSlotListeners();
        super.hostDisconnected();
    }
    /**
     * Sets up the portal attached event listener.
     *
     * @private
     */
    setupPortalListener() {
        if (this.host.shadowRoot) {
            this._portal = DomServiceLocator.current.findDescendant(this.host.shadowRoot, PortalElement.is, { strict: false });
            if (this._portal) {
                this._portal.addEventListener('portalAttached', this._portalAttachedHandler);
            }
        }
    }
    /**
     * Cleans up the portal attached event listener.
     *
     * @private
     */
    cleanupPortalListener() {
        if (this._portal) {
            this._portal.removeEventListener('portalAttached', this._portalAttachedHandler);
            this._portal = null;
        }
    }
    /**
     * @private
     */
    projectContent() {
        // First, check if we can actually project
        if (!this.host.shadowRoot) {
            console.warn('[ProjectionController]: Host does not have a shadow root. Projection will not work.');
            return;
        }
        const portal = this._portal ?? DomServiceLocator.current.findDescendant(this.host.shadowRoot, PortalElement.is, { strict: false });
        if (!portal) {
            // Portal not yet rendered, will retry when portalAttached fires
            return;
        }
        // After the portal projects its content to the body, the target selector
        // is no longer inside the portal element itself - it's in portal.projection.
        // We need to search in the projected element (PortalProjectionElement) instead.
        const searchRoot = portal.projection ?? portal;
        const target = DomServiceLocator.current.findDescendant(searchRoot, this._targetSelector, { strict: false });
        if (!target) {
            // Target not found yet - portal may not have projected its content.
            // The portalAttached event listener will trigger projectContent again.
            return;
        }
        // Get elements directly from the host's light DOM children.
        // We cannot use getAssignedElements() because slot assignments may not
        // have happened yet at this point in the lifecycle.
        const elements = this.getLightDomElements();
        if (elements.length === 0) {
            // No elements to project yet. The mutation observer or slot change
            // events will trigger projectContent again when content is available.
            return;
        }
        this._elements.length = 0; // clear previous elements
        elements.forEach((element) => {
            if (element instanceof HTMLElement) {
                this._elements.push(element);
            }
        });
        // Remove from host DOM to prevent double rendering
        elements.forEach((n) => {
            if (n.parentNode) {
                n.parentNode.removeChild(n);
            }
        });
        if (this._projectingFn) {
            this._elements.forEach((el) => this._projectingFn?.(el));
        }
        // Project the elements into the target
        target.replaceChildren(...elements);
    }
    /**
     * Gets elements from the host's light DOM that match the slot name.
     * When a child is an `HTMLSlotElement` (originating from an outer shadow
     * root), it is resolved to its `assignedElements` so that the actual
     * content — not the slot itself — is projected into the portal. The
     * `<slot>` element stays in the DOM to keep the outer shadow root intact.
     *
     * @private
     */
    getLightDomElements() {
        const hostElement = this.host;
        const children = Array.from(hostElement.children);
        let matchingChildren;
        if (this._slotName === '') {
            // Default slot: elements without a slot attribute or with empty slot
            matchingChildren = children.filter((child) => {
                const slot = child.getAttribute('slot');
                return !slot || slot === '';
            });
        }
        else {
            // Named slot: elements with matching slot attribute
            matchingChildren = children.filter((child) => child.getAttribute('slot') === this._slotName);
        }
        // Flatten <slot> elements: resolve to their assigned elements so that
        // projection works across nested shadow DOM boundaries. A <slot> placed
        // as light DOM content of the host originates from an outer shadow root
        // and would break if physically moved to a portal.
        const result = [];
        for (const child of matchingChildren) {
            if (child instanceof HTMLSlotElement) {
                this.observeSlotElement(child);
                const assigned = child.assignedElements({ flatten: true });
                result.push(...assigned);
            }
            else {
                result.push(child);
            }
        }
        return result;
    }
    /**
     * Adds a `slotchange` listener to a discovered `<slot>` element so that
     * changes to the outer component's slotted content trigger re-projection.
     *
     * @private
     */
    observeSlotElement(slot) {
        if (this._slotListeners.has(slot)) {
            return;
        }
        const handler = () => this.projectContent();
        slot.addEventListener('slotchange', handler);
        this._slotListeners.set(slot, handler);
    }
    /**
     * Removes all `slotchange` listeners that were added to discovered
     * `<slot>` elements.
     *
     * @private
     */
    cleanupSlotListeners() {
        this._slotListeners.forEach((handler, slot) => {
            slot.removeEventListener('slotchange', handler);
        });
        this._slotListeners.clear();
    }
}
//# sourceMappingURL=ProjectionController.js.map