import Vue from 'vue'

import QField from '../field/QField.js'
import QIcon from '../icon/QIcon.js'
import QChip from '../chip/QChip.js'

import QItem from '../item/QItem.js'
import QItemSection from '../item/QItemSection.js'
import QItemLabel from '../item/QItemLabel.js'

import QMenu from '../menu/QMenu.js'
import QDialog from '../dialog/QDialog.js'

import { isDeepEqual } from '../../utils/is.js'
import { stop, prevent, stopAndPrevent } from '../../utils/event.js'
import { normalizeToInterval } from '../../utils/format.js'
import { shouldIgnoreKey, isKeyCode } from '../../utils/private/key-composition.js'
import { mergeSlot } from '../../utils/private/slot.js'
import cache from '../../utils/private/cache.js'

import { FormFieldMixin } from '../../mixins/form.js'
import VirtualScroll from '../../mixins/virtual-scroll.js'
import CompositionMixin from '../../mixins/composition.js'
import ListenersMixin from '../../mixins/listeners.js'

const validateNewValueMode = v => ['add', 'add-unique', 'toggle'].includes(v)
const reEscapeList = '.*+?^${}()|[]\\'

export default Vue.extend({
  name: 'QSelect',

  mixins: [
    QField,
    VirtualScroll,
    CompositionMixin,
    FormFieldMixin,
    ListenersMixin
  ],

  props: {
    value: {
      required: true
    },

    multiple: Boolean,

    displayValue: [String, Number],
    displayValueSanitize: Boolean,
    dropdownIcon: String,

    options: {
      type: Array,
      default: () => []
    },

    optionValue: [Function, String],
    optionLabel: [Function, String],
    optionDisable: [Function, String],

    hideSelected: Boolean,
    hideDropdownIcon: Boolean,
    fillInput: Boolean,

    maxValues: [Number, String],

    optionsDense: Boolean,
    optionsDark: {
      type: Boolean,
      default: null
    },
    optionsSelectedClass: String,
    optionsSanitize: Boolean,

    optionsCover: Boolean,

    menuShrink: Boolean,
    menuAnchor: String,
    menuSelf: String,
    menuOffset: Array,

    popupContentClass: String,
    popupContentStyle: [String, Array, Object],

    useInput: Boolean,
    useChips: Boolean,

    newValueMode: {
      type: String,
      validator: validateNewValueMode
    },

    mapOptions: Boolean,
    emitValue: Boolean,

    inputDebounce: {
      type: [Number, String],
      default: 500
    },

    inputClass: [Array, String, Object],
    inputStyle: [Array, String, Object],

    tabindex: {
      type: [String, Number],
      default: 0
    },

    autocomplete: String,

    transitionShow: String,
    transitionHide: String,

    behavior: {
      type: String,
      validator: v => ['default', 'menu', 'dialog'].includes(v),
      default: 'default'
    },

    virtualScrollItemSize: {
      type: [ Number, String ],
      default: void 0
    }
  },

  data () {
    return {
      menu: false,
      dialog: false,
      optionIndex: -1,
      inputValue: '',
      dialogFieldFocused: false
    }
  },

  watch: {
    innerValue: {
      handler (val) {
        this.innerValueCache = val

        if (
          this.useInput === true &&
          this.fillInput === true &&
          this.multiple !== true &&
          // Prevent re-entering in filter while filtering
          // Also prevent clearing inputValue while filtering
          this.innerLoading !== true &&
          ((this.dialog !== true && this.menu !== true) || this.hasValue !== true)
        ) {
          this.userInputValue !== true && this.__resetInputValue()
          if (this.dialog === true || this.menu === true) {
            this.filter('')
          }
        }
      },
      immediate: true
    },

    fillInput () {
      this.__resetInputValue()
    },

    menu (show) {
      this.__updateMenu(show)
    },

    virtualScrollLength (newLength, oldLength) {
      if (this.menu === true && this.innerLoading === false) {
        this.__resetVirtualScroll(-1, true)

        this.$nextTick(() => {
          if (this.menu === true && this.innerLoading === false) {
            if (newLength > oldLength) {
              this.__resetVirtualScroll()
            }
            else {
              this.__updateMenu(true)
            }
          }
        })
      }
    }
  },

  computed: {
    isOptionsDark () {
      return this.optionsDark === null
        ? this.isDark
        : this.optionsDark
    },

    virtualScrollLength () {
      return Array.isArray(this.options)
        ? this.options.length
        : 0
    },

    fieldClass () {
      return `q-select q-field--auto-height q-select--with${this.useInput !== true ? 'out' : ''}-input` +
        ` q-select--with${this.useChips !== true ? 'out' : ''}-chips` +
        ` q-select--${this.multiple === true ? 'multiple' : 'single'}`
    },

    computedInputClass () {
      if (this.hideSelected === true || this.innerValue.length === 0) {
        return this.inputClass
      }

      return this.inputClass === void 0
        ? 'q-field__input--padding'
        : [this.inputClass, 'q-field__input--padding']
    },

    menuContentClass () {
      return (this.virtualScrollHorizontal === true ? 'q-virtual-scroll--horizontal' : '') +
        (this.popupContentClass ? ' ' + this.popupContentClass : '')
    },

    innerValue () {
      const
        mapNull = this.mapOptions === true && this.multiple !== true,
        val = this.value !== void 0 && (this.value !== null || mapNull === true)
          ? (this.multiple === true && Array.isArray(this.value) ? this.value : [ this.value ])
          : []

      if (this.mapOptions === true && Array.isArray(this.options) === true) {
        const cache = this.mapOptions === true && this.innerValueCache !== void 0
          ? this.innerValueCache
          : []
        const values = val.map(v => this.__getOption(v, cache))

        return this.value === null && mapNull === true
          ? values.filter(v => v !== null)
          : values
      }

      return val
    },

    noOptions () {
      return this.virtualScrollLength === 0
    },

    selectedString () {
      return this.innerValue
        .map(opt => this.getOptionLabel(opt))
        .join(', ')
    },

    ariaCurrentValue () {
      return this.displayValue !== void 0
        ? this.displayValue
        : this.selectedString
    },

    sanitizeFn () {
      return this.optionsSanitize === true
        ? () => true
        : opt => opt !== void 0 && opt !== null && opt.sanitize === true
    },

    displayAsText () {
      return this.displayValueSanitize === true || (
        this.displayValue === void 0 && (
          this.optionsSanitize === true ||
          this.innerValue.some(this.sanitizeFn)
        )
      )
    },

    computedTabindex () {
      return this.focused === true ? this.tabindex : -1
    },

    selectedScope () {
      return this.innerValue.map((opt, i) => ({
        index: i,
        opt,
        sanitize: this.sanitizeFn(opt),
        selected: true,
        removeAtIndex: this.__removeAtIndexAndFocus,
        toggleOption: this.toggleOption,
        tabindex: this.computedTabindex
      }))
    },

    optionScope () {
      if (this.virtualScrollLength === 0) {
        return []
      }

      const { from, to } = this.virtualScrollSliceRange
      const { options, optionEls } = this.__optionScopeCache

      return this.options.slice(from, to).map((opt, i) => {
        const disable = this.isOptionDisabled(opt)
        const index = from + i

        const itemProps = {
          clickable: true,
          active: false,
          activeClass: this.computedOptionsSelectedClass,
          manualFocus: true,
          focused: false,
          disable,
          tabindex: -1,
          dense: this.optionsDense,
          dark: this.isOptionsDark
        }

        const itemAttrs = {
          role: 'option',
          id: `${this.targetUid}_${index}`
        }

        if (disable !== true) {
          this.isOptionSelected(opt) === true && (itemProps.active = true)
          itemAttrs['aria-selected'] = itemProps.active === true ? 'true' : 'false'

          this.optionIndex === index && (itemProps.focused = true)
        }

        const itemEvents = {
          click: () => { this.toggleOption(opt) }
        }

        if (this.$q.platform.is.desktop === true) {
          itemEvents.mousemove = () => { this.menu === true && this.setOptionIndex(index) }
        }

        const option = {
          index,
          opt,
          sanitize: this.sanitizeFn(opt),
          selected: itemProps.active,
          focused: itemProps.focused,
          toggleOption: this.toggleOption,
          setOptionIndex: this.setOptionIndex,
          itemProps,
          itemAttrs
        }

        if (options[i] === void 0 || isDeepEqual(option, options[i]) !== true) {
          options[i] = option
          optionEls[i] = void 0
        }

        return {
          ...option,
          itemEvents
        }
      })
    },

    dropdownArrowIcon () {
      return this.dropdownIcon !== void 0
        ? this.dropdownIcon
        : this.$q.iconSet.arrow.dropdown
    },

    squaredMenu () {
      return this.optionsCover === false &&
        this.outlined !== true &&
        this.standout !== true &&
        this.borderless !== true &&
        this.rounded !== true
    },

    computedOptionsSelectedClass () {
      return this.optionsSelectedClass !== void 0
        ? this.optionsSelectedClass
        : (this.color !== void 0 ? `text-${this.color}` : '')
    },

    innerOptionsValue () {
      return this.innerValue.map(opt => this.getOptionValue(opt))
    },

    // returns method to get value of an option;
    // takes into account 'option-value' prop
    getOptionValue () {
      return this.__getPropValueFn('optionValue', 'value')
    },

    // returns method to get label of an option;
    // takes into account 'option-label' prop
    getOptionLabel () {
      return this.__getPropValueFn('optionLabel', 'label')
    },

    // returns method to tell if an option is disabled;
    // takes into account 'option-disable' prop
    isOptionDisabled () {
      const fn = this.__getPropValueFn('optionDisable', 'disable')
      return (...args) => fn.apply(null, args) === true
    },

    inputControlEvents () {
      const on = {
        input: this.__onInput,
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        change: this.__onChange,
        keydown: this.__onTargetKeydown,
        keyup: this.__onTargetAutocomplete,
        keypress: this.__onTargetKeypress,
        focus: this.__selectInputText,
        click: e => {
          this.hasDialog === true && stop(e)
        }
      }

      on.compositionstart = on.compositionupdate = on.compositionend = this.__onComposition

      return on
    },

    virtualScrollItemSizeComputed () {
      return this.virtualScrollItemSize === void 0
        ? (this.optionsDense === true ? 24 : 48)
        : this.virtualScrollItemSize
    },

    comboboxAttrs () {
      const attrs = {
        tabindex: this.tabindex,
        role: 'combobox',
        'aria-label': this.label,
        'aria-readonly': this.readonly === true ? 'true' : 'false',
        'aria-autocomplete': this.useInput === true ? 'list' : 'none',
        'aria-expanded': this.menu === true ? 'true' : 'false',
        'aria-controls': `${this.targetUid}_lb`
      }

      if (this.optionIndex >= 0) {
        attrs['aria-activedescendant'] = `${this.targetUid}_${this.optionIndex}`
      }

      return attrs
    },

    listboxAttrs () {
      return {
        id: `${this.targetUid}_lb`,
        role: 'listbox',
        'aria-multiselectable': this.multiple === true ? 'true' : 'false'
      }
    }
  },

  methods: {
    getEmittingOptionValue (opt) {
      return this.emitValue === true
        ? this.getOptionValue(opt)
        : opt
    },

    removeAtIndex (index) {
      if (index > -1 && index < this.innerValue.length) {
        if (this.multiple === true) {
          const model = this.value.slice()
          this.$emit('remove', { index, value: model.splice(index, 1)[0] })
          this.$emit('input', model)
        }
        else {
          this.$emit('input', null)
        }
      }
    },

    __removeAtIndexAndFocus (index) {
      this.removeAtIndex(index)
      this.__focus()
    },

    add (opt, unique) {
      const val = this.getEmittingOptionValue(opt)

      if (this.multiple !== true) {
        this.fillInput === true && this.updateInputValue(
          this.getOptionLabel(opt),
          true,
          true
        )

        this.$emit('input', val)
        return
      }

      if (this.innerValue.length === 0) {
        this.$emit('add', { index: 0, value: val })
        this.$emit('input', this.multiple === true ? [ val ] : val)
        return
      }

      if (unique === true && this.isOptionSelected(opt) === true) {
        return
      }

      if (this.maxValues !== void 0 && this.value.length >= this.maxValues) {
        return
      }

      const model = this.value.slice()

      this.$emit('add', { index: model.length, value: val })
      model.push(val)
      this.$emit('input', model)
    },

    toggleOption (opt, keepOpen) {
      if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
        return
      }

      const optValue = this.getOptionValue(opt)

      if (this.multiple !== true) {
        if (keepOpen !== true) {
          this.updateInputValue(
            this.fillInput === true ? this.getOptionLabel(opt) : '',
            true,
            true
          )

          this.dialogFieldFocused = false
          document.activeElement.blur()

          this.hidePopup()
        }

        this.$refs.target !== void 0 && this.$refs.target.focus()

        if (
          this.innerValue.length === 0 ||
          isDeepEqual(this.getOptionValue(this.innerValue[0]), optValue) !== true
        ) {
          this.$emit('input', this.emitValue === true ? optValue : opt)
        }
        return
      }

      (this.hasDialog !== true || this.dialogFieldFocused === true) && this.__focus()

      this.__selectInputText()

      if (this.innerValue.length === 0) {
        const val = this.emitValue === true ? optValue : opt
        this.$emit('add', { index: 0, value: val })
        this.$emit('input', this.multiple === true ? [ val ] : val)
        return
      }

      const
        model = this.value.slice(),
        index = this.innerOptionsValue.findIndex(v => isDeepEqual(v, optValue))

      if (index > -1) {
        this.$emit('remove', { index, value: model.splice(index, 1)[0] })
      }
      else {
        if (this.maxValues !== void 0 && model.length >= this.maxValues) {
          return
        }

        const val = this.emitValue === true ? optValue : opt

        this.$emit('add', { index: model.length, value: val })
        model.push(val)
      }

      this.$emit('input', model)
    },

    setOptionIndex (index) {
      if (this.$q.platform.is.desktop !== true) { return }

      const val = index > -1 && index < this.virtualScrollLength
        ? index
        : -1

      if (this.optionIndex !== val) {
        this.optionIndex = val
      }
    },

    moveOptionSelection (offset = 1, skipInputValue) {
      if (this.menu === true) {
        let index = this.optionIndex
        do {
          index = normalizeToInterval(
            index + offset,
            -1,
            this.virtualScrollLength - 1
          )
        }
        while (index !== -1 && index !== this.optionIndex && this.isOptionDisabled(this.options[index]) === true)

        if (this.optionIndex !== index) {
          cancelAnimationFrame(this.moveAnimationFrame)
          this.moveAnimationFrame = requestAnimationFrame(() => {
            this.setOptionIndex(index)
            this.scrollTo(index)

            if (skipInputValue !== true && this.useInput === true && this.fillInput === true) {
              this.__setInputValue(index >= 0
                ? this.getOptionLabel(this.options[index])
                : this.defaultInputValue
              )
            }
          })
        }
      }
    },

    __getOption (value, innerValueCache) {
      const fn = opt => isDeepEqual(this.getOptionValue(opt), value)
      return this.options.find(fn) || innerValueCache.find(fn) || value
    },

    __getPropValueFn (propName, defaultVal) {
      const val = this[propName] !== void 0
        ? this[propName]
        : defaultVal

      return typeof val === 'function'
        ? val
        : opt => (opt !== null && typeof opt === 'object' && val in opt ? opt[ val ] : opt)
    },

    isOptionSelected (opt) {
      const val = this.getOptionValue(opt)
      return this.innerOptionsValue.find(v => isDeepEqual(v, val)) !== void 0
    },

    __selectInputText (e) {
      if (
        this.useInput === true &&
        this.$refs.target !== void 0 &&
        (e === void 0 || (this.$refs.target === e.target && e.target.value === this.selectedString))
      ) {
        this.$refs.target.select()
      }
    },

    __onTargetKeyup (e) {
      // if ESC and we have an opened menu
      // then stop propagation (might be caught by a QDialog
      // and so it will also close the QDialog, which is wrong)
      if (isKeyCode(e, 27) === true && this.menu === true) {
        stop(e)
        // on ESC we need to close the dialog also
        this.hidePopup()
        this.__resetInputValue()
      }

      this.$emit('keyup', e)
    },

    __onTargetAutocomplete (e) {
      const { value } = e.target

      if (e.keyCode !== void 0) {
        this.__onTargetKeyup(e)
        return
      }

      e.target.value = ''
      clearTimeout(this.inputTimer)
      this.__resetInputValue()

      if (typeof value === 'string' && value.length > 0) {
        const needle = value.toLocaleLowerCase()
        const findFn = extractFn => {
          const option = this.options.find(opt => extractFn(opt).toLocaleLowerCase() === needle)

          if (option === void 0) {
            return false
          }

          if (this.innerValue.indexOf(option) === -1) {
            this.toggleOption(option)
          }
          else {
            this.hidePopup()
          }

          return true
        }
        const fillFn = afterFilter => {
          if (findFn(this.getOptionValue) === true) {
            return
          }
          if (findFn(this.getOptionLabel) === true || afterFilter === true) {
            return
          }

          this.filter(value, true, () => fillFn(true))
        }

        fillFn()
      }
      else {
        this.__clearValue(e)
      }
    },

    __onTargetKeypress (e) {
      this.$emit('keypress', e)
    },

    __onTargetKeydown (e) {
      this.$emit('keydown', e)

      if (shouldIgnoreKey(e) === true) {
        return
      }

      const newValueModeValid = this.inputValue.length > 0 &&
        (this.newValueMode !== void 0 || this.qListeners['new-value'] !== void 0)
      const tabShouldSelect = e.shiftKey !== true &&
        this.multiple !== true &&
        (this.optionIndex > -1 || newValueModeValid === true)

      // escape
      if (e.keyCode === 27) {
        prevent(e) // prevent clearing the inputValue
        return
      }

      // tab
      if (e.keyCode === 9 && tabShouldSelect === false) {
        this.__closeMenu()
        return
      }

      if (e.target === void 0 || e.target.id !== this.targetUid) { return }

      // down
      if (
        e.keyCode === 40 &&
        this.innerLoading !== true &&
        this.menu === false
      ) {
        stopAndPrevent(e)
        this.showPopup()
        return
      }

      // backspace
      if (
        e.keyCode === 8 &&
        this.hideSelected !== true &&
        this.inputValue.length === 0
      ) {
        if (this.multiple === true && Array.isArray(this.value)) {
          this.removeAtIndex(this.value.length - 1)
        }
        else if (this.multiple !== true && this.value !== null) {
          this.$emit('input', null)
        }
        return
      }

      // home, end - 36, 35
      if (
        (e.keyCode === 35 || e.keyCode === 36) &&
        (typeof this.inputValue !== 'string' || this.inputValue.length === 0)
      ) {
        stopAndPrevent(e)
        this.optionIndex = -1
        this.moveOptionSelection(e.keyCode === 36 ? 1 : -1, this.multiple)
      }

      // pg up, pg down - 33, 34
      if (
        (e.keyCode === 33 || e.keyCode === 34) &&
        this.virtualScrollSliceSizeComputed !== void 0
      ) {
        stopAndPrevent(e)
        this.optionIndex = Math.max(
          -1,
          Math.min(
            this.virtualScrollLength,
            this.optionIndex + (e.keyCode === 33 ? -1 : 1) * this.virtualScrollSliceSizeComputed.view
          )
        )
        this.moveOptionSelection(e.keyCode === 33 ? 1 : -1, this.multiple)
      }

      // up, down
      if (e.keyCode === 38 || e.keyCode === 40) {
        stopAndPrevent(e)
        this.moveOptionSelection(e.keyCode === 38 ? -1 : 1, this.multiple)
      }

      const optionsLength = this.virtualScrollLength

      // clear search buffer if expired
      if (this.searchBuffer === void 0 || this.searchBufferExp < Date.now()) {
        this.searchBuffer = ''
      }

      // keyboard search when not having use-input
      if (
        optionsLength > 0 &&
        this.useInput !== true &&
        e.key !== void 0 &&
        e.key.length === 1 && // printable char
        e.altKey === false && // not kbd shortcut
        e.ctrlKey === false && // not kbd shortcut
        e.metaKey === false && // not kbd shortcut, especially on macOS with Command key
        (e.keyCode !== 32 || this.searchBuffer.length > 0) // space in middle of search
      ) {
        this.menu !== true && this.showPopup(e)

        const
          char = e.key.toLocaleLowerCase(),
          keyRepeat = this.searchBuffer.length === 1 && this.searchBuffer[0] === char

        this.searchBufferExp = Date.now() + 1500
        if (keyRepeat === false) {
          stopAndPrevent(e)
          this.searchBuffer += char
        }

        const searchRe = new RegExp('^' + this.searchBuffer.split('').map(l => reEscapeList.indexOf(l) > -1 ? '\\' + l : l).join('.*'), 'i')

        let index = this.optionIndex

        if (keyRepeat === true || index < 0 || searchRe.test(this.getOptionLabel(this.options[index])) !== true) {
          do {
            index = normalizeToInterval(index + 1, -1, optionsLength - 1)
          }
          while (index !== this.optionIndex && (
            this.isOptionDisabled(this.options[index]) === true ||
            searchRe.test(this.getOptionLabel(this.options[index])) !== true
          ))
        }

        if (this.optionIndex !== index) {
          this.$nextTick(() => {
            this.setOptionIndex(index)
            this.scrollTo(index)

            if (index >= 0 && this.useInput === true && this.fillInput === true) {
              this.__setInputValue(this.getOptionLabel(this.options[index]))
            }
          })
        }

        return
      }

      // enter, space (when not using use-input and not in search), or tab (when not using multiple and option selected)
      // same target is checked above
      if (
        e.keyCode !== 13 &&
        (e.keyCode !== 32 || this.useInput === true || this.searchBuffer !== '') &&
        (e.keyCode !== 9 || tabShouldSelect === false)
      ) { return }

      e.keyCode !== 9 && stopAndPrevent(e)

      if (this.optionIndex > -1 && this.optionIndex < optionsLength) {
        this.toggleOption(this.options[this.optionIndex])
        return
      }

      if (newValueModeValid === true) {
        const done = (val, mode) => {
          if (mode) {
            if (validateNewValueMode(mode) !== true) {
              return
            }
          }
          else {
            mode = this.newValueMode
          }

          if (val === void 0 || val === null) {
            return
          }

          this.updateInputValue('', this.multiple !== true, true)

          this[mode === 'toggle' ? 'toggleOption' : 'add'](
            val,
            mode === 'add-unique'
          )

          if (this.multiple !== true) {
            this.$refs.target !== void 0 && this.$refs.target.focus()
            this.hidePopup()
          }
        }

        if (this.qListeners['new-value'] !== void 0) {
          this.$emit('new-value', this.inputValue, done)
        }
        else {
          done(this.inputValue)
        }

        if (this.multiple !== true) {
          return
        }
      }

      if (this.menu === true) {
        this.__closeMenu()
      }
      else if (this.innerLoading !== true) {
        this.showPopup()
      }
    },

    __getVirtualScrollEl () {
      return this.hasDialog === true
        ? this.$refs.menuContent
        : (
          this.$refs.menu !== void 0 && this.$refs.menu.__portal !== void 0
            ? this.$refs.menu.__portal.$el
            : void 0
        )
    },

    __getVirtualScrollTarget () {
      return this.__getVirtualScrollEl()
    },

    __getSelection (h) {
      if (this.hideSelected === true) {
        return []
      }

      if (this.$scopedSlots['selected-item'] !== void 0) {
        return this.selectedScope.map(scope => this.$scopedSlots['selected-item'](scope)).slice()
      }

      if (this.$scopedSlots.selected !== void 0) {
        return [].concat(this.$scopedSlots.selected())
      }

      if (this.useChips === true) {
        return this.selectedScope.map((scope, i) => h(QChip, {
          key: 'rem#' + i,
          props: {
            removable: this.editable === true && this.isOptionDisabled(scope.opt) !== true,
            dense: true,
            textColor: this.color,
            tabindex: this.computedTabindex
          },
          on: cache(this, 'rem#' + i, {
            remove () { scope.removeAtIndex(i) }
          })
        }, [
          h('span', {
            staticClass: 'ellipsis',
            domProps: {
              [scope.sanitize === true ? 'textContent' : 'innerHTML']: this.getOptionLabel(scope.opt)
            }
          })
        ]))
      }

      return [
        h('span', {
          domProps: {
            [this.displayAsText ? 'textContent' : 'innerHTML']: this.ariaCurrentValue
          }
        })
      ]
    },

    __getControl (h, fromDialog) {
      const child = this.__getSelection(h)
      const isTarget = fromDialog === true || this.dialog !== true || this.hasDialog !== true

      if (this.useInput === true) {
        child.push(this.__getInput(h, fromDialog, isTarget))
      }
      // there can be only one (when dialog is opened the control in dialog should be target)
      else if (this.editable === true) {
        const attrs = isTarget === true ? this.comboboxAttrs : void 0

        child.push(
          h('input', {
            ref: isTarget === true ? 'target' : void 0,
            key: 'd_t',
            staticClass: 'q-select__focus-target',
            attrs: {
              id: isTarget === true ? this.targetUid : void 0,
              readonly: true,
              'data-autofocus': (fromDialog === true ? isTarget === true : this.autofocus) || void 0,
              ...attrs
            },
            on: cache(this, 'f-tget', {
              keydown: this.__onTargetKeydown,
              keyup: this.__onTargetKeyup,
              keypress: this.__onTargetKeypress
            })
          })
        )

        if (isTarget === true && typeof this.autocomplete === 'string' && this.autocomplete.length > 0) {
          child.push(
            h('input', {
              key: 'autoinp',
              staticClass: 'q-select__autocomplete-input',
              domProps: { value: this.ariaCurrentValue },
              attrs: { autocomplete: this.autocomplete, tabindex: -1 },
              on: cache(this, 'autoinp', {
                keyup: this.__onTargetAutocomplete
              })
            })
          )
        }
      }

      if (this.nameProp !== void 0 && this.disable !== true && this.innerOptionsValue.length > 0) {
        const opts = this.innerOptionsValue.map(value => h('option', {
          attrs: { value, selected: true }
        }))

        child.push(
          h('select', {
            staticClass: 'hidden',
            attrs: {
              name: this.nameProp,
              multiple: this.multiple
            }
          }, opts)
        )
      }

      const attrs = this.useInput === true || isTarget !== true ? void 0 : this.qAttrs

      return h('div', { staticClass: 'q-field__native q-row items-center', attrs }, child)
    },

    __getOptions (h) {
      if (this.menu !== true) {
        return void 0
      }

      if (this.noOptions === true) {
        return this.$scopedSlots['no-option'] !== void 0
          ? this.$scopedSlots['no-option']({ inputValue: this.inputValue })
          : void 0
      }

      if (
        this.$scopedSlots.option !== void 0 &&
        this.__optionScopeCache.optionSlot !== this.$scopedSlots.option
      ) {
        this.__optionScopeCache.optionSlot = this.$scopedSlots.option
        this.__optionScopeCache.optionEls = []
      }

      const fn = this.$scopedSlots.option !== void 0
        ? this.$scopedSlots.option
        : scope => h(QItem, {
          key: scope.index,
          props: scope.itemProps,
          attrs: scope.itemAttrs,
          on: scope.itemEvents
        }, [
          h(QItemSection, [
            h(QItemLabel, {
              domProps: {
                [scope.sanitize === true ? 'textContent' : 'innerHTML']: this.getOptionLabel(scope.opt)
              }
            })
          ])
        ])

      const { optionEls } = this.__optionScopeCache

      let options = this.__padVirtualScroll(h, 'div', this.optionScope.map((scope, i) => {
        if (optionEls[i] === void 0) {
          optionEls[i] = fn(scope)
        }

        return optionEls[i]
      }))

      if (this.$scopedSlots['before-options'] !== void 0) {
        options = this.$scopedSlots['before-options']().concat(options)
      }

      return mergeSlot(options, this, 'after-options')
    },

    __getInnerAppend (h) {
      return this.loading !== true && this.innerLoadingIndicator !== true && this.hideDropdownIcon !== true
        ? [
          h(QIcon, {
            staticClass: 'q-select__dropdown-icon' + (this.menu === true ? ' rotate-180' : ''),
            props: { name: this.dropdownArrowIcon }
          })
        ]
        : null
    },

    __getInput (h, fromDialog, isTarget) {
      const attrs = isTarget === true ? { ...this.comboboxAttrs, ...this.qAttrs } : void 0

      const options = {
        ref: isTarget === true ? 'target' : void 0,
        key: 'i_t',
        staticClass: 'q-field__input q-placeholder q-col',
        style: this.inputStyle,
        class: this.computedInputClass,
        domProps: { value: this.inputValue !== void 0 ? this.inputValue : '' },
        attrs: {
          // required for Android in order to show ENTER key when in form
          type: 'search',
          ...attrs,
          id: isTarget === true ? this.targetUid : void 0,
          maxlength: this.maxlength, // this is converted to prop by QField
          autocomplete: this.autocomplete,
          'data-autofocus': (fromDialog === true ? isTarget === true : this.autofocus) || void 0,
          disabled: this.disable === true,
          readonly: this.readonly === true
        },
        on: this.inputControlEvents
      }

      if (fromDialog !== true && this.hasDialog === true) {
        options.staticClass += ' no-pointer-events'
      }

      return h('input', options)
    },

    __onChange (e) {
      this.__onComposition(e)
    },

    __onInput (e) {
      clearTimeout(this.inputTimer)

      if (e && e.target && e.target.qComposing === true) {
        return
      }

      this.__setInputValue(e.target.value || '')
      // mark it here as user input so that if updateInputValue is called
      // before filter is called the indicator is reset
      this.userInputValue = true
      this.defaultInputValue = this.inputValue

      if (
        this.focused !== true &&
        (this.hasDialog !== true || this.dialogFieldFocused === true)
      ) {
        this.__focus()
      }

      if (this.qListeners.filter !== void 0) {
        this.inputTimer = setTimeout(() => {
          this.filter(this.inputValue)
        }, this.inputDebounce)
      }
    },

    __setInputValue (inputValue) {
      if (this.inputValue !== inputValue) {
        this.inputValue = inputValue
        this.$emit('input-value', inputValue)
      }
    },

    updateInputValue (val, noFiltering, internal) {
      this.userInputValue = internal !== true

      if (this.useInput === true) {
        this.__setInputValue(val)

        if (noFiltering === true || internal !== true) {
          this.defaultInputValue = val
        }

        noFiltering !== true && this.filter(val)
      }
    },

    filter (val, keepClosed, afterUpdateFn) {
      if (this.qListeners.filter === void 0 || (keepClosed !== true && this.focused !== true)) {
        return
      }

      if (this.innerLoading === true) {
        this.$emit('filter-abort')
      }
      else {
        this.innerLoading = true
        this.innerLoadingIndicator = true
      }

      if (
        val !== '' &&
        this.multiple !== true &&
        this.innerValue.length > 0 &&
        this.userInputValue !== true &&
        val === this.getOptionLabel(this.innerValue[0])
      ) {
        val = ''
      }

      const filterId = setTimeout(() => {
        this.menu === true && (this.menu = false)
      }, 10)
      clearTimeout(this.filterId)
      this.filterId = filterId

      this.$emit(
        'filter',
        val,
        (fn, afterFn) => {
          if ((keepClosed === true || this.focused === true) && this.filterId === filterId) {
            clearTimeout(this.filterId)

            typeof fn === 'function' && fn()

            // hide indicator to allow arrow to animate
            this.innerLoadingIndicator = false

            this.$nextTick(() => {
              this.innerLoading = false

              if (this.editable === true) {
                if (keepClosed === true) {
                  this.menu === true && this.hidePopup()
                }
                else if (this.menu === true) {
                  this.__updateMenu(true)
                }
                else {
                  this.menu = true
                  this.hasDialog === true && (this.dialog = true)
                }
              }

              typeof afterFn === 'function' && this.$nextTick(() => { afterFn(this) })
              typeof afterUpdateFn === 'function' && this.$nextTick(() => { afterUpdateFn(this) })
            })
          }
        },
        () => {
          if (this.focused === true && this.filterId === filterId) {
            clearTimeout(this.filterId)
            this.innerLoading = false
            this.innerLoadingIndicator = false
          }
          this.menu === true && (this.menu = false)
        }
      )
    },

    __getControlEvents () {
      const focusout = e => {
        this.__onControlFocusout(e, () => {
          this.__resetInputValue()
          this.__closeMenu()
        })
      }

      return {
        focusin: this.__onControlFocusin,
        focusout,
        'popup-show': this.__onControlPopupShow,
        'popup-hide': e => {
          e !== void 0 && stop(e)
          this.$emit('popup-hide', e)
          this.hasPopupOpen = false
          focusout(e)
        },
        click: e => {
          // label from QField will propagate click on the input (except IE)
          prevent(e)

          if (this.hasDialog !== true && this.menu === true) {
            this.__closeMenu()
            this.$refs.target !== void 0 && this.$refs.target.focus()
            return
          }

          this.showPopup(e)
        }
      }
    },

    __getControlChild (h) {
      if (
        this.editable !== false && (
          this.dialog === true || // dialog always has menu displayed, so need to render it
          this.noOptions !== true ||
          this.$scopedSlots['no-option'] !== void 0
        )
      ) {
        return this[`__get${this.hasDialog === true ? 'Dialog' : 'Menu'}`](h)
      }
    },

    __getMenu (h) {
      return h(QMenu, {
        key: 'menu',
        ref: 'menu',
        props: {
          value: this.menu,
          fit: this.menuShrink !== true,
          cover: this.optionsCover === true && this.noOptions !== true && this.useInput !== true,
          anchor: this.menuAnchor,
          self: this.menuSelf,
          offset: this.menuOffset,
          contentClass: this.menuContentClass,
          contentStyle: this.popupContentStyle,
          dark: this.isOptionsDark,
          noParentEvent: true,
          noRefocus: true,
          noFocus: true,
          square: this.squaredMenu,
          transitionShow: this.transitionShow,
          transitionHide: this.transitionHide,
          separateClosePopup: true
        },
        attrs: this.listboxAttrs,
        on: cache(this, 'menu', {
          '&scroll': this.__onVirtualScrollEvt,
          'before-hide': this.__closeMenu,
          show: this.__onMenuShow
        })
      }, this.__getOptions(h))
    },

    __onMenuShow () {
      this.__setVirtualScrollSize()
    },

    __onDialogFieldFocus (e) {
      stop(e)
      this.$refs.target !== void 0 && this.$refs.target.focus()
      this.dialogFieldFocused = true
      window.scrollTo(window.pageXOffset || window.scrollX || document.body.scrollLeft || 0, 0)
    },

    __onDialogFieldBlur (e) {
      stop(e)
      this.$nextTick(() => {
        this.dialogFieldFocused = false
      })
    },

    __getDialog (h) {
      const content = [
        h(QField, {
          staticClass: `q-col-auto ${this.fieldClass}`,
          props: {
            ...this.$props,
            for: this.targetUid,
            dark: this.isOptionsDark,
            square: true,
            filled: true,
            itemAligned: false,
            loading: this.innerLoadingIndicator,
            stackLabel: this.inputValue.length > 0
          },
          on: {
            ...this.qListeners,
            focus: this.__onDialogFieldFocus,
            blur: this.__onDialogFieldBlur
          },
          scopedSlots: {
            ...this.$scopedSlots,
            rawControl: () => this.__getControl(h, true),
            before: void 0,
            after: void 0
          }
        })
      ]

      this.menu === true && content.push(
        h('div', {
          key: 'virtMenu',
          ref: 'menuContent',
          staticClass: 'q-col scroll',
          class: this.menuContentClass,
          style: this.popupContentStyle,
          attrs: this.listboxAttrs,
          on: cache(this, 'virtMenu', {
            click: prevent,
            '&scroll': this.__onVirtualScrollEvt
          })
        }, this.__getOptions(h))
      )

      return h(QDialog, {
        key: 'dialog',
        ref: 'dialog',
        props: {
          value: this.dialog,
          dark: this.isOptionsDark,
          position: this.useInput === true ? 'top' : void 0,
          transitionShow: this.transitionShowComputed,
          transitionHide: this.transitionHide
        },
        on: cache(this, 'dialog', {
          'before-hide': this.__onDialogBeforeHide,
          hide: this.__onDialogHide,
          show: this.__onDialogShow
        })
      }, [
        h('div', {
          staticClass: 'q-select__dialog' +
            (this.isOptionsDark === true ? ' q-select__dialog--dark q-dark' : '') +
            (this.dialogFieldFocused === true ? ' q-select__dialog--focused' : '')
        }, content)
      ])
    },

    __onDialogBeforeHide () {
      if (this.useInput !== true || this.$q.platform.is.desktop === true) {
        this.$refs.dialog.__refocusTarget = this.$el.querySelector('.q-field__native > [tabindex]:last-child')
      }
      this.focused = false
      this.dialogFieldFocused = false
    },

    __onDialogHide (e) {
      if (this.$q.platform.is.desktop !== true) {
        document.activeElement.blur()
      }
      this.hidePopup()
      this.focused === false && this.$emit('blur', e)
      this.__resetInputValue()
    },

    __onDialogShow () {
      const el = document.activeElement
      // IE can have null document.activeElement
      if (
        (el === null || el.id !== this.targetUid) &&
        this.$refs.target !== el &&
        this.$refs.target !== void 0
      ) {
        this.$refs.target.focus()
      }

      this.__setVirtualScrollSize()
    },

    __closeMenu () {
      if (this.__optionScopeCache !== void 0) {
        this.__optionScopeCache.optionEls = []
      }

      if (this.dialog === true) {
        return
      }

      this.optionIndex = -1

      if (this.menu === true) {
        this.menu = false
      }

      if (this.focused === false) {
        clearTimeout(this.filterId)
        this.filterId = void 0

        if (this.innerLoading === true) {
          this.$emit('filter-abort')
          this.innerLoading = false
          this.innerLoadingIndicator = false
        }
      }
    },

    showPopup (e) {
      if (this.editable !== true) {
        return
      }

      if (this.hasDialog === true) {
        this.__onControlFocusin(e)
        this.dialog = true
        this.$nextTick(() => {
          this.__focus()
        })
      }
      else {
        this.__focus()
      }

      if (this.qListeners.filter !== void 0) {
        this.filter(this.inputValue)
      }
      else if (this.noOptions !== true || this.$scopedSlots['no-option'] !== void 0) {
        this.menu = true
      }
    },

    hidePopup () {
      this.dialog = false
      this.__closeMenu()
    },

    __resetInputValue () {
      this.useInput === true && this.updateInputValue(
        this.multiple !== true && this.fillInput === true && this.innerValue.length > 0
          ? this.getOptionLabel(this.innerValue[0]) || ''
          : '',
        true,
        true
      )
    },

    __updateMenu (show) {
      let optionIndex = -1

      if (show === true) {
        if (this.innerValue.length > 0) {
          const val = this.getOptionValue(this.innerValue[0])
          optionIndex = this.options.findIndex(v => isDeepEqual(this.getOptionValue(v), val))
        }

        this.__resetVirtualScroll(optionIndex)
      }

      this.setOptionIndex(optionIndex)
    },

    __onPreRender () {
      this.hasDialog = this.$q.platform.is.mobile !== true && this.behavior !== 'dialog'
        ? false
        : this.behavior !== 'menu' && (
          this.useInput === true
            ? this.$scopedSlots['no-option'] !== void 0 || this.qListeners.filter !== void 0 || this.noOptions === false
            : true
        )

      this.transitionShowComputed = this.hasDialog === true && this.useInput === true && this.$q.platform.is.ios === true
        ? 'fade'
        : this.transitionShow
    },

    __onPostRender () {
      if (this.dialog === false && this.$refs.menu !== void 0) {
        this.$refs.menu.updatePosition()
      }
    },

    updateMenuPosition () {
      this.__onPostRender()
    }
  },

  beforeMount () {
    this.__optionScopeCache = {
      optionSlot: this.$scopedSlots.option,
      options: [],
      optionEls: []
    }
  },

  beforeDestroy () {
    this.__optionScopeCache = void 0
    clearTimeout(this.inputTimer)
  }
})
