/**
 * @license
 * Copyright 2023 Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { isDefined, isDeepEqual, randomChars } from '@tylertech/forge-core';
import { BASE_SELECT_CONSTANTS } from './base-select-constants';
import { isSelectOptionType, SelectOptionType } from './select-utils';
import { ListDropdownAwareFoundation } from '../../list-dropdown/list-dropdown-aware-foundation';
export class BaseSelectFoundation extends ListDropdownAwareFoundation {
    constructor(_adapter) {
        super();
        this._adapter = _adapter;
        this._options = [];
        this._value = [];
        this._multiple = false;
        this._open = false;
        this._selectedValues = [];
        this._selectedLabels = [];
        this._selectedIndexes = [];
        this._filterString = '';
        this._focusListener = evt => this._onFocus(evt);
        this._blurListener = evt => this._onBlur(evt);
        this._clickListener = evt => this._onClick(evt);
        this._keydownListener = evt => this._onKeydown(evt);
        this._optionsChangedListener = options => this._onOptionsChanged(options);
        this._activeChangeListener = id => this._onActiveOptionChanged(id);
        this._dropdownScrollEndListener = () => this._onDropdownScrollEnd();
        this._dismissListener = () => this._onDismiss();
        this._identifier = randomChars();
    }
    _onFocus(evt) { }
    initialize() {
        if (this._optionListenerDestructor) {
            this._optionListenerDestructor();
        }
        this._optionListenerDestructor = this._adapter.setOptionsListener(this._optionsChangedListener);
        this._initializeValue();
    }
    initializeTarget() {
        this._adapter.initializeAccessibility();
        this._adapter.addClickListener(this._clickListener);
        this._adapter.addTargetListener('blur', this._blurListener);
        this._adapter.addTargetListener('focus', this._focusListener);
        this._adapter.addTargetListener('keydown', this._keydownListener);
    }
    disconnect() {
        this._adapter.removeClickListener(this._clickListener);
        this._adapter.removeTargetListener('blur', this._blurListener);
        this._adapter.removeTargetListener('focus', this._focusListener);
        this._adapter.removeTargetListener('keydown', this._keydownListener);
        if (this._open) {
            this._closeDropdown();
        }
        if (this._optionListenerDestructor) {
            this._optionListenerDestructor();
        }
    }
    appendOptions(options) {
        this._adapter.setOptions(options, false);
        if (this._open) {
            this._adapter.appendDropdownOptions(options);
        }
    }
    selectAll() {
        if (this._multiple) {
            this.value = this._flatOptions.map(o => o.value);
        }
    }
    deselectAll() {
        if (this._multiple) {
            this.value = [];
        }
    }
    get _flatOptions() {
        if (isSelectOptionType(this._options, SelectOptionType.Group)) {
            return [].concat.apply([], this._options.map(g => g.options));
        }
        return this._options;
    }
    get _nonDividerOptions() {
        return this._flatOptions.filter(o => !o.divider);
    }
    _initializeValue() {
        const options = (this._options.length && this._options) || this._adapter.getOptions();
        if (isDefined(this._value) && options.length) {
            this._applyValue(this._value);
        }
    }
    _onClick(evt) {
        if (evt.button !== 0) {
            return;
        }
        if (!this._open) {
            this._openDropdown();
        }
        else {
            this._closeDropdown();
        }
    }
    _onBlur(evt) {
        // If the blur event was triggered by an element within the popup then ignore it
        if (this._adapter.isFocusWithinPopup(evt.relatedTarget)) {
            return;
        }
        if (this._open) {
            this._closeDropdown();
        }
    }
    _openDropdown() {
        this._options = this._adapter.getOptions();
        if (!this._flatOptions.length) {
            return;
        }
        this._open = true;
        const config = {
            options: this._options,
            referenceElement: this._adapter.hostElement,
            multiple: this._multiple,
            selectedValues: [...this._selectedValues],
            id: this._identifier,
            optionBuilder: this._optionBuilder,
            syncWidth: this._syncPopupWidth,
            constrainViewportWidth: this._constrainPopupWidth,
            wrapOptionText: this._wrapOptionText,
            observeScroll: this._observeScroll,
            observeScrollThreshold: this._observeScrollThreshold,
            scrollEndListener: this._dropdownScrollEndListener,
            activeChangeCallback: this._activeChangeListener,
            targetWidthCallback: this._targetWidthCallback,
            popupClasses: this._popupClasses,
            optionLimit: this._optionLimit,
            headerBuilder: this._popupHeaderBuilder,
            footerBuilder: this._popupFooterBuilder,
            closeCallback: () => this._closeDropdown(),
            selectCallback: (value) => {
                const flatOptions = this._flatOptions;
                const option = flatOptions.find(o => o.value === value);
                if (option) {
                    const index = flatOptions.indexOf(option);
                    this._onSelect(option, index, true);
                }
            }
        };
        this._adapter.open(config);
        this._adapter.setDismissListener(this._dismissListener);
    }
    /**
     * Closes the dropdown.
     */
    _closeDropdown() {
        this._open = false;
        this._adapter.close();
    }
    /**
     * Handles selecting an item in the dropdown.
     * @param {ISelectOption} option The selected option.
     * @param {number} optionIndex The index of the selected option.
     */
    async _onSelect(option, optionIndex, closeDropdown = true) {
        return new Promise(async (resolve) => {
            if (this._valueChanging) {
                return Promise.resolve(false);
            }
            const value = option ? option.value : '';
            const label = option ? option.label : '';
            // Store the current selections in case we need to rollback (if the event was cancelled)
            const prevValues = [...this._value];
            const prevSelectedValues = [...this._selectedValues];
            const prevSelectedLabels = [...this._selectedLabels];
            const prevSelectedIndexes = [...this._selectedIndexes];
            if (this._multiple) {
                if (this._selectedValues.includes(value)) {
                    const index = this._selectedValues.indexOf(value);
                    this._selectedValues.splice(index, 1);
                    this._selectedLabels.splice(index, 1);
                    this._selectedIndexes.splice(index, 1);
                }
                else {
                    this._selectedValues.push(value);
                    this._selectedLabels.push(label);
                    this._selectedIndexes.push(optionIndex);
                }
            }
            else {
                if (isDefined(value)) {
                    this._selectedValues[0] = value;
                    this._selectedLabels[0] = label;
                    this._selectedIndexes[0] = optionIndex;
                }
                else {
                    this._selectedValues = [];
                    this._selectedLabels = [];
                    this._selectedIndexes = [];
                }
            }
            this._value = [...this._selectedValues];
            const rollbackValue = () => {
                this._selectedValues = [...prevSelectedValues];
                this._selectedLabels = [...prevSelectedLabels];
                this._selectedIndexes = [...prevSelectedIndexes];
                this._value = [...prevValues];
            };
            const applyValue = () => {
                // If we're in multiselect mode, we need to toggle the selected option
                if (this._multiple) {
                    const isSelected = this._selectedIndexes.includes(optionIndex);
                    this._adapter.toggleOptionMultiple(optionIndex, isSelected);
                }
                this._applySelection();
            };
            const data = this.multiple ? [...this._selectedValues] : this._selectedValues[0];
            // We close the dropdown immediately if in single selection mode
            if (this._open && closeDropdown && !this._multiple) {
                this._closeDropdown();
            }
            // First we check to see if there is an before change callback and execute that
            if (typeof this._beforeValueChange === 'function') {
                this._valueChanging = Promise.resolve(this._beforeValueChange.call(null, data));
                const shouldContinue = await this._valueChanging;
                this._valueChanging = undefined;
                if (!shouldContinue) {
                    rollbackValue();
                    this._tryUpdateDropdownPosition();
                    return resolve(false);
                }
            }
            // Now we can emit the change event AFTER the before change callback has been executed and returned true
            const cancelled = !this._adapter.emitHostEvent(BASE_SELECT_CONSTANTS.events.CHANGE, data, true, true);
            if (!cancelled) {
                applyValue();
            }
            else {
                rollbackValue();
            }
            this._tryUpdateDropdownPosition();
            resolve(!cancelled);
        });
    }
    _selectActiveOption() {
        const activeOptionIndex = this._adapter.getActiveOptionIndex();
        if (activeOptionIndex >= 0 && this._nonDividerOptions[activeOptionIndex]) {
            this._onSelect(this._nonDividerOptions[activeOptionIndex], activeOptionIndex);
        }
    }
    _tryUpdateDropdownPosition() {
        if (this._open) {
            this._adapter.queueDropdownPositionUpdate();
        }
    }
    _reset() {
        this._selectedValues = [];
        this._selectedLabels = [];
        this._selectedIndexes = [];
    }
    _applyValue(value) {
        this._selectedValues = [];
        this._selectedLabels = [];
        this._options = this._adapter.getOptions();
        if (!Array.isArray(value)) {
            value = [value];
        }
        this._value = [];
        // Ensure that the values passed are actually existing options
        for (const val of value) {
            if (!this._value.includes(val)) {
                this._value.push(val);
            }
            const option = this._flatOptions.find(o => isDeepEqual(o.value, val));
            if (option) {
                this._selectedValues.push(option.value);
                this._selectedLabels.push(option.label);
            }
        }
        // Update the selected indexes based on the values that are now selected
        this._selectedIndexes = this._selectedValues.map(val => this._flatOptions.findIndex(o => o.value === val));
        // Update the selected options in the dropdown
        this._adapter.patchSelectedValues(this._selectedValues);
    }
    /**
     * Handles the user dismissing the dropdown.
     */
    _onDismiss() {
        this._closeDropdown();
    }
    /** Creates the selected text value from the selected label values. */
    _getSelectedText() {
        if (typeof this._selectedTextBuilder === 'function') {
            const selectedOptions = this._flatOptions.filter(o => this._selectedValues.includes(o.value));
            return this._selectedTextBuilder(selectedOptions);
        }
        if (this._multiple) {
            if (this._selectedLabels.length) {
                if (this._selectedLabels.length === 1) {
                    return this._selectedLabels[0];
                }
                else {
                    return `${this._selectedLabels.length} options selected`;
                }
            }
            else {
                return '';
            }
        }
        return this._selectedLabels
            .filter(v => v && v.length)
            .join(' ')
            .trim();
    }
    _onKeydown(evt) {
        const isEscapeKey = evt.key === 'Escape' || evt.keyCode === 27;
        const isEnter = evt.key === 'Enter' || evt.keyCode === 13;
        const isSpace = evt.key === 'Space' || evt.keyCode === 32;
        const isArrowDown = evt.key === 'ArrowDown' || evt.keyCode === 40;
        const isArrowUp = evt.key === 'ArrowUp' || evt.keyCode === 38;
        const isFilterableCharacter = evt.keyCode >= 48 && evt.keyCode <= 90;
        const isHomeKey = evt.key === 'Home' || evt.keyCode === 36;
        const isEndKey = evt.key === 'End' || evt.keyCode === 35;
        const isTabKey = evt.key === 'Tab';
        // We automatically select the active option if the dropdown is open and in single selection mode
        if (isTabKey && this._open && !this._multiple) {
            this._selectActiveOption();
            return;
        }
        // If an active filter was started, clear it now
        if (!isFilterableCharacter && this._filterTimeout) {
            window.clearTimeout(this._filterTimeout);
            this._filterString = '';
            this._filterTimeout = undefined;
        }
        if (isEscapeKey) {
            evt.preventDefault();
            evt.stopPropagation();
            if (this._open) {
                this._closeDropdown();
                return;
            }
        }
        if (isSpace) {
            evt.preventDefault();
            if (!this._open) {
                this._openDropdown();
            }
            else {
                this._closeDropdown();
            }
        }
        else if (isEnter) {
            if (this._open) {
                evt.stopPropagation();
                evt.preventDefault();
                this._options = this._adapter.getOptions();
                this._selectActiveOption();
            }
        }
        else if (isArrowUp || isArrowDown) {
            evt.preventDefault();
            if (!this._open) {
                this._openDropdown();
                this._adapter.activateFirstOption();
                return;
            }
            if (this._flatOptions.length === 0) {
                return;
            }
            let optionIndex = 0;
            if (this._open) {
                optionIndex = this._adapter.getActiveOptionIndex();
                if (optionIndex === -1) {
                    optionIndex = this._getFirstSelectedOptionIndex();
                }
            }
            else {
                optionIndex = this._getFirstSelectedOptionIndex();
            }
            if (isArrowUp) {
                optionIndex = this._getPreviousHighlightableOptionIndex(optionIndex, this._nonDividerOptions);
            }
            else {
                optionIndex = this._getNextHighlightableOptionIndex(optionIndex, this._nonDividerOptions);
            }
            this._adapter.highlightActiveOption(optionIndex);
        }
        else if (isHomeKey) {
            if (this._open) {
                evt.preventDefault();
                this._adapter.highlightActiveOption(this._nonDividerOptions.findIndex(o => !o.disabled));
            }
        }
        else if (isEndKey) {
            if (this._open) {
                evt.preventDefault();
                const options = this._nonDividerOptions;
                for (let i = options.length - 1; i >= 0; i--) {
                    if (!options[i].disabled) {
                        this._adapter.highlightActiveOption(i);
                        break;
                    }
                }
            }
        }
        else if (isFilterableCharacter) {
            this._filter(evt.key);
        }
    }
    _getFirstSelectedOptionIndex() {
        return this._nonDividerOptions.findIndex(option => this._selectedValues.includes(option.value));
    }
    _getPreviousHighlightableOptionIndex(startIndex, options) {
        let index = startIndex;
        if (index <= 0) {
            index = options.length - 1;
        }
        else {
            index--;
        }
        if (options[index].disabled) {
            return this._getPreviousHighlightableOptionIndex(index, options);
        }
        return index;
    }
    _getNextHighlightableOptionIndex(startIndex, options) {
        let index = startIndex;
        if (index === options.length - 1) {
            index = 0;
        }
        else {
            index++;
        }
        if (options[index].disabled) {
            return this._getNextHighlightableOptionIndex(index, options);
        }
        return index;
    }
    _filter(key) {
        // This allows for typing numbers and/or characters while the select is focused to auto-select the closest match
        if (this._filterTimeout) {
            window.clearTimeout(this._filterTimeout);
            this._filterTimeout = undefined;
        }
        this._filterString += key;
        this._filterTimeout = window.setTimeout(() => {
            this._filterString = '';
            this._filterTimeout = undefined;
        }, 300);
        this._options = this._adapter.getOptions();
        // TODO: Enhance this to cycle through closest matches (see the native select)
        const matchedOption = this._flatOptions.find(({ disabled, label }) => {
            return !disabled && label.trim().toLowerCase().startsWith(this._filterString.trim().toLowerCase());
        });
        if (matchedOption) {
            const optionIndex = this._flatOptions.indexOf(matchedOption);
            if (this._open) {
                this._adapter.highlightActiveOption(optionIndex);
            }
            else if (!this._multiple) {
                this._onSelect(matchedOption, optionIndex, false);
            }
        }
    }
    _onOptionsChanged(options) {
        this._options = options;
        this._applyValue(this._value);
    }
    _onActiveOptionChanged(id) {
        this._adapter.updateActiveDescendant(id);
    }
    /** Gets/sets the value of the component. */
    get value() {
        return this._multiple ? [...this._value] : this._value[0];
    }
    set value(value) {
        let _value;
        if (Array.isArray(value)) {
            _value = [...value];
        }
        else {
            _value = value;
        }
        this._applyValue(_value);
    }
    /** Gets/sets the selected index(s). */
    get selectedIndex() {
        return this._multiple ? [...this._selectedIndexes] : this._selectedIndexes[0];
    }
    set selectedIndex(indexes) {
        this._options = this._adapter.getOptions();
        let indicies;
        if (Array.isArray(indexes)) {
            indicies = [...indexes];
        }
        else {
            indicies = [indexes];
        }
        indicies.sort();
        if (this.multiple) {
            this.value = indicies
                .map(index => this._flatOptions[index])
                .filter(o => o)
                .map(o => o.value);
        }
        else {
            const option = this._flatOptions[indicies[indicies.length - 1]];
            if (!option) {
                return;
            }
            this.value = option.value;
        }
    }
    /** Gets/sets the available options. */
    get options() {
        return this._adapter.getOptions();
    }
    set options(value) {
        let _value;
        if (isSelectOptionType(value, SelectOptionType.Group)) {
            _value = value.map(v => (Object.assign({}, v)));
        }
        else {
            _value = value.map(v => (Object.assign({}, v)));
        }
        this._options = _value;
        this._adapter.setOptions(_value); // Must set options before intializing value
        this._initializeValue();
        if (this._open) {
            this._adapter.setDropdownOptions(_value);
        }
    }
    /** Gets/sets the multiple select state. */
    get multiple() {
        return this._multiple;
    }
    set multiple(value) {
        if (this._multiple !== value) {
            this._multiple = value;
            this._reset();
            if (this._open) {
                this._closeDropdown();
            }
        }
    }
    /** Gets/sets the open state of the dropdown. */
    get open() {
        return this._open;
    }
    set open(value) {
        if (this._open !== value) {
            if (value) {
                this._openDropdown();
            }
            else {
                this._closeDropdown();
            }
        }
    }
    /** Gets/sets the builder callback to use when building options. */
    get optionBuilder() {
        return this._optionBuilder;
    }
    set optionBuilder(value) {
        this._optionBuilder = value;
    }
    /** Sets the selected text builder callback that will be executed when getting the selected text to display in the field. */
    get selectedTextBuilder() {
        return this._selectedTextBuilder;
    }
    set selectedTextBuilder(fn) {
        this._selectedTextBuilder = fn;
    }
    get optionLimit() {
        return this._optionLimit;
    }
    set optionLimit(value) {
        this._optionLimit = value;
    }
    get popupClasses() {
        return this._popupClasses;
    }
    set popupClasses(value) {
        this._popupClasses = value;
    }
    get popupHeaderBuilder() {
        return this._popupHeaderBuilder;
    }
    set popupHeaderBuilder(value) {
        this._popupHeaderBuilder = value;
    }
    get popupFooterBuilder() {
        return this._popupFooterBuilder;
    }
    set popupFooterBuilder(value) {
        this._popupFooterBuilder = value;
    }
    /** Gets the currently active popup element when the dropdown is open. */
    get popupElement() {
        return this._adapter.popupElement;
    }
    /** Sets the callback to be executed when the user selects a value. */
    get beforeValueChange() {
        return this._beforeValueChange;
    }
    set beforeValueChange(value) {
        this._beforeValueChange = value;
    }
}
