<!--
 * User: CHT
 * Date: 2020/5/27
 * Time: 9:52
-->
<template>
  <div
    class="super-flow"
    ref="flow-canvas"
    @contextmenu.prevent.stop="contextmenu">
    <!-- @mousedown.left="selectMaskMouseDown"
    @mousemove.left="selectMaskMouseMove"
    @mouseup="selectMaskMouseUp" -->

    <graph-line
      v-if="temEdgeConf.visible"
      :padding="linkPadding"
      :graph="graph"
      :link="temEdgeConf.link"
      :link-base-style="linkBaseStyle"
      :link-desc="linkDesc"
      :link-style="linkStyle"
    />

    <graph-line
      v-for="(edge, idx) in graph.linkList"
      :index="idx"
      :padding="linkPadding"
      :graph.sync="graph"
      :link="edge"
      :key="edge.key"
      :link-base-style="linkBaseStyle"
      :link-desc="linkDesc"
      :link-style="linkStyle"
    />

    <mark-line
      v-if="moveNodeConf.isMove && hasMarkLine"
      :width="clientWidth"
      :height="clientHeight"
      :mark-color="markLineColor"
      :markLine="moveNodeConf.markLine"
    />

    <graph-node
      v-for="(node, idx) in graph.nodeList"
      :key="node.key"
      ref="node"
      :index="idx"
      :node="node"
      :active="moveNodeList.includes(node)"
      :graph.sync="graph"
      :is-move="node === moveNodeConf.node"
      :is-tem-edge="temEdgeConf.visible"
      :node-intercept="nodeIntercept(node)"
      :line-drop="linkAddable"
      :node-drop="draggable"
      @update-node="$event => node.active = $event"
      @node-mousedown="nodeMousedown"
      @node-mouseenter="nodeMouseenter"
      @node-mouseleave="nodeMouseleave"
      @node-mouseup="nodeMouseup"
      @node-mouseup-auto="nodeMouseupAuto"
      @side-mousedown="sideMousedown"
      @node-contextmenu="nodeContextmenu"
      @node-dblclick="nodeDblclick"
      @node-to-start="nodeToStart(node)"
      @node-to-end="nodeToEnd(node)"
    >
      <template v-slot="{meta, node}">
        <slot
          name="node"
          :node="node"
          :meta="meta">
        </slot>
      </template>
    </graph-node>

    <graph-menu
      :visible.sync="menuConf.visible"
      :graph="graph"
      :position="menuConf.position"
      :list="menuConf.list"
      :source="menuConf.source">
      <template v-slot="{item}">
        <slot
          name="menuItem"
          :item="item">
        </slot>
      </template>
    </graph-menu>

    <div
      class="select-all__mask"
      ref="selectAllMask"
      tabindex="-1"
      :style="maskStyle"
      @blur="graph.graphSelected = false"
      v-show="graph.graphSelected"
      @mousedown="selectAllMaskMouseDown"
      @contextmenu.prevent.stop>
    </div>

    <!-- <div
      ref="customMask"
      class="select-all__mask"
      :style="customStyle"
      v-show="moveConfig.showBox"
      @mousedown.left="customMaskMouseDown"
      @mouseup.left="customMaskMouseUp"
      @contextmenu.prevent.stop
    ></div> -->
  </div>
</template>

<script>
import Graph from './Graph'
import GraphMenu from './menu'
import GraphNode from './node'
import GraphLine from './link'
import MarkLine from './markLine'

import {
  getOffset,
  // isIntersect,
  uuid,
  isBool,
  isFun,
  vector,
  // debounce,
  arrayReplace
} from './utils'

export default {
  name: 'super-flow',
  props: {
    relationMark: {
      type: String,
      default: 'id'
    },
    startMark: {
      type: String,
      default: 'startId'
    },
    endMark: {
      type: String,
      default: 'endId'
    },
    draggable: {
      type: Boolean,
      default: true
    },
    linkAddable: {
      type: Boolean,
      default: true
    },
    linkEditable: {
      type: Boolean,
      default: true
    },
    hasMarkLine: {
      type: Boolean,
      default: true
    },
    linkDesc: {
      type: [Function, null],
      default: null
    },
    linkStyle: {
      type: [Function, null],
      default: null
    },
    linkBaseStyle: {
      type: Object,
      default: () => ({})
    },
    markLineColor: {
      type: String,
      default: '#55abfc'
    },
    origin: {
      type: Array,
      default: () => [0, 0]
    },
    nodeList: {
      type: Array,
      default: () => []
    },
    linkList: {
      type: Array,
      default: () => []
    },
    graphMenu: {
      type: Array,
      default: () => []
    },
    nodeMenu: {
      type: Array,
      default: () => []
    },
    linkMenu: {
      type: Array,
      default: () => []
    },
    linkPadding: {
      type: Number,
      default: 50
    },
    enterIntercept: {
      type: Function,
      default: () => true
    },
    outputIntercept: {
      type: Function,
      default: () => true
    }
  },
  data () {
    return {
      graph: new Graph({
        relationMark: this.relationMark,
        startMark: this.startMark,
        endMark: this.endMark,
        origin: this.origin
      }),
      menuConf: {
        visible: false,
        position: [0, 0],
        source: null,
        list: []
      },
      moveNodeConf: {
        isMove: false,
        node: null,
        nodeList: [],
        offset: null,
        verticalList: [],
        horizontalList: [],
        markLine: []
      },
      moveAllConf: {
        isMove: false,
        downPosition: [0, 0]
      },
      temEdgeConf: {
        visible: false,
        link: null
      },
      loaded: false,
      clientWidth: 0,
      clientHeight: 0,
      isShiftPressed: false,
      moveNodeList: []
    }
  },
  components: {
    GraphMenu,
    GraphNode,
    GraphLine,
    MarkLine
  },
  computed: {
    maskStyle () {
      const {
        top,
        right,
        bottom,
        left
      } = this.graph.maskBoundingClientRect
      return {
        width: `${right - left}px`,
        height: `${bottom - top}px`,
        // top: `${top + this.originPoint[1]}px`,
        // left: `${left + this.originPoint[0]}px`
        top: `${top + this.graph.origin[1]}px`,
        left: `${left + this.graph.origin[0]}px`
      }
    }
  },
  mounted () {
    document.addEventListener('mouseup', this.docMouseup)
    document.addEventListener('mousemove', this.docMousemove)

    document.addEventListener('keydown', this.handleKeyDown)
    document.addEventListener('keyup', this.handleKeyUp)
    this.$once('hook:beforeDestroy', () => {
      document.removeEventListener('mouseup', this.docMouseup)
      document.removeEventListener('mousemove', this.docMousemove)

      document.addEventListener('keydown', this.handleKeyDown)
      document.addEventListener('keyup', this.handleKeyUp)
    })
    this.$nextTick(() => {
      this.graph.initNode(this.nodeList)
      this.graph.initLink(this.linkList)
    })
  },
  methods: {
    nodeToStart (node) {
      this.handleNodeToLink(node, true)
    },
    nodeToEnd (node) {
      this.handleNodeToLink(node, false)
    },
    // 快捷连线
    handleNodeToLink (node, isStart) {
      const { nodeList, linkList } = this.toJSON()
      // 判断当前节点距离最近的一个节点
      const list = nodeList.filter(e => {
        // 选择前方和后方元素
        const coordinate = isStart ? e.coordinate[0] < node.coordinate[0] : e.coordinate[0] > node.coordinate[0]
        // 在一定的高度范围内可连接
        const height = Math.abs(e.coordinate[1] - node.coordinate[1]) < 150
        return coordinate && height
      })
      if (!list.length) return
      const nextItem = list.sort((a, b) => {
        const _a = a.coordinate[0]
        const _b = b.coordinate[0]
        return !isStart ? _b - _a : _a - _b
      }).pop()
      // 判断当前节点是否已经连接
      const isConnect = linkList.some(e => e.startId === node.id && e.endId === nextItem.id)
      if (isConnect) return
      // 快速生成开始结束节点
      const startAt = isStart ? [0, node.height / 2] : [node.width, node.height / 2]
      const endAt = isStart ? [nextItem.width, nextItem.height / 2] : [0, nextItem.height / 2]
      const linkItem = {
        startAt,
        endAt,
        startId: node.id,
        endId: nextItem.id,
        id: this.getUUID('link'),
        meta: null
      }
      linkList.push(linkItem)
      this.graph.initLink(linkList)
    },
    handleKeyDown (event) {
      switch (event.keyCode) {
        // shift
        case 16:
          this.isShiftPressed = true
          break
        // backspace
        case 8:
          this.deleteNode()
          break
        default:
          break
      }
    },
    deleteNode () {
      if (!this.$refs.node) return
      const item = this.$refs.node.find(e => {
        const val = e.isChoose()
        return val.visible
      })
      if (!item) return

      this.$emit('deleteNode', item.node)
      // this.graph.removeNode(item.node)
    },

    clearChoose () {
      this.moveNodeList = []
      this.graph.nodeList = this.graph.nodeList.map(ele => {
        if (ele.active) {
          ele.active = false
        }
        return ele
      })
    },

    handleKeyUp (event) {
      if (!event.shiftKey) {
        this.isShiftPressed = false
      }
    },

    initMenu (menu, source) {
      return menu.map(subList => subList
        .map(item => {
          let disable
          let hidden

          if (isFun(item.disable)) {
            disable = item.disable(source)
          } else if (isBool(item.disable)) {
            disable = item.disable
          } else {
            disable = Boolean(item.disable)
          }

          if (isFun(item.hidden)) {
            hidden = item.hidden(source)
          } else if (isBool(item.hidden)) {
            hidden = item.hidden
          } else {
            hidden = Boolean(item.hidden)
          }

          return {
            ...item,
            disable,
            hidden
          }
        })
        .filter(item => !item.hidden)
      ).filter(sublist => sublist.length)
    },

    showContextMenu (position, list, source) {
      if (!list.length) return
      this.$set(this.menuConf, 'position', position)
      this.$set(this.menuConf, 'list', list)
      this.$set(this.menuConf, 'source', source)
      this.menuConf.visible = true
    },

    docMouseup (evt) {
      if (this.moveNodeConf.isMove) {
        evt.stopPropagation()
        evt.preventDefault()
      }

      this.moveNodeConf.isMove = false
      this.moveNodeConf.node = null
      this.moveNodeConf.offset = null
      arrayReplace(this.moveNodeConf.markLine, [])

      this.temEdgeConf.visible = false
      this.temEdgeConf.link = null

      this.moveAllConf.isMove = false
    },

    docMousemove (evt) {
      if (this.moveNodeConf.isMove) {
        this.moveNode(evt)
      } else if (this.temEdgeConf.visible) {
        this.moveTemEdge(evt)
      } else if (this.graph.graphSelected) {
        this.moveWhole(evt)
      } else if (this.linkEditable) {
        this.graph.dispatch({
          type: 'mousemove',
          evt: evt
        }, true)
      }
    },

    moveNode (evt) {
      const distance = 10
      const conf = this.moveNodeConf
      const origin = this.graph.origin
      const position = vector(conf.offset)
        .differ(getOffset(evt, this.$el))
        .minus(origin)
        .add([conf.node.width / 2, conf.node.height / 2])
        .end
      if (position[0] + origin[0] < conf.node.width / 2) {
        position[0] = conf.node.width / 2 - origin[0]
      }
      if (position[1] + origin[1] < conf.node.height / 2) {
        position[1] = conf.node.height / 2 - origin[1]
      }

      if (this.hasMarkLine) {
        const resultList = []
        conf.verticalList.some(vertical => {
          const x = position[0]
          const result = vertical - distance < x && vertical + distance > x

          if (result) {
            position[0] = vertical
            vertical = Math.floor(vertical)
            vertical += origin[0]
            vertical += vertical % 1 === 0 ? 0.5 : 0
            resultList.push([
              [vertical, 0],
              [vertical, this.clientHeight]
            ])
          }
          return result
        })
        conf.horizontalList.some(horizontal => {
          const y = position[1]
          const result = horizontal - distance < y && horizontal + distance > y
          if (result) {
            position[1] = horizontal
            horizontal = Math.floor(horizontal)
            horizontal += origin[1]
            horizontal += horizontal % 1 === 0 ? 0.5 : 0
            resultList.push([
              [0, horizontal],
              [this.clientWidth, horizontal]
            ])
          }
          return result
        })

        arrayReplace(conf.markLine, resultList)
      }
      const offsetCenter = [position[0] - conf.node.center[0], position[1] - conf.node.center[1]]
      conf.node.center = position
      if (!this.moveNodeList.find(e => e.id === conf.node.id)) return
      this.moveNodeList.forEach(ele => {
        if (ele.id === conf.node.id) return
        ele.center = [ele.center[0] + offsetCenter[0], ele.center[1] + offsetCenter[1]]
      })
    },

    moveTemEdge (evt) {
      this.temEdgeConf.link.movePosition =
        getOffset(evt, this.$el)
    },

    moveWhole (evt) {
      if (this.moveAllConf.isMove) {
        const offset = vector(this.moveAllConf.downPosition)
          .differ([evt.clientX, evt.clientY])
          .end
        // console.log(offset)
        arrayReplace(
          // this.originPoint,
          this.graph.origin,
          vector(this.moveAllConf.origin)
            .add(offset)
            .end
        )
      }
    },

    contextmenu (evt) {
      const mouseOnLink = this.graph.mouseOnLink
      const position = getOffset(evt)
      let list, source

      if (mouseOnLink && mouseOnLink.isPointInLink(position)) {
        list = this.initMenu(this.linkMenu, mouseOnLink)
        source = mouseOnLink
      } else {
        if (mouseOnLink) this.graph.mouseOnLink = null
        list = this.initMenu(this.graphMenu, this.graph)
        source = this.graph
      }

      this.showContextMenu(
        position,
        list,
        source
      )
    },

    nodeMousedown (node, offset) {
      this.$emit('node-mousedown', node, offset)
      if (this.isShiftPressed) {
        const index = this.moveNodeList.findIndex(ele => ele.id === node.id)
        if (index > -1) {
          this.moveNodeList.splice(index, 1)
        } else {
          this.moveNodeList.push(node)
        }
      }

      if (this.draggable) {
        this.clientWidth = this.$el.clientWidth
        this.clientHeight = this.$el.clientHeight

        const verticalList = this.moveNodeConf.verticalList
        const horizontalList = this.moveNodeConf.horizontalList

        const centerList = this.graph.nodeList
          .filter(item => item !== node)
          .map(node => node.center)

        arrayReplace(verticalList, [
          ...new Set(centerList.map(center => center[0]))
        ].sort((prev, next) => prev - next))

        arrayReplace(horizontalList, [
          ...new Set(centerList.map(center => center[1]))
        ].sort((prev, next) => prev - next))

        this.moveNodeConf.isMove = true
        this.moveNodeConf.node = node
        this.moveNodeConf.offset = offset
      }
    },

    nodeMouseenter (evt, node, offset) {
      const link = this.temEdgeConf.link
      if (this.enterIntercept(link.start, node, this.graph)) {
        link.end = node
        link.endAt = offset
      }
    },

    nodeMouseleave () {
      this.temEdgeConf.link.end = null
    },

    nodeMouseup (evt, node) {
      this.graph.addLink(this.temEdgeConf.link)
    },
    nodeMouseupAuto (evt, node) {
      // 单个选中
      if (!this.isShiftPressed) this.moveNodeList = [node]
    },

    nodeContextmenu (evt, node) {
      const list = this.initMenu(this.nodeMenu, node)
      if (!list.length) return
      this.$set(this.menuConf, 'position', getOffset(evt, this.$el))
      this.$set(this.menuConf, 'list', list)
      this.$set(this.menuConf, 'source', node)
      this.menuConf.visible = true
    },

    nodeDblclick (evt, node) {
      this.$emit('node-dblclick', node)
    },

    sideMousedown (evt, node, startAt) {
      if (this.linkAddable) {
        const link = this.graph.createLink({
          start: node,
          startAt
        })
        link.movePosition = getOffset(evt, this.$el)
        this.$set(this.temEdgeConf, 'link', link)
        this.temEdgeConf.visible = true
      }
    },

    nodeIntercept (node) {
      return () => this.outputIntercept(node, this.graph)
    },

    menuItemSelect () {
      this.menuConf.visible = false
    },

    selectAllMaskMouseDown (evt) {
      this.moveAllConf.isMove = true
      // this.moveAllConf.origin = [...this.originPoint]
      this.moveAllConf.origin = [...this.graph.origin]
      this.moveAllConf.downPosition = [
        evt.clientX,
        evt.clientY
      ]
    },

    selectedAll () {
      this.graph.selectAll()
    },

    selectedNodeList (nodeList) {
      this.graph.selectByCreate(nodeList)
    },

    toJSON () {
      return this.graph.toJSON()
    },

    getUUID (before) {
      return uuid(before)
    },

    getMouseCoordinate (clientX, clientY) {
      const offset = getOffset({ clientX, clientY }, this.$el)
      return vector(offset)
        .minus(this.graph.origin)
        .end
    },

    addNode (options) {
      return this.graph.addNode(options)
    }
  },
  watch: {
    'graph.graphSelected' () {
      if (this.graph.graphSelected) {
        this.$nextTick(() => {
          this.$refs.selectAllMask.focus()
        })
      }
    },
    'graph.mouseOnLink' () {
      if (this.graph.mouseOnLink) {
        document.body.style.cursor = 'pointer'
      } else {
        document.body.style.cursor = ''
      }
    },
    origin () {
      this.graph.origin = this.origin || []
    },
    nodeList () {
      this.graph.initNode(this.nodeList)
    },
    linkList () {
      this.graph.initLink(this.linkList)
    }
  },
  install (Vue) {
    Vue.component(this.name, this)
  }
}
</script>

<style lang="less">
.super-flow {
  font-family      : Apple System,
  'SF Pro SC',
  'SF Pro Display',
  'Helvetica Neue',
  Arial,
  'PingFang SC',
  'Hiragino Sans GB',
  STHeiti,
  'Microsoft YaHei',
  'Microsoft JhengHei',
  'Source Han Sans SC',
  'Noto Sans CJK SC',
  'Source Han Sans CN',
  sans-serif;

  position         : relative;
  background-color : transparent;
  width            : 100%;
  height           : 100%;
  overflow         : hidden;

  > .select-all__mask {
    position         : absolute;
    background-color : rgba(85, 175, 255, 0.1);
    z-index          : 20;
    border           : 1px dashed #55abfc;
    cursor           : move;
    outline          : none;
  }
}
</style>
