// #region Imports
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var AutoCompleteBoxElement_1;
import { CssLength } from '@breadstone/mosaik-themes';
import { html } from '../../../../Dom/Html';
import { AutoCompleteable } from '../../../Behaviors/AutoCompleteable';
import { Attribute } from '../../../Decorators/AttributeDecorator';
import { Component } from '../../../Decorators/ComponentDecorator';
import { CustomElement } from '../../Abstracts/CustomElement';
import { autoCompleteBoxElementTemplate } from './AutoCompleteBoxElementTemplate';
import { autoCompleteBoxElementJoyStyle } from './Themes/AutoCompleteBoxElement.Joy';
import { autoCompleteBoxElementMemphisStyle } from './Themes/AutoCompleteBoxElement.Memphis';
import { autoCompleteBoxElementCosmopolitanStyle } from './Themes/AutoCompleteBoxElement.Cosmopolitan';
import { Themeable } from '../../../Behaviors/Themeable';
import { Placement } from '../../../Types/Placement';
import { Strategy } from '../../../Types/Strategy';
import { FloatingSync } from '../../../Types/FloatingSync';
import { PortalElement } from '../../Primitives/Portal/PortalElement';
import { FloatingElement } from '../../Primitives/Floating/FloatingElement';
import { DropDownable } from '../../../Behaviors/DropDownable';
import { Disableable } from '../../../Behaviors/Disableable';
import { Focusable } from '../../../Behaviors/Focusable';
import { Watch } from '../../../Decorators/WatchDecorator';
import { IntlController } from '../../../../Intl/IntlController';
import { AutoCompleteBoxElementIntl } from './AutoCompleteBoxElementIntl';
// #endregion
/**
 * Auto Complete Box - An input field with auto-completion functionality for enhanced user experience.
 *
 * @description
 * The Auto Complete Box component provides real-time suggestions as users type, helping them quickly find and select
 * from a list of predefined options. It combines the functionality of a text input with a dropdown list of suggestions,
 * making data entry faster and more accurate. This component is ideal for search fields, location inputs, and any
 * scenario where users need to select from a large set of possible values.
 *
 * @name Auto Complete Box
 * @element mosaik-autocompletebox
 * @category Inputs
 *
 * @csspart input - the input field.
 * @csspart list - the suggestions list.
 * @csspart item - individual suggestion items.
 * @csspart portal - the floating portal container for the suggestions.
 * @csspart popup - the floating popup container for the suggestions.
 *
 * @cssprop {String} --auto-complete-box-background-color - The background color.
 * @cssprop {String} --auto-complete-box-border-color - The border color.
 * @cssprop {String} --auto-complete-box-border-radius - The border radius.
 * @cssprop {String} --auto-complete-box-border-style - The border style.
 * @cssprop {String} --auto-complete-box-border-width - The border width.
 * @cssprop {String} --auto-complete-box-foreground-color - The foreground color.
 * @cssprop {String} --auto-complete-box-gap - The gap between elements.
 * @cssprop {String} --auto-complete-box-height - The height.
 * @cssprop {String} --auto-complete-box-padding-bottom - The padding bottom.
 * @cssprop {String} --auto-complete-box-padding-left - The padding left.
 * @cssprop {String} --auto-complete-box-padding-right - The padding right.
 * @cssprop {String} --auto-complete-box-padding-top - The padding top.
 * @cssprop {String} --auto-complete-box-shadow - The shadow.
 * @cssprop {String} --auto-complete-box-width - The width.
 *
 * @dependency mosaik-portal - Provides a portal host for rendering the suggestion popup.
 * @dependency mosaik-floating - Positions the suggestion popup relative to the host element.
 *
 * @example
 * ```html
 * <mosaik-autocompletebox
 *   placeholder="Search countries..."
 *   items='["Germany", "France", "Italy", "Spain"]'>
 * </mosaik-autocompletebox>
 * ```
 *
 * @public
 */
let AutoCompleteBoxElement = class AutoCompleteBoxElement extends Themeable(DropDownable(Focusable(Disableable(AutoCompleteable(CustomElement))))) {
    static { AutoCompleteBoxElement_1 = this; }
    // #region Fields
    static _idCounter = 0;
    static _floatingFlipFallbackPlacements = Object.freeze([
        Placement.Top,
        Placement.Bottom
    ]);
    _text;
    _shouldSyncSearchFromText;
    _highlightedIndex;
    _lastFilterText;
    _inputId;
    _listboxId;
    _intlController;
    _closeTimeoutHandle;
    _suggestionSync;
    // #endregion
    // #region Ctor
    /**
     * @public
     */
    constructor() {
        super();
        this._text = '';
        this._shouldSyncSearchFromText = true;
        this._highlightedIndex = -1;
        this._lastFilterText = '';
        this._closeTimeoutHandle = null;
        this._inputId = AutoCompleteBoxElement_1.generateId('mosaik-autocomplete-input');
        this._listboxId = AutoCompleteBoxElement_1.generateId('mosaik-autocomplete-list');
        this._suggestionSync = FloatingSync.Width;
        this.dropDownPlacement = Placement.BottomStart;
        this.dropDownStrategy = Strategy.Fixed;
        this.dropDownDistance = 8;
        this.dropDownSkidding = 0;
        this.maxDropDownHeight = '280px';
        this._intlController = new IntlController(this, {
            factory: () => new AutoCompleteBoxElementIntl()
        });
    }
    // #endregion
    // #region Properties
    /**
     * Returns the `{@link is}` property.
     * The `{@link is}` property represents natural name of this element.
     *
     * @public
     * @static
     * @readonly
     */
    static get is() {
        return 'mosaik-autocompletebox';
    }
    /**
     * Gets or sets the `text` property.
     *
     * @public
     * @attr
     */
    get text() {
        return this._text;
    }
    set text(value) {
        if (this._text !== value) {
            this._text = value;
            this.requestUpdate('text');
            if (this._shouldSyncSearchFromText) {
                this._lastFilterText = value;
                this.updateSearchText(value);
            }
        }
    }
    /**
     * Gets the identifier of the input element.
     *
     * @public
     * @readonly
     */
    get inputId() {
        return this._inputId;
    }
    /**
     * Gets the identifier of the suggestion list element.
     *
     * @public
     * @readonly
     */
    get listboxId() {
        return this._listboxId;
    }
    /**
     * Gets the index of the highlighted suggestion.
     *
     * @public
     * @readonly
     */
    get highlightedIndex() {
        return this._highlightedIndex;
    }
    /**
     * Gets a value indicating whether the suggestion list should be rendered.
     *
     * @public
     * @readonly
     */
    get shouldRenderSuggestions() {
        if (!this.isFocused) {
            return false;
        }
        if (!this.isDropDownOpen) {
            return false;
        }
        return this.hasSuggestions || this.shouldShowNoResultsMessage || this.shouldShowLoadingState;
    }
    /**
     * Gets a value indicating whether suggestions are available.
     *
     * @public
     * @readonly
     */
    get hasSuggestions() {
        return this.filteredItems.length > 0;
    }
    /**
     * Gets the active descendant identifier for accessibility.
     *
     * @public
     * @readonly
     */
    get activeDescendantId() {
        return this._highlightedIndex >= 0
            ? this.getOptionId(this._highlightedIndex)
            : null;
    }
    /**
     * Gets a value indicating whether the no-results message should be displayed.
     *
     * @public
     * @readonly
     */
    get shouldShowNoResultsMessage() {
        return this.isFocused && !this.isPopulating && this._lastFilterText.length >= this.minimumPrefixLength && this.filteredItems.length === 0;
    }
    /**
     * Gets a value indicating whether the loading state should be displayed.
     *
     * @public
     * @readonly
     */
    get shouldShowLoadingState() {
        return this.isPopulating && !this.hasSuggestions;
    }
    /**
     * Gets or sets the preferred placement for the suggestion popup.
     *
     * @public
     * @attr
     */
    get suggestionPlacement() {
        return this.dropDownPlacement;
    }
    set suggestionPlacement(value) {
        if (this.dropDownPlacement !== value) {
            this.dropDownPlacement = value;
            this.requestUpdate('suggestionPlacement');
        }
    }
    /**
     * Gets or sets the positioning strategy for the suggestion popup.
     *
     * @public
     * @attr
     */
    get suggestionStrategy() {
        return this.dropDownStrategy;
    }
    set suggestionStrategy(value) {
        if (this.dropDownStrategy !== value) {
            this.dropDownStrategy = value;
            this.requestUpdate('suggestionStrategy');
        }
    }
    /**
     * Gets or sets the distance in pixels between the host and the suggestion popup.
     *
     * @public
     * @attr
     */
    get suggestionDistance() {
        return this.dropDownDistance;
    }
    set suggestionDistance(value) {
        if (this.dropDownDistance !== value) {
            this.dropDownDistance = value;
            this.requestUpdate('suggestionDistance');
        }
    }
    /**
     * Gets or sets the horizontal skidding offset for the suggestion popup.
     *
     * @public
     * @attr
     */
    get suggestionSkidding() {
        return this.dropDownSkidding;
    }
    set suggestionSkidding(value) {
        if (this.dropDownSkidding !== value) {
            this.dropDownSkidding = value;
            this.requestUpdate('suggestionSkidding');
        }
    }
    /**
     * Gets or sets the size synchronization mode for the suggestion popup.
     *
     * @public
     * @attr
     */
    get suggestionSync() {
        return this._suggestionSync;
    }
    set suggestionSync(value) {
        if (this._suggestionSync !== value) {
            this._suggestionSync = value;
            this.requestUpdate('suggestionSync');
        }
    }
    /**
     * Gets or sets the maximum height for the suggestion popup.
     *
     * @public
     * @attr
     */
    get maxSuggestionHeight() {
        return this.maxDropDownHeight;
    }
    set maxSuggestionHeight(value) {
        if (this.maxDropDownHeight !== value) {
            this.maxDropDownHeight = value;
            this.requestUpdate('maxSuggestionHeight');
        }
    }
    /**
     * Gets the fallback placements used when positioning the suggestion popup.
     *
     * @public
     * @readonly
     */
    get suggestionFlipFallbackPlacements() {
        return Array.from(AutoCompleteBoxElement_1._floatingFlipFallbackPlacements);
    }
    /**
     * Returns the `intl` property.
     *
     * @public
     * @readonly
     */
    get intl() {
        return this._intlController.intl;
    }
    // #endregion
    // #region Methods
    /**
     * @internal
     */
    onFilterCallback(items) {
        super.onFilterCallback?.(items);
        if (items.length > 0) {
            this.openSuggestions();
            this.setHighlightedIndex(0);
        }
        else if (this.shouldShowNoResultsMessage && this.isFocused) {
            this.openSuggestions();
            this._highlightedIndex = -1;
        }
        else if (!this.isFocused) {
            this.closeSuggestions();
        }
        this.applyTextCompletion();
    }
    /**
       * Synchronizes the suggestion visibility with the floating popup state.
       *
       * @public
       * @param isActive - Indicates whether the floating popup is active.
       */
    onFloatingActiveChanged(isActive) {
        if (!isActive) {
            this.closeSuggestions();
        }
    }
    /**
     * @public
     * @override
     */
    onSelectCallback(item) {
        const displayValue = this.getDisplayValue(item);
        this.executeWithoutSearchSync(() => {
            this.text = displayValue;
        });
        this._lastFilterText = displayValue;
        this.closeSuggestions();
    }
    /**
     * @public
     * @override
     */
    disconnectedCallback() {
        this.clearCloseTimeout();
        super.disconnectedCallback();
    }
    /**
     * Handles input events raised by the native text box.
     *
     * @public
     * @param event - The event that triggered the handler.
     */
    onInput(event) {
        if (this.disabled) {
            return;
        }
        const value = event.target.value;
        this.cancelCloseSuggestions();
        this._shouldSyncSearchFromText = true;
        this._lastFilterText = value;
        this.text = value;
        if (value.length === 0) {
            this._highlightedIndex = -1;
        }
        this.openSuggestions();
    }
    /**
     * Handles focus events raised by the native input element.
     *
     * @public
     */
    onFocus(_event) {
        this.isFocused = true;
        this.cancelCloseSuggestions();
        if (this.hasSuggestions || this.shouldShowNoResultsMessage) {
            this.openSuggestions();
        }
    }
    /**
     * Handles blur events raised by the native input element.
     *
     * @public
     */
    onBlur(_event) {
        this.isFocused = false;
        this.scheduleCloseSuggestions();
    }
    /**
     * Handles keyboard interactions for navigating the suggestion list.
     *
     * @public
     * @param event - The keyboard event that triggered the handler.
     */
    onKeyDown(event) {
        if (this.disabled) {
            return;
        }
        switch (event.key) {
            case 'ArrowDown': {
                event.preventDefault();
                this.cancelCloseSuggestions();
                this.openSuggestions();
                this.moveHighlight(1);
                break;
            }
            case 'ArrowUp': {
                event.preventDefault();
                this.cancelCloseSuggestions();
                this.openSuggestions();
                this.moveHighlight(-1);
                break;
            }
            case 'Home': {
                if (this.filteredItems.length > 0) {
                    event.preventDefault();
                    this.setHighlightedIndex(0);
                }
                break;
            }
            case 'End': {
                if (this.filteredItems.length > 0) {
                    event.preventDefault();
                    this.setHighlightedIndex(this.filteredItems.length - 1);
                }
                break;
            }
            case 'Enter': {
                if (this._highlightedIndex >= 0 && this._highlightedIndex < this.filteredItems.length) {
                    event.preventDefault();
                    const item = this.filteredItems[this._highlightedIndex];
                    this.commitSelection(item);
                }
                break;
            }
            case 'Tab': {
                if (this._highlightedIndex >= 0 && this._highlightedIndex < this.filteredItems.length) {
                    const item = this.filteredItems[this._highlightedIndex];
                    this.commitSelection(item);
                }
                break;
            }
            case 'Escape': {
                if (this.isDropDownOpen) {
                    event.preventDefault();
                    this.closeSuggestions();
                }
                break;
            }
            default:
                break;
        }
    }
    /**
     * Handles pointer events to keep the suggestion list open while interacting with it.
     *
     * @public
     * @param event - The mouse event that triggered the handler.
     */
    onSuggestionMouseDown(event) {
        event.preventDefault();
        this.cancelCloseSuggestions();
    }
    /**
     * Handles click events on suggestion items.
     *
     * @public
     * @param index - The index of the clicked suggestion.
     */
    onSuggestionClick(index) {
        if (this.disabled) {
            return;
        }
        const item = this.filteredItems[index];
        if (item !== undefined) {
            this.commitSelection(item);
        }
    }
    /**
     * Renders a suggestion with highlighted text.
     *
     * @public
     * @param item - The item to render.
     */
    renderHighlightedItem(item) {
        const value = this.getDisplayValue(item);
        const searchText = this._lastFilterText;
        if (!searchText) {
            return html `${value}`;
        }
        const lowerValue = value.toLowerCase();
        const lowerSearch = searchText.toLowerCase();
        const segments = [];
        let cursor = 0;
        let matchIndex = lowerValue.indexOf(lowerSearch, cursor);
        while (matchIndex !== -1) {
            const unmatched = value.slice(cursor, matchIndex);
            if (unmatched) {
                segments.push(unmatched);
            }
            const matched = value.slice(matchIndex, matchIndex + searchText.length);
            segments.push(html `<span class="match">${matched}</span>`);
            cursor = matchIndex + searchText.length;
            matchIndex = lowerValue.indexOf(lowerSearch, cursor);
        }
        const tail = value.slice(cursor);
        if (tail) {
            segments.push(tail);
        }
        if (segments.length === 0) {
            segments.push(value);
        }
        return html `${segments}`;
    }
    /**
     * Gets the identifier for the suggestion option at the specified index.
     *
     * @public
     * @param index - The index of the option.
     */
    getOptionId(index) {
        return `${this._listboxId}-option-${index}`;
    }
    /**
     * Synchronizes internal state when the dropdown open state changes.
     *
     * @protected
     */
    onDropDownOpenChanged(_previous, next) {
        if (!next) {
            this._highlightedIndex = -1;
            this.clearCloseTimeout();
        }
    }
    /**
     * @protected
     */
    onDisabledChanged(_previous, next) {
        if (next) {
            this.closeSuggestions();
        }
    }
    static generateId(prefix) {
        AutoCompleteBoxElement_1._idCounter += 1;
        return `${prefix}-${AutoCompleteBoxElement_1._idCounter}`;
    }
    getInputElement() {
        return this.renderRoot.querySelector(`#${this._inputId}`) ?? null;
    }
    executeWithoutSearchSync(action) {
        const previous = this._shouldSyncSearchFromText;
        this._shouldSyncSearchFromText = false;
        try {
            action();
        }
        finally {
            this._shouldSyncSearchFromText = previous;
        }
    }
    openSuggestions() {
        if (!this.isDropDownOpen) {
            this.open();
        }
    }
    closeSuggestions() {
        if (this.isDropDownOpen) {
            this.close();
        }
        this._highlightedIndex = -1;
        this.clearCloseTimeout();
    }
    scheduleCloseSuggestions() {
        this.clearCloseTimeout();
        this._closeTimeoutHandle = window.setTimeout(() => {
            this._closeTimeoutHandle = null;
            this.closeSuggestions();
        }, 150);
    }
    cancelCloseSuggestions() {
        this.clearCloseTimeout();
    }
    clearCloseTimeout() {
        if (this._closeTimeoutHandle !== null) {
            window.clearTimeout(this._closeTimeoutHandle);
            this._closeTimeoutHandle = null;
        }
    }
    commitSelection(item) {
        this.updateSelectedItem(item);
        const input = this.getInputElement();
        if (input) {
            const value = this.text;
            input.value = value;
            input.setSelectionRange(value.length, value.length);
        }
    }
    applyTextCompletion() {
        if (!this.isTextCompletionEnabled) {
            return;
        }
        if (!this.isFocused) {
            return;
        }
        if (!this._lastFilterText) {
            return;
        }
        if (this.filteredItems.length === 0) {
            return;
        }
        const firstItem = this.filteredItems[0];
        const completion = this.getDisplayValue(firstItem);
        if (!completion.toLowerCase().startsWith(this._lastFilterText.toLowerCase())) {
            return;
        }
        const typedLength = this._lastFilterText.length;
        this.executeWithoutSearchSync(() => {
            this.text = completion;
        });
        const input = this.getInputElement();
        if (input) {
            input.value = completion;
            input.setSelectionRange(typedLength, completion.length);
        }
    }
    moveHighlight(step) {
        if (this.filteredItems.length === 0) {
            this._highlightedIndex = -1;
            return;
        }
        const length = this.filteredItems.length;
        const currentIndex = this._highlightedIndex;
        const nextIndex = currentIndex === -1
            ? step > 0 ? 0 : length - 1
            : (currentIndex + step + length) % length;
        this.setHighlightedIndex(nextIndex);
    }
    setHighlightedIndex(index) {
        if (this.filteredItems.length === 0) {
            this._highlightedIndex = -1;
            return;
        }
        const normalizedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
        if (this._highlightedIndex !== normalizedIndex) {
            this._highlightedIndex = normalizedIndex;
            this.requestUpdate();
            this.scrollHighlightedOptionIntoView();
        }
    }
    scrollHighlightedOptionIntoView() {
        if (this._highlightedIndex < 0) {
            return;
        }
        this.scrollOptionIntoView(this._highlightedIndex);
    }
    scrollOptionIntoView(index) {
        const option = this.renderRoot.querySelector(`#${this.getOptionId(index)}`);
        option?.scrollIntoView({ block: 'nearest' });
    }
};
__decorate([
    Attribute({ type: String }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], AutoCompleteBoxElement.prototype, "text", null);
__decorate([
    Attribute({ type: Placement }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], AutoCompleteBoxElement.prototype, "suggestionPlacement", null);
__decorate([
    Attribute({ type: Strategy }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], AutoCompleteBoxElement.prototype, "suggestionStrategy", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], AutoCompleteBoxElement.prototype, "suggestionDistance", null);
__decorate([
    Attribute({ type: Number }),
    __metadata("design:type", Number),
    __metadata("design:paramtypes", [Number])
], AutoCompleteBoxElement.prototype, "suggestionSkidding", null);
__decorate([
    Attribute({ type: FloatingSync }),
    __metadata("design:type", String),
    __metadata("design:paramtypes", [String])
], AutoCompleteBoxElement.prototype, "suggestionSync", null);
__decorate([
    Attribute({ type: CssLength }),
    __metadata("design:type", Object),
    __metadata("design:paramtypes", [Object])
], AutoCompleteBoxElement.prototype, "maxSuggestionHeight", null);
__decorate([
    Watch('isDropDownOpen'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Boolean, Boolean]),
    __metadata("design:returntype", void 0)
], AutoCompleteBoxElement.prototype, "onDropDownOpenChanged", null);
__decorate([
    Watch('disabled'),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Boolean, Boolean]),
    __metadata("design:returntype", void 0)
], AutoCompleteBoxElement.prototype, "onDisabledChanged", null);
AutoCompleteBoxElement = AutoCompleteBoxElement_1 = __decorate([
    Component({
        selector: 'mosaik-autocompletebox',
        template: autoCompleteBoxElementTemplate,
        themes: {
            joy: autoCompleteBoxElementJoyStyle,
            memphis: autoCompleteBoxElementMemphisStyle,
            cosmopolitan: autoCompleteBoxElementCosmopolitanStyle
        },
        options: {
            mode: 'open',
            delegatesFocus: true
        },
        imports: [
            PortalElement,
            FloatingElement
        ]
    }),
    __metadata("design:paramtypes", [])
], AutoCompleteBoxElement);
export { AutoCompleteBoxElement };
//# sourceMappingURL=AutoCompleteBoxElement.js.map