import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { Popup } from "./popup/index";
const INPUT_UNSTYLED = "block m-0 p-0 border-0 focus:outline-none focus:ring-0 bg-transparent";
const INPUT_NO_PADDING = "block sm:text-sm sm:leading-6 rounded-md border-0 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-slate-600 dark:text-slate-50 dark:bg-slate-800 placeholder:text-gray-400 dark:placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 dark:focus:ring-indigo-800";
const INPUT = INPUT_NO_PADDING + " py-1.5";
const COMBOBOX_POPUP = "combobox-popup";
function genComboboxPopupId() {
    return `combobox-popup-${Math.floor(Math.random() * 1000000)}`;
}
export class OptionAdapter {
    filter(items, text) {
        const lcText = text.toLowerCase();
        return items.filter((item) => this.valueOf(item).toLowerCase().includes(lcText));
    }
    renderOption(item) {
        return this.valueOf(item);
    }
    findById(items, id) {
        return items.find(item => this.idOf(item) === id);
    }
    // override to support creating new items
    createItem(_value) {
        return null; // default is no new item
    }
}
export class StringOptionAdapter extends OptionAdapter {
    valueOf(item) {
        return String(item);
    }
    idOf(item) {
        return String(item);
    }
    static instance = new StringOptionAdapter();
}
export class StringOptionAdapterWithCreate extends StringOptionAdapter {
    createItem(value) {
        return value;
    }
    static instance = new StringOptionAdapterWithCreate();
}
export function getDefaultComboBoxLayout(fullWidth, unstyledInput) {
    return {
        buttonRight: 4,
        buttonWidth: 24,
        maxMenuHeight: 240,
        menuClass: "w-72 bg-white mt-1 shadow-md border border-gray-200 overflow-auto p-0 z-10",
        inputClass: clsx(unstyledInput ? INPUT_UNSTYLED : INPUT, fullWidth ? "w-full" : "!w-auto"),
        optionClass: "py-2 px-3 shadow-sm flex flex-col [&.option-selected]:font-semibold [&.option-highlighted]:bg-blue-300",
        Input: (ComboInput),
        Menu: (ComboMenu),
        Toggle: (ComboToggle),
    };
}
export function ComboBox({ menuAlign = "fill", menuGap, focusOnMount, onSelect, value, zIndex, unstyledInput, fullWidth, api, layout: layoutOpts, adapter, items, placeholder }) {
    const [popupId] = useState(genComboboxPopupId());
    const popupCtrl = useRef(undefined);
    const inputRef = useRef(null);
    const layout = layoutOpts ? Object.assign(getDefaultComboBoxLayout(fullWidth, unstyledInput), layoutOpts) : getDefaultComboBoxLayout(fullWidth, unstyledInput);
    const inputBoxRef = React.useRef(null);
    const ctrl = useComboboxCtrl({
        adapter,
        items,
        value,
        popupId
    });
    useEffect(() => {
        if (inputRef.current) {
            focusOnMount && inputRef.current.focus();
        }
    }, [inputRef.current]);
    // the onSelect callback may change so we need to refresh it.
    useEffect(() => {
        ctrl.onSelect = onSelect;
        ctrl.popupCtrl = popupCtrl.current;
    }, [onSelect, popupCtrl.current]);
    useEffect(() => {
        if (api && ctrl && inputRef.current) {
            api.current = {
                open: () => ctrl.openMenu(),
                close: () => ctrl.closeMenu(),
                toggle: () => ctrl.toggleMenu(),
                setInputValue: (value) => ctrl.inputText = value,
                inputValue: ctrl.inputText || '',
                selectedItem: ctrl.selectedItem,
                focus: () => inputRef.current?.focus()
            };
            return () => {
                api.current = null;
            };
        }
    }, [api, ctrl, inputRef.current]);
    const showMenu = ctrl.isMenuOpen && ctrl.filteredItems.length > 0;
    return (_jsxs(_Fragment, { children: [_jsx(layout.Input, { boxRef: inputBoxRef, inputRef: inputRef, ctrl: ctrl, layout: layout, placeholder: placeholder }), _jsx(Popup, { id: popupId, ctrlRef: popupCtrl, className: COMBOBOX_POPUP, closeOnClick: true, closeOnEsc: true, onClose: () => ctrl.closeMenu(), isOpen: showMenu, anchor: inputBoxRef, zIndex: zIndex, constraints: {
                    position: "bottom",
                    align: menuAlign,
                    gap: menuGap != null ? menuGap : 4
                }, children: _jsx(layout.Menu, { fillWidth: menuAlign === "fill", items: ctrl.filteredItems, ctrl: ctrl, layout: layout, adapter: adapter }) })] }));
}
function ComboInput({ inputRef, placeholder, boxRef, ctrl, layout }) {
    const buttonWidth = layout.buttonWidth;
    const style = buttonWidth > 0 ? { paddingRight: `${buttonWidth}px` } : undefined;
    const Toggle = layout.Toggle;
    return (_jsxs("div", { className: "relative", ref: boxRef, children: [_jsx("input", { ref: inputRef, placeholder: placeholder, ...ctrl.getInputProps(), style: style, className: layout.inputClass }), Toggle &&
                _jsx("button", { style: {
                        top: 0, bottom: 0, right: `${layout.buttonRight}px`, width: `${buttonWidth}px`,
                        position: "absolute",
                        border: "none",
                        padding: 0,
                        margin: 0,
                        backgroundColor: "transparent",
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center"
                    }, ...ctrl.getToggleButtonProps(), children: _jsx(Toggle, { ctrl: ctrl, layout: layout }) })] }));
}
function ComboToggle({ ctrl }) {
    return ctrl.isMenuOpen ? _jsx(ChevronUpIcon, { className: "w-4 h-4" }) : _jsx(ChevronDownIcon, { className: "w-4 h-4" });
}
function ComboMenu({ fillWidth, items, layout, ctrl, adapter }) {
    const { highlightedIndex, selectedItem } = ctrl;
    return (_jsx("ul", { style: { width: fillWidth ? "100%" : undefined, maxHeight: layout.maxMenuHeight ? `${layout.maxMenuHeight}px` : '240px' }, className: layout.menuClass, ...ctrl.getMenuProps(), children: items.map((item, index) => (_jsx("li", { "data-index": index, className: clsx(layout.optionClass, highlightedIndex === index && "option-highlighted", selectedItem === item && "option-selected"), ...ctrl.getItemProps(item, index), children: adapter.renderOption(item) }, adapter.idOf(item)))) }));
}
export function useComboboxCtrl(props) {
    const [ctrl, setCtrl] = useState(new ComboboxController(props));
    useEffect(() => {
        ctrl?.withState(setCtrl);
    }, []);
    return ctrl;
}
class ComboboxController {
    popupId;
    items;
    adapter;
    onSelect;
    setState;
    _selectedItem = null;
    _filteredItems;
    _inputText = "";
    _highlightedIndex = null;
    _isMenuOpen = false;
    popupCtrl;
    constructor({ adapter, items, value, popupId }) {
        this.adapter = adapter;
        this.items = items;
        this.popupId = popupId;
        if (typeof value === "string") {
            this._inputText = value;
        }
        else if (value) {
            this._selectedItem = adapter.findById(items, adapter.idOf(value)) || null;
            if (this._selectedItem) {
                this._inputText = adapter.valueOf(value);
            }
        }
        if (this._inputText) {
            this._filteredItems = this.adapter.filter(this.items, this._inputText);
        }
        else {
            this._filteredItems = this.items;
        }
    }
    withState(setState) {
        this.setState = setState;
        return this;
    }
    clone() {
        const clone = new ComboboxController({ adapter: this.adapter, items: this.items, popupId: this.popupId });
        clone.setState = this.setState;
        clone.onSelect = this.onSelect;
        clone._inputText = this._inputText;
        clone._highlightedIndex = this._highlightedIndex;
        clone._selectedItem = this._selectedItem;
        clone._isMenuOpen = this._isMenuOpen;
        clone._filteredItems = this._filteredItems;
        clone.popupCtrl = this.popupCtrl;
        return clone;
    }
    updateState() {
        this.setState?.(this.clone());
    }
    get filteredItems() {
        if (this._inputText) {
            return this.adapter.filter(this.items, this._inputText);
        }
        else {
            return this.items;
        }
    }
    get selectedItem() {
        return this._selectedItem;
    }
    set selectedItem(item) {
        this._selectedItem = item;
        this._inputText = item ? this.adapter.valueOf(item) : "";
        this._filteredItems = this._inputText ?
            this.adapter.filter(this.items, this._inputText)
            : this.items;
        this.updateState();
        this.onSelect?.(item);
    }
    get isMenuOpen() {
        return this._isMenuOpen;
    }
    set inputText(inputText) {
        this._inputText = inputText;
        if (inputText) {
            this._filteredItems = this.adapter.filter(this.items, inputText);
        }
        else {
            this._filteredItems = this.items;
        }
        this._highlightedIndex = null;
        this._selectedItem = null;
        this.updateState();
        // TODO -- experimental - it works but it's not perfect
        // we need to update if the popup is on top and the filtered items changed
        if (this.isMenuOpen && this.popupCtrl) {
            const popupCtrl = this.popupCtrl;
            const popupPosition = popupCtrl.context?.position?.position;
            if (popupPosition && popupPosition === "top") {
                window.setTimeout(() => {
                    popupCtrl.update();
                }, 100);
            }
        }
    }
    get inputText() {
        return this._inputText;
    }
    set highlightedIndex(index) {
        this._highlightedIndex = index;
        this.updateState();
    }
    get highlightedIndex() {
        return this._highlightedIndex;
    }
    openMenu() {
        if (this._filteredItems.length > 0) {
            this._isMenuOpen = true;
            //this._highlightedIndex = 0;
            this.updateState();
        }
        else {
            // TODO nothing to show.
            // display a create value option?
        }
    }
    closeMenu(_item) {
        if (this._isMenuOpen) {
            this._highlightedIndex = null;
            this._isMenuOpen = false;
            this.updateState();
        }
    }
    toggleMenu() {
        if (this._isMenuOpen) {
            this.closeMenu();
        }
        else {
            this.openMenu();
        }
    }
    highlightIndex(index, navigateToTop = false) {
        this.highlightedIndex = index;
        const popup = document.getElementById(this.popupId);
        if (popup) {
            popup.querySelector(`li[data-index="${index}"]`)?.scrollIntoView(navigateToTop);
        }
    }
    getMenuProps() {
        return {};
    }
    getToggleButtonProps() {
        return {
            onClick: () => {
                this._isMenuOpen = !this._isMenuOpen;
                this.updateState();
            }
        };
    }
    getItemProps(item, index) {
        return {
            "aria-selected": this._highlightedIndex === index,
            onClick: () => {
                this.selectedItem = item;
                this.closeMenu();
            },
            onMouseEnter: () => {
                if (this.highlightedIndex !== index) {
                    this.highlightedIndex = index;
                }
            },
            onMouseLeave: () => {
                if (this.highlightedIndex === index) {
                    this.highlightedIndex = null;
                }
            }
        };
    }
    getInputProps() {
        const items = this._filteredItems;
        return {
            onClick: () => {
                this.openMenu();
            },
            onChange: (ev) => {
                const value = ev.target.value;
                this.inputText = value;
                this.openMenu();
            },
            value: this.inputText,
            onKeyDown: (ev) => {
                const key = ev.key;
                if (key === "Enter") {
                    if (this.highlightedIndex != null) {
                        this.selectedItem = items[this.highlightedIndex || 0];
                    }
                    else {
                        // create new value?
                        if (this.inputText) {
                            const item = this.adapter.createItem(this.inputText);
                            if (item) {
                                this.items.push(item);
                                this.selectedItem = item;
                            }
                        }
                    }
                    this.closeMenu();
                }
                else if (key === "ArrowDown") {
                    if (this.isMenuOpen) {
                        this.highlightIndex(this.highlightedIndex === null ? 0 : incrModulo(this.highlightedIndex, items.length), false);
                    }
                    else {
                        this.openMenu();
                    }
                }
                else if (key === "ArrowUp") {
                    if (this.isMenuOpen) {
                        this.highlightIndex(this.highlightedIndex === null ? 0 : decrModulo(this.highlightedIndex, items.length), true);
                    }
                }
            }
        };
    }
}
function incrModulo(value, max) {
    return (value + 1) % max;
}
function decrModulo(value, max) {
    return (value - 1 + max) % max;
}
//# sourceMappingURL=ComboBox.js.map