<template>
  <div
    :class="{
      'mm-tree--highlight-current': highlightCurrent,
      'is-dragging': !!dragState.draggingNode,
      'is-drop-not-allow': !dragState.allowDrop,
      'is-drop-inner': dragState.dropType === 'inner'
    }"
    class="mm-tree"
    role="tree">
    <ui-tree-node
      v-for="child in root.childNodes"
      :key="getNodeKey(child)"
      :node="child"
      :keymap="keymap"
      :render-after-expand="renderAfterExpand"
      :render-content="renderContent"
      @node-expand="handleNodeExpand">
      <template #nodeIcon="scopedData">
        <slot
          v-bind="scopedData"
          name="nodeIcon"></slot>
      </template>
    </ui-tree-node>
    <div
      v-if="isEmpty"
      class="mm-tree-empty-block">
      <span class="mm-tree-empty-text">
        {{ emptyText }}
      </span>
    </div>
    <div
      v-show="dragState.showDropIndicator"
      ref="dropIndicator"
      class="mm-tree-drop-indicator">
    </div>
  </div>
</template>

<script>
import { t } from "../../locale"
import TreeStore from "./model/tree-store"
import { getNodeKey, findNearestComponent } from "./model/util"
import TreeNode from "./tree-node.vue"
import { getClassName } from "../../utils/helper"

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

config.defaults({
  "Tree.renderAfterExpand": true,
  "Tree.defaultExpandAll": false,
  "Tree.expandOnClickNode": true,
  "Tree.checkOnClickNode": false,
  "Tree.keymap": () => ({
    children: "children",
    label: "label",
    icon: "icon",
    disabled: "disabled",
  }),
  "Tree.indent": 18,
  "Tree.highlightOnlyLeaf": false,
})

export default {
  name: "mm-tree",

  components: {
    "ui-tree-node": TreeNode,
  },

  props: {
    data: {
      type: Array,
    },
    emptyText: {
      type: String,
      default: t("ui.tree.noData"),
    },
    renderAfterExpand: {
      type: Boolean,
      default: () => config.get("Tree.renderAfterExpand"),
    },
    nodeKey: String,
    checkStrictly: Boolean,
    defaultExpandAll: {
      type: Boolean,
      default: () => config.get("Tree.defaultExpandAll"),
    },
    expandOnClickNode: {
      type: Boolean,
      default: () => config.get("Tree.expandOnClickNode"),
    },
    checkOnClickNode: {
      type: Boolean,
      default: () => config.get("Tree.checkOnClickNode"),
    },
    checkDescendants: {
      type: Boolean,
      default: false,
    },
    autoExpandParent: {
      type: Boolean,
      default: true,
    },
    defaultCheckedKeys: Array,
    defaultExpandedKeys: Array,
    renderContent: Function,
    showCheckbox: {
      type: Boolean,
      default: false,
    },
    draggable: {
      type: Boolean,
      default: false,
    },
    allowDrag: Function,
    allowDrop: Function,
    keymap: {
      type: null,
      default() {
        return config.get("Tree.keymap").call(this)
      },
    },
    lazy: {
      type: Boolean,
      default: false,
    },
    highlightCurrent: Boolean,
    load: Function,
    filterMethod: Function,
    accordion: Boolean,
    indent: {
      type: [Number, Function],
      default: () => config.get("Tree.indent"),
    },
    multiple: {
      type: Boolean,
      default: false,
    },
    highlightOnlyLeaf: {
      type: Boolean,
      default: config.get("Tree.highlightOnlyLeaf"),
    },
  },

  data() {
    return {
      store: null,
      root: null,
      currentNode: null,
      treeItems: null,
      checkboxItems: [],
      dragState: {
        showDropIndicator: false,
        draggingNode: null,
        dropNode: null,
        allowDrop: true,
      },
    }
  },

  computed: {
    children: {
      set(value) {
        this.data = value
      },
      get() {
        return this.data
      },
    },

    treeItemArray() {
      return Array.prototype.slice.call(this.treeItems)
    },
    isEmpty() {
      const { childNodes } = this.root
      return !childNodes || childNodes.length === 0 || childNodes.every(({ visible }) => !visible)
    },
  },

  watch: {
    defaultCheckedKeys(newVal) {
      this.store.defaultCheckedKeys = newVal
      this.store.setDefaultCheckedKey(newVal)
    },

    defaultExpandedKeys(newVal) {
      this.store.defaultExpandedKeys = newVal
      this.store.setDefaultExpandedKeys(newVal)
    },

    data(newVal) {
      this.store.setData(newVal)
    },

    checkboxItems(val) {
      Array.prototype.forEach.call(val, checkbox => {
        checkbox.setAttribute("tabindex", -1)
      })
    },
  },

  created() {
    this.isTree = true

    this.store = new TreeStore({
      key: this.nodeKey,
      data: this.data,
      lazy: this.lazy,
      keymap: this.keymap,
      load: this.load,
      currentNodeKey: this.currentNodeKey,
      checkStrictly: this.checkStrictly,
      checkDescendants: this.checkDescendants,
      defaultCheckedKeys: this.defaultCheckedKeys,
      defaultExpandedKeys: this.defaultExpandedKeys,
      autoExpandParent: this.autoExpandParent,
      defaultExpandAll: this.defaultExpandAll,
      filterMethod: this.filterMethod,
    })

    this.root = this.store.root

    let dragState = this.dragState
    this.$on("tree-node-drag-start", (event, treeNode) => {
      if (typeof this.allowDrag === "function" && !this.allowDrag(treeNode.node)) {
        event.preventDefault()
        return false
      }
      event.dataTransfer.effectAllowed = "move"

      // wrap in try catch to address IE's error when first param is 'text/plain'
      try {
        // setData is required for draggable to work in FireFox
        // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
        event.dataTransfer.setData("text/plain", "")
      } catch (e) {}
      dragState.draggingNode = treeNode
      this.$emit("node-drag-start", treeNode.node, event)
    })

    this.$on("tree-node-drag-over", (event) => {
      const dropNode = findNearestComponent(event.target, "mm-tree-node")
      const oldDropNode = dragState.dropNode
      if (oldDropNode && oldDropNode !== dropNode) {
        oldDropNode.$el.classList.remove("is-drop-inner")
      }
      const draggingNode = dragState.draggingNode
      if (!draggingNode || !dropNode) {
        return
      }

      let dropPrev = true
      let dropInner = true
      let dropNext = true
      let userAllowDropInner = true

      if (typeof this.allowDrop === "function") {
        dropPrev = this.allowDrop(draggingNode.node, dropNode.node, "prev")
        userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, "inner")
        dropNext = this.allowDrop(draggingNode.node, dropNode.node, "next")
      }
      event.dataTransfer.dropEffect = dropInner ? "move" : "none"

      if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
        if (oldDropNode) {
          this.$emit("node-drag-leave", draggingNode.node, oldDropNode.node, event)
        }
        this.$emit("node-drag-enter", draggingNode.node, dropNode.node, event)
      }

      if (dropPrev || dropInner || dropNext) {
        dragState.dropNode = dropNode
      }

      if (dropNode.node.nextSibling === draggingNode.node) {
        dropNext = false
      }
      if (dropNode.node.previousSibling === draggingNode.node) {
        dropPrev = false
      }
      if (dropNode.node.contains(draggingNode.node, false)) {
        dropInner = false
      }
      if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
        dropPrev = false
        dropInner = false
        dropNext = false
      }

      const targetPosition = dropNode.$el.getBoundingClientRect()
      const treePosition = this.$el.getBoundingClientRect()

      let dropType
      const prevPercent = dropPrev ? (dropInner ? 0.25 : dropNext ? 0.45 : 1) : -1
      const nextPercent = dropNext ? (dropInner ? 0.75 : dropPrev ? 0.55 : 0) : 1

      let indicatorTop = -9999
      const distance = event.clientY - targetPosition.top

      if (distance < targetPosition.height * prevPercent) {
        dropType = "before"
      } else if (distance > targetPosition.height * nextPercent) {
        dropType = "after"
      } else if (dropInner) {
        dropType = "inner"
      } else {
        dropType = "none"
      }

      const dropIndicator = this.$refs.dropIndicator
      if (dropType === "before") {
        indicatorTop = targetPosition.top - treePosition.top
      } else if (dropType === "after") {
        indicatorTop = targetPosition.bottom - treePosition.top
      }
      dropIndicator.style.top = indicatorTop + "px"
      dropIndicator.style.left = targetPosition.right - treePosition.left + "px"
      if (dropType === "inner") {
        dropNode.$el.classList.add("is-drop-inner")
      } else {
        dropNode.$el.classList.remove("is-drop-inner")
      }

      dragState.showDropIndicator = dropType === "before" || dropType === "after"
      dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner
      dragState.dropType = dropType
      this.$emit("node-drag-over", draggingNode.node, dropNode.node, event)
    })

    this.$on("tree-node-drag-end", event => {
      const { draggingNode, dropType, dropNode } = dragState
      event.preventDefault()
      event.dataTransfer.dropEffect = "move"

      if (draggingNode && dropNode) {
        const draggingNodeCopy = { data: draggingNode.node.data }
        if (dropType !== "none") {
          draggingNode.node.remove()
        }
        if (dropType === "before") {
          dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node)
        } else if (dropType === "after") {
          dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node)
        } else if (dropType === "inner") {
          dropNode.node.insertChild(draggingNodeCopy)
        }
        if (dropType !== "none") {
          this.store.registerNode(draggingNodeCopy)
        }

        dropNode.$el.classList.remove("is-drop-inner")

        this.$emit("node-drag-end", draggingNode.node, dropNode.node, dropType, event)
        if (dropType !== "none") {
          this.$emit("node-drop", draggingNode.node, dropNode.node, dropType, event)
        }
      }
      if (draggingNode && !dropNode) {
        this.$emit("node-drag-end", draggingNode.node, null, dropType, event)
      }

      dragState.showDropIndicator = false
      dragState.draggingNode = null
      dragState.dropNode = null
      dragState.allowDrop = true
    })
  },

  mounted() {
    this.initTabIndex()
    this.$el.addEventListener("keydown", this.handleKeydown)
  },

  updated() {
    this.treeItems = this.$el.querySelectorAll("[role=treeitem]")
    this.checkboxItems = this.$el.querySelectorAll("input[type=checkbox]")
  },

  methods: {
    filter(value) {
      if (!this.filterMethod) {
        throw new Error("[Tree] filterMethod is required when filter")
      }
      this.store.filter(value)
    },

    getNodeKey(node) {
      return getNodeKey(this.nodeKey, node.data)
    },

    getNodePath(data) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in getNodePath")
      }
      const node = this.store.getNode(data)
      if (!node) {
        return []
      }
      const path = [node.data]
      let parent = node.parent
      while (parent && parent !== this.root) {
        path.push(parent.data)
        parent = parent.parent
      }
      return path.reverse()
    },

    getCheckedNodes(leafOnly, includeHalfChecked) {
      return this.store.getCheckedNodes(leafOnly, includeHalfChecked)
    },

    getCheckedKeys(leafOnly, includeHalfChecked) {
      return this.store.getCheckedKeys(leafOnly, includeHalfChecked)
    },

    getCurrentNode() {
      const currentNode = this.store.getCurrentNode()
      return currentNode ? currentNode.data : null
    },

    getCurrentKey() {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in getCurrentKey")
      }
      const currentNode = this.getCurrentNode()
      return currentNode ? currentNode[this.nodeKey] : null
    },

    setCheckedNodes(nodes, leafOnly) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in setCheckedNodes")
      }
      this.store.setCheckedNodes(nodes, leafOnly)
    },

    setCheckedKeys(keys, leafOnly) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in setCheckedKeys")
      }
      this.store.setCheckedKeys(keys, leafOnly)
    },

    setChecked(data, checked, deep) {
      this.store.setChecked(data, checked, deep)
    },

    getHalfCheckedNodes() {
      return this.store.getHalfCheckedNodes()
    },

    getHalfCheckedKeys() {
      return this.store.getHalfCheckedKeys()
    },

    setCurrentNode(node) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in setCurrentNode")
      }
      this.store.setUserCurrentNode(node)
    },

    setCurrentKey(key) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in setCurrentKey")
      }
      this.store.setCurrentNodeKey(key)
    },

    getNode(data) {
      return this.store.getNode(data)
    },

    remove(data) {
      this.store.remove(data)
    },

    append(data, parentNode) {
      this.store.append(data, parentNode)
    },

    insertBefore(data, refNode) {
      this.store.insertBefore(data, refNode)
    },

    insertAfter(data, refNode) {
      this.store.insertAfter(data, refNode)
    },

    handleNodeExpand(nodeData, node, instance) {
      if (this.accordion) {
        this.root.childNodes.forEach(childNode => {
          if (node !== childNode) {
            childNode.collapse()
          }
        })
      }

      this.$emit("node-expand", nodeData, node, instance)
    },

    updateKeyChildren(key, data) {
      if (!this.nodeKey) {
        throw new Error("[Tree] nodeKey is required in updateKeyChild")
      }
      this.store.updateChildren(key, data)
    },

    initTabIndex() {
      this.treeItems = this.$el.querySelectorAll(".is-focusable[role=treeitem]")
      this.checkboxItems = this.$el.querySelectorAll("input[type=checkbox]")
      const checkedItem = this.$el.querySelectorAll(".is-checked[role=treeitem]")
      if (checkedItem.length) {
        checkedItem[0].setAttribute("tabindex", 0)
        return
      }
      this.treeItems[0] && this.treeItems[0].setAttribute("tabindex", 0)
    },

    handleKeydown(ev) {
      const currentItem = ev.target
      if (getClassName(currentItem).indexOf("mm-tree-node") === -1) {
        return
      }
      ev.preventDefault()
      const keyCode = ev.keyCode
      this.treeItems = this.$el.querySelectorAll(".is-focusable[role=treeitem]")
      const currentIndex = this.treeItemArray.indexOf(currentItem)
      let nextIndex
      if ([38, 40].indexOf(keyCode) > -1) {
        ev.preventDefault()
        // up、down
        if (keyCode === 38) {
          // up
          nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
        } else {
          nextIndex = currentIndex < this.treeItemArray.length - 1 ? currentIndex + 1 : 0
        }
        this.treeItemArray[nextIndex].focus() // 选中
      }
      if ([37, 39].indexOf(keyCode) > -1) {
        ev.preventDefault()
        // left、right 展开
        currentItem.click() // 选中
      }
      const hasInput = currentItem.querySelector("[type=\"checkbox\"]")
      if ([13, 32].indexOf(keyCode) > -1 && hasInput) {
        ev.preventDefault()
        // space enter选中checkbox
        hasInput.click()
      }
    },
  },
}
</script>
