/**
 * @fileOverview random layout
 * @author shiwu.wyy@antfin.com
 */

const d3Force = require('d3-force')
const isArray = require('@antv/util/lib/type/is-array')
const isNumber = require('@antv/util/lib/type/is-number')
const isFunction = require('@antv/util/lib/type/is-function')
const Layout = require('./layout')
const Util = require('../util/layout')
const layoutConst = require('./worker/layoutConst')

const { LAYOUT_MESSAGE } = layoutConst

/**
 * 经典力导布局 force-directed
 */
Layout.registerLayout('force', {
  getDefaultCfg() {
    return {
      center: [0, 0], // 向心力作用点
      nodeStrength: null, // 节点作用力
      preventOverlap: false, // 是否防止节点相互覆盖
      nodeSize: undefined, // 节点大小 / 直径，用于防止重叠时的碰撞检测
      nodeSpacing: undefined, // 节点间距，防止节点重叠时节点之间的最小距离（两节点边缘最短距离）
      edgeStrength: null, // 边的作用力, 默认为根据节点的入度出度自适应
      linkDistance: 50, // 默认边长度
      forceSimulation: null, // 自定义 force 方法
      alphaDecay: 0.028, // 迭代阈值的衰减率 [0, 1]，0.028 对应最大迭代数为 300
      alphaMin: 0.001, // 停止迭代的阈值
      alpha: 0.3, // 当前阈值
      collideStrength: 1, // 防止重叠的力强度
      tick() {},
      onLayoutEnd() {}, // 布局完成回调
      onTick() {}, // 每一迭代布局回调
      // 是否启用web worker。前提是在web worker里执行布局，否则无效
      workerEnabled: false
    }
  },
  /**
   * 初始化
   * @param {object} data 数据
   */
  init(data) {
    const self = this
    self.nodes = data.nodes
    self.edges = data.edges
    self.ticking = false
  },
  /**
   * 执行布局
   */
  execute() {
    const self = this
    const nodes = self.nodes
    const edges = self.edges
    // 如果正在布局，忽略布局请求
    if (self.ticking) {
      return
    }
    let simulation = self.forceSimulation
    const alphaMin = self.alphaMin
    const alphaDecay = self.alphaDecay
    const alpha = self.alpha
    if (!simulation) {
      try {
        // 定义节点的力
        const nodeForce = d3Force.forceManyBody()
        if (self.nodeStrength) {
          nodeForce.strength(self.nodeStrength)
        }
        simulation = d3Force.forceSimulation()
          .nodes(nodes)
          .force('center', d3Force.forceCenter(self.center[0], self.center[1]))
          .force('charge', nodeForce)
          .alpha(alpha)
          .alphaDecay(alphaDecay)
          .alphaMin(alphaMin)

        if (self.preventOverlap) {
          self.overlapProcess(simulation)
        }
        // 如果有边，定义边的力
        if (edges) {
          // d3 的 forceLayout 会重新生成边的数据模型，为了避免污染源数据
          const d3Edges = edges.map(edge => {
            return {
              id: edge.id,
              source: edge.source,
              target: edge.target
            }
          })
          const edgeForce = d3Force.forceLink().id(function(d) { return d.id }).links(d3Edges)
          if (self.edgeStrength) {
            edgeForce.strength(self.edgeStrength)
          }
          if (self.linkDistance) {
            edgeForce.distance(self.linkDistance)
          }
          simulation.force('link', edgeForce)
        }

        if (self.workerEnabled && !isInWorker()) {
          // 如果不是运行在web worker里，不用web worker布局
          self.workerEnabled = false
          console.warn('workerEnabled option is only supported when running in web worker.')
        }
        if (!self.workerEnabled) {
          simulation
            .on('tick', () => {
              self.tick()
            })
            .on('end', () => {
              self.ticking = false
              self.onLayoutEnd && self.onLayoutEnd()
            })
          self.ticking = true
        } else {
          simulation.stop()
          const totalTicks = getSimulationTicks(simulation)
          for (let currentTick = 1; currentTick <= totalTicks; currentTick++) {
            simulation.tick()
            // currentTick starts from 1.
            postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes })
          }
          self.ticking = false
        }

        self.forceSimulation = simulation
      } catch (e) {
        self.ticking = false
        console.warn(e)
      }
    } else {
      if (self.preventOverlap) {
        self.overlapProcess(simulation)
      }
      simulation
        .alpha(alpha)
        .restart()
      this.ticking = true
    }
  },
  /**
   * 防止重叠
   * @param {object} simulation 力模拟模型
   */
  overlapProcess(simulation) {
    const self = this
    const nodeSize = self.nodeSize
    let nodeSizeFunc
    const nodeSpacing = self.nodeSpacing
    let nodeSpacingFunc
    const collideStrength = self.collideStrength

    if (isNumber(nodeSpacing)) {
      nodeSpacingFunc = () => {
        return nodeSpacing
      }
    } else if (typeof nodeSpacing === 'function') {
      nodeSpacingFunc = nodeSpacing
    } else {
      nodeSpacingFunc = () => {
        return 0
      }
    }

    if (!nodeSize) {
      nodeSizeFunc = d => {
        if (d.size) {
          if (isArray(d.size)) {
            const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1]
            return res / 2 + nodeSpacingFunc(d)
          }
          return d.size / 2 + nodeSpacingFunc(d)
        }
        return 10 + nodeSpacingFunc(d)
      }
    } else if (isFunction(nodeSize)) {
      nodeSizeFunc = nodeSize
    } else if (!isNaN(nodeSize)) {
      const radius = nodeSize / 2
      nodeSizeFunc = d => {
        return radius + nodeSpacingFunc(d)
      }
    } else if (isArray(nodeSize)) {
      const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1]
      const radius = larger / 2
      nodeSizeFunc = d => {
        return radius + nodeSpacingFunc(d)
      }
    }

    // forceCollide's parameter is a radius
    simulation.force('collisionForce', d3Force.forceCollide(nodeSizeFunc).strength(collideStrength))
  },
  /**
   * 更新布局配置，但不执行布局
   * @param {object} cfg 需要更新的配置项
   */
  updateCfg(cfg) {
    const self = this
    if (self.ticking) {
      self.forceSimulation.stop()
      self.ticking = false
    }
    self.forceSimulation = null
    Util.mix(self, cfg)
  },
  destroy() {
    const self = this
    if (self.ticking) {
      self.forceSimulation.stop()
      self.ticking = false
    }
    self.nodes = null
    self.edges = null
    self.destroyed = true
  }
})

// Return total ticks of d3-force simulation
function getSimulationTicks(simulation) {
  const alphaMin = simulation.alphaMin()
  const alphaTarget = simulation.alphaTarget()
  const alpha = simulation.alpha()
  const totalTicksFloat = Math.log((alphaMin - alphaTarget) / (alpha - alphaTarget)) / Math.log(1 - simulation.alphaDecay())
  const totalTicks = Math.ceil(totalTicksFloat)

  return totalTicks
}

// 判断是否运行在web worker里
function isInWorker() {
  // eslint-disable-next-line no-undef
  return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope
}
