<template>
  <transition name="fade">
    <g
      class="node"
      :class="{
        hovered: isHovered,
        selected: isSelected,
        detailed: isDetailed,
        highlighted: isHighlighted,
        removed: isRemoved
      }"
      :transform="`translate(${graphX}, ${graphY})`"
    >
      <g v-if="!isRemoved && !inDebug" class="node-label">
        <rect
          class="node-label-box"
          :width="150"
          height="22"
          rx="5"
          ry="5"
          y="-12"
          :x="node.type == 'group' ? visuals.groupNodeRadiusDifference / 2.0 : 0"
        />
        <text
          :id="`node-label-text-${node.id}`"
          class="node-label-text"
          dx="24"
          dy="4"
          :x="node.type == 'group' ? visuals.groupNodeRadiusDifference : 0"
        >
          {{ node.label }}
        </text>
      </g>

      <g
        v-dragged="nodeDragged"
        class="node-icon"
        @click="onClick()"
        @dblclick="onDoubleClick()"
        @mousedown.prevent
        @mousedown.left.exact="isUserSelecting = true"
        @click.right.prevent
        @mouseover="
          if (!existsInDataset) return
          if (!overrideMouseEvents) $emit('node-mouseover', node.id)
          showNodePopover(node.id)
        "
        @mouseover.ctrl.exact="
          SET_NODE_HIGHLIGHT(node.id)
          linksConnectedToNode.forEach(l => {
            SET_NODE_HIGHLIGHT(l.source)
            SET_NODE_HIGHLIGHT(l.target)
            SET_LINK_HIGHLIGHT(l.id)
          })
        "
        @mouseout="
          if (!existsInDataset) return
          if (!overrideMouseEvents) $emit('node-mouseout', node.id)
          clearHighlights()
          hideNodePopover()
        "
      >
        <!-- @mousedown.prevent because defaults fire dragging on the image/text selection -->
        
        <circle
          v-if="node.type === 'group'"
          class="node-circle"
          :r="visCircleRadius + visuals.groupNodeRadiusDifference"
        />

        <circle
          v-if="isDetailed && isSelected"
          class="node-circle-detailed-and-selected"
          :r="visCircleRadius"
        />

        <!-- alias -->
        <circle
          v-if="node.type === 'alias'"
          class="node-circle"
          :cx="-visCircleRadius / 2.0"
          :cy="-visCircleRadius / 2.0"
          :r="visCircleRadius"
        />

        <!-- bundle -->
        <g v-if="bundleSize > 0" class="bundle-bubble">
          <circle
            class="node-circle bundle-circle"
            :r="visCircleRadius / 3.0"
            :cx="visCircleRadius + visBundleCircleDistance"
            :cy="-visCircleRadius - visBundleCircleDistance"
          ></circle>
          <text
            class="bundle-text"
            text-anchor="middle"
            :x="visCircleRadius + visBundleCircleDistance"
            :y="-visCircleRadius + visBundleCircleDistance - (['group', 'alias'].includes(node.type) ? visuals.groupNodeRadiusDifference * 2 : 0)"
          >
            {{ bundleSize }}
          </text>
        </g>

        <circle v-if="isHighlighted" class="node-circle-highlighted" :r="visCircleRadius" />
        <circle
          :id="`node-circle-${node.id}`"
          class="node-circle"
          :class="{ 'connected-link-hovered': isConnectedLinkHovered }"
          :r="visCircleRadius"
          :fill="`#fff`"
        />
        <BaseIcon
          v-if="!isRemoved && !inDebug"
          use-svg
          :name="node.type === 'group' ? 'group' : isRemoved ? 'deleted' : node.dataClass"
          :radius="visCircleRadius"
        />
        <text v-if="inDebug" class="node-label-id" dx="-6" dy="8">{{ node.id }}</text>
      </g>
    </g>
  </transition>
</template>

<script>
/*eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }]*/
import Vue from 'vue'
import { debounce, map } from 'lodash'
import { mapMutations, mapState, mapActions } from 'vuex'
import VDragged from 'v-dragged'
Vue.use(VDragged)

export default {
  name: 'GraphNode',

  props: {
    dataId: { type: Number, required: true },
    visuals: { type: Object, required: true },
    // all d3-force calculated data:
    graphX: { type: Number, required: true },
    graphY: { type: Number, required: true },
    moveCallback: { type: Function, required: true }
  },

  data() {
    return {
      isHovering: false,
      isDragging: false,
      currentDragLength: 0, // approx. px to distinguish single-click from drag
      isUserSelecting: false,
      overrideMouseEvents: false,
      debouncedClickEvents: []
    }
  },

  computed: {
    ...mapState('interaction', ['inDebug']),
    node() {
      return this.$store.getters['data/getNodeById'](this.dataId)
    },
    linksConnectedToNode() {
      return this.$store.getters['data/getLinkIdsConnectedToNode'](this.dataId)
    },
    existsInDataset() {
      return this.node !== undefined
    },
    isConnectedLinkHovered() {
      return false
    },
    isHighlighted() {
      return this.$store.getters['interaction/isNodeHighlighted'](this.dataId)
    },
    isDetailed() {
      return this.$store.getters['interaction/isNodeDetailed'](this.dataId)
    },
    isSelected() {
      return this.$store.getters['interaction/isNodeSelected'](this.dataId)
    },
    isRemoved() {
      return this.node.vis && this.node.vis.isRemoved
    },
    isHovered: {
      cache: false,
      get: function () {
        return this.$store.getters['interaction/isNodeHovered'](this.dataId)
      }
    },
    visCircleRadius() {
      if (this.isDetailed && this.isSelected) return this.visuals.baseNodeRadius + 1
      if (this.isRemoved) return this.visuals.baseNodeRadius
      return this.visuals.baseNodeRadius
    },
    visBundleCircleDistance() {
      return 2 + (['group', 'alias'].includes(this.node.type) ? this.visuals.groupNodeRadiusDifference : 0)
    },
    bundleSize: {
      cache: false,
      get: function () {
        return this.$store.getters['data/getNodeBundledNodeCount'](this.dataId)
      }
    }
  },

  watch: {
    node: function(n) {
      // Node may be removed from dataset. However, if we have fade animation,
      // it would still trigger user events, which would call data storage and request
      // a removed node, resulting in error. So we remove the component instance beforehand.
      if (n === undefined && this) this.vm.$destroy()
    }
  },

  methods: {
    ...mapMutations('interaction', [
      'SET_NODE_HIGHLIGHT',
      'CLEAR_NODE_HIGHLIGHT',
      'SET_NODE_DETAIL',
      'UNSET_NODE_DETAIL',
      'SELECT_NODE',
      'DESELECT_NODES',
      'SET_TOOL_SHOW',
      'SET_LINK_HIGHLIGHT',
      'CLEAR_LINK_HIGHLIGHT',
      'SET_NODE_HOVER',
      'UNSET_NODE_HOVER'
    ]),
    ...mapActions('interaction', [
      'showNodeDetail',
      'hideNodeDetail',
      'showNodeSelection',
      'hideNodeSelectionIfEmpty',
      'showNodePopover',
      'hideNodePopover'
    ]),

    nodeDragged({ deltaX, deltaY, first, last, offsetX, offsetY }) {
      if (!this.existsInDataset) return

      if (first) {
        this.isDragging = true
        this.overrideMouseEvents = true
        this.currentDragLength = 0
        return
      }
      if (last) {
        this.isDragging = false
        this.overrideMouseEvents = false

        // if user held the node without dragging, interpret is as a selection
        if (this.currentDragLength < 4) {
          if (this.isUserSelecting) {
            this.isSelected ? this.DESELECT_NODES([this.node.id]) : this.SELECT_NODE(this.node.id)
            this.isSelected ? this.showNodeSelection() : this.hideNodeSelectionIfEmpty()
            this.isUserSelecting = false
          }
        }
        return
      }
      this.currentDragLength = Math.abs(offsetX) + Math.abs(offsetY)
      this.moveCallback({ id: this.node.id, x: deltaX, y: deltaY })
    },

    clearHighlights() {
      this.CLEAR_NODE_HIGHLIGHT()
      this.CLEAR_LINK_HIGHLIGHT()
    },

    switchNodeDetail() {
      this.isDetailed ? this.UNSET_NODE_DETAIL() : this.SET_NODE_DETAIL(this.node.id)
      this.isDetailed ? this.showNodeDetail(this.node.id) : this.hideNodeDetail()
    },

    onClick() {
      this.debouncedClickEvents = this.debouncedClickEvents || []

      const callback = debounce(_ => {
        // code for single clicking is handled during node dragging
        this.debouncedClickEvents = []
      }, 200)
      this.debouncedClickEvents.push(callback)
      callback()
    },

    onDoubleClick() {
      // If there were click events registered we cancel them
      if (this.debouncedClickEvents.length > 0) {
        map(this.debouncedClickEvents, debounce => debounce.cancel())
        this.debouncedClickEvents = []
      }

      this.switchNodeDetail()
    }
  }
}
</script>

<style lang="sass" scoped>
.hovered .node-circle
  stroke: #979797

.node-circle
  stroke-width: 1.5px
  stroke: #d8d8d8
  fill: #fff

.node.removed
  .node-circle
    stroke-dasharray: 4
    image
      fill: red

.node-label-box
  display: inline
  fill: rgba(220, 220, 220, 0.65) // #f2f2f2

.node-label-text
  font-size: 0.85em
  fill: #666
  display: inline

.node-label-id
  font-size: 1.3em
  font-weight: 600
  fill: #4903fc
  user-select: none

.detailed .node-icon .node-circle
  stroke: #757575

.highlighted .node-icon .node-circle
  stroke: #979797
  fill: #979797

.selected .node-icon .node-circle
  stroke: #1e2dff
  stroke-width: 1.5px

.node-circle-detailed-and-selected
  stroke: #979797
  stroke-width: 5px

.node-circle.connected-link-hovered
  stroke: #757575

.bundle .bundle-bubble
  display: inline

.bundle-circle
  stroke-width: 1.5px
  stroke: #D8D8D8
  fill: #fff

.bundle-text
  font-size: 0.65em
  font-weight: 600
  fill: #666
</style>
