/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { debounce, isDefined, isString, Platform, randomChars } from '@tylertech/forge-core';
import { highlightTextHTML } from '../core';
import { ListDropdownAsyncStyle } from '../list-dropdown';
import { ListDropdownAwareCore } from '../list-dropdown/list-dropdown-aware-core';
import { AutocompleteMode, AUTOCOMPLETE_CONSTANTS } from './autocomplete-constants';
import { getSelectedOption, isOptionType, optionEqualPredicate, OptionType } from './autocomplete-utils';
export class AutocompleteCore extends ListDropdownAwareCore {
    constructor(_adapter) {
        super();
        this._adapter = _adapter;
        this._isInitialized = false;
        this._isDropdownOpen = false;
        this._mode = AutocompleteMode.Default;
        this._multiple = false;
        this._debounce = AUTOCOMPLETE_CONSTANTS.numbers.DEFAULT_DEBOUNCE_TIME;
        this._allowUnmatched = false;
        this._filterOnFocus = true;
        this._filterFocusFirst = true;
        this._options = [];
        this._filterText = '';
        this._selectedOptions = [];
        this._values = [];
        this._pendingFilterPromises = [];
        this._matchKey = null;
        this._clickListener = evt => this._onClick(evt);
        this._focusListener = () => this._onFocus();
        this._blurListener = evt => this._onBlur(evt);
        this._keydownListener = evt => this._onKeydown(evt);
        this._dropdownIconClickListener = evt => this._onDropdownIconClick(evt);
        this._dropdownScrollEndListener = () => this._onDropdownScrollEnd();
        this._dropdownIconMouseDownListener = evt => this._onDropdownIconMouseDown(evt);
        this._clearButtonListener = evt => this._onClear(evt);
        this._dismissListener = () => this._onDismiss();
        this._activeChangeListener = id => this._adapter.updateActiveDescendant(id);
        this._targetWidthCallback = () => this._adapter.getTargetElementWidth(this._popupTarget);
        this._identifier = randomChars();
    }
    async initialize() {
        if (!this._adapter.hasInputElement() && !this._adapter.setInputElement()) {
            throw new Error('An input element is required as a child of the autocomplete component.');
        }
        this._setInputListener();
        this._setFilterCallback();
        this._attachListeners();
        this._initializeAccessibility();
        if (this._values.length) {
            if (!this._selectedOptions.length) {
                try {
                    await this._executeFilter();
                }
                catch (e) {
                    console.error(e);
                }
                this._updateSelectedOptions(this._values);
            }
            this._adapter.setSelectedText(this._getSelectedText());
        }
        this._isInitialized = true;
    }
    destroy() {
        if (this._isInitialized) {
            this._detachListeners();
            this._isInitialized = false;
        }
        this._closeDropdown({ destroy: true });
    }
    async forceFilter({ preserveValue }) {
        // Clear any existing options since they are expected to no longer be valid
        this._options = [];
        // Execute the filter callback to fetch new options if the consumer has provided any.
        // This allows us to update the current value(s) with new label(s) if there are any matches
        await this._executeFilter(true, true);
        // Edge case, but if the consumer has a need to preserve the existing selection if it doesn't exist in the new options, this will support that
        if (preserveValue) {
            this._options.push(...this._selectedOptions);
        }
        // This will update our current state, but it's expected that consumers will manage their own values so it's likely that this will be called again soon
        this._applyValue(this._values);
    }
    _attachListeners() {
        this._adapter.addInputListener('click', this._clickListener);
        this._adapter.addInputListener('focus', this._focusListener);
        this._adapter.addInputListener('blur', this._blurListener);
        this._adapter.addInputListener('input', this._inputListener);
        this._adapter.addInputListener('keydown', this._keydownListener);
        this._adapter.setDropdownIconListener('click', this._dropdownIconClickListener);
        this._adapter.setDropdownIconListener('mousedown', this._dropdownIconMouseDownListener);
        this._adapter.setClearButtonListener('click', this._clearButtonListener);
    }
    _detachListeners() {
        this._adapter.removeInputListener('click', this._clickListener);
        this._adapter.removeInputListener('focus', this._focusListener);
        this._adapter.removeInputListener('blur', this._blurListener);
        this._adapter.removeInputListener('input', this._inputListener);
        this._adapter.removeInputListener('keydown', this._keydownListener);
        this._adapter.removeDropdownIconListener('click', this._dropdownIconClickListener);
        this._adapter.removeDropdownIconListener('mousedown', this._dropdownIconMouseDownListener);
        this._adapter.removeClearButtonListener('click', this._clearButtonListener);
    }
    _setInputListener() {
        this._inputListener = (evt) => this._onInput(evt);
    }
    _setFilterCallback() {
        this._filterFn = isDefined(this._debounce) && this._debounce > 0 ? debounce(this._debounceFilter, this._debounce, false) : this._debounceFilter;
    }
    _initializeAccessibility() {
        this._adapter.initializeInputAccessibility(this._identifier);
    }
    get _flatOptions() {
        if (isOptionType(this._options, OptionType.Group)) {
            return this._options.reduce((previousValue, currentValue) => previousValue.concat(currentValue.options), []);
        }
        return this._options;
    }
    _onClick(evt) {
        if (!this._isDropdownOpen && this._filterOnFocus) {
            this._showDropdown();
        }
    }
    _onDropdownIconMouseDown(evt) {
        evt.preventDefault(); // Always avoid changing focus when clicking the dropdown
    }
    _onDropdownIconClick(evt) {
        if (!this._isDropdownOpen) {
            this._adapter.focus();
            window.requestAnimationFrame(() => this._showDropdown());
        }
        else {
            this._closeDropdown();
        }
    }
    _onClear(evt) {
        this._filterText = '';
        this._clearValue();
        this._adapter.setSelectedText(this._getSelectedText());
    }
    _onDropdownScrollEnd() {
        this._adapter.emitHostEvent(AUTOCOMPLETE_CONSTANTS.events.SCROLLED_BOTTOM);
    }
    _onFocus() {
        if (!this._isDropdownOpen && this._adapter.getInputValue() && !Platform.isMobile) {
            this._adapter.selectInputValue();
        }
    }
    _onBlur(evt) {
        // If the blur event was triggered by an element within the popup then ignore it
        if (this._adapter.isFocusWithinPopup(evt.relatedTarget)) {
            return;
        }
        this._applyBlur();
    }
    _applyBlur() {
        if (this._isDropdownOpen) {
            this._closeDropdown();
        }
        // If we are in stateless mode, we don't need to do anything further
        if (this._mode === AutocompleteMode.Stateless) {
            return;
        }
        if (!this._selectedOptions.length) {
            if (!this._allowUnmatched) {
                this._filterText = '';
                this._adapter.setSelectedText('');
            }
        }
        else {
            this._adapter.setSelectedText(this._getSelectedText());
        }
    }
    _onInput(evt) {
        if (this._selectedOptions.length && !this._multiple && (!this._adapter.getInputValue() || this._allowUnmatched) && !this._adapter.isWrappingChipField()) {
            this._selectedOptions = [];
            this._values = [];
            this._emitChangeEvent();
        }
        this._filterText = this._adapter.getInputValue();
        this._filterFn();
    }
    async _debounceFilter({ checkFocus = true } = {}) {
        // There is the potential that the user could have started a filter, then moved focus to another element
        // while a debounced filter is running. This check detects that scenario and stops all pending filters.
        if (!checkFocus && !this._adapter.hasFocus()) {
            this._pendingFilterPromises = [];
            if (this._isDropdownOpen) {
                this._closeDropdown();
            }
            return;
        }
        // Keep track of these variables so we can use them when each filter callback completes
        const filterText = this._filterText; // Filter text just before the execution
        const filterPromise = this._executeFilter(); // Execute the filter and keep track of the filter promise object for use after it resolves
        // Keep track of all pending promises
        this._pendingFilterPromises.push(filterPromise);
        // If the dropdown is open, show the spinner and execute the filter. If not open, then we need to
        // show the dropdown (which will handle showing the spinner and executing the filter for us) so
        // we use the promise from that method instead.
        if (this._isDropdownOpen) {
            this._adapter.setBusyVisibility(true);
        }
        else {
            this._showDropdown({ filter: false });
        }
        try {
            await filterPromise;
        }
        catch {
            // When an exception occurs, we just flush the pending promises and clean up
            this._pendingFilterPromises = [];
            if (this._isDropdownOpen) {
                this._closeDropdown();
            }
            return;
        }
        // We only continue if the filter text before this result matches the latest filter text
        if (filterText === this._filterText) {
            this._pendingFilterPromises = [];
            this._onFilterComplete();
        }
        else {
            // If this is not the latest filter result, we ignore it and remove it from the pending promises
            const promiseIndex = this._pendingFilterPromises.indexOf(filterPromise);
            if (promiseIndex !== -1) {
                this._pendingFilterPromises.splice(promiseIndex, 1);
            }
        }
    }
    _onKeydown(evt) {
        switch (evt.key) {
            case 'Tab':
                if (this._isDropdownOpen && !this._multiple) {
                    this._selectActiveOption(false);
                }
                break;
            case 'Esc':
            case 'Escape':
                if (this._isDropdownOpen) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    this._closeDropdown();
                }
                break;
            case 'Down':
            case 'ArrowDown':
                evt.preventDefault();
                if (!this._isDropdownOpen) {
                    this._showDropdown({ activateFirst: true, activateSelected: true });
                }
                else {
                    this._adapter.propagateKey(evt.key);
                }
                break;
            case 'Up':
            case 'ArrowUp':
                evt.preventDefault();
                if (this._isDropdownOpen) {
                    this._adapter.propagateKey(evt.key);
                }
                break;
            case 'Enter':
            case 'Home':
            case 'End':
                if (this._isDropdownOpen) {
                    if (evt.key === 'Enter') {
                        evt.stopPropagation();
                    }
                    evt.preventDefault();
                    this._adapter.propagateKey(evt.key);
                }
                break;
            case 'Backspace':
            case 'Delete':
                const input = evt.target;
                const value = this._adapter.getInputValue();
                const isRemovingAllChars = input.value.substring(input.selectionStart, input.selectionEnd) === input.value;
                const isBackspacingLastChar = value.length === 1 && input.selectionEnd === 1;
                const isDeletingLastChar = value.length === 1 && input.selectionEnd === 0;
                const isEmpty = !value || isRemovingAllChars || isBackspacingLastChar || isDeletingLastChar;
                // We only clear the value on empty when not around a chip-field and NOT in multiple mode
                if (!this._adapter.isWrappingChipField() && isEmpty && !this._multiple && this._values.length) {
                    this._clearValue();
                }
                break;
        }
    }
    _executeFilter(sendFilterText = true, sendValue = false) {
        if (!this._filter || typeof this._filter !== 'function') {
            throw new Error('A filter callback must be provided. Did you set the "filter" property?');
        }
        const filter = this._filter;
        const filterText = sendFilterText ? this._filterText : '';
        const value = sendValue ? this._getValue() : null;
        return new Promise((resolve, reject) => {
            return Promise.resolve(filter(filterText, value))
                .then(options => {
                this._options = options;
                resolve(this._options);
            })
                .catch(e => reject(`An unexpected error occurred while filtering: ${e}`));
        });
    }
    _onFilterComplete() {
        if (!this._adapter.hasFocus()) {
            if (this._isDropdownOpen) {
                this._closeDropdown();
            }
            return;
        }
        if (this._options.length) {
            const sendFilterText = this._allowUnmatched && !this._selectedOptions.length;
            this._dropdownReady({ userTriggered: sendFilterText });
            if (this._filterFocusFirst && this._filterText) {
                this._adapter.activateFirstOption();
            }
        }
        else {
            this._closeDropdown();
        }
    }
    _clearValue() {
        this._selectedOptions = [];
        this._values = [];
        if (this._isDropdownOpen) {
            this._adapter.setSelectedOptions([]);
            this._adapter.clearActiveOption();
        }
        this._emitChangeEvent();
    }
    async _showDropdown({ filter = true, userTriggered = true, activateFirst = false, activateSelected = false } = {}) {
        if (!this._isInitialized || !this._adapter.hasInputElement()) {
            return;
        }
        const sendFilterText = this._allowUnmatched && !this._selectedOptions.length;
        this._isDropdownOpen = true;
        let listOptionBuilder;
        if (this._optionBuilder) {
            const optionBuilder = this._optionBuilder;
            listOptionBuilder = (option, parentElement) => {
                return optionBuilder(option, this._filterText, parentElement);
            };
        }
        const config = {
            options: this._options,
            referenceElement: this._adapter.inputElement,
            multiple: this._multiple,
            selectedValues: [...this._values],
            id: `forge-autocomplete-${this._identifier}`,
            asyncStyle: ListDropdownAsyncStyle.Skeleton,
            optionLimit: this._optionLimit,
            popupClasses: this._popupClasses,
            headerBuilder: this._popupHeaderBuilder,
            footerBuilder: this._popupFooterBuilder,
            transform: label => {
                if (this._filterText) {
                    // Highlight the filter text within the label
                    const highlightElement = highlightTextHTML(label, this._filterText);
                    if (highlightElement) {
                        return highlightElement;
                    }
                }
                return label;
            },
            allowBusy: true,
            optionBuilder: listOptionBuilder,
            syncWidth: this._syncPopupWidth,
            observeScroll: this._observeScroll,
            observeScrollThreshold: this._observeScrollThreshold,
            scrollEndListener: this._dropdownScrollEndListener,
            activeChangeCallback: this._activeChangeListener,
            targetWidthCallback: this._targetWidthCallback,
            selectCallback: (value) => this._onSelect(value),
            closeCallback: () => this._closeDropdown()
        };
        this._adapter.show(config, this._popupTarget);
        this._adapter.toggleHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.OPEN, this._isDropdownOpen);
        if (filter) {
            if (this._options.length) {
                this._adapter.setBusyVisibility(true);
            }
            try {
                await this._executeFilter(sendFilterText);
            }
            catch (e) {
                console.error(e);
            }
            this._updateSelectedOptions(this._values);
        }
        // If we have any pending filter promises, then we will let those complete and update the dropdown separately after
        if (this._pendingFilterPromises.length) {
            return;
        }
        this._dropdownReady({ userTriggered, activateFirst, activateSelected });
    }
    _dropdownReady({ userTriggered = true, activateFirst = false, activateSelected = false } = {}) {
        if (!this._isDropdownOpen || !this._options.length || (userTriggered && !this._adapter.hasFocus())) {
            this._closeDropdown();
            return;
        }
        this._sortSelectedOptions();
        this._adapter.setBusyVisibility(false);
        this._adapter.setOptions(this._options);
        this._adapter.setSelectedOptions(this._selectedOptions);
        this._adapter.setDismissListener(this._dismissListener);
        if (activateSelected && this._selectedOptions.length) {
            this._adapter.activateSelectedOption();
        }
        else if (activateFirst) {
            this._adapter.activateFirstOption();
        }
    }
    _closeDropdown({ destroy = false } = {}) {
        if (this._multiple) {
            this._filterText = '';
        }
        this._isDropdownOpen = false;
        this._adapter.hide(this._dismissListener, { destroy });
        this._sortSelectedOptions();
        this._adapter.toggleHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.OPEN, this._isDropdownOpen);
    }
    _sortSelectedOptions() {
        // When in multiple selection mode, we need to make sure that the selected options appear at the top of the list
        if (this._multiple && this._selectedOptions.length && isOptionType(this._options, OptionType.Option)) {
            const selectedOptions = [];
            const unselectedOptions = [];
            this._options.forEach(option => {
                if (this._selectedOptions.find(o => optionEqualPredicate(o, option.value, this._matchKey))) {
                    selectedOptions.push(option);
                }
                else {
                    unselectedOptions.push(option);
                }
            });
            this._options = [...selectedOptions, ...unselectedOptions];
        }
    }
    /**
     * Handles selecting an item in the dropdown.
     * @param {string} selectedValue The string value to select.
     */
    async _onSelect(selectedValue, keepFocus = true) {
        if (this._valueChanging) {
            return;
        }
        // If we are in stateless mode, then we need to just emit an event when selecting an option. The
        // selected values are not tracked in stateless mode
        if (this._mode === AutocompleteMode.Stateless) {
            const data = { value: selectedValue };
            const result = this._adapter.emitHostEvent(AUTOCOMPLETE_CONSTANTS.events.SELECT, data, true, true);
            if (result) {
                this._filterText = '';
                if (!this._multiple) {
                    this._closeDropdown();
                }
            }
            return;
        }
        const select = () => {
            const flatOptions = this._flatOptions;
            const option = flatOptions.find(o => optionEqualPredicate(o, selectedValue, this._matchKey));
            const value = option ? option.value : '';
            const label = option ? option.label : '';
            if (this._multiple) {
                const selectedOption = getSelectedOption(this._selectedOptions, value);
                if (selectedOption) {
                    const index = this._selectedOptions.indexOf(selectedOption);
                    this._selectedOptions.splice(index, 1);
                }
                else {
                    this._selectedOptions.push(option);
                }
            }
            else {
                if (isDefined(value)) {
                    this._selectedOptions[0] = option;
                    this._filterText = label;
                }
                else {
                    this._selectedOptions = [];
                    this._filterText = '';
                }
            }
            // Update the dropdown selection status if its open
            if (this._isDropdownOpen) {
                this._adapter.setSelectedOptions(this._selectedOptions);
            }
            // Keep the values in sync with the currently selected options
            this._values = this._selectedOptions.map(o => o.value);
            // Update the selected text in the input
            this._adapter.setSelectedText(this._getSelectedText());
            // Select the text in the input to allow for the next filter
            if (!Platform.isMobile && keepFocus) {
                this._adapter.selectInputValue();
            }
            this._emitChangeEvent();
            this._tryUpdateDropdownPosition();
        };
        // We close the dropdown immediately if in single selection mode
        if (this._isDropdownOpen && !this._multiple) {
            this._closeDropdown();
        }
        if (typeof this._beforeValueChange === 'function') {
            this._valueChanging = Promise.resolve(this._beforeValueChange.call(null, selectedValue, this._matchKey));
            const shouldContinue = await this._valueChanging;
            if (shouldContinue) {
                select();
            }
            else {
                this._tryUpdateDropdownPosition();
            }
            this._valueChanging = undefined;
        }
        else {
            select();
        }
    }
    _selectActiveOption(keepFocus = true) {
        const activeOptionIndex = this._adapter.getActiveOptionIndex();
        if (typeof activeOptionIndex === 'number' && activeOptionIndex >= 0) {
            const option = this._flatOptions[activeOptionIndex];
            if (option) {
                this._onSelect(option.value, keepFocus);
            }
        }
    }
    _emitChangeEvent() {
        this._adapter.emitHostEvent(AUTOCOMPLETE_CONSTANTS.events.CHANGE, this._getValue(), true);
    }
    _tryUpdateDropdownPosition() {
        if (this._isDropdownOpen) {
            this._adapter.queueDropdownPositionUpdate();
        }
    }
    /**
     * Retrieves the current value(s) from the selected options array based on whether
     * we are in multi-select mode or not.
     */
    _getValue() {
        if (!this._values) {
            return null;
        }
        if (!this._values.length) {
            return this._multiple ? [] : null;
        }
        return this._multiple ? [...this._values] : this._values[0];
    }
    /**
     * Creates the selected text value from the selected label values.
     */
    _getSelectedText() {
        if (this._adapter.isWrappingChipField()) {
            return '';
        }
        if (this._selectedTextBuilder) {
            return this._selectedTextBuilder(this._selectedOptions);
        }
        else {
            if (this._multiple) {
                if (this._values.length) {
                    if (this._values.length === 1) {
                        return this._selectedOptions[0]?.label ?? '';
                    }
                    else {
                        return `${this._values.length} options selected`;
                    }
                }
                else {
                    return '';
                }
            }
            else {
                return this._selectedOptions
                    .filter(o => o && o.label)
                    .map(o => o.label)
                    .join(' ')
                    .trim();
            }
        }
    }
    /**
     * Handles the user dismissing the dropdown. This is only called if the blur event was triggered
     * from within the popup element itself (in a custom template most likely).
     * @param keepFocus Keep focus on the dropdown or not.
     */
    _onDismiss() {
        this._closeDropdown();
    }
    async _applyValue(value) {
        let values = [];
        this._selectedOptions = [];
        if (!Array.isArray(value)) {
            values = isDefined(value) ? [value] : [];
        }
        else {
            values = value;
        }
        // If this is not a multi-select, then make sure we only allow one selected value
        if (!this._multiple && values.length > 1) {
            values = [values[0]];
        }
        if (isOptionType(values, OptionType.Option)) {
            this._values = values.map((o) => o.value);
            this._selectedOptions = values;
        }
        else {
            this._values = values;
        }
        // Ensure that the values passed are actually existing options
        const allOptions = this._flatOptions;
        if (values.length && allOptions.length) {
            this._updateSelectedOptions(values);
        }
        // Execute the filter now if we have a value, but not options yet (only if it has been initialized already)
        if (this._values.length && !this._selectedOptions.length && !!this._filter && this._isInitialized) {
            try {
                await this._executeFilter(false, true);
            }
            catch (e) {
                console.error(e);
            }
            this._updateSelectedOptions(this._values);
        }
        // Update filter text to match the label when on in multiselect mode
        if (!this._multiple) {
            this._filterText = this._selectedOptions.length ? this._selectedOptions[0].label : '';
        }
        else {
            this._filterText = '';
        }
        // Update the state of the component based on the existence of a selected value
        if (this._isInitialized) {
            this._adapter.setSelectedText(this._getSelectedText());
        }
        // When the value is changed programmatically we should update the selected options
        if (this._isDropdownOpen) {
            this._adapter.setSelectedOptions(this._selectedOptions);
        }
    }
    _updateSelectedOptions(values) {
        const flatOptions = [...this._flatOptions, ...this._selectedOptions];
        if (this._selectedOptions.length) {
            this._selectedOptions = [];
        }
        if (isOptionType(values, OptionType.Option)) {
            for (const option of values) {
                const actualOption = flatOptions.find(o => optionEqualPredicate(o, option.value, this._matchKey));
                if (actualOption) {
                    this._selectedOptions.push(actualOption);
                }
                else {
                    this._selectedOptions.push(option);
                }
            }
        }
        else {
            for (const value of values) {
                const actualOption = flatOptions.find(o => optionEqualPredicate(o, value, this._matchKey));
                if (actualOption) {
                    this._selectedOptions.push(actualOption);
                }
                else if (this._allowUnmatched) {
                    this._selectedOptions.push({ label: value, value });
                }
            }
        }
    }
    /** Gets/sets the mode state. */
    get mode() {
        return this._mode;
    }
    set mode(value) {
        if (this._mode !== value) {
            this._mode = value;
            if (this._mode === AutocompleteMode.Stateless) {
                this._selectedOptions = [];
                if (this._isDropdownOpen) {
                    this._closeDropdown();
                }
            }
            this._adapter.setHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.MODE, this._mode);
        }
    }
    /** Gets/sets the multi-select state. */
    get multiple() {
        return this._multiple;
    }
    set multiple(value) {
        if (this._multiple !== value) {
            this._multiple = value;
            this._adapter.setHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.MULTIPLE, isDefined(this._multiple) ? this._multiple.toString() : '');
        }
    }
    /** Gets/sets the value of the component. */
    get value() {
        return this._getValue();
    }
    set value(value) {
        let values = [];
        if (value == null) {
            values = [];
        }
        else if (Array.isArray(value)) {
            values = structuredClone(value);
        }
        else if (isString(value)) {
            values = [value];
        }
        else {
            values = [structuredClone(value)];
        }
        const hasNewValue = values.length !== this._values.length || values.some(v => !this._values.includes(v));
        if (!hasNewValue) {
            return;
        }
        // We only apply the new values if they have actually changed
        this._applyValue(value);
    }
    /** Gets/sets filter on focus settings which controls whether the dropdown displays automatically when focused. */
    get filterOnFocus() {
        return this._filterOnFocus;
    }
    set filterOnFocus(value) {
        if (this._filterOnFocus !== value) {
            this._filterOnFocus = value;
            this._adapter.setHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.FILTER_ON_FOCUS, isDefined(this._filterOnFocus) ? this._filterOnFocus.toString() : '');
        }
    }
    /** Gets/sets whether the first option in the dropdown will be focused automatically when opened or not. */
    get filterFocusFirst() {
        return this._filterFocusFirst;
    }
    set filterFocusFirst(value) {
        if (this._filterFocusFirst !== value) {
            this._filterFocusFirst = value;
            this._adapter.toggleHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.FILTER_FOCUS_FIRST, this._filterFocusFirst);
        }
    }
    /** Controls whether unmatched text entered by the user will stay visible an option in the dropdown is not found. */
    get allowUnmatched() {
        return this._allowUnmatched;
    }
    set allowUnmatched(value) {
        if (this._allowUnmatched !== value) {
            this._allowUnmatched = value;
            if (isDefined(this._allowUnmatched)) {
                this._adapter.setHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.ALLOW_UNMATCHED, this._allowUnmatched.toString());
            }
        }
    }
    /** Gets/sets the property key to match the value to an option.  */
    get matchKey() {
        return this._matchKey;
    }
    set matchKey(value) {
        if (this._matchKey !== value) {
            this._matchKey = value;
        }
    }
    /** Gets/sets the selector that will be used to find an element to attach the popup to. Defaults to the input element. */
    get popupTarget() {
        return this._popupTarget;
    }
    set popupTarget(value) {
        if (this._popupTarget !== value) {
            this._popupTarget = value;
        }
    }
    /**
     * Gets/sets the filter text.
     *
     * Setting the filter text only applies when allowUnmatched is enabled.
     */
    get filterText() {
        return this._filterText;
    }
    set filterText(value) {
        if (this._filterText !== value) {
            this._filterText = this._allowUnmatched ? value : '';
            if (this._isInitialized && this._allowUnmatched) {
                this._adapter.setInputValue(this._filterText);
                if (this._isDropdownOpen) {
                    this._debounceFilter({ checkFocus: false });
                }
            }
        }
    }
    /** Gets/sets the list of classes to apply to the popup element. */
    get popupClasses() {
        return Array.isArray(this._popupClasses) ? [...this._popupClasses] : [this._popupClasses];
    }
    set popupClasses(value) {
        if (this._popupClasses !== value) {
            this._popupClasses = Array.isArray(value) ? [...value] : [value];
        }
    }
    /** Gets/sets the builder callback to use for the popup header. */
    set popupHeaderBuilder(value) {
        this._popupHeaderBuilder = value;
    }
    /** Gets/sets the builder callback to use for the popup footer. */
    set popupFooterBuilder(value) {
        this._popupFooterBuilder = value;
    }
    /** Gets/sets whether the popup width is synchronized with the popup target. */
    get syncPopupWidth() {
        return this._syncPopupWidth;
    }
    set syncPopupWidth(value) {
        if (this._syncPopupWidth !== value) {
            this._syncPopupWidth = value;
        }
    }
    /** Gets/sets the maximum number of options to display in the dropdown. */
    get optionLimit() {
        return this._optionLimit;
    }
    set optionLimit(value) {
        if (this._optionLimit !== value) {
            this._optionLimit = value;
        }
    }
    /** Gets/sets the debounce delay (milliseconds) for keyboard events. */
    get debounce() {
        return this._debounce;
    }
    set debounce(value) {
        if (this._debounce !== value) {
            this._debounce = value;
            if (this._isInitialized) {
                this._setFilterCallback();
            }
            this._adapter.setHostAttribute(AUTOCOMPLETE_CONSTANTS.attributes.DEBOUNCE, isDefined(this._debounce) ? this._debounce.toString() : '');
        }
    }
    /** Sets the item builder callback that will be executed when building the option list in the dropdown. */
    get optionBuilder() {
        return this._optionBuilder;
    }
    set optionBuilder(fn) {
        this._optionBuilder = fn;
    }
    /** Sets the filter callback that will be executed when fetching options for the autocomplete dropdown. */
    get filter() {
        return this._filter;
    }
    set filter(cb) {
        if (this._filter !== cb) {
            this._filter = cb;
            // If we have a value, but don't have any options yet then execute the filter
            if (this._isInitialized && this._values.length && !this._flatOptions.length && !!this._filter) {
                this._executeFilter().then(() => {
                    this._updateSelectedOptions(this._values);
                    this._adapter.setSelectedText(this._getSelectedText());
                });
            }
        }
    }
    /** Sets the selected text builder callback that will be executed when getting the selected text. */
    get selectedTextBuilder() {
        return this._selectedTextBuilder;
    }
    set selectedTextBuilder(fn) {
        this._selectedTextBuilder = fn;
        // If there are selected options, set the selected text again
        if (this._selectedOptions.length) {
            this._adapter.setSelectedText(this._getSelectedText());
        }
    }
    get observeScroll() {
        return this._observeScroll;
    }
    set observeScroll(value) {
        this._observeScroll = value;
    }
    get observeScrollThreshold() {
        return this._observeScrollThreshold;
    }
    set observeScrollThreshold(value) {
        this._observeScrollThreshold = value;
    }
    appendOptions(options) {
        if (!this._isDropdownOpen) {
            return;
        }
        this._options = [...this._options, ...options];
        this._adapter.appendOptions(options);
    }
    get isInitialized() {
        return this._isInitialized;
    }
    get open() {
        return this._isDropdownOpen;
    }
    set open(value) {
        if (this._isDropdownOpen !== value) {
            if (value) {
                this._showDropdown({ userTriggered: false });
            }
            else {
                this._closeDropdown();
            }
        }
    }
    get beforeValueChange() {
        return this._beforeValueChange;
    }
    set beforeValueChange(value) {
        if (value !== this._beforeValueChange) {
            this._beforeValueChange = value;
        }
    }
    get popupElement() {
        return this._adapter.getPopupElement();
    }
}
