/* eslint-disable @typescript-eslint/naming-convention */
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);
};
// #region Imports
import { HTMLElementEventEmitter } from '@breadstone/mosaik-elements';
import { Emit } from '../../Events/Decorators/EmitDecorator';
import { AutoCompleteController } from '../Controllers/AutoCompleteController';
import { Attribute } from '../Decorators/AttributeDecorator';
import { Property } from '../Decorators/PropertyDecorator';
import { AutoCompleteTextSearchMode } from '../Types/AutoCompleteTextSearchMode';
/**
 * @public
 */
export const AutoCompleteable = (base) => {
    class AutoCompleteableElement extends base {
        // #region Fields
        _autoCompleteController;
        _populating;
        _populated;
        _minimumPrefixLength;
        _minimumPopulateDelay;
        _isTextCompletionEnabled;
        _searchMode;
        _displayMemberPath;
        _items;
        _filteredItems;
        _populateTimeoutHandle;
        _searchText;
        _itemsProvider;
        _isPopulating;
        _populateRequestIdCounter;
        _activePopulateRequestId;
        _activePopulateAbortController;
        // #endregion
        // #region Ctor
        /**
         * Constructs a new instance of the {@link AutoCompleteable} class.
         *
         * @public
         */
        constructor(...args) {
            super(args);
            this._minimumPrefixLength = 1;
            this._minimumPopulateDelay = 500;
            this._isTextCompletionEnabled = false;
            this._searchMode = AutoCompleteTextSearchMode.Contains;
            this._displayMemberPath = '';
            this._items = [];
            this._filteredItems = [];
            this._populateTimeoutHandle = null;
            this._searchText = '';
            this._itemsProvider = null;
            this._isPopulating = false;
            this._populateRequestIdCounter = 0;
            this._activePopulateRequestId = 0;
            this._activePopulateAbortController = null;
            this._populating = new HTMLElementEventEmitter(this, 'populating');
            this._populated = new HTMLElementEventEmitter(this, 'populated');
            this._autoCompleteController = new AutoCompleteController(this, {
                filterBehavior: 'empty',
                filterCallback: (items) => {
                    this.onFilterCallback(items);
                },
                selectCallback: (item) => {
                    this.onSelectCallback(item);
                }
            });
        }
        // #endregion
        // #region Properties
        /**
         * Gets or sets the {@link minimumPrefixLength} property.
         * The default value is {@code 1}, which means the search will start after typing one character.
         *
         * @public
         * @attr
         */
        get minimumPrefixLength() {
            return this._minimumPrefixLength;
        }
        set minimumPrefixLength(value) {
            if (this._minimumPrefixLength !== value) {
                this._minimumPrefixLength = value;
                this.requestUpdate('minimumPrefixLength');
                this.refreshFilter();
            }
        }
        /**
         * Gets or sets the {@link minimumPopulateDelay} property.
         * The default value is {@code 500}, which means the search will be delayed by 500 milliseconds after the last keystroke.
         *
         * @public
         * @attr
         */
        get minimumPopulateDelay() {
            return this._minimumPopulateDelay;
        }
        set minimumPopulateDelay(value) {
            if (this._minimumPopulateDelay !== value) {
                this._minimumPopulateDelay = value;
                this.requestUpdate('minimumPopulateDelay');
                this.refreshFilter();
            }
        }
        /**
         * Gets or sets the {@link isTextCompletionEnabled} property.
         * The default value is {@code false}, which means text completion is disabled.
         *
         * @public
         * @attr
         */
        get isTextCompletionEnabled() {
            return this._isTextCompletionEnabled;
        }
        set isTextCompletionEnabled(value) {
            if (this._isTextCompletionEnabled !== value) {
                this._isTextCompletionEnabled = value;
                this.requestUpdate('isTextCompletionEnabled');
            }
        }
        /**
         * Gets or sets the {@link searchMode} property.
         * The default value is {@link AutoCompleteTextSearchMode.Contains}, which means the search will match any part of the text.
         *
         * @public
         * @attr
         */
        get searchMode() {
            return this._searchMode;
        }
        set searchMode(value) {
            if (this._searchMode !== value) {
                this._searchMode = value;
                this.requestUpdate('searchMode');
                this.refreshFilter();
            }
        }
        /**
         * Gets or sets the {@link displayMemberPath} property.
         * The default value is an empty string, which means the item itself will be used for display.
         *
         * @public
         * @attr
         */
        get displayMemberPath() {
            return this._displayMemberPath;
        }
        set displayMemberPath(value) {
            if (this._displayMemberPath !== value) {
                this._displayMemberPath = value;
                this.requestUpdate('displayMemberPath');
                this.refreshFilter();
            }
        }
        /**
         * Gets or sets the {@link items} property.
         * The default value is an empty array, which means no items are available.
         *
         * @public
         */
        get items() {
            return this._items;
        }
        set items(value) {
            if (this._items !== value) {
                this._items = value;
                this.requestUpdate('items');
                this.refreshFilter();
            }
        }
        /**
         * Gets or sets the filtered items.
         *
         * @public
         */
        get filteredItems() {
            return this._filteredItems;
        }
        set filteredItems(value) {
            if (this._filteredItems !== value) {
                this._filteredItems = value;
                this.requestUpdate('filteredItems');
            }
        }
        /**
         * Gets or sets the asynchronous items provider.
         * When assigned, the provider will be invoked for every search request instead of filtering the static {@link items} list.
         *
         * @public
         */
        get itemsProvider() {
            return this._itemsProvider;
        }
        set itemsProvider(value) {
            if (this._itemsProvider !== value) {
                this._itemsProvider = value;
                this.requestUpdate('itemsProvider');
                this.refreshFilter();
            }
        }
        /**
         * Gets a value indicating whether the control is currently populating suggestions.
         *
         * @public
         * @readonly
         */
        get isPopulating() {
            return this._isPopulating;
        }
        set isPopulating(value) {
            if (this._isPopulating !== value) {
                this._isPopulating = value;
                this.requestUpdate('isPopulating');
            }
        }
        /**
         * Called when the items are being populated.
         * Provides reference to {@link IAutoCompletePopulatingEventDetail} as event detail.
         *
         * @public
         * @readonly
         * @eventProperty
         */
        get populating() {
            return this._populating;
        }
        /**
         * Called when the items have been populated.
         * Provides reference to {@link IAutoCompletePopulatedEventDetail} as event detail.
         *
         * @public
         * @readonly
         * @eventProperty
         */
        get populated() {
            return this._populated;
        }
        // #endregion
        // #region Methods
        /**
         * @public
         * @virtual
         */
        onFilterCallback(items) {
            this.filteredItems = items;
        }
        /**
         * @public
         * @virtual
         */
        onSelectCallback(_item) {
            // Override
        }
        /**
         * @public
         */
        updateSearchText(searchText) {
            this._searchText = searchText;
            this.scheduleFilterExecution();
        }
        /**
         * @public
         */
        updateSelectedItem(item) {
            this._autoCompleteController.selectItem(item);
        }
        /**
         * @public
         */
        getDisplayValue(item) {
            return this._autoCompleteController.getDisplayValue(item, this.displayMemberPath);
        }
        /**
         * Emits the {@link populating} event.
         *
         * @protected
         */
        onPopulating(args) {
            this._populating.emit(args);
        }
        /**
         * Emits the {@link populated} event.
         *
         * @protected
         */
        onPopulated(args) {
            this._populated.emit(args);
        }
        // #endregion
        // #region Helpers
        refreshFilter() {
            if (!this._searchText && this.minimumPrefixLength > 0) {
                this.cancelPendingRequest(true);
                if (this._itemsProvider) {
                    this._items = [];
                    this.requestUpdate('items');
                }
                this.onFilterCallback([]);
                const populatedDetail = {
                    searchText: this._searchText,
                    items: [],
                    requestId: 0
                };
                this.onPopulated(populatedDetail);
                return;
            }
            this.scheduleFilterExecution();
        }
        scheduleFilterExecution() {
            this.clearPopulateTimeout();
            if (this.minimumPopulateDelay <= 0) {
                this.executeFilter();
                return;
            }
            this._populateTimeoutHandle = window.setTimeout(() => {
                this._populateTimeoutHandle = null;
                this.executeFilter();
            }, this.minimumPopulateDelay);
        }
        executeFilter() {
            this.cancelPendingRequest(false);
            const searchText = this._searchText;
            const requestId = this.createPopulateRequestId();
            const abortController = new AbortController();
            this._activePopulateRequestId = requestId;
            this._activePopulateAbortController = abortController;
            this.isPopulating = true;
            const populatingDetail = {
                searchText,
                requestId,
                signal: abortController.signal
            };
            this.onPopulating(populatingDetail);
            if (this._itemsProvider) {
                if (searchText.length < this.minimumPrefixLength) {
                    this.completePopulate(requestId, searchText, [], true);
                    return;
                }
                this.onFilterCallback([]);
                this.consumeItemsFromProvider(requestId, searchText, abortController, this._itemsProvider);
                return;
            }
            this.completePopulate(requestId, searchText, this.items, false);
        }
        clearPopulateTimeout() {
            if (this._populateTimeoutHandle !== null) {
                window.clearTimeout(this._populateTimeoutHandle);
                this._populateTimeoutHandle = null;
            }
        }
        disconnectedCallback() {
            this.clearPopulateTimeout();
            this.cancelPendingRequest(true);
            super.disconnectedCallback();
        }
        cancelPendingRequest(resetPopulating) {
            if (this._activePopulateAbortController !== null) {
                this._activePopulateAbortController.abort();
                this._activePopulateAbortController = null;
            }
            this._activePopulateRequestId = 0;
            if (resetPopulating) {
                this.isPopulating = false;
            }
        }
        createPopulateRequestId() {
            this._populateRequestIdCounter += 1;
            return this._populateRequestIdCounter;
        }
        consumeItemsFromProvider(requestId, searchText, abortController, provider) {
            try {
                const result = provider({
                    requestId,
                    searchText,
                    signal: abortController.signal
                });
                if (isPromiseLike(result)) {
                    result
                        .then((items) => {
                        if (abortController.signal.aborted) {
                            return;
                        }
                        this.completePopulate(requestId, searchText, Array.isArray(items) ? items : [], true);
                    })
                        .catch((error) => {
                        if (abortController.signal.aborted) {
                            return;
                        }
                        this.handlePopulateError(requestId, searchText, error);
                    });
                    return;
                }
                this.completePopulate(requestId, searchText, Array.isArray(result) ? result : [], true);
            }
            catch (error) {
                if (!abortController.signal.aborted) {
                    this.handlePopulateError(requestId, searchText, error);
                }
            }
        }
        handlePopulateError(requestId, searchText, _error) {
            this.completePopulate(requestId, searchText, [], true);
        }
        completePopulate(requestId, searchText, items, shouldStoreItems) {
            if (!this.isRequestActive(requestId)) {
                return;
            }
            if (shouldStoreItems) {
                this._items = items;
                this.requestUpdate('items');
            }
            const sourceItems = shouldStoreItems ? items : this.items;
            this._autoCompleteController.updateSearchText(sourceItems, searchText, this.minimumPrefixLength, this.searchMode, this.displayMemberPath);
            const populatedDetail = {
                searchText,
                items: [...this.filteredItems],
                requestId
            };
            this.onPopulated(populatedDetail);
            this.isPopulating = false;
            this.clearActiveRequest(requestId);
        }
        isRequestActive(requestId) {
            return requestId !== 0 && this._activePopulateRequestId === requestId;
        }
        clearActiveRequest(requestId) {
            if (this._activePopulateRequestId === requestId) {
                this._activePopulateRequestId = 0;
                this._activePopulateAbortController = null;
            }
        }
    }
    __decorate([
        Attribute({ type: Number }),
        __metadata("design:type", Number),
        __metadata("design:paramtypes", [Number])
    ], AutoCompleteableElement.prototype, "minimumPrefixLength", null);
    __decorate([
        Attribute({ type: Number }),
        __metadata("design:type", Number),
        __metadata("design:paramtypes", [Number])
    ], AutoCompleteableElement.prototype, "minimumPopulateDelay", null);
    __decorate([
        Attribute({ type: Boolean }),
        __metadata("design:type", Boolean),
        __metadata("design:paramtypes", [Boolean])
    ], AutoCompleteableElement.prototype, "isTextCompletionEnabled", null);
    __decorate([
        Attribute({ type: String }),
        __metadata("design:type", String),
        __metadata("design:paramtypes", [String])
    ], AutoCompleteableElement.prototype, "searchMode", null);
    __decorate([
        Attribute({ type: String }),
        __metadata("design:type", String),
        __metadata("design:paramtypes", [String])
    ], AutoCompleteableElement.prototype, "displayMemberPath", null);
    __decorate([
        Property({ type: Array }),
        __metadata("design:type", Array),
        __metadata("design:paramtypes", [Array])
    ], AutoCompleteableElement.prototype, "items", null);
    __decorate([
        Property({ type: Array }),
        __metadata("design:type", Array),
        __metadata("design:paramtypes", [Array])
    ], AutoCompleteableElement.prototype, "filteredItems", null);
    __decorate([
        Property({ type: Function }),
        __metadata("design:type", Object),
        __metadata("design:paramtypes", [Object])
    ], AutoCompleteableElement.prototype, "itemsProvider", null);
    __decorate([
        Property({ type: Boolean }),
        __metadata("design:type", Boolean),
        __metadata("design:paramtypes", [Boolean])
    ], AutoCompleteableElement.prototype, "isPopulating", null);
    __decorate([
        Emit({ eventName: 'populating' }),
        __metadata("design:type", Object),
        __metadata("design:paramtypes", [])
    ], AutoCompleteableElement.prototype, "populating", null);
    __decorate([
        Emit({ eventName: 'populated' }),
        __metadata("design:type", Object),
        __metadata("design:paramtypes", [])
    ], AutoCompleteableElement.prototype, "populated", null);
    return AutoCompleteableElement;
};
function isPromiseLike(value) {
    if (value === null || value === undefined) {
        return false;
    }
    return typeof value.then === 'function';
}
//# sourceMappingURL=AutoCompleteable.js.map