<template>
  <div class="full-sized">
    <svg
      :id="`network-graph-${uuid}`"
      ref="graphSvg"
      class="graph"
      :class="{
        primary: layoutPosition === 'primary',
        panning: graph.isPanningOrZooming
      }"
    >
      <defs>
        <GraphLinkMarker
          v-for="t in [
            'single-link-arrow',
            'multi-link-arrow',
            'multi-link-arrow-opposite',
            'multi-link-arrow-bar',
            'multi-link-arrow-bar-opposite'
          ]"
          :key="`marker-${t}`"
          :type="t"
        />
      </defs>

      <rect
        class="zoom-pan-interaction-layer"
        width="100%"
        height="100%"
        fill-opacity="0"
        @contextmenu.prevent="$refs.menu.open"
      />
      <!-- @contextmenu="handler($event)" -->

      <g class="g-elements">
        <g
          class="g-links"
          :transform="
            `translate(${graph.transform.x}, ${graph.transform.y}) 
               scale(${graph.transform.k})`
          "
        >
          <!-- when running force simulation, show a dumbed down version of links due to performance -->
          <transition name="fade-quick">
            <g v-if="graph.showTemporaryLinks && data.links.length < 100">
              <line
                v-for="(link, linkIndex) in data.links"
                :key="`temp-link-${link.uuid ? link.uuid : linkIndex}`"
                :x1="link.source.x"
                :y1="link.source.y"
                :x2="link.target.x"
                :y2="link.target.y"
                class="temporary-link"
              />
            </g>
          </transition>
          <transition name="fade">
            <g v-if="!isSimulationRunning">
              <GraphLink
                v-for="link in data.links"
                :key="`link-${link.uuid}`"
                :source="{
                  uuid: link.source.uuid,
                  x: link.source.x,
                  y: link.source.y
                }"
                :target="{
                  uuid: link.target.uuid,
                  x: link.target.x,
                  y: link.target.y
                }"
                :link="getLinkData(link.uuid)"
                :isHighlighted="false"
                :isDetailed="interaction.detailedLinkId === link.uuid"
                :isSelected="false"
                :isFocused="interaction.focusedLinkId === link.uuid"
                :isHovered="
                  Boolean(interaction.hoveredLink && interaction.hoveredLink.uuid === link.uuid)
                "
                :isGloballyHighlighted="
                  isLinkGloballyHighlighted(link.uuid) &&
                    (!interaction.hoveredLink || interaction.hoveredLink.uuid !== link)
                "
                :isHighlightedInDiff="isLinkHighlightedInDiff(link.uuid)"
                :sourceNodeType="getNodeData(link.source.uuid).type"
                :targetNodeType="getNodeData(link.target.uuid).type"
                :isSourceNodeMiniature="isNodeMiniature(link.source.uuid)"
                :isTargetNodeMiniature="isNodeMiniature(link.target.uuid)"
                :distanceFromFocusedNode="link.distanceFromFocusedNode"
                :visuals="{
                  ...visuals,
                  opacity: link.temporaryOpacity !== NaN ? link.temporaryOpacity : link.opacity,
                  diffHighlightColor: visualDiff ? visualDiff.highlightColor : null,
                  inDebug: inDebug
                }"
                @link-mouseout="interaction.hoveredLink = null"
                @link-mouseover="
                  interaction.hoveredLink = {
                    uuid: link.uuid,
                    source: link.source.uuid,
                    target: link.target.uuid
                  }
                "
              />
            </g>
          </transition>
        </g>

        <g
          class="g-nodes"
          :transform="
            `translate(${graph.transform.x}, ${graph.transform.y}) 
               scale(${graph.transform.k})`
          "
        >
          <GraphNode
            v-for="node in data.nodes"
            :id="`network-graph-${uuid}-node-${node.uuid}`"
            :key="`node-${node.uuid}`"
            :node="getNodeData(node.uuid)"
            :tags="getTagsForNode(node.uuid)"
            :linksConnectedToNode="getLinksConnectedToNode(node.uuid)"
            :isSelected="isNodeSelected(node.uuid)"
            :isSelectionBlocked="interaction.blockedSelection.includes(node.uuid)"
            :isHovered="interaction.hoveredNode && interaction.hoveredNode.uuid === node.uuid"
            :isDetailed="interaction.detailedNode && interaction.detailedNode.uuid === node.uuid"
            :isFocused="interaction.focusedNodeId === node.uuid"
            :isPermanentlyFocused="isNodePermanentlyFocused(node.uuid)"
            :isGloballyHighlighted="
              isNodeGloballyHighlighted(node.uuid) &&
                (!interaction.hoveredNode || interaction.hoveredNode.uuid !== node.uuid) &&
                interaction.movingNodeId !== node.uuid
            "
            :isHighlightedInDiff="isNodeHighlightedInDiff(node.uuid)"
            :distanceFromFocusedNode="node.distanceFromFocusedNode"
            :visuals="{
              ...visuals,
              posX: node.x,
              posY: node.y,
              opacity: node.temporaryOpacity !== NaN ? node.temporaryOpacity : node.opacity,
              diffHighlightColor: visualDiff ? visualDiff.highlightColor : null,
              inDebug: inDebug
            }"
            :focusVisuals="visuals.focusContext"
            :moveCallback="moveNode"
            @node-mouseout="handleNodeMouseout(node)"
            @node-mouseover="handleNodeMouseover(node)"
            @node-click="handleNodeClick(node.uuid)"
            @node-doubleclick="handleNodeDoubleclick(node.uuid)"
            @node-dragstart="interaction.movingNodeId = node.uuid"
            @node-dragend="interaction.movingNodeId = null"
            @context-menu="(...args) => handleNodeContext(...args)"
          />
          <!-- @contextmenu.prevent="$refs.menu.open" -->
          <!-- @context-menu="$refs.menu.open" -->

          <!-- Moving a node shall render it on top, ideally reordering the DOM
            – so events are triggered properly. For now, we simply render a copy 
            of the last moved node. The downside is, the order will reset after 
          moving another node, but it is acceptable.-->
          <!-- <use
            v-if="graph.lastMovedNodeId !== null"
            :xlink:href="`#network-graph-${uuid}-node-${graph.lastMovedNodeId}`"
          /> -->
          />
        </g>
      </g>
    </svg>

    <vue-context ref="menu">
      <li>
        <a @click.prevent="handleNewNodeRequest($event)">
          <!-- onClick($event.target.innerText)"> -->
          Create new node
        </a>
      </li>
      <li>
        <!-- <a @click.prevent="handleContextMenu($event)"> -->
        <a>
          Nothing will happen
        </a>
      </li>
    </vue-context>
    <vue-context ref="nodemenu">
      <li>
        <a>
          <!-- @click.prevent="handleNewNodeRequest($event)"> -->
          Pew pew node
        </a>
      </li>
      <li>
        <!-- <a @click.prevent="handleContextMenu($event)"> -->
        <a>
          Volání analýzy nad uzlem třeba. Dvou úrovňový?
        </a>
      </li>
      <li>
        <a>
          Jednou třeba vytvoření vazby
        </a>
      </li>
      <li>
        <a>
          Jednou edit atributů
        </a>
      </li>
    </vue-context>

    <DetailPopover
      v-if="interaction.detailedNode || interaction.detailedLinkId !== null"
      :pos-x="interaction.cursorPos.x"
      :pos-y="interaction.cursorPos.y"
      :max-x="graph.width"
    >
      <NodeDetailContent
        v-if="interaction.detailedNode"
        :instance-id="instanceId"
        :node="getNodeData(interaction.detailedNode.uuid)"
      />
      <LinkDetailContent
        v-if="interaction.detailedLinkId"
        :instance-id="instanceId"
        :link="getLinkData(interaction.detailedLinkId)"
      />
    </DetailPopover>
    <div id="overlay-top-left">
      <ControlsHelp
        v-if="!hideControlsHelp && view !== 'report-viewer'"
        v-show="getSelectedNodes().length === 0"
        :interaction-state="interaction.isActiveFocusContext ? 'focus-context' : 'default'"
      />
      <GraphSelection
        id="selectionTool"
        :selected-nodes="getSelectedNodes()"
        @clear-selection="deselectAllNodes()"
        @request-aggregation="handleAggregationRequest($event)"
        @block-existing-selection="interaction.blockedSelection = getSelectedNodes()"
        @unblock-selection="interaction.blockedSelection = []"
      />
    </div>

    <slot name="overlay" />
  </div>
</template>

<script>
import Vue from 'vue'
import { mapActions, mapState, mapGetters, mapMutations } from 'vuex'
import * as d3 from 'd3' // TODO: import only needed modules

import VueContext from 'vue-context'

import uniqueComponentIdMixin from '@/mixins/uuid.js'
import { sleep, getLastElementByIndex } from '@/mixins/helpers.js'
import GraphNode from '@/components/NetworkVis/NetworkGraph/GraphNode.vue'
import GraphLink from '@/components/NetworkVis/NetworkGraph/GraphLink.vue'
import GraphLinkMarker from '@/components/NetworkVis/NetworkGraph/GraphLinkMarker.vue'
import GraphSelection from '@/components/NetworkVis/NetworkGraph/GraphSelection.vue'
import ControlsHelp from '@/components/NetworkVis/NetworkGraph/ControlsHelp.vue'
import DetailPopover from '@/components/NetworkVis/NetworkGraph/DetailPopover.vue'
import NodeDetailContent from '@/components/NetworkVis/NetworkGraph/DetailPopover/NodeDetailContent.vue'
import LinkDetailContent from '@/components/NetworkVis/NetworkGraph/DetailPopover/LinkDetailContent.vue'

// FIXME
// eslint-disable-next-line no-unused-vars
import {
  getLinkSource,
  getLinkTarget,
  hasVisAttr,
  isNodeAggregated
} from '../../store/modules/data/helpers' //findConnectedLinks,

import { createAdjacencyList, runDijkstra } from '../../store/modules/data/focusContext'

export default {
  components: {
    GraphNode,
    GraphLink,
    GraphLinkMarker,
    GraphSelection,
    DetailPopover,
    ControlsHelp,
    NodeDetailContent,
    LinkDetailContent,
    VueContext
  },

  mixins: [uniqueComponentIdMixin],

  props: {
    /**
     * Associates with store/data slot of the same ID
     */
    instanceId: { type: Number, required: true },
    hideControlsHelp: { type: Boolean, default: false },
    view: { type: String, default: 'network' },
    layoutPosition: { type: String, default: 'primary' }, // primary|split-left|split-right
    visualDiff: { type: Object, default: null } // highlightedNodes, highlightedLinks, highlightColor, forceNodePositions
  },

  data() {
    return {
      data: {
        nodes: [
          /**
           * Presentational node
           * @typedef {Object} Node
           * @property {String} uuid dataset UUID
           * @property {Number} index d3-force index (d3-force)
           * @property {Number} x current position (d3-force)
           * @property {Number} y current position (d3-force)
           * @property {Number} vx current velocity (d3-force)
           * @property {Number} vy current velocity (d3-force)
           * @property {Number} fx fixed position (d3-force)
           * @property {Number} fy fixed position (d3-force)
           * @property {Number} opacity 0-1
           * @property {Number} temporaryOpacity 0-1 or NaN if not applied
           */
        ],
        links: [
          /**
           * Presentational link
           * @typedef {Object} Link
           * @property {String} uuid dataset UUID
           * @property {Node} source
           * @property {Node} target
           * @property {Number} opacity 0-1
           * @property {Number} temporaryOpacity 0-1 or NaN if not applied
           */
        ]
      },
      graph: {
        width: null,
        height: null,
        transform: { x: 0, y: 0, k: 1 }, // set by d3-zoom
        zoomMin: 0.2,
        zoomMax: 15,
        isPanningOrZooming: false,
        lastMovedNodeId: NaN, // to maintain moving node on top in DOM
        showTemporaryLinks: true
      },
      interaction: {
        isActiveFocusContext: false,
        movingNodeId: null,
        hoveredNode: null, // { uuid, x, y } | null
        detailedNode: null, // { uuid, x, y } | null
        focusedNodeId: null,
        hoveredLink: null, // { uuid, source, target } | null
        focusedLinkId: null,
        detailedLinkId: null,
        blockedSelection: [],
        activeKeys: {
          shift: false,
          ctrl: false
        },
        cursorPos: {
          x: NaN,
          y: NaN
        }
      },
      visuals: {
        baseNodeRadius: 20,
        groupNodeRadiusDifference: 5,
        linkPawRadius: 3,
        focusContext: { c1: 0.4, c2: 0.01, c3: 0.5 }
      },
      forceSimulation: null,
      isSimulationRunning: true
    }
  },

  computed: {
    ...mapState('interaction', ['inDebug'])
  },

  watch: {
    // show/hide temporary links depending on simulation status
    isSimulationRunning: function(isRunning) {
      // hide after normal links are visible
      if (!isRunning) {
        setTimeout(() => (this.graph.showTemporaryLinks = false), 100)

        this.saveNodePositions(this.data.nodes)
        this.data.nodes.forEach(n => {
          n.fx = n.x
          n.fy = n.y
        })
      } else {
        this.graph.showTemporaryLinks = true
      }
    },

    interaction: {
      handler: function(val) {
        // hovering over node + shift -> details-on-demand
        if (val.activeKeys.shift) {
          if (val.hoveredNode) {
            this.interaction.detailedNode = val.hoveredNode
          } else {
            this.interaction.detailedNode = null
          }

          if (val.hoveredLink) {
            this.interaction.detailedLinkId = val.hoveredLink.uuid
          } else {
            this.interaction.detailedLinkId = null
          }
        } else {
          this.interaction.detailedNode = null
          this.interaction.detailedLinkId = null
        }

        // hovering over node + ctrl -> focus-context
        if (val.activeKeys.ctrl) {
          this.interaction.isActiveFocusContext = true
          if (val.hoveredNode) {
            this.focusNode(this.interaction.hoveredNode.uuid)
          } else {
            this.removeNodeFocus()
          }
          if (val.hoveredLink) {
            this.focusLink(this.interaction.hoveredLink)
          } else {
            this.removeLinkFocus()
          }
        } else {
          this.removeNodeFocus()
          this.removeLinkFocus()
          this.interaction.isActiveFocusContext = false
        }
      },
      deep: true
    },

    visualDiff: {
      // eslint-disable-next-line no-unused-vars
      handler: function(newVal, oldVal) {
        console.log(`VisualDiff changing node positions: ${JSON.stringify(newVal)}`)
        if (!this.visualDiff || this.visualDiff.forceNodePositions.length === 0) {
          this.reload()
          return
        }

        this.recalculateLayout()
        this.visualDiff.forceNodePositions.forEach(order => {
          const localNode = this.data.nodes.find(n => n.uuid === order.uuid)
          if (!localNode) return
          console.log(`node ${localNode.uuid} with posX ${localNode.x} to ${order.x}`)
          localNode.fx = order.x
          localNode.fy = order.y
          localNode.x = order.x
          localNode.y = order.y
        })
        // this.initForceSimulation()

        // if (newVal.forceNodePositions !== oldVal.forceNodePositions) {
        //   this.reload()
        // }
      },
      deep: true
    }
  },

  created() {
    this.setupData()
  },

  beforeDestroy() {
    window.removeEventListener('resize', this.updateViewportSize)
    window.removeEventListener('keyup', this.handleKeyUpEvent)
    window.removeEventListener('keydown', this.handleKeyDownEvent)
    window.removeEventListener('mousemove', this.handleMouseMoveEvent)
  },

  mounted() {
    this.updateViewportSize()
    this.$nextTick(function() {
      window.addEventListener('resize', this.updateViewportSize)
      window.addEventListener('keyup', this.handleKeyUpEvent)
      window.addEventListener('keydown', this.handleKeyDownEvent)
      window.addEventListener('mousemove', this.handleMouseMoveEvent)
    })

    if (this.data.nodes[0].hasOwnProperty('fx')) this.initForceSimulationRetainLayout()
    else this.initForceSimulation()
    this.setupZoomPan()

    this.$store.subscribe(mutation => {
      const pl = mutation.payload
      if (!pl || pl.slotId !== this.instanceId) return

      switch (mutation.type) {
        case 'data/INIT':
          this.reload()
          break
        case 'data/ADD_NODE':
          this.respondAddNode(pl)
          break
        case 'data/AGGREGATE_NODES':
          this.respondAggregateNodes(pl)
          break
        case 'data/DISAGGREGATE_NODE':
          this.respondDisaggregateNode(pl)
          break
        default:
          break
      }
    })
  },

  methods: {
    ...mapActions('data', ['updateNodes', 'groupNodes', 'bundleNodes']),
    ...mapActions('data', {
      _selectNodes: 'selectNodes',
      _deselectNodes: 'deselectNodes',
      _deselectAllNodes: 'deselectAllNodes',
      _focusNodes: 'focusNodes',
      _unfocusNodes: 'unfocusNodes',
      _ungroupNode: 'ungroupNode',
      _unbundleNode: 'unbundleNode',
      _createNode: 'createNode'
    }),
    ...mapGetters('data', [
      'getVisibleNodes',
      'getVisibleLinks',
      'getNodeById',
      'getLinkById',
      'getLinkIdsConnectedToNode'
    ]),
    ...mapGetters('data', {
      _getSelectedNodes: 'getSelectedNodes',
      _isNodeSelected: 'isNodeSelected',
      _getTagsByNodeId: 'getTagsByNodeId',
      _isNodePermanentlyFocused: 'isNodeFocused'
    }),
    ...mapGetters('interaction', ['isObjectGloballyHighlighted']),
    ...mapMutations('interaction', ['SET_GLOBAL_HIGHLIGHTS', 'UNSET_GLOBAL_HIGHLIGHTS']),

    handleNodeContext(event, node) {
      console.log(event, node)
      this.$refs.nodemenu.open(event)
      // e.preventDefault()
    },

    handleNewNodeRequest(e) {
      let properties = new Map()
      properties
        .set('label', 'New node!')
        .set('isUserDefined', true)
        .set('graph.x', e.clientX)
        .set('graph.y', e.clientY)
        .set('icon', 'person')

      this._createNode({
        slotId: this.instanceId,
        node: {
          name: 'No name person',
          type: 'regular',
          properties
        }
      })
      // this.$store.dispatch('data/createNode', {
      //   id: '',
      //   uuid: '',
      //   slotId: this.instanceId,
      // })
    },

    handleMouseMoveEvent(e) {
      this.interaction.cursorPos.x = e.clientX
      this.interaction.cursorPos.y = e.clientY
    },

    handleKeyUpEvent(e) {
      if (e.repeat) return

      if (e.key === 'Shift' || e.keyCode === 16) this.interaction.activeKeys.shift = false
      if (e.key === 'Control' || e.keyCode === 17) this.interaction.activeKeys.ctrl = false
    },

    handleKeyDownEvent(e) {
      if (e.repeat) return

      if (e.key === 'Shift' || e.keyCode === 16) this.interaction.activeKeys.shift = true
      if (e.key === 'Control' || e.keyCode === 17) this.interaction.activeKeys.ctrl = true
    },

    isNodeSelected(nodeId) {
      return this._isNodeSelected()(this.instanceId, nodeId)
    },

    getSelectedNodes() {
      return this._getSelectedNodes()(this.instanceId)
    },

    handleNodeClick(nodeId) {
      if (this.interaction.isActiveFocusContext) {
        this.isNodePermanentlyFocused(nodeId)
          ? this._unfocusNodes({ slotId: this.instanceId, nodeIds: [nodeId] })
          : this._focusNodes({ slotId: this.instanceId, nodeIds: [nodeId] })
        return
      }

      this.isNodeSelected(nodeId) ? this.deselectNodes([nodeId]) : this.selectNodes([nodeId])
    },

    handleNodeDoubleclick(nodeId) {
      const n = this.getNodeData(nodeId)
      if (isNodeAggregated(n)) this._ungroupNode({ slotId: this.instanceId, nodeId: nodeId })

      if (hasVisAttr(n, 'bundledNodes'))
        this._unbundleNode({ slotId: this.instanceId, nodeId: nodeId })
    },

    handleNodeMouseover(node) {
      if (this.interaction.movingNodeId !== null) return
      this.interaction.hoveredNode = { uuid: node.uuid, x: node.x, y: node.y }
      this.SET_GLOBAL_HIGHLIGHTS([this.getNodeData(node.uuid).uuid])
    },

    handleNodeMouseout(node) {
      if (this.interaction.movingNodeId !== null) return
      this.saveNodePosition(node.uuid, node.x, node.y)
      this.interaction.hoveredNode = null
      this.UNSET_GLOBAL_HIGHLIGHTS([this.getNodeData(node.uuid).uuid])
    },

    selectNodes(nodeIds) {
      this._selectNodes({ slotId: this.instanceId, nodeIds: nodeIds })
    },

    deselectNodes(nodeIds) {
      this._deselectNodes({ slotId: this.instanceId, nodeIds: nodeIds })
    },

    deselectAllNodes() {
      this._deselectAllNodes({ slotId: this.instanceId })
    },

    isNodePermanentlyFocused(nodeId) {
      return this._isNodePermanentlyFocused()(this.instanceId, nodeId)
    },

    isNodeGloballyHighlighted(nodeId) {
      return this.isObjectGloballyHighlighted()(this.getNodeData(nodeId)?.uuid)
    },

    isLinkGloballyHighlighted(linkId) {
      const link = this.getLinkData(linkId)
      if (!link) throw Error(`Cannot fetch data for link`)
      return this.isObjectGloballyHighlighted()(link.uuid)
    },

    respondAddNode(pl) {
      console.log('NetworkGraph: data/ADD_NODE with payload:' + JSON.stringify(pl))
      if (pl.node.properties.graph) {
        this.data.nodes.push({
          uuid: pl.node.uuid,
          index: 0,
          x: (pl.node.properties.get('graph.x') - this.graph.transform.x) / this.graph.transform.k,
          y: (pl.node.properties.get('graph.y') - this.graph.transform.y) / this.graph.transform.k,
          vx: 0,
          vy: 0,
          opacity: 1
        })
      } else {
        this.data.nodes.push({
          uuid: pl.node.uuid,
          index: 0,
          x: 200,
          y: 200,
          vx: 0,
          vy: 0,
          opacity: 1
        })
      }
    },

    respondAggregateNodes(pl) {
      console.log('NetworkGraph: data/AGGREGATE_NODES with payload:' + JSON.stringify(pl))
      const action = pl.action,
        nodesIdArr = pl.nodeIds,
        groupNode = pl.targetGroupNode,
        newLinksArr = pl.newVisibleLinks,
        hideLinksIdArr = pl.removedLinkIds

      const animationSpeedMs = action === 'bundle' ? 320 : 720
      const revealSpeedMs = action === 'bundle' ? 0 : animationSpeedMs / 2
      const animationTargetPosition =
        action === 'bundle' ? groupNode.properties : this.getAveragePositionFromNodes(nodesIdArr)

      // animate: move the old nodes together
      const removedNodes = this.data.nodes.filter(n => nodesIdArr.includes(n.uuid))
      removedNodes.forEach(n => {
        this.$anime({
          targets: n,
          x: animationTargetPosition.get('graph.x'),
          y: animationTargetPosition.get('graph.y'),
          easing: 'easeInOutQuart',
          duration: animationSpeedMs
        })
      })
      // animate: decrease opacity after moving
      removedNodes.forEach(n => {
        this.$anime({
          targets: n,
          opacity: 0.8,
          easing: 'easeInQuart',
          duration: animationSpeedMs
        })
      })

      const updateGraph = async () => {
        await sleep(animationSpeedMs - revealSpeedMs)

        if (action === 'group' || action === 'alias') {
          // create the new group node
          this.data.nodes.push({
            uuid: groupNode.uuid,
            index: this.getNewNodeId(),
            x: animationTargetPosition.get('graph.x'),
            y: animationTargetPosition.get('graph.y'),
            vx: 0,
            vy: 0,
            opacity: 0
          })
          this.$anime({
            targets: this.data.nodes.find(n => n.uuid === groupNode.uuid),
            opacity: 1,
            easing: 'easeInCubic',
            duration: revealSpeedMs
          })
          this.saveNodePosition(
            groupNode.uuid,
            animationTargetPosition.get('graph.x'),
            animationTargetPosition.get('graph.y')
          )
        }

        await sleep(revealSpeedMs)
        // remove links from old nodes
        Vue.set(
          this.data,
          'links',
          this.data.links.filter(l => !hideLinksIdArr.includes(l.uuid))
        )
        // remove nodes
        Vue.set(
          this.data,
          'nodes',
          this.data.nodes.filter(n => !nodesIdArr.includes(n.uuid))
        )

        // create new links
        for (const link of newLinksArr) {
          this.data.links.push({
            uuid: link.uuid,
            source: this.findNode(link.source),
            target: this.findNode(link.target),
            opacity: 1
          })
        }
      }
      updateGraph()
    },

    respondDisaggregateNode(pl) {
      console.log('NetworkGraph: data/DISAGGREGATE_NODE with payload:' + JSON.stringify(pl))
      const groupId = pl.nodeId
      const newLinks = pl.newVisibleLinks
      const newNodes = pl.newVisibleNodes
      const removedLinkIds = pl.removedLinkIds

      const animSpeedMovingNewNodesMs = 250
      let targetNode = this.data.nodes.find(n => n.uuid === groupId)
      const targetNodePos = { x: targetNode.x, y: targetNode.y }

      if (pl.removeNode) {
        Vue.set(
          this.data,
          'nodes',
          this.data.nodes.filter(n => n.uuid !== groupId)
        )
        targetNode = undefined
      }
      Vue.set(
        this.data,
        'links',
        this.data.links.filter(l => !removedLinkIds.includes(l.uuid))
      )

      // animate: recreate original nodes
      newNodes.forEach(n =>
        this.data.nodes.push({
          uuid: n.uuid,
          index: this.getNewNodeId(),
          x: targetNodePos.x,
          y: targetNodePos.y,
          vx: 0,
          vy: 0,
          opacity: 1
        })
      )
      // recreate original links
      newLinks.forEach(l =>
        this.data.links.push({
          uuid: l.uuid,
          source: this.findNode(l.source),
          target: this.findNode(l.target),
          opacity: 1
        })
      )
      // animate: move nodes to their original positions
      newNodes.forEach(newNode => {
        this.$anime({
          targets: this.data.nodes.find(n => n.uuid === newNode.uuid),
          x: newNode.properties.get('graph.x'),
          y: newNode.properties.get('graph.y'),
          easing: 'easeOutCubic',
          duration: animSpeedMovingNewNodesMs
        })
      })
    },

    getNodeData(id) {
      return this.getNodeById()(this.instanceId, id)
    },

    getLinkData(id) {
      return this.getLinkById()(this.instanceId, id)
    },

    getTagsForNode(nodeId) {
      return this._getTagsByNodeId()(this.instanceId, nodeId)
    },

    getLinksConnectedToNode(nodeId) {
      return this.getLinkIdsConnectedToNode()(this.instanceId, nodeId)
    },

    isNodeMiniature(nodeId) {
      return hasVisAttr(this.getNodeData(nodeId), 'isMiniature')
    },

    /**
     * @brief Saves current position (d3-force)
     * @param {Object[]} nodes
     * @param {String} nodes[].uuid
     * @param {Number} nodes[].x
     * @param {Number} nodes[].y
     */
    saveNodePositions(nodes) {
      const diff = []
      nodes.forEach(n => {
        let properties = new Map()
        properties.set('graph.x', n.x)
        properties.set('graph.y', n.y)
        diff.push({ uuid: n.uuid, properties })
      })

      this.updateNodes({ slotId: this.instanceId, diff: diff })
    },

    saveNodePosition(nodeId, x, y) {
      let properties = new Map()
      properties.set('graph.x', x)
      properties.set('graph.y', y)
      this.updateNodes({
        slotId: this.instanceId,
        diff: [{ uuid: nodeId, properties }]
      })
    },

    getNewNodeId() {
      if (this.data.nodes.length === 0) return 0
      return getLastElementByIndex(this.data.nodes).index + 1
    },

    findNode(dataId) {
      return this.data.nodes.find(n => n.uuid === dataId)
    },

    convertVisibleGraphToAdjacencyList() {
      const nodes = this.data.nodes.reduce((acc, n) => acc.concat({ uuid: n.uuid }), [])
      const links = this.data.links.reduce(
        (acc, l) => acc.concat({ uuid: l.uuid, source: l.source, target: l.target }),
        []
      )
      return createAdjacencyList(nodes, links)
    },

    calculateDistancesFromNode(nodeId) {
      return runDijkstra(this.convertVisibleGraphToAdjacencyList(), nodeId)
    },

    focusNode(nodeId) {
      this.interaction.focusedNodeId = nodeId
      const res = this.calculateDistancesFromNode(nodeId)

      this.data.nodes.forEach(node => {
        Vue.set(node, 'distanceFromFocusedNode', res.distanceFromS.get(node.uuid))
        Vue.set(node, 'temporaryOpacity', 1.0 / (res.distanceFromS.get(node.uuid) / 1.7))
      })

      this.data.links.forEach(link => {
        Vue.set(
          link,
          'distanceFromFocusedNode',
          Math.max(res.distanceFromS.get(link.source.uuid), res.distanceFromS.get(link.target.uuid))
        )
        Vue.set(
          link,
          'temporaryOpacity',
          1.0 /
            (Math.max(
              res.distanceFromS.get(link.source.uuid),
              res.distanceFromS.get(link.target.uuid)
            ) /
              1.2)
        )
      })
    },

    removeNodeFocus() {
      this.interaction.focusedNodeId = null

      this.data.nodes.forEach(node => {
        Vue.set(node, 'distanceFromFocusedNode', NaN)
        Vue.set(node, 'temporaryOpacity', NaN)
      })

      this.data.links.forEach(link => {
        Vue.set(link, 'distanceFromFocusedNode', NaN)
        Vue.set(link, 'temporaryOpacity', NaN)
      })
    },

    focusLink(link) {
      this.interaction.focusedLinkId = link.uuid
      Vue.set(this.findNode(link.source), 'distanceFromFocusedNode', 3)
      Vue.set(this.findNode(link.target), 'distanceFromFocusedNode', 3)
    },

    removeLinkFocus() {
      this.interaction.focusedLinkId = null
    },

    isNodeHighlightedInDiff(id) {
      if (!this.visualDiff) return false
      return this.visualDiff.highlightedNodes.includes(id)
    },
    isLinkHighlightedInDiff(id) {
      if (!this.visualDiff) return false
      return this.visualDiff.highlightedLinks.includes(id)
    },

    handleAggregationRequest(pl) {
      switch (pl.type) {
        case 'group':
        case 'alias':
          this.groupNodes({
            slotId: this.instanceId,
            action: pl.type,
            nodeIds: [...this.getSelectedNodes()],
            label: pl.label
          })
          break
        case 'bundle':
          this.bundleNodes({
            slotId: this.instanceId,
            nodeIds: [...this.getSelectedNodes()].slice(0, this.getSelectedNodes().length - 1),
            representativeNodeId: pl.targetNode
          })
          break
      }
      this.deselectAllNodes()
    },

    initForceSimulationRetainLayout() {
      this.forceSimulation = d3
        .forceSimulation()
        .alphaMin(1)
        .alphaDecay(1)
        .nodes(this.data.nodes)
        .force(
          'link',
          d3.forceLink(this.data.links).id(link => link.uuid)
        )
        .on('end', () => {
          this.isSimulationRunning = false
        })
      this.data.links.map(link => Object.assign(link, { uuid: link.id }))
    },

    initForceSimulation() {
      this.forceSimulation = d3
        .forceSimulation()
        .alphaMin(0.004)
        .alphaDecay(0.05)
        .nodes(this.data.nodes)
        .force(
          'link',
          d3
            .forceLink(this.data.links)
            .id(link => link.uuid)
            .distance(() => 120)
            .strength(() => 1)
        )
        .force(
          'charge',
          d3
            .forceManyBody()
            .strength(-2000)
            .distanceMin(50)
            .distanceMax(400)
        ) // electrostatic force between all nodes
        .force('center', d3.forceCenter(this.graph.width / 2, this.graph.height / 2)) // translate to svg center
        .force(
          'colide',
          d3
            .forceCollide()
            .radius(100)
            .strength(0.9)
        )
        .on('end', () => {
          this.isSimulationRunning = false
        })
        .on('tick', () => {
          this.isSimulationRunning = true
        })
      this.data.links.map(link => Object.assign(link, { uuid: link.id }))
    },

    setupZoomPan() {
      // We create two d3-zoom instances:
      //   - pan+zoom on invisible background layer
      //   - only zoom on all graph elements
      // Mind that the zoom transformations are not mutual,
      // and we need to keep them in sync.
      let zoomBehavior = d3
        .zoom()
        .on('zoom', this.setZoomTransformation)
        .on('start', () => (this.graph.isPanningOrZooming = true))
        .on('end', () => (this.graph.isPanningOrZooming = false))
        .scaleExtent([this.graph.zoomMin, this.graph.zoomMax])

      d3.select(`#network-graph-${this.uuid} .zoom-pan-interaction-layer`).call(zoomBehavior)
      d3.select(`#network-graph-${this.uuid} .g-elements`)
        .call(zoomBehavior)
        .on('mousedown.zoom', null)
        .on('touchstart.zoom', null)
        .on('touchmove.zoom', null)
        .on('touchend.zoom', null)
        .on('dblclick.zoom', null)
    },

    setZoomTransformation() {
      let t = d3.event.transform
      // We need to keep both zoom transformations in sync.
      // This is a nasty, yet quick solution: d3-zoom stores
      // the transformations on the DOM nodes, we update both and also
      // store one copy in vue data.
      d3.select(`#network-graph-${this.uuid} .zoom-pan-interaction-layer`).node().__zoom = t
      d3.select(`#network-graph-${this.uuid} .g-elements`).node().__zoom = t
      this.graph.transform = t
    },

    setupData() {
      let nodes = []
      this.getVisibleNodes()(this.instanceId).forEach(n => {
        const newNode = {
          uuid: n.uuid,
          index: 0,
          x: 0,
          y: 0,
          opacity: 1,
          temporaryOpacity: NaN
        }
        if (hasVisAttr(n, 'graph') && n.properties.get('graph.x') && n.properties.get('graph.y')) {
          newNode.fx = n.properties.get('graph.x')
          newNode.fy = n.properties.get('graph.y')
        }
        nodes.push(newNode)
      })
      Vue.set(this.data, 'nodes', nodes)

      let links = []
      this.getVisibleLinks()(this.instanceId).forEach(l => {
        links.push({
          id: l.uuid,
          source: getLinkSource(l),
          target: getLinkTarget(l),
          opacity: 1,
          temporaryOpacity: NaN
        })
      })
      Vue.set(this.data, 'links', links)
    },

    reload() {
      console.log('Reloading graph.')
      this.setupData()
      this.initForceSimulation()
    },

    moveNode(child) {
      let node = this.data.nodes.find(n => n.uuid === child.uuid)
      node.x += child.x / this.graph.transform.k
      node.y += child.y / this.graph.transform.k
      this.graph.lastMovedNodeId = node.uuid
    },

    updateViewportSize() {
      this.graph.width = this.$refs.graphSvg.getBoundingClientRect().width
      this.graph.height = this.$refs.graphSvg.getBoundingClientRect().height
    },

    getAveragePositionFromNodes(nodesIdArr) {
      let x = 0,
        y = 0
      const nodes = this.data.nodes.filter(n => nodesIdArr.includes(n.uuid))
      for (const n of nodes) {
        x += n.x
        y += n.y
      }
      x /= nodes.length
      y /= nodes.length
      let properties = new Map()
      properties.set('graph.x', x)
      properties.set('graph.y', y)
      return properties
    },

    recalculateLayout() {
      this.data.nodes.forEach(n => {
        Vue.delete(n, 'fx')
        Vue.delete(n, 'fy')
      })
      this.forceSimulation
        .nodes(this.data.nodes)
        .alpha(1)
        .restart()
    }
  }
}
</script>

<style lang="sass" scoped>
@import  '~vue-context/src/sass/vue-context'

svg.graph
  width: 100%
  height: 100%
  &.primary
    background: $netvis-graph-background

.fade-enter-active
  transition: opacity 0.2s
.fade-leave-active
  transition: opacity 0.7s
.fade-enter, .fade-leave-to
  opacity: 0

.fade-quick-enter-active
  transition: opacity 0.1s
.fade-quick-leave-active
  transition: opacity .2s
.fade-quick-enter, .fade-quick-leave-to
  opacity: 0

.temporary-link
  stroke: #ddd
  stroke-dasharray: 3px

.full-sized
  width: 100%
  height: 100%
  position: relative

#overlay-top-left
  position: absolute
  left: $workspace-margin
  top: $workspace-margin

#selectionTool
  position: absolute
  left: 0
  top: 0
</style>

<style lang="sass">
svg text
    -webkit-user-select: none
       -moz-user-select: none
        -ms-user-select: none
            user-select: none

svg text::selection
    background: none
</style>
