// Styles
import '../VAutocomplete/VAutocomplete.sass'

// Extensions
import VSelect from '../VSelect/VSelect'
import VAutocomplete from '../VAutocomplete/VAutocomplete'

// Utils
import { keyCodes } from '../../util/helpers'

// Types
import { PropValidator } from 'vue/types/options'

/* @vue/component */
export default VAutocomplete.extend({
  name: 'v-combobox',

  props: {
    delimiters: {
      type: Array,
      default: () => ([]),
    } as PropValidator<string[]>,
    returnObject: {
      type: Boolean,
      default: true,
    },
  },

  data: () => ({
    editingIndex: -1,
  }),

  computed: {
    computedCounterValue (): number {
      return this.multiple
        ? this.selectedItems.length
        : (this.internalSearch || '').toString().length
    },
    hasSlot (): boolean {
      return VSelect.options.computed.hasSlot.call(this) || this.multiple
    },
    isAnyValueAllowed (): boolean {
      return true
    },
    menuCanShow (): boolean {
      if (!this.isFocused) return false

      return this.hasDisplayedItems ||
        (!!this.$slots['no-data'] && !this.hideNoData)
    },
    searchIsDirty (): boolean {
      return this.internalSearch != null
    },
  },

  methods: {
    onInternalSearchChanged (val: any) {
      if (
        val &&
        this.multiple &&
        this.delimiters.length
      ) {
        const delimiter = this.delimiters.find(d => val.endsWith(d))
        if (delimiter != null) {
          this.internalSearch = val.slice(0, val.length - delimiter.length)
          this.updateTags()
        }
      }

      this.updateMenuDimensions()
    },
    genInput () {
      const input = VAutocomplete.options.methods.genInput.call(this)

      delete input.data!.attrs!.name
      input.data!.on!.paste = this.onPaste

      return input
    },
    genChipSelection (item: object, index: number) {
      const chip = VSelect.options.methods.genChipSelection.call(this, item, index)

      // Allow user to update an existing value
      if (this.multiple) {
        chip.componentOptions!.listeners! = {
          ...chip.componentOptions!.listeners!,
          dblclick: () => {
            this.editingIndex = index
            this.internalSearch = this.getText(item)
            this.selectedIndex = -1
          },
        }
      }

      return chip
    },
    onChipInput (item: object) {
      VSelect.options.methods.onChipInput.call(this, item)

      this.editingIndex = -1
    },
    // Requires a manual definition
    // to overwrite removal in v-autocomplete
    onEnterDown (e: Event) {
      e.preventDefault()
      // If has menu index, let v-select-list handle
      if (this.getMenuIndex() > -1) return

      this.$nextTick(this.updateSelf)
    },
    onKeyDown (e: KeyboardEvent) {
      const keyCode = e.keyCode

      if (
        e.ctrlKey ||
        ![keyCodes.home, keyCodes.end].includes(keyCode)
      ) {
        VSelect.options.methods.onKeyDown.call(this, e)
      }

      // If user is at selection index of 0
      // create a new tag
      if (this.multiple &&
        keyCode === keyCodes.left &&
        this.$refs.input.selectionStart === 0
      ) {
        this.updateSelf()
      } else if (keyCode === keyCodes.enter) {
        this.onEnterDown(e)
      }

      // The ordering is important here
      // allows new value to be updated
      // and then moves the index to the
      // proper location
      this.changeSelectedIndex(keyCode)
    },
    onTabDown (e: KeyboardEvent) {
      // When adding tags, if searching and
      // there is not a filtered options,
      // add the value to the tags list
      if (this.multiple &&
        this.internalSearch &&
        this.getMenuIndex() === -1
      ) {
        e.preventDefault()
        e.stopPropagation()

        return this.updateTags()
      }

      VAutocomplete.options.methods.onTabDown.call(this, e)
    },
    selectItem (item: object) {
      // Currently only supports items:<string[]>
      if (this.editingIndex > -1) {
        this.updateEditing()
      } else {
        VAutocomplete.options.methods.selectItem.call(this, item)

        // if selected item contains search value,
        // remove the search string
        if (
          this.internalSearch &&
          this.multiple &&
          this.getText(item).toLocaleLowerCase().includes(this.internalSearch.toLocaleLowerCase())
        ) {
          this.internalSearch = null
        }
      }
    },
    setSelectedItems () {
      if (this.internalValue == null ||
        this.internalValue === ''
      ) {
        this.selectedItems = []
      } else {
        this.selectedItems = this.multiple ? this.internalValue : [this.internalValue]
      }
    },
    setValue (value?: any) {
      VSelect.options.methods.setValue.call(this, value === undefined ? this.internalSearch : value)
    },
    updateEditing () {
      const value = this.internalValue.slice()
      const index = this.selectedItems.findIndex(item =>
        this.getText(item) === this.internalSearch)

      // If user enters a duplicate text on chip edit,
      // don't add it, move it to the end of the list
      if (index > -1) {
        const item = typeof value[index] === 'object'
          ? Object.assign({}, value[index])
          : value[index]

        value.splice(index, 1)
        value.push(item)
      } else {
        value[this.editingIndex] = this.internalSearch
      }

      this.setValue(value)
      this.editingIndex = -1
      this.internalSearch = null
    },
    updateCombobox () {
      // If search is not dirty, do nothing
      if (!this.searchIsDirty) return

      // The internal search is not matching
      // the internal value, update the input
      if (this.internalSearch !== this.getText(this.internalValue)) this.setValue()

      // Reset search if using slot to avoid a double input
      const isUsingSlot = Boolean(this.$scopedSlots.selection) || this.hasChips
      if (isUsingSlot) this.internalSearch = null
    },
    updateSelf () {
      this.multiple ? this.updateTags() : this.updateCombobox()
    },
    updateTags () {
      const menuIndex = this.getMenuIndex()

      // If the user is not searching
      // and no menu item is selected
      // or if the search is empty
      // do nothing
      if ((menuIndex < 0 && !this.searchIsDirty) ||
          !this.internalSearch) return

      if (this.editingIndex > -1) {
        return this.updateEditing()
      }

      const index = this.selectedItems.findIndex(item =>
        this.internalSearch === this.getText(item))

      // If the duplicate item is an object,
      // copy it, so that it can be added again later
      const itemToSelect = index > -1 && typeof this.selectedItems[index] === 'object'
        ? Object.assign({}, this.selectedItems[index])
        : this.internalSearch

      // If it already exists, do nothing
      // this might need to change to bring
      // the duplicated item to the last entered
      if (index > -1) {
        const internalValue = this.internalValue.slice()
        internalValue.splice(index, 1)

        this.setValue(internalValue)
      }

      // If menu index is greater than 1
      // the selection is handled elsewhere
      // TODO: find out where
      if (menuIndex > -1) return (this.internalSearch = null)

      this.selectItem(itemToSelect)

      this.internalSearch = null
    },
    onPaste (event: ClipboardEvent) {
      this.$emit('paste', event)
      if (!this.multiple || this.searchIsDirty) return

      const pastedItemText = event.clipboardData?.getData('text/vnd.vuetify.autocomplete.item+plain')
      if (pastedItemText && this.findExistingIndex(pastedItemText as any) === -1) {
        event.preventDefault()
        VSelect.options.methods.selectItem.call(this, pastedItemText as any)
      }
    },
    clearableCallback () {
      this.editingIndex = -1

      VAutocomplete.options.methods.clearableCallback.call(this)
    },
  },
})
