<template>
  <div
    :class="themeClasses"
    class="mm-cascader">
    <single-selector
      ref="selector"
      :search-value.sync="searchValue"
      v-bind="selectorProps"
      @click.native="clickHandler">
    </single-selector>

    <ui-popper
      ref="popper"
      :class="['mm-cascader-popper', popperClass]"
      :visible.sync="visible"
      :placement="placement"
      :track-ref-width="isOverlap"
      target="selector">
      <div
        tabindex="0"
        @focusin="dropdownFocusHandler">
        <div
          v-if="isOverlap"
          class="mm-cascader-menu-path">
          <div
            v-for="(p, index) of selectPath"
            :key="index"
            :title="p[labelKey]"
            class="mm-cascader-menu-path-item"
            @click="selectPathHandler(index)">
            {{ p[labelKey] }}
          </div>
        </div>
        <div
          v-if="!lists || !lists.length"
          class="mm-empty-no-data">
          <ui-empty type="small"></ui-empty>
        </div>
        <template
          v-for="(list, colIndex) of lists"
          v-else>
          <ul
            v-if="!(isOverlap && colIndex !== lists.length - 1)"
            :key="colIndex"
            :class="{
              'mm-cascader-menu--hidden': isOverlap && colIndex !== lists.length - 1
            }"
            class="mm-cascader-menu">
            <li
              v-for="(item, rowIndex) of list"
              :key="rowIndex"
              :class="{
                'mm-cascader-menu-item': true,
                'mm-cascader-menu-item--disabled': item[disabledKey],
                'mm-cascader-menu-item--selected': path.indexOf(item[valueKey]) > -1,
              }"
              :title="item[labelKey]"
              :style="optionStyle"
              @click="select(item, colIndex)"
              @mouseenter="beforeSelect(item, colIndex)">
              <span>{{ item[labelKey] }}</span>
              <ui-icon
                v-if="hasChildren(item)"
                class="mm-cascader-chevronright"
                name="chevronright"
                size="0.875rem"
                fill="#b0adab"></ui-icon>
            </li>
          </ul>
        </template>
      </div>
    </ui-popper>
  </div>
</template>

<script>
import { encode } from "../../utils/reg"
import isEqual from "lodash/isEqual"
import isUndefined from "lodash/isUndefined"
import isNumber from "lodash/isNumber"
import debounce from "lodash/debounce"
import input from "../../mixins/input"
import themeSelect from "../../mixins/theme-select"

import Popper from "../popper"
import Icon from "../icon"
import { scrollIntoView } from "../../utils/scroll"
import locale from "../../mixins/locale"
import { t } from "../../locale"
import SingleSelector from "../selector/single"
import Empty from "../empty"

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

config.defaults({
  "Cascader.separator": " / ",
  "Cascader.clearable": true,
  "Cascader.showAllLevels": true,
  "Cascader.filterable": false,
  "Cascader.debounce": () => ({
    wait: 300,
    options: {},
  }),
  "Cascader.changeOnSelect": false,
  "Cascader.hideOnChange": true,
  "Cascader.placement": "bottom-start",
  "Cascader.mode": "",
  "Cascader.showArrow": true,
  "Cascader.optionStyle": () => ({}),
})

export default {
  name: "mm-cascader",
  components: {
    "ui-popper": Popper,
    "ui-icon": Icon,
    SingleSelector,
    "ui-empty": Empty,
  },
  mixins: [input, locale, themeSelect],
  inheritAttrs: false,
  props: {
    options: {
      type: Array,
      required: true,
      default: () => [],
    },
    keymap: {
      type: Object,
      default() {
        return {}
      },
    },
    onChange: {
      type: Function,
      default: () => {},
    },
    onClear: {
      type: Function,
      default: () => {},
    },
    value: {
      type: Array,
      default: () => [],
    },
    separator: {
      type: String,
      default: () => config.get("Cascader.separator"),
    },
    popperClass: {
      type: [Array, String, Object],
    },
    placeholder: {
      type: String,
    },
    clearable: {
      type: Boolean,
      default: () => config.get("Cascader.clearable"),
    },
    showAllLevels: {
      type: Boolean,
      default: () => config.get("Cascader.showAllLevels"),
    },
    filterable: {
      type: Boolean,
      default: () => config.get("Cascader.filterable"),
    },
    filterMethod: {
      type: Function,
    },
    debounce: {
      type: [Object, Number],
      default: () => config.get("Cascader.debounce").call(this),
    },
    changeOnSelect: {
      type: Boolean,
      default: () => config.get("Cascader.changeOnSelect"),
    },
    hideOnChange: {
      type: Boolean,
      default: () => config.get("Cascader.hideOnChange"),
    },
    expandTrigger: {
      type: String,
      default() {
        return this.changeOnSelect ? "hover" : "click"
      },
      validator(val) {
        return ["click", "hover"].includes(val)
      },
    },
    placement: {
      type: String,
      default: () => config.get("Cascader.placement"),
    },
    mode: {
      type: String,
      default: () => config.get("Cascader.mode"),
      validator(val) {
        return ["", "overlap"].includes(val)
      },
    },
    levelLimit: {
      type: Number,
    },
    filterResultLimit: {
      type: Number,
    },
    pathAllLabel: {
      type: String,
      default() {
        return t("ui.cascader.pathAllLabel")
      },
    },
    showArrow: {
      type: Boolean,
      default: () => config.get("Cascader.showArrow"),
    },
    optionStyle: {
      type: Object,
      default: () => config.get("Cascader.optionStyle").call(this),
    },
  },
  data() {
    return {
      visible: false,
      suffixIcon: "switch",
      selected: [],
      inputValue: "",
      firstCol: [],
      path: [], // for display
      searchValue: "",
    }
  },
  computed: {
    selectorProps() {
      return {
        disabled: this.disabled,
        visible: this.visible,
        value: this.inputValue,
        filterable: this.filterable,
        showArrow: this.showArrow,

        clearable: this.clearable,

        onClear: this.clearHandler,
        placeholder: this.placeholder,
      }
    },
    pathEntities() {
      return this.path.map(this.getEntityByValue)
    },
    lists() {
      return [
        this.firstCol,
        ...this.pathEntities
          .map(option => option && option[this.childrenKey])
          .filter(children => !!children),
      ].filter(list => list.length)
    },
    labelKey() {
      return this.keymap.label || "label"
    },
    valueKey() {
      return this.keymap.value || "value"
    },
    childrenKey() {
      return this.keymap.children || "children"
    },
    disabledKey() {
      return this.keymap.disabled || "disabled"
    },
    selectedValue() {
      return this.selected.join(this.separator)
    },
    selectPath() {
      return [{ [this.labelKey]: this.pathAllLabel }].concat(this.pathEntities)
    },
    isOverlap() {
      return this.mode === "overlap"
    },
    localOptions() {
      return this.transformOptions(this.options)
    },
    valuesEntities() {
      const entities = {}
      const loop = options => {
        options.forEach(option => {
          entities[option[this.valueKey]] = option
          if (option[this.childrenKey]) {
            loop(option[this.childrenKey])
          }
        })
      }
      loop(this.localOptions)
      return entities
    },
    debouncedInputChange() {
      const { wait, options } = {
        ...(isNumber(debounce) ? { wait: this.debounce } : this.debounce),
        ...this.$options.props.debounce.default(),
      }

      return debounce(this.searchHandler, wait, options)
    },
  },
  watch: {
    localOptions: {
      immediate: true,
      handler(val) {
        this.flatOptions = Object.freeze(this.flattenOptions(val))
        this.firstCol = Object.freeze(this.localOptions)
        this.resetSelectedValue(this.value)

        if (this.visible && this.path.length) {
          this.resetSelectedList(val)
        }
      },
    },
    visible(val) {
      if (val && this.value.length) {
        this.$nextTick(() => {
          this.scrollToSelected()
        })
      }

      if (!val) {
        const pathValue = this.path.join(this.separator)

        if (pathValue !== this.selectedValue) {
          this.firstCol = Object.freeze(this.localOptions)
          this.path = [...this.selected]
          this.setInputValue()
        }
      }
    },
    value: {
      immediate: true,
      handler: "resetSelectedValue",
    },
    selected(val) {
      if (!isEqual(val, this.value)) {
        this.$emit("input", val)
        this.setInputValue()
      }
    },
    searchValue(value) {
      this.debouncedInputChange(value)
    },
  },
  beforeCreate() {
    this.flatOptions = []
  },
  methods: {
    selectPathHandler(index) {
      this.path = this.path.slice(0, index)
    },
    transformOptions(options, level = 0) {
      return options.map(option => {
        option[this.childrenKey] =
          option[this.childrenKey] &&
          (!this.levelLimit || (this.levelLimit && level < this.levelLimit - 1))
            ? this.transformOptions(option[this.childrenKey], level + 1)
            : null
        return option
      })
    },
    flattenOptions(options, ancestor = [], allLevel = false) {
      let flatOptions = []
      allLevel = allLevel || this.changeOnSelect

      options
        .filter(option => !option[this.disabledKey])
        .forEach(option => {
          const optionsStack = ancestor.concat(option)

          if (!option[this.childrenKey] || !option[this.childrenKey].length) {
            flatOptions.push(optionsStack)
          } else {
            if (allLevel) {
              flatOptions.push(optionsStack)
            }
            flatOptions = flatOptions.concat(
              this.flattenOptions(
                option[this.childrenKey],
                optionsStack,
                allLevel
              )
            )
          }
        })

      return flatOptions
    },
    hasChildren(option) {
      return option[this.childrenKey] && option[this.childrenKey].length > 0
    },
    clickHandler() {
      if (!this.readonly) {
        this.visible = !this.visible
      }
    },
    beforeSelect(option, level) {
      if (
        this.expandTrigger === "hover" &&
        option[this.childrenKey] &&
        !option[this.disabledKey]
      ) {
        if (isUndefined(this.path[level])) {
          this.path.push(option[this.valueKey])
        } else {
          this.path.splice(
            level,
            this.path.length - level,
            option[this.valueKey]
          )
        }
      }
    },
    select(option, level) {
      if (option[this.disabledKey]) {
        return
      }

      if (option.isFlatOption) {
        this.visible = false
        this.firstCol = Object.freeze(this.localOptions)
        const values = option.optionsStack.map(o => o[this.valueKey])
        this.path = [...values]
        this.selected = [...values]
        return
      }

      this.$emit("active-item-change", option)

      if (isUndefined(this.path[level])) {
        this.path.push(option[this.valueKey])
      } else {
        this.path.splice(
          level,
          this.path.length - level,
          option[this.valueKey]
        )
      }

      if (
        this.changeOnSelect ||
        (!option[this.childrenKey] || option[this.childrenKey].length === 0)
      ) {
        this.selected = [...this.path]
        this.$emit("change", this.selected, option)
        this.onChange(this.selected, option)
        if (this.hideOnChange) {
          this.visible = false
        }
      }
    },
    clearHandler(e) {
      if (this.clearable && this.path.length) {
        e.stopPropagation()
        this.path = []
        this.selected = []
        this.$emit("change")
        this.onChange()
        this.$emit("clear")
        this.onClear()
      }
    },
    searchHandler(val) {
      const keyword = val.trim()

      if (!keyword) {
        this.firstCol = Object.freeze(this.localOptions)
        this.path = [...this.selected]
        return
      }

      const { valueKey, labelKey, childrenKey } = this
      const reg = new RegExp(encode(keyword), "i")
      const filterHandler = this.filterMethod
        ? this.filterMethod.bind(null, keyword)
        : optionsStack => {
          return optionsStack.some(option => reg.test(option[labelKey]))
        }
      let lists = []

      // use for to replace filter + slice
      for (let i = 0, count = 0; i < this.flatOptions.length; i++) {
        const optionsStack = this.flatOptions[i]
        const pass = filterHandler(optionsStack, i)
        if (pass) {
          count++
          lists.push(optionsStack)
        }
        if (this.filterResultLimit && count >= this.filterResultLimit) {
          break
        }
      }

      if (lists.length) {
        lists = lists.map(optionsStack => {
          return {
            optionsStack,
            isFlatOption: true,
            [labelKey]: optionsStack
              .map(option => option[labelKey])
              .join(" / "),
            [valueKey]: optionsStack.map(option => option[valueKey]),
            [childrenKey]: null,
          }
        })
      } else {
        lists = []
      }

      this.firstCol = Object.freeze(lists)
      this.path = []
    },
    resetSelectedList(options) {
      const { path, valueKey, separator } = this
      const flatOptions = this.flattenOptions(options, [], true)
      const text = path.map(option => option[valueKey]).join(separator)
      const selected = flatOptions.find(optionsStack => {
        return (
          optionsStack.map(option => option[valueKey]).join(separator) === text
        )
      })

      if (selected) {
        this.path = [...selected.map(option => option[valueKey])]
      }
    },
    resetSelectedValue(val) {
      if (val.length) {
        if ([null,undefined,''].includes(val[0]) && this.selected.length) {
          return
        }
        this.setDefaultSelected(val)
      } else {
        this.selected = []
        this.inputValue = ""
      }
    },
    setDefaultSelected(val) {
      const { separator } = this
      const text = val.join(separator)
      if (text && text === this.selectedValue) {
        return
      }

      const selected = this.flatOptions.find(optionsStack => {
        return (
          optionsStack.map(option => option[this.valueKey]).join(separator) ===
          text
        )
      })

      if (selected) {
        const values = selected.map(option => option[this.valueKey])
        this.path = [...values]
        this.selected = [...values]
        this.setInputValue()
      }
    },
    getEntityByValue(value) {
      return this.valuesEntities[value]
    },
    setInputValue() {
      const { showAllLevels, selected, labelKey, separator } = this
      if (selected.length) {
        const last = this.getEntityByValue(selected[selected.length - 1])
        this.inputValue = showAllLevels
          ? selected
            .map(v => this.getEntityByValue(v)[labelKey])
            .join(separator)
          : last[labelKey]
      } else {
        this.inputValue = ""
      }
    },
    scrollToSelected() {
      const lists = this.$refs.popper.$el.querySelectorAll(".mm-cascader-menu")

      Array.from(lists).forEach(list => {
        scrollIntoView(
          list,
          list.querySelector(".mm-cascader-menu-item--selected")
        )
      })
    },
    dropdownFocusHandler() {
      this.$refs.selector.focus()
    },
  },
}
</script>
