import { walk, bfsWalk, throttle } from './utils/'
import { v4 as uuid } from 'uuid'
import {
  getAssociativeLineTargetIndex,
  computeCubicBezierPathPoints,
  joinCubicBezierPath,
  cubicBezierPath,
  getNodePoint,
  computeNodePoints,
  getNodeLinePath,
  getDefaultControlPointOffsets
} from './utils/associativeLineUtils'

// 关联线类
class AssociativeLine {
  constructor(opt = {}) {
    this.mindMap = opt.mindMap
    this.draw = this.mindMap.draw
    // 当前所有连接线
    this.lineList = []
    // 当前激活的连接线
    this.activeLine = null
    // 当前正在创建连接线
    this.isCreatingLine = false // 是否正在创建连接线中
    this.creatingStartNode = null // 起始节点
    this.creatingLine = null // 创建过程中的连接线
    this.overlapNode = null // 创建过程中的目标节点
    // 是否有节点正在被拖拽
    this.isNodeDragging = false
    // 箭头图标
    this.markerPath = null
    this.marker = this.createMarker()
    // 控制点
    this.controlLine1 = null
    this.controlLine2 = null
    this.controlPoint1 = null
    this.controlPoint2 = null
    this.controlPointDiameter = 10
    this.isControlPointMousedown = false
    this.mousedownControlPointKey = ''
    this.controlPointMousemoveState = {
      pos: null,
      startPoint: null,
      endPoint: null,
      targetIndex: ''
    }
    // 节流一下，不然很卡
    this.checkOverlapNode = throttle(this.checkOverlapNode, 100, this)
    this.bindEvent()
  }

  // 监听事件
  bindEvent() {
    // 节点树渲染完毕后渲染连接线
    this.renderAllLines = this.renderAllLines.bind(this)
    this.mindMap.on('node_tree_render_end', this.renderAllLines)
    // 状态改变后重新渲染连接线
    this.mindMap.on('data_change', this.renderAllLines)
    // 监听画布和节点点击事件，用于清除当前激活的连接线
    this.mindMap.on('draw_click', () => {
      if (this.isControlPointMousedown) {
        return
      }
      this.clearActiveLine()
    })
    this.mindMap.on('node_click', node => {
      if (this.isCreatingLine) {
        this.completeCreateLine(node)
      } else {
        this.clearActiveLine()
      }
    })
    // 注册删除快捷键
    this.mindMap.keyCommand.addShortcut(
      'Del|Backspace',
      this.removeLine.bind(this)
    )
    // 注册添加连接线的命令
    this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine.bind(this))
    // 监听鼠标移动事件
    this.mindMap.on('mousemove', this.onMousemove.bind(this))
    // 节点拖拽事件
    this.mindMap.on('node_dragging', this.onNodeDragging.bind(this))
    this.mindMap.on('node_dragend', this.onNodeDragend.bind(this))
    // 拖拽控制点
    window.addEventListener('mousemove', e => {
      this.onControlPointMousemove(e)
    })
    window.addEventListener('mouseup', e => {
      this.onControlPointMouseup(e)
    })
  }

  // 创建箭头
  createMarker() {
    return this.draw.marker(20, 20, add => {
      add.ref(2, 5)
      add.size(10, 10)
      add.attr('orient', 'auto-start-reverse')
      this.markerPath = add.path('M0,0 L2,5 L0,10 L10,5 Z')
    })
  }

  // 渲染所有连线
  renderAllLines() {
    // 先移除
    this.removeAllLines()
    this.removeControls()
    this.clearActiveLine()
    let tree = this.mindMap.renderer.root
    if (!tree) return
    let idToNode = new Map()
    let nodeToIds = new Map()
    walk(
      tree,
      null,
      cur => {
        if (!cur) return
        let data = cur.nodeData.data
        if (
          data.associativeLineTargets &&
          data.associativeLineTargets.length > 0
        ) {
          nodeToIds.set(cur, data.associativeLineTargets)
        }
        if (data.id) {
          idToNode.set(data.id, cur)
        }
      },
      () => {},
      true,
      0
    )
    nodeToIds.forEach((ids, node) => {
      ids.forEach(id => {
        let toNode = idToNode.get(id)
        if (!node || !toNode) return
        let [startPoint, endPoint] = computeNodePoints(node, toNode)
        this.drawLine(startPoint, endPoint, node, toNode)
      })
    })
  }

  // 绘制连接线
  drawLine(startPoint, endPoint, node, toNode) {
    let {
      associativeLineWidth,
      associativeLineColor,
      associativeLineActiveWidth,
      associativeLineActiveColor
    } = this.mindMap.themeConfig
    // 箭头
    this.markerPath
      .stroke({ color: associativeLineColor })
      .fill({ color: associativeLineColor })
    // 路径
    let { path: pathStr, controlPoints } = getNodeLinePath(
      startPoint,
      endPoint,
      node,
      toNode
    )
    // 虚线
    let path = this.draw.path()
    path
      .stroke({
        width: associativeLineWidth,
        color: associativeLineColor,
        dasharray: [6, 4]
      })
      .fill({ color: 'none' })
    path.plot(pathStr)
    path.marker('end', this.marker)
    // 不可见的点击线
    let clickPath = this.draw.path()
    clickPath
      .stroke({ width: associativeLineActiveWidth, color: 'transparent' })
      .fill({ color: 'none' })
    clickPath.plot(pathStr)
    // 点击事件
    clickPath.click(e => {
      e.stopPropagation()
      // 如果当前存在激活节点，那么取消激活节点
      if (this.mindMap.renderer.activeNodeList.length > 0) {
        this.clearActiveNodes()
      } else {
        // 否则清除当前的关联线的激活状态，如果有的话
        this.clearActiveLine()
        // 保存当前激活的关联线信息
        this.activeLine = [path, clickPath, node, toNode]
        // 让不可见的点击线显示
        clickPath.stroke({ color: associativeLineActiveColor })
        // 渲染控制点和连线
        this.renderControls(
          startPoint,
          endPoint,
          controlPoints[0],
          controlPoints[1]
        )
        this.mindMap.emit(
          'associative_line_click',
          path,
          clickPath,
          node,
          toNode
        )
      }
    })
    this.lineList.push([path, clickPath, node, toNode])
  }

  // 移除所有连接线
  removeAllLines() {
    this.lineList.forEach(line => {
      line[0].remove()
      line[1].remove()
    })
    this.lineList = []
  }

  // 从当前激活节点开始创建连接线
  createLineFromActiveNode() {
    if (this.mindMap.renderer.activeNodeList.length <= 0) return
    let node = this.mindMap.renderer.activeNodeList[0]
    this.createLine(node)
  }

  // 创建连接线
  createLine(fromNode) {
    let { associativeLineWidth, associativeLineColor } =
      this.mindMap.themeConfig
    if (this.isCreatingLine || !fromNode) return
    this.isCreatingLine = true
    this.creatingStartNode = fromNode
    this.creatingLine = this.draw.path()
    this.creatingLine
      .stroke({
        width: associativeLineWidth,
        color: associativeLineColor,
        dasharray: [6, 4]
      })
      .fill({ color: 'none' })
    this.creatingLine.marker('end', this.marker)
  }

  // 鼠标移动事件
  onMousemove(e) {
    if (!this.isCreatingLine) return
    this.updateCreatingLine(e)
  }

  // 更新创建过程中的连接线
  updateCreatingLine(e) {
    let { x, y } = this.getTransformedEventPos(e)
    let startPoint = getNodePoint(this.creatingStartNode)
    let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x, y)
    this.creatingLine.plot(pathStr)
    this.checkOverlapNode(x, y)
  }

  // 获取转换后的鼠标事件对象的坐标
  getTransformedEventPos(e) {
    let { x, y } = this.mindMap.toPos(e.clientX, e.clientY)
    let { scaleX, scaleY, translateX, translateY } =
      this.mindMap.draw.transform()
    return {
      x: (x - translateX) / scaleX,
      y: (y - translateY) / scaleY
    }
  }

  // 检测当前移动到的目标节点
  checkOverlapNode(x, y) {
    this.overlapNode = null
    bfsWalk(this.mindMap.renderer.root, node => {
      if (node.nodeData.data.isActive) {
        this.mindMap.renderer.setNodeActive(node, false)
      }
      if (node === this.creatingStartNode || this.overlapNode) {
        return
      }
      let { left, top, width, height } = node
      let right = left + width
      let bottom = top + height
      if (x >= left && x <= right && y >= top && y <= bottom) {
        this.overlapNode = node
      }
    })
    if (this.overlapNode && !this.overlapNode.nodeData.data.isActive) {
      this.mindMap.renderer.setNodeActive(this.overlapNode, true)
    }
  }

  // 完成创建连接线
  completeCreateLine(node) {
    if (this.creatingStartNode === node) return
    this.addLine(this.creatingStartNode, node)
    if (this.overlapNode && this.overlapNode.nodeData.data.isActive) {
      this.mindMap.renderer.setNodeActive(this.overlapNode, false)
    }
    this.isCreatingLine = false
    this.creatingStartNode = null
    this.creatingLine.remove()
    this.creatingLine = null
    this.overlapNode = null
  }

  // 添加连接线
  addLine(fromNode, toNode) {
    if (!fromNode || !toNode) return
    // 目标节点如果没有id，则生成一个id
    let id = toNode.nodeData.data.id
    if (!id) {
      id = uuid()
      this.mindMap.execCommand('SET_NODE_DATA', toNode, {
        id
      })
    }
    // 将目标节点id保存起来
    let list = fromNode.nodeData.data.associativeLineTargets || []
    list.push(id)
    // 保存控制点
    let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
    let controlPoints = computeCubicBezierPathPoints(
      startPoint.x,
      startPoint.y,
      endPoint.x,
      endPoint.y
    )
    let offsetList =
      fromNode.nodeData.data.associativeLineTargetControlOffsets || []
    // 保存的实际是控制点和端点的差值，否则当节点位置改变了，控制点还是原来的位置，连线就不对了
    offsetList[list.length - 1] = [
      {
        x: controlPoints[0].x - startPoint.x,
        y: controlPoints[0].y - startPoint.y
      },
      {
        x: controlPoints[1].x - endPoint.x,
        y: controlPoints[1].y - endPoint.y
      }
    ]
    this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
      associativeLineTargets: list,
      associativeLineTargetControlOffsets: offsetList
    })
  }

  // 删除连接线
  removeLine() {
    if (!this.activeLine) return
    let [, , node, toNode] = this.activeLine
    this.removeControls()
    let { associativeLineTargets, associativeLineTargetControlOffsets } =
      node.nodeData.data
    let targetIndex = getAssociativeLineTargetIndex(node, toNode)
    this.mindMap.execCommand('SET_NODE_DATA', node, {
      associativeLineTargets: associativeLineTargets.filter((_, index) => {
        return index !== targetIndex
      }),
      associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
        ? associativeLineTargetControlOffsets.filter((_, index) => {
            return index !== targetIndex
          })
        : []
    })
  }

  // 清除当前激活的节点
  clearActiveNodes() {
    if (this.mindMap.renderer.activeNodeList.length > 0) {
      this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
    }
  }

  // 清除激活的线
  clearActiveLine() {
    if (this.activeLine) {
      this.activeLine[1].stroke({
        color: 'transparent'
      })
      this.activeLine = null
      this.removeControls()
    }
  }

  // 处理节点正在拖拽事件
  onNodeDragging() {
    if (this.isNodeDragging) return
    this.isNodeDragging = true
    this.lineList.forEach(line => {
      line[0].hide()
      line[1].hide()
    })
    this.hideControls()
  }

  // 处理节点拖拽完成事件
  onNodeDragend() {
    if (!this.isNodeDragging) return
    this.lineList.forEach(line => {
      line[0].show()
      line[1].show()
    })
    this.showControls()
    this.isNodeDragging = false
  }

  // 创建控制点、连线节点
  createControlNodes() {
    let { associativeLineActiveColor } = this.mindMap.themeConfig
    // 连线
    this.controlLine1 = this.draw
      .line()
      .stroke({ color: associativeLineActiveColor, width: 2 })
    this.controlLine2 = this.draw
      .line()
      .stroke({ color: associativeLineActiveColor, width: 2 })
    // 控制点
    this.controlPoint1 = this.createOneControlNode('controlPoint1')
    this.controlPoint2 = this.createOneControlNode('controlPoint2')
  }

  // 创建控制点
  createOneControlNode(pointKey) {
    let { associativeLineActiveColor } = this.mindMap.themeConfig
    return this.draw
      .circle(this.controlPointDiameter)
      .stroke({ color: associativeLineActiveColor })
      .fill({ color: '#fff' })
      .click(e => {
        e.stopPropagation()
      })
      .mousedown(e => {
        this.onControlPointMousedown(e, pointKey)
      })
  }

  // 控制点的鼠标按下事件
  onControlPointMousedown(e, pointKey) {
    e.stopPropagation()
    this.isControlPointMousedown = true
    this.mousedownControlPointKey = pointKey
  }

  // 控制点的鼠标移动事件
  onControlPointMousemove(e) {
    if (
      !this.isControlPointMousedown ||
      !this.mousedownControlPointKey ||
      !this[this.mousedownControlPointKey]
    )
      return
    e.stopPropagation()
    e.preventDefault()
    let radius = this.controlPointDiameter / 2
    // 转换鼠标当前的位置
    let { x, y } = this.getTransformedEventPos(e)
    this.controlPointMousemoveState.pos = {
      x,
      y
    }
    // 更新当前拖拽的控制点的位置
    this[this.mousedownControlPointKey].x(x - radius).y(y - radius)
    let [path, clickPath, node, toNode] = this.activeLine
    let [startPoint, endPoint] = computeNodePoints(node, toNode)
    this.controlPointMousemoveState.startPoint = startPoint
    this.controlPointMousemoveState.endPoint = endPoint
    let targetIndex = getAssociativeLineTargetIndex(node, toNode)
    this.controlPointMousemoveState.targetIndex = targetIndex
    let offsets = []
    let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
    if (!associativeLineTargetControlOffsets) {
      // 兼容0.4.5版本，没有associativeLineTargetControlOffsets的情况
      offsets = getDefaultControlPointOffsets(startPoint, endPoint)
    } else {
      offsets = associativeLineTargetControlOffsets[targetIndex]
    }
    let point1 = null
    let point2 = null
    // 拖拽的是控制点1
    if (this.mousedownControlPointKey === 'controlPoint1') {
      point1 = {
        x,
        y
      }
      point2 = {
        x: endPoint.x + offsets[1].x,
        y: endPoint.y + offsets[1].y
      }
      // 更新控制点1的连线
      this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
    } else {
      // 拖拽的是控制点2
      point1 = {
        x: startPoint.x + offsets[0].x,
        y: startPoint.y + offsets[0].y
      }
      point2 = {
        x,
        y
      }
      // 更新控制点2的连线
      this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
    }
    // 更新关联线
    let pathStr = joinCubicBezierPath(startPoint, endPoint, point1, point2)
    path.plot(pathStr)
    clickPath.plot(pathStr)
  }

  // 控制点的鼠标移动事件
  onControlPointMouseup(e) {
    if (!this.isControlPointMousedown) return
    e.stopPropagation()
    e.preventDefault()
    let { pos, startPoint, endPoint, targetIndex } =
      this.controlPointMousemoveState
    let [, , node] = this.activeLine
    let offsetList = []
    let associativeLineTargetControlOffsets = node.nodeData.data.associativeLineTargetControlOffsets
    if (!associativeLineTargetControlOffsets) {
      // 兼容0.4.5版本，没有associativeLineTargetControlOffsets的情况
      offsetList[targetIndex] = getDefaultControlPointOffsets(startPoint, endPoint)
    } else {  
      offsetList = associativeLineTargetControlOffsets
    }
    let offset1 = null
    let offset2 = null
    if (this.mousedownControlPointKey === 'controlPoint1') {
      // 更新控制点1数据
      offset1 = {
        x: pos.x - startPoint.x,
        y: pos.y - startPoint.y
      }
      offset2 = offsetList[targetIndex][1]
    } else {
      // 更新控制点2数据
      offset1 = offsetList[targetIndex][0]
      offset2 = {
        x: pos.x - endPoint.x,
        y: pos.y - endPoint.y
      }
    }
    offsetList[targetIndex] = [offset1, offset2]
    this.mindMap.execCommand('SET_NODE_DATA', node, {
      associativeLineTargetControlOffsets: offsetList
    })
    // 这里要加个setTimeout0是因为draw_click事件比mouseup事件触发的晚，所以重置isControlPointMousedown需要等draw_click事件触发完以后
    setTimeout(() => {
      this.resetControlPoint()
    }, 0)
  }

  // 复位控制点移动
  resetControlPoint() {
    this.isControlPointMousedown = false
    this.mousedownControlPointKey = ''
    this.controlPointMousemoveState = {
      pos: null,
      startPoint: null,
      endPoint: null,
      targetIndex: ''
    }
  }

  // 渲染控制点
  renderControls(startPoint, endPoint, point1, point2) {
    if (!this.controlLine1) {
      this.createControlNodes()
    }
    let radius = this.controlPointDiameter / 2
    // 控制点和起终点的连线
    this.controlLine1.plot(startPoint.x, startPoint.y, point1.x, point1.y)
    this.controlLine2.plot(endPoint.x, endPoint.y, point2.x, point2.y)
    // 控制点
    this.controlPoint1.x(point1.x - radius).y(point1.y - radius)
    this.controlPoint2.x(point2.x - radius).y(point2.y - radius)
  }

  // 删除控制点
  removeControls() {
    if (!this.controlLine1) return
    ;[
      this.controlLine1,
      this.controlLine2,
      this.controlPoint1,
      this.controlPoint2
    ].forEach(item => {
      item.remove()
    })
    this.controlLine1 = null
    this.controlLine2 = null
    this.controlPoint1 = null
    this.controlPoint2 = null
  }

  // 隐藏控制点
  hideControls() {
    if (!this.controlLine1) return
    ;[
      this.controlLine1,
      this.controlLine2,
      this.controlPoint1,
      this.controlPoint2
    ].forEach(item => {
      item.hide()
    })
  }

  // 显示控制点
  showControls() {
    if (!this.controlLine1) return
    ;[
      this.controlLine1,
      this.controlLine2,
      this.controlPoint1,
      this.controlPoint2
    ].forEach(item => {
      item.show()
    })
  }
}

AssociativeLine.instanceName = 'associativeLine'

export default AssociativeLine
