// (C) 2026 GoodData Corporation
import { useCallback, useMemo, useRef, useState } from "react";
/**
 * Owns popup open/close, highlight, and scroll. The highlight is tracked by
 * option **id**, not index: when the option list changes (async results arrive,
 * a row is filtered out) a highlighted row that's gone simply resolves to no
 * index, so the highlight can never point at a different row than the user
 * navigated to. Callers pass the current `options` so the id ⇄ index mapping
 * and `focusByDelta` wrapping stay in sync with what's rendered.
 *
 * @internal
 */
export function useComboboxChrome(options) {
    const [isOpen, setIsOpenState] = useState(false);
    const [activeId, setActiveId] = useState(null);
    // A highlight only has meaning while the popup is open, so closing always
    // drops it — reopening then starts fresh. This is the single point that
    // keeps a stale highlight from surviving Escape / blur / reset and being
    // Enter-selected on the next open.
    const setIsOpen = useCallback((open) => {
        setIsOpenState(open);
        if (!open) {
            setActiveId(null);
        }
    }, []);
    const anchorRef = useRef(null);
    const itemRefs = useRef([]);
    // Target index for when the scroll request runs before the item mounts.
    const pendingScrollIndexRef = useRef(null);
    const activeIndex = activeId == null ? null : options.findIndex((o) => o.id === activeId);
    // findIndex returns -1 for a vanished row; surface that as "no highlight".
    const safeActiveIndex = activeIndex != null && activeIndex >= 0 ? activeIndex : null;
    const activeOption = safeActiveIndex == null ? undefined : options[safeActiveIndex];
    const scrollIndexIntoView = useCallback((index) => {
        const node = itemRefs.current[index];
        if (node) {
            pendingScrollIndexRef.current = null;
            node.scrollIntoView({ block: "nearest" });
        }
        else {
            pendingScrollIndexRef.current = index;
        }
    }, []);
    const registerItemRef = useCallback((node, index) => {
        itemRefs.current[index] = node;
        if (node != null && pendingScrollIndexRef.current === index) {
            pendingScrollIndexRef.current = null;
            node.scrollIntoView({ block: "nearest" });
        }
    }, []);
    const setActiveIndex = useCallback((index) => setActiveId(index == null ? null : (options[index]?.id ?? null)), [options]);
    const focusByDelta = useCallback((delta) => {
        const total = options.length;
        if (total === 0) {
            setIsOpen(true);
            return;
        }
        // Resolve against the current list so a stale/removed highlight still
        // produces a sensible next target.
        const current = safeActiveIndex;
        const next = !isOpen || current == null
            ? delta === 1
                ? 0
                : total - 1
            : (current + delta + total) % total;
        setIsOpen(true);
        setActiveId(options[next]?.id ?? null);
        scrollIndexIntoView(next);
    }, [isOpen, options, safeActiveIndex, scrollIndexIntoView, setIsOpen]);
    return useMemo(() => ({
        isOpen,
        setIsOpen,
        activeIndex: safeActiveIndex,
        activeOption,
        setActiveIndex,
        anchorRef,
        registerItemRef,
        focusByDelta,
    }), [
        isOpen,
        setIsOpen,
        safeActiveIndex,
        activeOption,
        setActiveIndex,
        anchorRef,
        registerItemRef,
        focusByDelta,
    ]);
}
