/**
 * MiniSelect — compact, icon-less dropdown that replaces plain ``<select>``.
 *
 * IconSelect requires an SVG icon per option, which doesn't fit the
 * dashboard's inline perf-cell controls (mode / window / yScale). Plain
 * ``<select>`` is banned project-wide because it breaks the UI's visual
 * consistency. MiniSelect fills the gap: a styled trigger button that
 * shows the current option label, plus a small popup with the option
 * labels. The original ``<select>`` is hidden but kept in the DOM and
 * receives a native ``change`` event whenever the user picks an option,
 * so existing handlers keep working with no changes.
 *
 * Usage:
 *     const sel = document.getElementById('perf-mode') as HTMLSelectElement;
 *     new MiniSelect(sel);
 *
 * The trigger displays each option's visible text from its ``<option>``
 * label; option values come from the underlying ``<select>``.
 */

import { closeAllIconSelects } from './icon-select.ts';
import { escapeHtml } from './api.ts';
import { desktopFocus } from './ui.ts';

const POPUP_CLASS = 'mini-select-popup';
const FOCUSED_CLASS = 'focused';
const CELL_SELECTOR = '.mini-select-option';
const FOCUSED_SELECTOR = `${CELL_SELECTOR}.${FOCUSED_CLASS}`;

const _registry: Set<MiniSelect> = new Set();

/** Close every open MiniSelect popup. */
export function closeAllMiniSelects(): void {
    for (const ms of _registry) ms._close();
}

let _globalListenerAdded = false;
function _ensureGlobalListener(): void {
    if (_globalListenerAdded) return;
    _globalListenerAdded = true;
    document.addEventListener('click', (e) => {
        const t = e.target as HTMLElement;
        if (!t.closest(`.${POPUP_CLASS}`) && !t.closest('.mini-select-trigger')) {
            closeAllMiniSelects();
        }
    });
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') closeAllMiniSelects();
    });
}

interface MiniSelectOption {
    value: string;
    label: string;
}

export class MiniSelect {
    _select: HTMLSelectElement;
    _options: MiniSelectOption[];
    _trigger: HTMLButtonElement;
    _popup: HTMLDivElement;
    _focusedIndex = -1;

    constructor(target: HTMLSelectElement) {
        // Picking up plain ``<select>``s in the same modal as IconSelects is
        // intentional — we want the same click-away/Escape behaviour, so we
        // also close any open IconSelect popup when ours opens.
        _ensureGlobalListener();

        this._select = target;
        this._options = Array.from(target.options).map((opt) => ({
            value: opt.value,
            label: opt.textContent || opt.value,
        }));

        target.style.display = 'none';

        this._trigger = document.createElement('button');
        this._trigger.type = 'button';
        this._trigger.className = 'mini-select-trigger';
        this._trigger.setAttribute('aria-haspopup', 'listbox');
        this._trigger.setAttribute('aria-expanded', 'false');
        if (target.title) this._trigger.title = target.title;
        this._trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            this._toggle();
        });
        target.parentNode!.insertBefore(this._trigger, target.nextSibling);

        this._popup = document.createElement('div');
        this._popup.className = POPUP_CLASS;
        this._popup.tabIndex = -1;
        this._popup.setAttribute('role', 'listbox');
        this._popup.addEventListener('click', (e) => e.stopPropagation());
        this._popup.addEventListener('keydown', (e) => this._onKey(e));
        this._popup.innerHTML = this._buildPopup();
        document.body.appendChild(this._popup);

        this._bindOptionClicks();
        this._syncTrigger();
        _registry.add(this);
    }

    /** Refresh trigger label + popup content after an external change. */
    refresh(): void {
        // Rebuild option list in case the underlying <select> changed.
        this._options = Array.from(this._select.options).map((opt) => ({
            value: opt.value,
            label: opt.textContent || opt.value,
        }));
        this._popup.innerHTML = this._buildPopup();
        this._bindOptionClicks();
        this._syncTrigger();
    }

    /** Remove the enhancement and restore the native <select>. */
    destroy(): void {
        _registry.delete(this);
        this._trigger.remove();
        this._popup.remove();
        this._select.style.display = '';
    }

    // ── Internals ─────────────────────────────────────────────────

    _buildPopup(): string {
        return this._options
            .map(
                (o, i) =>
                    `<div class="mini-select-option" role="option" data-value="${escapeHtml(o.value)}" data-index="${i}">${escapeHtml(o.label)}</div>`,
            )
            .join('');
    }

    _bindOptionClicks(): void {
        this._popup.querySelectorAll<HTMLElement>(CELL_SELECTOR).forEach((cell) => {
            cell.addEventListener('click', () => {
                this._pick(cell.dataset.value || '');
            });
        });
    }

    _syncTrigger(): void {
        const cur = this._options.find((o) => o.value === this._select.value);
        const label = cur ? cur.label : this._options[0]?.label || '';
        this._trigger.innerHTML = `<span class="mini-select-trigger-label">${escapeHtml(label)}</span><span class="mini-select-trigger-arrow">&#x25BE;</span>`;
        this._popup.querySelectorAll<HTMLElement>(CELL_SELECTOR).forEach((cell) => {
            const active = cell.dataset.value === this._select.value;
            cell.classList.toggle('active', active);
            cell.setAttribute('aria-selected', active ? 'true' : 'false');
        });
    }

    _position(): void {
        const rect = this._trigger.getBoundingClientRect();
        const pad = 8;
        const gap = 4;
        const popupW = Math.max(rect.width, 120);
        const spaceBelow = window.innerHeight - rect.bottom - gap - pad;
        const spaceAbove = rect.top - gap - pad;
        const openUp = spaceBelow < 160 && spaceAbove > spaceBelow;
        const available = openUp ? spaceAbove : spaceBelow;

        let left = rect.left;
        if (left + popupW > window.innerWidth - pad) {
            left = window.innerWidth - pad - popupW;
        }
        if (left < pad) left = pad;
        this._popup.style.left = `${left}px`;
        this._popup.style.width = `${popupW}px`;
        this._popup.style.maxHeight = `${available}px`;
        if (openUp) {
            this._popup.style.top = '';
            this._popup.style.bottom = `${window.innerHeight - rect.top + gap}px`;
        } else {
            this._popup.style.top = `${rect.bottom + gap}px`;
            this._popup.style.bottom = '';
        }
    }

    _toggle(): void {
        const isOpen = this._popup.classList.contains('open');
        closeAllIconSelects();
        closeAllMiniSelects();
        if (isOpen) return;
        this._position();
        this._popup.classList.add('open');
        this._trigger.setAttribute('aria-expanded', 'true');
        requestAnimationFrame(() => desktopFocus(this._popup));
        const activeIdx = this._options.findIndex((o) => o.value === this._select.value);
        this._setFocused(activeIdx >= 0 ? activeIdx : 0);
    }

    _close(): void {
        if (!this._popup.classList.contains('open')) return;
        this._popup.classList.remove('open');
        this._trigger.setAttribute('aria-expanded', 'false');
        this._clearFocused();
    }

    _clearFocused(): void {
        this._popup.querySelectorAll(FOCUSED_SELECTOR).forEach((c) => c.classList.remove(FOCUSED_CLASS));
        this._focusedIndex = -1;
    }

    _setFocused(idx: number): void {
        const cells = Array.from(this._popup.querySelectorAll<HTMLElement>(CELL_SELECTOR));
        if (cells.length === 0) return;
        const clamped = Math.max(0, Math.min(idx, cells.length - 1));
        this._clearFocused();
        cells[clamped].classList.add(FOCUSED_CLASS);
        cells[clamped].scrollIntoView({ block: 'nearest', inline: 'nearest' });
        this._focusedIndex = clamped;
    }

    _onKey(e: KeyboardEvent): void {
        if (!this._popup.classList.contains('open')) return;
        const total = this._options.length;
        const cur = this._focusedIndex >= 0 ? this._focusedIndex : 0;
        switch (e.key) {
            case 'ArrowDown':
                e.preventDefault();
                this._setFocused(Math.min(cur + 1, total - 1));
                break;
            case 'ArrowUp':
                e.preventDefault();
                this._setFocused(Math.max(cur - 1, 0));
                break;
            case 'Home':
                e.preventDefault();
                this._setFocused(0);
                break;
            case 'End':
                e.preventDefault();
                this._setFocused(total - 1);
                break;
            case 'Enter':
                e.preventDefault();
                if (this._focusedIndex >= 0) {
                    this._pick(this._options[this._focusedIndex].value);
                }
                break;
        }
    }

    _pick(value: string): void {
        this._select.value = value;
        this._syncTrigger();
        // Dispatch the native event so existing handlers attached to the
        // underlying <select> keep working without modification.
        this._select.dispatchEvent(new Event('change', { bubbles: true }));
        this._close();
        desktopFocus(this._trigger);
    }
}

/**
 * Enhance every plain ``<select>`` matching *selector* under *root* into a
 * MiniSelect. Selects that are already enhanced by another component
 * (``IconSelect`` / ``EntitySelect`` — both hide the target via
 * ``style.display = 'none'``) are skipped so the two wrappers don't compete
 * for the same `<select>`.
 */
export function enhanceMiniSelects(root: ParentNode, selector = 'select'): MiniSelect[] {
    const out: MiniSelect[] = [];
    root.querySelectorAll<HTMLSelectElement>(selector).forEach((sel) => {
        if (sel.dataset.miniEnhanced === '1') return;
        // Skip selects already hidden by an upstream IconSelect/EntitySelect.
        if (sel.style.display === 'none') return;
        sel.dataset.miniEnhanced = '1';
        out.push(new MiniSelect(sel));
    });
    return out;
}
