window.tmMultiSelect = {
    _listeners: new Map(),
    _stickyPositioners: new Map(),

    bind(id, dotNetRef) {
        setTimeout(() => {
            const container = document.querySelector(`[data-ms-id="${id}"]`);
            if (!container) return;

            const panel = container.querySelector('.tm-ms-panel');
            const trigger = container.querySelector('.tm-ms-trigger');
            if (panel && trigger) {
                // Detect a clipping ancestor (typically .tm-modal). If found, switch the panel
                // to position:fixed pinned to the trigger's viewport rect so the dropdown floats
                // OVER the modal instead of being clipped by its overflow.
                const inOverflowParent = this._isInsideClippingAncestor(trigger);
                console.debug('[tmMultiSelect] bind', id, 'clipping ancestor:', inOverflowParent);
                const placeFixed = () => this._placePanelFixed(trigger, panel);
                if (inOverflowParent) {
                    panel.classList.add('tm-ms-panel-fixed');
                    placeFixed();
                    const scroller = this._findScrollableAncestor(trigger);
                    const onMove = () => placeFixed();
                    window.addEventListener('resize', onMove);
                    if (scroller) scroller.addEventListener('scroll', onMove, { passive: true });
                    this._stickyPositioners.set(id, { onMove, scroller });
                } else {
                    // Original "is there room below or do we flip up?" check, only relevant for
                    // the absolute (non-fixed) case.
                    const rect = trigger.getBoundingClientRect();
                    const panelHeight = panel.offsetHeight || 280;
                    const spaceBelow = window.innerHeight - rect.bottom;
                    const spaceAbove = rect.top;
                    if (spaceBelow < panelHeight + 20 && spaceAbove > spaceBelow) {
                        panel.classList.add('tm-ms-panel-up');
                    }
                }
            }

            const search = container.querySelector('.tm-ms-search');
            if (search) { try { search.focus(); } catch { } }

            const handler = (e) => {
                const c = document.querySelector(`[data-ms-id="${id}"]`);
                if (c && !c.contains(e.target)) {
                    try { dotNetRef.invokeMethodAsync('Close'); } catch { }
                }
            };
            document.addEventListener('mousedown', handler);
            this._listeners.set(id, handler);
        }, 0);
    },

    unbind(id) {
        const handler = this._listeners.get(id);
        if (handler) {
            document.removeEventListener('mousedown', handler);
            this._listeners.delete(id);
        }
        const sticky = this._stickyPositioners.get(id);
        if (sticky) {
            window.removeEventListener('resize', sticky.onMove);
            if (sticky.scroller) sticky.scroller.removeEventListener('scroll', sticky.onMove);
            this._stickyPositioners.delete(id);
        }
    },

    // --- helpers --------------------------------------------------------------------------

    _isInsideClippingAncestor(el) {
        // Walk up looking for an ancestor that creates a clipping context. We can't gate on
        // `scrollHeight > clientHeight` (i.e. "currently overflowing") because the modal often
        // fits at the moment the dropdown opens — once the dropdown's content expands, the
        // dropdown gets clipped. Just check the overflow CSS itself.
        let p = el.parentElement;
        while (p && p !== document.body) {
            const cs = getComputedStyle(p);
            const ox = cs.overflow, oy = cs.overflowY, ohx = cs.overflowX;
            if (ox === 'auto' || ox === 'scroll' || ox === 'hidden' ||
                oy === 'auto' || oy === 'scroll' || oy === 'hidden' ||
                ohx === 'auto' || ohx === 'scroll' || ohx === 'hidden') {
                return true;
            }
            p = p.parentElement;
        }
        return false;
    },

    _findScrollableAncestor(el) {
        let p = el.parentElement;
        while (p && p !== document.body) {
            const cs = getComputedStyle(p);
            if ((cs.overflowY === 'auto' || cs.overflowY === 'scroll') &&
                p.scrollHeight > p.clientHeight) {
                return p;
            }
            p = p.parentElement;
        }
        return null;
    },

    _placePanelFixed(trigger, panel) {
        const rect = trigger.getBoundingClientRect();
        const panelMaxH = 280; // match CSS .tm-ms-panel max-height
        const spaceBelow = window.innerHeight - rect.bottom - 8;
        const spaceAbove = rect.top - 8;
        const openUp = spaceBelow < 180 && spaceAbove > spaceBelow;

        // setProperty with priority='important' so inline overrides beat the original
        // .tm-ms-panel { position: absolute; left: 0; right: 0; top: calc(100% + 4px); }
        // rules — without 'important', cascade specificity can leave `right: 0` in effect,
        // which forces the panel to span to the modal's right edge and ignore our width.
        panel.style.setProperty('position', 'fixed', 'important');
        panel.style.setProperty('left', rect.left + 'px', 'important');
        panel.style.setProperty('width', rect.width + 'px', 'important');
        panel.style.setProperty('right', 'auto', 'important');
        panel.style.setProperty('z-index', '1000', 'important');
        if (openUp) {
            panel.style.setProperty('bottom', (window.innerHeight - rect.top + 4) + 'px', 'important');
            panel.style.setProperty('top', 'auto', 'important');
            panel.style.setProperty('max-height', Math.min(panelMaxH, spaceAbove) + 'px', 'important');
        } else {
            panel.style.setProperty('top', (rect.bottom + 4) + 'px', 'important');
            panel.style.setProperty('bottom', 'auto', 'important');
            panel.style.setProperty('max-height', Math.min(panelMaxH, spaceBelow) + 'px', 'important');
        }
    }
};
