<script>
import PxBaseItems from './PxBaseItems.vue';
import { arraySafeIndex } from '../utils/utils.js';

// helper to define what an "empty" key or set of keys looks like
const isKeySetEmpty = keyOrKeys => {
    return (
        keyOrKeys == null ||
        keyOrKeys == undefined ||
        (Array.isArray(keyOrKeys) && keyOrKeys.length == 0)
    );
};

// helper to ensure that a potential key or set of keys is an array
const ensureSet = valueOrArray => {
    if (isKeySetEmpty(valueOrArray)) {
        return [];
    }

    return Array.isArray(valueOrArray) ? valueOrArray : [valueOrArray];
};

export default {
    mixins: [PxBaseItems],

    props: {
        // TEMP-MIGRATION
        modelValue: { default: null },

        /**
         * Set the selection as either a single item key or an array of item keys.
         */
        selected: { default: null },

        /**
         * If true, the first item in a collection will be automatically selected on load or when
         * items is updated and no other item is selected
         */
        autoSelectFirst: { type: Boolean, default: false },

        /**
         * If true, the update and change events will only be emitted after if the component is
         * mounted.
         */
        emitEventsBeforeMounted: { type: Boolean, default: false },
    },

    data() {
        return {
            selectedKeys: [],
            baseUpdateMode: 'single',
            isMounted: false,
        };
    },

    computed: {
        /**
         * Indicates whether the update event arg is a single key or an array of keys.  Note that
         * selection is always stored internally (selectedKeys) as an array of keys.
         *
         * Derived classes should override this to indicate the current update mode and can expose
         * an additional prop to control it. We're returning the value of a data value so that it
         * can be set when testing but there's no internal process that would ever change this
         * value.
         *
         * In `single` mode the event is the first selected item or null when empty. The intended
         * use is components that don't support multiselection.
         *
         * In `multi` mode the event is always an array with all selected items.
         *
         * @values single, multi
         */
        updateMode() {
            return this.baseUpdateMode;
        },

        /**
         * Returns an array that contains the keys for all fo the items in items.
         */
        itemKeys() {
            return this.items.map(item => this.getItemKey(item));
        },

        /**
         * Returns the first selected key
         */
        selectedKey() {
            return this.selectedKeys?.length ? this.selectedKeys[0] : null;
        },

        /**
         * Returns the currently selected items.
         */
        selectedItems() {
            return (this.selectedKeys || []).map(key => {
                return this.getItem(key);
            });
        },

        /**
         * Returns the first selected item or null if selection is empty.
         */
        selectedItem() {
            return this.selectedItems.length ? this.selectedItems[0] : null;
        },

        /**
         * Returns the indices of the currently selected items.
         */
        selectedIndices() {
            return (this.selectedKeys || []).map(key => {
                return this.getItemKeyIndex(key);
            });
        },

        /**
         * Returns the index of the first selected item or null if selection is empty.
         */
        selectedIndex() {
            return this.selectedIndices.length ? this.selectedIndices[0] : null;
        },
    },

    watch: {
        // VUE3 MIGRATION, can remove once complete
        modelValue: {
            immediate: true,
            handler(nv) {
                if (nv) {
                    const name =
                        'bem' in this ? this.bem() : this.$options?.name || 'UnknownComponent';
                    console.error(
                        `[MIGRATION] PxBaseItemsSelect (${name}): v-model has syntax has changed. Please replace <${name} v-model="mySelection" /> with <${name} v-model:selected="mySelection" />.`,
                    );
                }
            },
        },
        selected: {
            immediate: true,
            handler(nv, ov) {
                // check for matching values
                const nvArray = ensureSet(nv);
                const ovArray = ensureSet(ov);

                if (nvArray.length === ovArray.length) {
                    let match = true;
                    for (let i = 0; i < nvArray.length; i++) {
                        match = match && nvArray[i] === ovArray[i];
                    }
                    if (match) {
                        return;
                    }
                }

                this.setSelection(nv);
            },
        },
        selectedKeys: {
            immediate: true,
            handler() {
                this.emitUpdateEvent();
            },
        },

        items() {
            this.tryAutoSelect();
        },
    },

    mounted() {
        this.isMounted = true;
        this.$nextTick(() => {
            this.tryAutoSelect();
        });
    },

    unmounted() {
        this.isMounted = false;
    },

    methods: {
        tryAutoSelect() {
            if (this.autoSelectFirst && isKeySetEmpty(this.selected) && this.items?.length) {
                this.selectAt(0);
            }
        },

        /**
         * Emit the change / update:selected event
         */
        emitUpdateEvent() {
            if (!this.isMounted && !this.emitEventsBeforeMounted) {
                return;
            }

            let updateArg;

            if (this.updateMode == 'single') {
                updateArg = this.selectedKeys?.length ? this.selectedKeys[0] : null;
            } else {
                updateArg = this.selectedKeys;
            }

            this.$emit('change', updateArg);
            this.$emit('update:selected', updateArg);
        },

        /**
         * Replace the current set of selected keys with the keys provided here. Keys that don't
         * have a corresponding item in the items collection in items will be ignored.
         */
        setSelection(keyOrKeys) {
            const validKeys = ensureSet(keyOrKeys).filter(key => this.isValidItemKey(key));
            this.selectedKeys = validKeys;
        },

        // ----------------------------------------------------------------------
        // Key-based methods

        /**
         * Check if the provided key is selected.
         */
        isSelected(key) {
            return !!(this.selectedKeys?.length && this.selectedKeys.includes(key));
        },

        /**
         * Add the provided key or keys to the current selection.
         */
        selectKeys(addKeyOrKeys, replaceSelection = 'auto') {
            const replace = this.getShouldReplace(replaceSelection);

            if (isKeySetEmpty(addKeyOrKeys)) {
                if (replace) {
                    this.selectedKeys = [];
                }
                return;
            }

            const addKeys = ensureSet(addKeyOrKeys).filter(key => this.isValidItemKey(key));

            if (replace) {
                this.selectedKeys = addKeys;
                return;
            } else {
                this.selectedKeys = [
                    ...this.selectedKeys,
                    ...addKeys.filter(key => !this.selectedKeys.includes(key)),
                ];
            }
        },

        /**
         * Select the provided key and replace the selection if updateMode is 'single' or add to the
         * selection if updateMode is 'multi'
         */
        selectKey(key, replaceSelection = 'auto') {
            this.selectKeys(key, this.getShouldReplace(replaceSelection));
        },

        /**
         * Select the provided key (and replace the current selection)
         */
        selectKeyOnly(key) {
            this.selectKeys(key, true);
        },

        /**
         * Select the provided key or keys (and replace the current selection)
         */
        selectKeysOnly(addKeyOrKeys) {
            this.selectKeys(addKeyOrKeys, true);
        },

        /**
         * Remove the provided key or keys from the current selection.
         */
        unselectKeys(removeKeyOrKeys) {
            const removeKeys = ensureSet(removeKeyOrKeys);
            if (removeKeys.length) {
                this.selectedKeys = this.selectedKeys.filter(key => !removeKeys.includes(key));
            }
        },

        /**
         * Remove the provided key from the current selection (convenience for better syntax when
         * working with 'single' mode).
         */
        unselectKey(key) {
            this.unselectKeys(key);
        },

        /**
         * Toggle selection of the given key.
         */
        toggleKey(key) {
            if (this.isSelected(key)) {
                this.unselectKeys(key);
            } else {
                this.selectKeys(key);
            }
        },

        // ----------------------------------------------------------------------
        // Item-based methods

        /**
         * Check if the provided key is selected.
         */
        isItemSelected(item) {
            return this.isSelected(this.getItemKey(item));
        },

        /**
         * Add the provided item or items to the current selection. (Note: this is a convenience
         * method and selection will continue to be stored as keys.)
         */
        selectItems(items, replaceSelection = 'auto') {
            const keys = ensureSet(items).map(item => this.getItemKey(item));
            this.selectKeys(keys, this.getShouldReplace(replaceSelection));
        },

        /**
         * Select the provided item and replace the selection if updateMode is 'single' or add to
         * the selection if updateMode is 'multi'. (Note: this is a convenience method and selection
         * will continue to be stored as keys.)
         */
        selectItem(item, replaceSelection = 'auto') {
            this.selectItems(item, this.getShouldReplace(replaceSelection));
        },

        /**
         * Select the provided item (and replace the current selection). (Note: this is a convenience
         * method and selection will continue to be stored as keys.)
         */
        selectItemOnly(item) {
            this.selectItem(item, true);
        },

        /**
         * Select the provided item or items (and replace the current selection). (Note: this is a
         * convenience method and selection will continue to be stored as keys.)
         */
        selectItemsOnly(items) {
            this.selectItems(items, true);
        },

        /**
         * Remove the provided item or items from the current selection.
         */
        unselectItems(itemOrItems) {
            const keys = ensureSet(itemOrItems).map(item => this.getItemKey(item));
            this.unselectKeys(keys);
        },

        /**
         * Remove the provided key from the current selection (convenience for better syntax when
         * working with 'single' mode).
         */
        unselectItem(item) {
            this.unselectItems(item);
        },

        /**
         * Toggle selection of the given item.
         */
        toggleItem(item) {
            const key = this.getItemKey(item);
            if (key) {
                this.toggleKey(key);
            }
        },

        // ----------------------------------------------------------------------
        // Index-based methods

        selectAt(index, replaceSelection = 'auto') {
            const keyAtIndex = this.itemKeys[index];

            const replace =
                replaceSelection == 'auto' ? this.updateMode == 'single' : replaceSelection;

            this.selectKeys(keyAtIndex, replace);
        },

        selectAtSafe(index, wrap = false, replaceSelection = 'auto') {
            const replace =
                replaceSelection == 'auto' ? this.updateMode == 'single' : replaceSelection;

            index = arraySafeIndex(this.items, index, 0, wrap);
            this.selectAt(index, replace);
        },

        selectOffset(offsetFromSelectedIndex, wrap = true) {
            if (this.selectedIndex === null) {
                this.selectAt(0);
            } else {
                const index = arraySafeIndex(
                    this.items,
                    this.selectedIndex,
                    offsetFromSelectedIndex,
                    wrap,
                );
                this.selectAt(index, true);
            }
        },

        selectNext(wrap = true) {
            this.selectOffset(1, wrap);
        },

        selectPrevious(wrap = true) {
            this.selectOffset(-1, wrap);
        },

        unselectAt(index) {},

        // ----------------------------------------------------------------------
        // Other Selection

        selectAll() {
            if (Array.isArray(this.items) && this.items.length) {
                if (this.updateMode == 'multi') {
                    this.selectedKeys = this.itemKeys;
                } else {
                    this.selectedKeys = ensureSet(this.getItemKey(this.items[0]));
                }
            }
        },

        selectNone() {
            this.selectedKeys = [];
        },

        // ----------------------------------------------------------------------
        // Internal / helper

        getShouldReplace(value) {
            if (value == 'auto') {
                return this.updateMode == 'single';
            }
            return value;
        },
    },
};
</script>
