<template>
  <span
    :class="themeClasses"
    class="mm-select">
    <ui-selector
      ref="selector"
      :search-value.sync="localSearchValue"
      :visible.sync="visible"
      v-bind="selectorProps">
      <template slot="selection">
        <slot name="selection"></slot>
      </template>
      <template slot="suffix-icon">
        <slot name="suffix-icon"></slot>
      </template>
    </ui-selector>

    <ui-popper
      ref="popper"
      :visible.sync="visible"
      :transition="transition"
      :track-ref-width="trackRefWidth"
      :popper-width="popperWidth"
      :popper-class="popperClass"
      :popper-options="popperOptions"
      :placement="placement"
      :class="{
        'mm-select-dropdown--multiple': multiple,
      }"
      class="mm-select-dropdown"
      target="selector"
      @after-enter="$emit('after-enter')"
      @before-leave="onPopperHideChange">
      <template v-if="!localLoading">
        <slot name="prefix"></slot>
        <ul
          v-if="realOptions.length"
          class="mm-select-dropdown-menu"
          tabindex="0"
          @focusin="dropdownFocusHandler">
          <template v-for="(item, groupIndex) of realOptions">
            <ui-option-group
              v-if="item.options"
              :key="getKey(item, groupIndex)"
              :label="getLabel(item)">
              <ui-option
                v-for="(option, index) of item.options"
                :key="getKey(option, index)"
                :label="getLabel(option)"
                :value="getValue(option)"
                :disabled="option.disabled">
                <slot
                  v-bind="option"
                  name="option"></slot>
              </ui-option>
            </ui-option-group>
            <ui-option
              v-else
              :key="getKey(item, groupIndex)"
              :label="getLabel(item)"
              :value="getValue(item)"
              :disabled="item.disabled">
              <slot
                v-bind="item"
                name="option"></slot>
            </ui-option>
          </template>
        </ul>
        <div
          v-else
          class="mm-empty-no-data">
          <ui-empty type="small"></ui-empty>
        </div>
        <slot name="suffix"></slot>
      </template>
      <template v-else>
        <div class="mm-select-dropdown-loading">
          <ui-loading size="30px"></ui-loading>
        </div>
      </template>
    </ui-popper>
  </span>
</template>

<script>
import isFunction from "lodash/isFunction"
import isObject from "lodash/isObject"
import get from "lodash/get"
import isEqual from "lodash/isEqual"
import isNil from "lodash/isNil"
import { toArray } from "../../utils/helper"
import { scrollIntoView } from "../../utils/scroll"

import locale from "../../mixins/locale"
import Option from "./option"
import OptionGroup from "./option-group"
import Selector from "../selector"
import Loading from "../loading"

import input from "../../mixins/input"
import themeSelect from "../../mixins/theme-select"
import KeyCode from "../../utils/key-code"

import Empty from "../empty"
import Popper from "../popper"
import config from "../../managers/config"

config.defaults({
  "Select.transition": "zoom-in-top",
  "Select.multiple": false,
  "Select.filterable": function() {
    return this.multiple
  },
  "Select.filterKey": "label",
  "Select.clearable": false,
  "Select.showArrow": true,
  "Select.showIcon": true,
  "Select.showMissValue": false,
  "Select.maxCount": undefined,
  "Select.trackRefWidth": function() {
    return !this.borderless
  },
  "Select.placement": "bottom-start",
  "Select.supportValueEmptyString": true,
  "Select.keymap": () => ({
    "label": "label",
    "value": "value",
    "disabled": "disabled",
  }),
  "Select.itemKey": function(item, index) {
    return index
  },
})

export default {
  name: "mm-select",
  components: {
    "ui-popper": Popper,
    "ui-option": Option,
    "ui-option-group": OptionGroup,
    "ui-loading": Loading,
    "ui-selector": Selector,
    "ui-empty": Empty,
  },
  mixins: [input, locale, themeSelect],
  inheritAttrs: false,
  model: {
    prop: "value",
    event: "input",
  },
  props: {
    options: {
      type: Array,
      default: () => [],
    },
    value: null,
    placeholder: {
      type: String,
    },
    popperWidth: {
      type: String,
    },
    transition: {
      type: String,
      default: () => config.get("Select.transition"),
    },
    multiple: {
      type: Boolean,
      default: () => config.get("Select.multiple"),
    },
    multiplePlaceholder: {
      type: String,
    },
    multipleLimit: {
      type: Number,
      default: 0,
    },
    multipleLazySync: {
      type: Boolean,
      default: false,
    },
    maxTagTextLength: {
      type: Number,
    },
    maxTagWidth: {
      type: String,
    },
    filterable: {
      type: Boolean,
      default() {
        return config.get("Select.filterable").call(this)
      },
    },
    filterMethod: {
      type: [Function, Boolean],
      default: true,
    },
    filterKey: {
      type: [String, Function],
      default: () => config.get("Select.filterKey"),
    },
    clearable: {
      type: Boolean,
      default: () => config.get("Select.clearable"),
    },
    showArrow: {
      type: Boolean,
      default: () => config.get("Select.showArrow"),
    },
    showIcon: {
      type: Boolean,
      default: () => config.get("Select.showIcon"),
    },
    showMissValue: {
      type: Boolean,
      default: () => config.get("Select.showMissValue"),
    },
    maxCount: {
      type: Number,
      default: () => config.get("Select.maxCount"),
    },
    onChange: {
      type: Function,
      default: () => {},
    },
    searchValue: {
      type: String,
      default: "",
    },
    trackRefWidth: {
      type: Boolean,
      default() {
        return config.get("Select.trackRefWidth").call(this)
      },
    },
    placement: {
      type: String,
      default: () => config.get("Select.placement"),
    },
    renderTag: {
      type: Function,
    },
    popperClass: {
      type: String,
    },
    loading: {
      type: Boolean,
      default: false,
    },

    supportValueEmptyString: {
      type: Boolean,
      default: () => config.get("Select.supportValueEmptyString"),
    },
    keymap: {
      type: Object,
      default() {
        return config.get("Select.keymap").call(this)
      },
    },
    onSearchKeydown: {
      type: Function,
    },
    onSearchChange: {
      type: Function,
    },
    onSearchInput: {
      type: Function,
    },
    onRemoveLastValue: {
      type: Function,
    },
    itemKey: {
      type: [String, Function],
      default: () => config.get("Select.itemKey"),
    },
    popperOptions: {
      type: [Object, Function],
      default: () => ({}),
    },
    reserveOptions: {
      type: Array,
      default: () => [],
    },
    hideOnBlur: {
      type: Boolean,
      default: true,
    },
  },
  provide() {
    return {
      "mm-select": this,
    }
  },
  data() {
    return {
      visible: false,
      localValue: [],
      localSearchValue: this.searchValue,
      localLoading: this.loading,
      missValue: [],
      hoverIndex: -1,
      hoverOption: [],
    }
  },
  computed: {
    labelKey() {
      return get(this.keymap, "label", "label")
    },
    valueKey() {
      return get(this.keymap, "value", "value")
    },

    valueObjectKey() {
      return get(this.keymap, "valueObjectKey", "value")
    },

    flattenOptions() {
      return this.options.reduce(
        (r, o) => r.concat(Array.isArray(o.options) ? o.options : o),
        []
      )
    },

    realOptions() {
      let options = this.options
      if (
        ((this.filterable && !this.filterMethod === false) || this.multiple) &&
        this.localSearchValue
      ) {
        const filterHandler = isFunction(this.filterMethod)
          ? this.filterMethod
          : this.filterMethodHandler
        options = options
          .reduce(
            (r, o) => r.concat(Array.isArray(o.options) ? o.options : o),
            []
          )
          .filter(item => filterHandler(item, this.localSearchValue))
      }

      return options
    },
    valueList() {
      return this.localValue.map(this.getValue)
    },

    selectorProps() {
      const value = this.localValue
        .filter(entity => this.showMissValue || !this.missValue.includes(this.getValue(entity)))
        .map(item => ({
          label: this.getLabel(item),
          value: this.getValue(item),
          raw: item,
        }))
      return {
        value: this.multiple ? value : get(value, "[0].label", undefined),
        placeholder: this.placeholder,
        multiplePlaceholder: this.multiplePlaceholder,
        disabled: this.realDisabled,
        filterable: this.filterable,
        clearable: this.clearable,
        showArrow: this.showArrow,
        showIcon: this.showIcon,
        maxCount: this.maxCount,
        maxTagTextLength: this.maxTagTextLength,
        maxTagWidth: this.maxTagWidth,
        multiple: this.multiple,
        multipleLimit: this.multipleLimit,
        hideOnBlur: this.hideOnBlur,

        onKeydown: this.onSelectorKeydown,
        onClear: this.onSelectorClear,
        onSelectionRemove: this.onMultipleSelectorRemove,

        onSearchKeydown: this.onSelectorSearchKeydown,
        onSearchInput: this.onSelectorSearchInput,
        onSearchChange: this.onSelectorSearchChange,

        renderTag: this.renderTag,
      }
    },

    valueEntities() {
      return [...this.reserveOptions, ...this.options].reduce((entities, item) => {
        if (item.options) {
          item.options.forEach(option => {
            entities[this.getValue(option)] = option
          })
        } else {
          entities[this.getValue(item)] = item
        }

        return entities
      }, {})
    },

    mayAdd() {
      return (
        this.multipleLimit <= 0 || this.localValue.length < this.multipleLimit
      )
    },

    realValue() {
      let value = this.localValue.map(this.getValue)

      if (!this.multiple) {
        value = value[0]
        if (!this.supportValueEmptyString && value === undefined) {
          value = ""
        }
      }

      return value
    },
    shouldLazySync() {
      return this.multipleLazySync && this.visible
    },
  },
  watch: {
    value: {
      immediate: true,
      handler(val) {
        if (!isEqual(val, this.realValue)) {
          this.localValue = toArray(val, this.supportValueEmptyString).map(
            this.getEntityByValue
          )
        }
      },
    },

    localValue() {
      if (!isEqual(this.realValue, this.value) && !this.shouldLazySync) {
        this.$emit("input", this.copyRealValue())
      }

      this.updatePopper()
    },

    loading(val) {
      this.localLoading = val
    },
    localLoading(val) {
      this.$emit("update:loading", val)
    },

    visible(val) {
      if (val && this.localValue) {
        const optionValues = this.flattenOptions.map(item => item.value)
        if (!this.multiple) {
          this.hoverIndex = optionValues.indexOf(this.realValue)
        } else {
          if (this.valueList.length > 0) {
            this.hoverIndex = Math.min.apply(null, this.valueList.map(item => optionValues.indexOf(item)))
          } else {
            this.hoverIndex = -1
          }
        }
        if (this.realOptions.length) {
          this.scrollToOptions()
        }
      }
    },
    searchValue(value) {
      this.localSearchValue = value
    },
    localSearchValue(value) {
      this.$emit("update:searchValue", value)
      this.$emit("search", value)
      if (value) {
        this.visible = true
        this.updatePopper()
      }
    },
    flattenOptions() {
      this.localValue = this.localValue.map(entity => {
        if (this.valueEntities[this.getValue(entity)]) {
          return this.getEntityByValue(this.getValue(entity))
        }
        return entity
      })
    },
    hoverIndex(val) {
      if (typeof val === "number" && val > -1) {
        this.hoverOption = this.flattenOptions[val] || {}
      }
    },
  },

  methods: {
    getKey(item, index) {
      if (isFunction(this.itemKey)) {
        return this.itemKey(item, index)
      } else {
        return isNil(item[this.itemKey]) ? index : item[this.itemKey]
      }
    },
    getEntityByValue(v) {
      const entity = this.valueEntities[v]
      const index = this.missValue.indexOf(v)

      if (~index) {
        this.missValue.splice(index, 1)
      }

      if (!entity) {
        this.missValue.push(v)
      }

      return entity || { [this.labelKey]: v, [this.valueKey]: v }
    },
    updatePopper() {
      this.$nextTick(() => {
        if (this.$refs.popper) {
          this.$refs.popper.update()
        }
      })
    },
    filterMethodHandler(option, searchValue) {
      let propValue
      if (this.filterKey === "value") {
        propValue = this.getValue(option)
      } else if (this.filterKey === "label") {
        propValue = this.getLabel(option)
      } else {
        propValue = isFunction(this.filterKey)
          ? this.filterKey(option, searchValue)
          : option[this.filterKey]
      }
      const value = String(propValue).toUpperCase()
      return value.indexOf(String(searchValue).toUpperCase()) !== -1
    },
    getLabel(item) {
      return item[this.labelKey]
    },
    getValue(item) {
      const value = item[this.valueKey]
      return isObject(value) && this.valueObjectKey
        ? value[this.valueObjectKey]
        : value
    },
    handleSelect(value, event) {
      const item = this.getEntityByValue(value)
      if (this.multiple) {
        const optionIndex = this.getValueIndex(value)
        if (optionIndex > -1) {
          this.localValue.splice(optionIndex, 1)
        } else if (this.mayAdd) {
          this.localValue.push(item)
        }
      } else {
        this.localValue = [item]
        this.visible = false
      }
      this.localSearchValue = undefined
      if (this.shouldLazySync) {
        return
      }
      this.$emit("change", event, value, item)
      this.onChange(event, value, item)
    },
    getValueIndex(value) {
      return this.localValue.findIndex(item => this.getValue(item) === value)
    },

    scrollToOptions() {
      this.$nextTick(() => {
        const container = this.$refs.popper.$el.querySelector(".mm-select-dropdown-menu")
        scrollIntoView(container, container.querySelector(".mm-select-dropdown-menu-item--hover"))
      })
    },
    onMultipleSelectorRemove(event, removeValue) {
      if (this.realDisabled) {
        return
      }

      const optionIndex = this.getValueIndex(removeValue)

      if (optionIndex > -1) {
        const removeItem = this.localValue[optionIndex]

        this.localValue.splice(optionIndex, 1)
        if (this.shouldLazySync) {
          return
        }
        this.$emit("change", event, removeValue, removeItem)
        this.onChange(event, removeValue, removeItem)
      }
    },

    onSelectorSearchInput(event) {
      this.$emit("search-input", event)
      if (isFunction(this.onSearchInput)) {
        this.onSearchInput(event)
      }
    },

    onSelectorSearchChange(event) {
      this.$emit("search-change", event)
      if (isFunction(this.onSearchChange)) {
        this.onSearchChange(event)
      }
    },

    onSelectorSearchKeydown(event) {
      if (this.realDisabled) {
        return
      }

      this.$emit("search-keydown", event)

      if (isFunction(this.onSearchKeydown)) {
        this.onSearchKeydown(event)
      }

      const localValue = this.localValue
      if (
        this.multiple &&
        KeyCode.BACKSPACE === event.keyCode &&
        !event.target.value &&
        localValue.length
      ) {
        const lastValue = this.getValue(localValue[localValue.length - 1])

        this.$emit("remove-last-value", lastValue)
        const continueRemove = isFunction(this.onRemoveLastValue)
          ? this.onRemoveLastValue(lastValue)
          : true

        if (continueRemove !== false) {
          this.onMultipleSelectorRemove(event, lastValue, localValue.length)
        }
      }
    },
    navigateOptions(direction) {
      if (this.flattenOptions.length === 0) {
        return
      }
      if (!this.realDisabled) {
        if (direction === "next") {
          this.hoverIndex++
          if (this.hoverIndex === this.flattenOptions.length) {
            this.hoverIndex = 0
          }
        } else if (direction === "prev") {
          this.hoverIndex--
          if (this.hoverIndex < 0) {
            this.hoverIndex = this.flattenOptions.length - 1
          }
        }
        const option = this.flattenOptions[this.hoverIndex]
        if (option.disabled === true) {
          this.navigateOptions(direction)
        }
        this.scrollToOptions()
      }
    },
    onSelectorKeydown(event) {
      if (this.realDisabled) {
        return
      }
      const { keyCode } = event
      if (!this.visible) {
        if ([KeyCode.ENTER, KeyCode.DOWN].indexOf(keyCode) !== -1) {
          this.visible = true
        }
      } else if (KeyCode.ESC === keyCode) {
        this.visible = false
      } else if (
        [KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT, KeyCode.ENTER].indexOf(
          keyCode
        ) !== -1
      ) {
        if (this.visible) {
          event.preventDefault()
          // Esc
          if (keyCode === KeyCode.ESC) {
            this.visible = false
          }
          // prev
          if (keyCode === KeyCode.UP) {
            this.navigateOptions("prev")
          }
          // next
          if (keyCode === KeyCode.DOWN) {
            this.navigateOptions("next")
          }
          if (keyCode === KeyCode.ENTER) {
            this.handleSelect(get(this.hoverOption, this.valueKey), event)
          }
        }
      }
    },
    onSelectorClear(event) {
      if (this.realDisabled) {
        return
      }
      this.localValue = []
      if (this.shouldLazySync) {
        return
      }
      this.$emit("change", event)
      this.onChange(event)
    },
    onPopperHideChange() {
      if (this.multipleLazySync && !isEqual(this.realValue, this.value)) {
        this.$emit("input", this.copyRealValue())
        this.$emit("change")
        this.onChange()
      }
      this.$emit('after-leave')
      // this.localSearchValue = ""
    },
    dropdownFocusHandler() {
      this.$refs.selector.focus()
    },
    focus() {
      this.$refs.selector.focus()
    },
    blur() {
      this.$refs.selector.blur()
    },
    copyRealValue() {
      return Array.isArray(this.realValue) ? [...this.realValue] : this.realValue
    },
  },
}
</script>
