import { walk, bfsWalk, throttle } from '../utils'
import { v4 as uuid } from 'uuid'
import {
  getAssociativeLineTargetIndex,
  computeCubicBezierPathPoints,
  cubicBezierPath,
  getNodePoint,
  computeNodePoints,
  getNodeLinePath
} from './associativeLine/associativeLineUtils'
import associativeLineControlsMethods from './associativeLine/associativeLineControls'
import associativeLineTextMethods from './associativeLine/associativeLineText'

const styleProps = [
  'associativeLineWidth',
  'associativeLineColor',
  'associativeLineActiveWidth',
  'associativeLineActiveColor',
  'associativeLineDasharray',
  'associativeLineTextColor',
  'associativeLineTextFontSize',
  'associativeLineTextLineHeight',
  'associativeLineTextFontFamily'
]

// 关联线插件
class AssociativeLine {
  constructor(opt = {}) {
    this.mindMap = opt.mindMap
    this.associativeLineDraw = this.mindMap.associativeLineDraw
    // 本次不要重新渲染连线
    this.isNotRenderAllLines = false
    // 当前所有连接线
    this.lineList = []
    // 当前激活的连接线
    this.activeLine = null
    // 当前正在创建连接线
    this.isCreatingLine = false // 是否正在创建连接线中
    this.creatingStartNode = null // 起始节点
    this.creatingLine = null // 创建过程中的连接线
    this.overlapNode = null // 创建过程中的目标节点
    // 是否有节点正在被拖拽
    this.isNodeDragging = false
    // 控制点
    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)
    // 控制点相关方法
    Object.keys(associativeLineControlsMethods).forEach(item => {
      this[item] = associativeLineControlsMethods[item].bind(this)
    })
    // 关联线文字相关方法
    Object.keys(associativeLineTextMethods).forEach(item => {
      this[item] = associativeLineTextMethods[item].bind(this)
    })
    this.bindEvent()
  }

  // 监听事件
  bindEvent() {
    this.renderAllLines = this.renderAllLines.bind(this)
    this.onDrawClick = this.onDrawClick.bind(this)
    this.onNodeClick = this.onNodeClick.bind(this)
    this.removeLine = this.removeLine.bind(this)
    this.addLine = this.addLine.bind(this)
    this.onMousemove = this.onMousemove.bind(this)
    this.onNodeDragging = this.onNodeDragging.bind(this)
    this.onNodeDragend = this.onNodeDragend.bind(this)
    this.onControlPointMouseup = this.onControlPointMouseup.bind(this)
    this.onBeforeDestroy = this.onBeforeDestroy.bind(this)

    // 节点树渲染完毕后渲染连接线
    this.mindMap.on('node_tree_render_end', this.renderAllLines)
    // 状态改变后重新渲染连接线
    this.mindMap.on('data_change', this.renderAllLines)
    // 监听画布和节点点击事件，用于清除当前激活的连接线
    this.mindMap.on('draw_click', this.onDrawClick)
    this.mindMap.on('node_click', this.onNodeClick)
    this.mindMap.on('contextmenu', this.onDrawClick)
    // 注册删除快捷键
    this.mindMap.keyCommand.addShortcut('Del|Backspace', this.removeLine)
    // 注册添加连接线的命令
    this.mindMap.command.add('ADD_ASSOCIATIVE_LINE', this.addLine)
    // 监听鼠标移动事件
    this.mindMap.on('mousemove', this.onMousemove)
    // 节点拖拽事件
    this.mindMap.on('node_dragging', this.onNodeDragging)
    this.mindMap.on('node_dragend', this.onNodeDragend)
    // 拖拽控制点
    this.mindMap.on('mouseup', this.onControlPointMouseup)
    // 缩放事件
    this.mindMap.on('scale', this.onScale)
    // 实例销毁事件
    this.mindMap.on('beforeDestroy', this.onBeforeDestroy)
  }

  // 解绑事件
  unBindEvent() {
    this.mindMap.off('node_tree_render_end', this.renderAllLines)
    this.mindMap.off('data_change', this.renderAllLines)
    this.mindMap.off('draw_click', this.onDrawClick)
    this.mindMap.off('node_click', this.onNodeClick)
    this.mindMap.off('contextmenu', this.onDrawClick)
    this.mindMap.keyCommand.removeShortcut('Del|Backspace', this.removeLine)
    this.mindMap.command.remove('ADD_ASSOCIATIVE_LINE', this.addLine)
    this.mindMap.off('mousemove', this.onMousemove)
    this.mindMap.off('node_dragging', this.onNodeDragging)
    this.mindMap.off('node_dragend', this.onNodeDragend)
    this.mindMap.off('mouseup', this.onControlPointMouseup)
    this.mindMap.off('scale', this.onScale)
    this.mindMap.off('beforeDestroy', this.onBeforeDestroy)
  }

  // 获取关联线的样式配置
  // 优先级：关联线自定义样式、节点自定义样式、主题的节点层级样式、主题的最外层样式
  getStyleConfig(node, toNode) {
    let lineStyle = {}
    if (toNode) {
      const associativeLineStyle = node.getData('associativeLineStyle') || {}
      lineStyle = associativeLineStyle[toNode.getData('uid')] || {}
    }
    const res = {}
    styleProps.forEach(prop => {
      if (typeof lineStyle[prop] !== 'undefined') {
        res[prop] = lineStyle[prop]
      } else {
        res[prop] = node.getStyle(prop)
      }
    })
    return res
  }

  // 实例销毁时清除关联线文字编辑框
  onBeforeDestroy() {
    this.hideEditTextBox()
    this.removeTextEditEl()
  }

  // 画布点击事件
  onDrawClick() {
    // 取消创建关联线
    if (this.isCreatingLine) {
      this.cancelCreateLine()
    }
    // 取消激活关联线
    if (!this.isControlPointMousedown) {
      this.clearActiveLine()
    }
  }

  // 节点点击事件
  onNodeClick(node) {
    if (this.isCreatingLine) {
      this.completeCreateLine(node)
    } else {
      this.clearActiveLine()
    }
  }

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

  // 判断关联线坐标是否变更，有变更则使用变化后的坐标，无则默认坐标
  updateAllLinesPos(node, toNode, associativeLinePoint) {
    associativeLinePoint = associativeLinePoint || {}
    let [startPoint, endPoint] = computeNodePoints(node, toNode)
    let nodeRange = 0
    let nodeDir = ''
    let toNodeRange = 0
    let toNodeDir = ''
    if (associativeLinePoint.startPoint) {
      nodeRange = associativeLinePoint.startPoint.range || 0
      nodeDir = associativeLinePoint.startPoint.dir || 'right'
      startPoint = getNodePoint(node, nodeDir, nodeRange)
    }
    if (associativeLinePoint.endPoint) {
      toNodeRange = associativeLinePoint.endPoint.range || 0
      toNodeDir = associativeLinePoint.endPoint.dir || 'right'
      endPoint = getNodePoint(toNode, toNodeDir, toNodeRange)
    }
    return [startPoint, endPoint]
  }

  // 渲染所有连线
  renderAllLines() {
    if (this.isNotRenderAllLines) {
      this.isNotRenderAllLines = false
      return
    }
    // 先移除
    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.getData()
        if (
          data.associativeLineTargets &&
          data.associativeLineTargets.length > 0
        ) {
          nodeToIds.set(cur, data.associativeLineTargets)
        }
        if (data.uid) {
          idToNode.set(data.uid, cur)
        }
      },
      () => {},
      true,
      0
    )
    nodeToIds.forEach((ids, node) => {
      ids.forEach((uid, index) => {
        let toNode = idToNode.get(uid)
        if (!node || !toNode) return
        const associativeLinePoint = (node.getData('associativeLinePoint') ||
          [])[index]
        // 切换结构和布局，都会更新坐标
        const [startPoint, endPoint] = this.updateAllLinesPos(
          node,
          toNode,
          associativeLinePoint
        )
        this.drawLine(startPoint, endPoint, node, toNode)
      })
    })
  }

  // 绘制连接线
  drawLine(startPoint, endPoint, node, toNode) {
    let {
      associativeLineWidth,
      associativeLineColor,
      associativeLineActiveWidth,
      associativeLineDasharray
    } = this.getStyleConfig(node, toNode)
    // 箭头
    let markerPath = null
    const marker = this.createMarker(p => {
      markerPath = p
    })
    markerPath
      .stroke({ color: associativeLineColor })
      .fill({ color: associativeLineColor })
    // 路径
    let { path: pathStr, controlPoints } = getNodeLinePath(
      startPoint,
      endPoint,
      node,
      toNode
    )
    // 虚线
    let path = this.associativeLineDraw.path()
    path
      .stroke({
        width: associativeLineWidth,
        color: associativeLineColor,
        dasharray: associativeLineDasharray || [6, 4]
      })
      .fill({ color: 'none' })
    path.plot(pathStr)
    path.marker('end', marker)
    // 不可见的点击线
    let clickPath = this.associativeLineDraw.path()
    clickPath
      .stroke({ width: associativeLineActiveWidth, color: 'transparent' })
      .fill({ color: 'none' })
    clickPath.plot(pathStr)
    // 文字
    let text = this.createText({
      path,
      clickPath,
      markerPath,
      node,
      toNode,
      startPoint,
      endPoint,
      controlPoints
    })
    // 点击事件
    clickPath.click(e => {
      e.stopPropagation()
      this.setActiveLine({
        path,
        clickPath,
        markerPath,
        text,
        node,
        toNode,
        startPoint,
        endPoint,
        controlPoints
      })
    })
    // 双击进入关联线文本编辑状态
    clickPath.dblclick(() => {
      if (!this.activeLine) return
      this.showEditTextBox(text)
    })
    // 渲染关联线文字
    this.renderText(this.getText(node, toNode), path, text, node, toNode)
    this.lineList.push([path, clickPath, text, node, toNode])
  }

  // 更新当前激活连线的样式，一般在自定义了节点关联线的样式后调用
  // 直接调用node.setStyle方法更新样式会直接触发关联线更新，但是关联线的激活状态会丢失
  // 所以可以调用node.setData方法更新数据，然后再调用该方法更新样式，这样关联线激活状态不会丢失
  updateActiveLineStyle() {
    if (!this.activeLine) return
    this.isNotRenderAllLines = true
    const [path, clickPath, text, node, toNode, markerPath] = this.activeLine
    const {
      associativeLineWidth,
      associativeLineColor,
      associativeLineDasharray,
      associativeLineActiveWidth,
      associativeLineActiveColor,
      associativeLineTextColor,
      associativeLineTextFontFamily,
      associativeLineTextFontSize
    } = this.getStyleConfig(node, toNode)
    path
      .stroke({
        width: associativeLineWidth,
        color: associativeLineColor,
        dasharray: associativeLineDasharray || [6, 4]
      })
      .fill({ color: 'none' })
    clickPath
      .stroke({
        width: associativeLineActiveWidth,
        color: associativeLineActiveColor
      })
      .fill({ color: 'none' })
    markerPath
      .stroke({ color: associativeLineColor })
      .fill({ color: associativeLineColor })
    text.find('text').forEach(textNode => {
      textNode
        .fill({
          color: associativeLineTextColor
        })
        .css({
          'font-family': associativeLineTextFontFamily,
          'font-size': associativeLineTextFontSize + 'px'
        })
    })
    if (this.controlLine1) {
      this.controlLine1.stroke({ color: associativeLineActiveColor })
    }
    if (this.controlLine2) {
      this.controlLine2.stroke({ color: associativeLineActiveColor })
    }
    if (this.controlPoint1) {
      this.controlPoint1.stroke({ color: associativeLineActiveColor })
    }
    if (this.controlPoint2) {
      this.controlPoint2.stroke({ color: associativeLineActiveColor })
    }
  }

  // 激活某根关联线
  setActiveLine({
    path,
    clickPath,
    markerPath,
    text,
    node,
    toNode,
    startPoint,
    endPoint,
    controlPoints
  }) {
    let { associativeLineActiveColor } = this.getStyleConfig(node, toNode)
    // 如果当前存在激活节点，那么取消激活节点
    this.mindMap.execCommand('CLEAR_ACTIVE_NODE')
    // 否则清除当前的关联线的激活状态，如果有的话
    this.clearActiveLine()
    // 保存当前激活的关联线信息
    this.activeLine = [path, clickPath, text, node, toNode, markerPath]
    // 让不可见的点击线显示
    clickPath.stroke({ color: associativeLineActiveColor })
    // 如果没有输入过关联线文字，那么显示默认文字
    if (!this.getText(node, toNode)) {
      this.renderText(
        this.mindMap.opt.defaultAssociativeLineText,
        path,
        text,
        node,
        toNode
      )
    }
    // 渲染控制点和连线
    this.renderControls(
      startPoint,
      endPoint,
      controlPoints[0],
      controlPoints[1],
      node,
      toNode
    )
    this.mindMap.emit('associative_line_click', path, clickPath, node, toNode)
    this.front()
  }

  // 移除所有连接线
  removeAllLines() {
    this.lineList.forEach(line => {
      line[0].remove()
      line[1].remove()
      line[2].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,
      associativeLineDasharray
    } = this.getStyleConfig(fromNode)
    if (this.isCreatingLine || !fromNode) return
    this.front()
    this.isCreatingLine = true
    this.creatingStartNode = fromNode
    this.creatingLine = this.associativeLineDraw.path()
    this.creatingLine
      .stroke({
        width: associativeLineWidth,
        color: associativeLineColor,
        dasharray: associativeLineDasharray || [6, 4]
      })
      .fill({ color: 'none' })
    // 箭头
    let markerPath = null
    const marker = this.createMarker(p => {
      markerPath = p
    })
    markerPath
      .stroke({ color: associativeLineColor })
      .fill({ color: associativeLineColor })
    this.creatingLine.marker('end', marker)
  }

  // 取消创建关联线
  cancelCreateLine() {
    this.isCreatingLine = false
    this.creatingStartNode = null
    this.creatingLine.remove()
    this.creatingLine = null
    this.overlapNode = null
    this.back()
  }

  // 鼠标移动事件
  onMousemove(e) {
    this.onControlPointMousemove(e)
    this.updateCreatingLine(e)
  }

  // 更新创建过程中的连接线
  updateCreatingLine(e) {
    if (!this.isCreatingLine) return
    let { x, y } = this.getTransformedEventPos(e)
    let startPoint = getNodePoint(this.creatingStartNode)
    let offsetX = x > startPoint.x ? -10 : 10
    let pathStr = cubicBezierPath(startPoint.x, startPoint.y, x + offsetX, 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
    }
  }

  // 计算节点偏移位置
  getNodePos(node) {
    const { scaleX, scaleY, translateX, translateY } =
      this.mindMap.draw.transform()
    const { left, top, width, height } = node
    let translateLeft = left * scaleX + translateX
    let translateTop = top * scaleY + translateY
    return {
      left,
      top,
      translateLeft,
      translateTop,
      width,
      height
    }
  }

  // 检测当前移动到的目标节点
  checkOverlapNode(x, y) {
    this.overlapNode = null
    bfsWalk(this.mindMap.renderer.root, node => {
      if (node.getData('isActive')) {
        this.mindMap.execCommand('SET_NODE_ACTIVE', node, false)
      }
      if (node.uid === this.creatingStartNode.uid || 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.getData('isActive')) {
      this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, true)
    }
  }

  // 完成创建连接线
  completeCreateLine(node) {
    if (this.creatingStartNode.uid === node.uid) return
    const { beforeAssociativeLineConnection } = this.mindMap.opt
    let stop = false
    if (typeof beforeAssociativeLineConnection === 'function') {
      stop = beforeAssociativeLineConnection(node)
    }
    if (stop) return
    this.addLine(this.creatingStartNode, node)
    if (this.overlapNode && this.overlapNode.getData('isActive')) {
      this.mindMap.execCommand('SET_NODE_ACTIVE', this.overlapNode, false)
    }
    this.cancelCreateLine()
  }

  // 添加连接线
  addLine(fromNode, toNode) {
    if (!fromNode || !toNode) return
    // 目标节点如果没有id，则生成一个id
    let uid = toNode.getData('uid')
    if (!uid) {
      uid = uuid()
      this.mindMap.execCommand('SET_NODE_DATA', toNode, {
        uid
      })
    }
    // 将目标节点id保存起来
    let list = fromNode.getData('associativeLineTargets') || []
    // 连线节点是否存在相同的id,存在则阻止添加关联线
    const sameLine = list.some(item => item === uid)
    if (sameLine) {
      return
    }
    list.push(uid)
    // 保存控制点
    let [startPoint, endPoint] = computeNodePoints(fromNode, toNode)
    let controlPoints = computeCubicBezierPathPoints(
      startPoint.x,
      startPoint.y,
      endPoint.x,
      endPoint.y
    )
    // 检查是否存在固定位置的配置
    const { associativeLineInitPointsPosition } = this.mindMap.opt
    if (associativeLineInitPointsPosition) {
      const { from, to } = associativeLineInitPointsPosition
      if (from) {
        startPoint.dir = from
      }
      if (to) {
        endPoint.dir = to
      }
    }
    let offsetList =
      fromNode.getData('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
      }
    ]
    let associativeLinePoint = fromNode.getData('associativeLinePoint') || []
    // 记录关联的起始|结束坐标
    associativeLinePoint[list.length - 1] = { startPoint, endPoint }
    this.mindMap.execCommand('SET_NODE_DATA', fromNode, {
      associativeLineTargets: list,
      associativeLineTargetControlOffsets: offsetList,
      associativeLinePoint
    })
  }

  // 删除连接线
  removeLine() {
    if (!this.activeLine) return
    let [, , , node, toNode] = this.activeLine
    this.removeControls()
    let {
      associativeLineTargets,
      associativeLinePoint,
      associativeLineTargetControlOffsets,
      associativeLineText,
      associativeLineStyle
    } = node.getData()
    associativeLinePoint = associativeLinePoint || []
    let targetIndex = getAssociativeLineTargetIndex(node, toNode)
    // 更新关联线文本数据
    let newAssociativeLineText = {}
    if (associativeLineText) {
      Object.keys(associativeLineText).forEach(item => {
        if (item !== toNode.getData('uid')) {
          newAssociativeLineText[item] = associativeLineText[item]
        }
      })
    }
    // 更新关联线样式数据
    let newAssociativeLineStyle = {}
    if (associativeLineStyle) {
      Object.keys(associativeLineStyle).forEach(item => {
        if (item !== toNode.getData('uid')) {
          newAssociativeLineStyle[item] = associativeLineStyle[item]
        }
      })
    }
    this.mindMap.execCommand('SET_NODE_DATA', node, {
      // 目标
      associativeLineTargets: associativeLineTargets.filter((_, index) => {
        return index !== targetIndex
      }),
      // 连接线坐标
      associativeLinePoint: associativeLinePoint.filter((_, index) => {
        return index !== targetIndex
      }),
      // 偏移量
      associativeLineTargetControlOffsets: associativeLineTargetControlOffsets
        ? associativeLineTargetControlOffsets.filter((_, index) => {
            return index !== targetIndex
          })
        : [],
      // 文本
      associativeLineText: newAssociativeLineText,
      // 样式
      associativeLineStyle: newAssociativeLineStyle
    })
  }

  // 清除激活的线
  clearActiveLine() {
    if (this.activeLine) {
      let [, clickPath, text, node, toNode] = this.activeLine
      clickPath.stroke({
        color: 'transparent'
      })
      // 隐藏关联线文本编辑框
      this.hideEditTextBox()
      // 如果当前关联线没有文字，则清空文字节点
      if (!this.getText(node, toNode)) {
        text.clear()
      }
      this.activeLine = null
      this.removeControls()
      this.back()
      this.mindMap.emit('associative_line_deactivate')
    }
  }

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

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

  // 关联线顶层显示
  front() {
    if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
    this.associativeLineDraw.front()
  }

  // 关联线回到原有层级
  back() {
    if (this.mindMap.opt.associativeLineIsAlwaysAboveNode) return
    this.associativeLineDraw.back() // 最底层
    this.associativeLineDraw.forward() // 连线层上面
  }

  // 插件被移除前做的事情
  beforePluginRemove() {
    this.unBindEvent()
  }

  // 插件被卸载前做的事情
  beforePluginDestroy() {
    this.unBindEvent()
  }
}

AssociativeLine.instanceName = 'associativeLine'

export default AssociativeLine
