<template>
  <div
    :class="themeClasses"
    class="mm-tree-select">
    <selector
      ref="selector"
      :visible.sync="localVisible"
      :search-value.sync="localSearchValue"
      v-bind="selectorProps">
      <template slot="selection">
        <slot name="selection"></slot>
      </template>
    </selector>

    <popper
      ref="popper"
      :visible.sync="localVisible"
      :track-ref-width="trackRefWidth"
      :update-on-resize="updateOnPopperResize"
      :popper-class="popperClass"
      :popper-style="popperStyle"
      :popper-width="popperWidth"
      :popper-options="popperOptions"
      :placement="placement"
      class="mm-tree-select-popper"
      target="selector">
      <div
        v-if="!treeData || !treeData.length"
        class="mm-empty-no-data">
        <ui-empty type="small"></ui-empty>
      </div>

      <template v-else>
        <slot name="prefix"></slot>
        <tree
          ref="tree"
          :default-expand-all="treeDefaultExpandAll"
          :default-expanded-keys="treeDefaultExpandedKeys"
          :data="treeData"
          :keymap="treeKeymap"
          :expand-on-click-node="expandOnClickNode"
          :check-on-click-node="true"
          :show-checkbox="treeCheckable"
          :default-checked-keys="checkedKeys"
          :node-key="realKeymap.value"
          :check-strictly="checkStrictly"
          :filter-method="filterNode"
          :render-content="renderContent"
          :multiple="isMultiple"
          tabindex="0"
          @focusin.native="dropdownFocusHandler"
          @check="handleCheckChange">
        </tree>
        <slot name="suffix"></slot>
      </template>
    </popper>
  </div>
</template>

<script>
import differenceWith from "lodash/differenceWith"
import isEqual from "lodash/isEqual"
import isObject from "lodash/isObject"
import isFunction from "lodash/isFunction"
import unionWith from "lodash/unionWith"
import omit from "lodash/omit"
import cloneDeep from "lodash/cloneDeep"

import { toArray } from "../../utils/helper"
import input from "../../mixins/input"
import visible from "../../mixins/visible"
import themeSelect from "../../mixins/theme-select"

import Selector from "../selector"
import Tree from "../tree"
import Popper from "../popper"
import Empty from "../empty"

import locale from "../../mixins/locale"

import {
  isPosRelated,
  conductCheck,
  flatToHierarchy,
  convertTreeToEntities,
} from "./util"

import KeyCode from "../../utils/key-code"

import config from "../../managers/config"

export const SHOW_ALL = "SHOW_ALL"
export const SHOW_PARENT = "SHOW_PARENT"
export const SHOW_CHILD = "SHOW_CHILD"

config.defaults({
  "TreeSelect.treeCheckable": false,
  "TreeSelect.multiple": function() {
    return this.treeCheckable
  },
  "TreeSelect.showArrow": true,
  "TreeSelect.clearable": false,
  "TreeSelect.showCheckedStrategy": SHOW_CHILD,
  "TreeSelect.keymap": () => ({
    value: "value",
    children: "children",
    label: "label",
    icon: "icon",
    disabled: "disabled",
  }),
  "TreeSelect.trackRefWidth": true,
  "TreeSelect.showAllLevels": false,
  "TreeSelect.separator": " > ",
  "TreeSelect.placement": "bottom-start",
})

export default {
  name: "mm-tree-select",
  components: {
    Selector,
    Tree,
    Popper,
    "ui-empty": Empty,
  },
  mixins: [input, visible, themeSelect, locale],
  model: {
    prop: "value",
    event: "input",
  },
  props: {
    treeCheckable: {
      type: Boolean,
      default: () => config.get("TreeSelect.treeCheckable"),
    },

    multiple: {
      type: Boolean,
      default() {
        return config.get("TreeSelect.multiple").call(this)
      },
    },
    showArrow: {
      type: Boolean,
      default: () => config.get("TreeSelect.showArrow"),
    },

    value: [String, Number, Array],

    clearable: {
      type: Boolean,
      default: () => config.get("TreeSelect.clearable"),
    },

    filterable: {
      type: Boolean,
      default() {
        return this.multiple
      },
    },

    placeholder: {
      type: String,
    },

    multiplePlaceholder: {
      type: String,
    },
    searchValue: String,
    disabled: Boolean,

    labelInValue: {
      type: Boolean,
      default: false,
    },

    maxCount: {
      type: Number,
    },

    renderContent: {
      type: Function,
    },

    maxTagTextLength: {
      type: Number,
    },
    maxTagWidth: {
      type: String,
    },

    showCheckedStrategy: {
      type: String,
      default: () => config.get("TreeSelect.showCheckedStrategy"),
      validate(val) {
        return [SHOW_ALL, SHOW_PARENT, SHOW_CHILD].includes(val)
      },
    },

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

    treeCheckStrictly: {
      type: Boolean,
      default: false,
    },

    treeDefaultExpandAll: Boolean,
    treeDefaultExpandedKeys: Array,
    treeExpandOnClickNode: {
      type: Boolean,
      default: false,
    },

    keymap: {
      type: Object,
      default() {
        return config.get("TreeSelect.keymap").call(this)
      },
    },
    filterSelectorValue: {
      type: Function,
      default: value => value,
    },

    renderTag: {
      type: Function,
    },

    supportValueEmptyString: {
      type: Boolean,
      default: true,
    },

    trackRefWidth: {
      type: Boolean,
      default: () => config.get("TreeSelect.trackRefWidth"),
    },
    updateOnPopperResize: {
      type: Boolean,
      default: false,
    },
    onChange: {
      type: Function,
      default: () => {},
    },
    onClear: {
      type: Function,
      default: () => {},
    },
    popperClass: {
      type: [String, Object, Array],
    },
    popperWidth: {
      type: String,
    },
    popperStyle: {
      type: Object,
      default: () => ({}),
    },
    popperOptions: {
      type: [Object, Function],
      default: () => ({}),
    },
    onSearchKeydown: {
      type: Function,
    },
    onSearchChange: {
      type: Function,
    },
    onSearchInput: {
      type: Function,
    },
    onRemoveLastValue: {
      type: Function,
    },
    showAllLevels: {
      type: Boolean,
      default: () => config.get("TreeSelect.showAllLevels"),
    },
    separator: {
      type: String,
      default: () => config.get("TreeSelect.separator"),
    },
    placement: {
      type: String,
      default: () => config.get('TreeSelect.placement')
    },
    hideOnBlur: {
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      valueList: [],
      missValueList: [],
      selectorValueList: [],
      localSearchValue: this.searchValue,
      localValue: [],
      realValue: [],
      checkedKeys: [],
    }
  },
  computed: {
    realKeymap() {
      return {
        value: "value",
        children: "children",
        label: "label",
        icon: "icon",
        disabled: "disabled",
        ...this.keymap,
      }
    },
    treeKeymap() {
      return omit(this.realKeymap, ["value"])
    },
    isMultiple() {
      return this.multiple || this.treeCheckable
    },

    checkStrictly() {
      return (
        !this.isMultiple ||
        (this.showCheckedStrategy === SHOW_PARENT && this.treeCheckStrictly)
      )
    },

    expandOnClickNode() {
      return !this.isMultiple && this.treeExpandOnClickNode
    },

    entities() {
      return convertTreeToEntities(this.treeData, {
        keymap: this.realKeymap,
      })
    },
    isLabelInValue() {
      return (
        (this.treeCheckable && this.treeCheckStrictly) || this.labelInValue
      )
    },

    connectValueList() {
      return [...this.missValueList, ...this.selectorValueList]
    },

    selectorProps() {
      const value = this.filterSelectorValue(
        this.connectValueList,
        this.entities
      ).map(item => ({
        label: this.getLabel(item),
        value: this.getValue(item),
        raw: item,
      }))
      return {
        value: this.isMultiple ? this.getmultipleLabel(value) : this.getSingleLabel(value[0]),
        searchValue: this.localSearchValue,
        placeholder: this.placeholder,
        multiplePlaceholder: this.multiplePlaceholder,
        disabled: this.realDisabled,
        maxCount: this.maxCount,
        maxTagTextLength: this.maxTagTextLength,
        maxTagWidth: this.maxTagWidth,
        filterable: this.filterable,
        showArrow: this.showArrow,
        clearable: this.clearable,
        multiple: this.isMultiple,
        multipleLimit: this.multipleLimit,
        hideOnBlur: this.hideOnBlur,

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

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

        renderTag: this.renderTag,
      }
    },
  },
  watch: {
    realValue(value) {
      if (!isEqual(this.value, value)) {
        this.$emit("input", value)
        this.updatePopper()
      }
    },
    value: {
      immediate: true,
      handler(value) {
        if (!isEqual(value, this.realValue)) {
          this.localValue = value
        }
      },
    },
    valueList(value) {
      this.selectorValueList = this.formatSelectorValue(value)
    },

    showCheckedStrategy() {
      this.selectorValueList = this.formatSelectorValue(this.valueList)
    },

    searchValue(value) {
      this.localSearchValue = value
    },
    localSearchValue(value) {
      this.$emit("update:searchValue", value)
      if (value) {
        this.localVisible = true
        if (this.$refs.tree) {
          this.$refs.tree.filter(value)
        }
        this.updatePopper()
      }
    },

    connectValueList(value) {
      this.checkedKeys = value.map(item => item[this.keymap.value])

      const [returnValue, selectedNodes] = this.getChangedValue(value)
      this.realValue = cloneDeep(returnValue)

      this.$emit("change", returnValue, selectedNodes)
      this.onChange(returnValue, selectedNodes)
    },

    checkedKeys() {
      if (this.localVisible) {
        this.$nextTick(() => {
          if (this.$refs.tree) {
            this.$refs.tree.setCheckedKeys(this.checkedKeys)
          }
        })
      }
    },

    localVisible(value) {
      if (value) {
        this.$nextTick(() => {
          if (this.$refs.tree) {
            this.$refs.tree.setCheckedKeys(this.checkedKeys)
          }
        })
      }
    },

    treeData() {
      this.processData()
    },

    localValue: {
      immediate: true,
      handler() {
        this.processData()
      },
    },
  },
  methods: {
    getChangedValue(value) {
      let returnValue

      let connectValueList = value

      let selectedNodes
      const { keyEntities } = this.entities

      if (this.isLabelInValue) {
        returnValue = connectValueList
      } else {
        returnValue = connectValueList.map(this.getKey)
      }

      if (!this.isMultiple) {
        returnValue = returnValue[0]

        if (!this.supportValueEmptyString && returnValue === undefined) {
          returnValue = ""
        }
        selectedNodes = keyEntities[returnValue]
      } else {
        selectedNodes = returnValue.map(key => keyEntities[key])
      }
      return [returnValue, selectedNodes]
    },
    getSingleLabel(selectorItem) {
      if (selectorItem === undefined) {
        return undefined
      }

      const { label, value } = selectorItem

      let current = this.entities.keyEntities[value]
      if (this.showAllLevels && current) {
        const path = [current.node]
        while (current.parent) {
          path.push(current.parent.node)
          current = current.parent
        }
        return path.map(this.getLabel).reverse().join(this.separator)
      }

      return label
    },
    getmultipleLabel(list) {
      return this.showAllLevels ? list.map(item => {
        const label = this.getSingleLabel(item)
        return {
          ...item,
          label,
        }
      }) : list
    },
    processData() {
      let valueList = this.formatInternalValue(this.localValue)

      const missValueList = []
      const filteredValueList = []
      const keyList = []

      const { keyEntities } = this.entities

      valueList.forEach(wrapperValue => {
        const key = this.getKey(wrapperValue)
        const entity = keyEntities[key]

        if (entity) {
          keyList.push(key)
          filteredValueList.push(wrapperValue)
        } else {
          // 如果不匹配， 它就可能是要异步加载的， 我们保留数据
          missValueList.push(wrapperValue)
        }
      })

      if (this.treeCheckable && !this.treeCheckStrictly) {
        // 计算出checked的 keys
        const { checkedKeys } = conductCheck(keyList, true, keyEntities)

        // 把key转换成值
        valueList = checkedKeys.map(this.getWrapperValue)
      } else {
        valueList = filteredValueList
      }

      this.missValueList = missValueList
      this.valueList = valueList
    },
    updatePopper() {
      this.$nextTick(() => {
        if (this.$refs.popper) {
          this.$refs.popper.update()
        }
      })
    },

    getKey(node) {
      return node[this.realKeymap.value]
    },

    getWrapperValue(key) {
      const entity = this.entities.keyEntities[key]
      return {
        [this.realKeymap.label]: entity ? this.getLabel(entity.node) : key,
        [this.realKeymap.value]: entity ? this.getValue(entity.node) : key,
      }
    },
    filterNode(searchValue, data) {
      if (!searchValue) {
        return true
      }

      const value = String(data[this.realKeymap.label]).toUpperCase()
      return value.indexOf(String(searchValue).toUpperCase()) !== -1
    },

    formatInternalValue(value) {
      const valueList = toArray(value, this.supportValueEmptyString)
      return valueList.map(item => {
        if (this.isLabelInValue && isObject(item)) {
          return item
        } else {
          return this.getWrapperValue(item)
        }
      })
    },

    formatSelectorValue(valueList) {
      const {
        showCheckedStrategy,
        entities: { keyEntities },
      } = this

      if (this.treeCheckable && !this.treeCheckStrictly) {
        const hierarchyList = flatToHierarchy(
          valueList.map(item => keyEntities[this.getKey(item)])
        )

        if (showCheckedStrategy === SHOW_PARENT) {
          return hierarchyList.map(({ key }) => this.getWrapperValue(key))
        } else if (showCheckedStrategy === SHOW_CHILD) {
          const targetValueList = []

          const traverse = ({ key, children }) => {
            if (!children || children.length === 0) {
              targetValueList.push(this.getWrapperValue(key))
              return
            }

            children.forEach(entity => {
              traverse(entity)
            })
          }

          hierarchyList.forEach(entity => {
            traverse(entity)
          })

          return targetValueList
        }
      }

      return valueList
    },

    getValue(wrappedValue) {
      return wrappedValue[this.realKeymap.value]
    },

    getLabel(wrappedValue) {
      return wrappedValue[this.realKeymap.label]
    },

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

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

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

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

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

      const valueList = this.connectValueList
      if (
        this.isMultiple &&
        KeyCode.BACKSPACE === event.keyCode &&
        !event.target.value &&
        valueList.length
      ) {
        const lastValue = this.getKey(valueList[valueList.length - 1])

        this.$emit("remove-last-value", lastValue)

        const continueRemove = isFunction(this.onRemoveLastValue)
          ? this.onRemoveLastValue(lastValue)
          : true

        if (continueRemove !== false) {
          this.onMultipleSelectorRemove(event, lastValue)
        }
      }
    },

    getValueIndex(value) {
      return this.localValue.findIndex(item => item.value === value)
    },

    onMultipleSelectorRemove(event, removeKey) {
      if (this.realDisabled) {
        return
      }

      const { keyEntities } = this.entities
      const triggerEntity = keyEntities[removeKey]

      if (triggerEntity) {
        if (this.treeCheckable && !this.treeCheckStrictly) {
          this.valueList = this.valueList.filter(item => {
            const entity = keyEntities[this.getKey(item)]
            return !isPosRelated(entity.pos, triggerEntity.pos)
          })
        } else {
          this.valueList = this.valueList.filter(
            item => this.getKey(item) !== removeKey
          )
        }
      }

      this.missValueList = this.missValueList.filter(
        item => this.getKey(item) !== removeKey
      )

      this.$emit("remove", removeKey)
    },

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

      const { keyCode } = event
      if (!this.localVisible) {
        if ([KeyCode.ENTER, KeyCode.DOWN].indexOf(keyCode) !== -1) {
          this.localVisible = true
        }
      } else if (KeyCode.ESC === keyCode) {
        this.localVisible = false
      } else if (
        [KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].indexOf(
          keyCode
        ) !== -1
      ) {
        event.stopPropagation()
      }
    },

    onSelectorClear(event) {
      if (this.realDisabled) {
        return
      }
      this.localValue = []
      this.$emit("clear", event)
      this.onClear()

      const [returnValue, selectedNodes] = this.getChangedValue(this.localValue)
      this.$emit("change", returnValue, selectedNodes)
      this.onChange(returnValue, selectedNodes)
    },

    handleCheckChange(node, checkedSet) {
      let beforeCheckedValues = []
      let checkedValues = []

      if (!node.checked) {
        this.$emit("remove", this.getKey(node.data))
      }

      const getWrapperValueByNode = node => {
        return {
          [this.realKeymap.label]: this.getLabel(node),
          [this.realKeymap.value]: this.getValue(node),
        }
      }

      if (this.isLabelInValue) {
        beforeCheckedValues = checkedSet.beforeCheckedNodes.map(
          getWrapperValueByNode
        )
        checkedValues = checkedSet.checkedNodes.map(getWrapperValueByNode)
      } else {
        beforeCheckedValues = checkedSet.beforeCheckedNodes.map(this.getValue)
        checkedValues = checkedSet.checkedNodes.map(this.getValue)
      }

      if (this.isMultiple) {
        const originalCheckedValues = this.connectValueList.map(item =>
          this.isLabelInValue ? item : this.getValue(item)
        )

        // all: [1, 2, 3, 4, 5, 6]
        // current: [0, 1, 2]
        // before: [1, 2]
        // after: [2, 3]
        // final: [0, 2, 3]

        //
        // subtract = difference(before, after)
        // current - difference(before, after) + difference(after, before)

        this.localValue = unionWith(
          // 过滤掉取消选择
          differenceWith(
            originalCheckedValues,
            // 取消掉
            differenceWith(beforeCheckedValues, checkedValues, isEqual),
            isEqual
          ),
          // 过滤出来新添加
          differenceWith(checkedValues, beforeCheckedValues, isEqual),
          isEqual
        )
      } else {
        this.localValue = checkedValues
        this.localVisible = false
      }

      this.localSearchValue = ""
    },
    dropdownFocusHandler() {
      this.$refs.selector.focus()
    },
  },
}
</script>
