<template>
  <g
    class="node"
    :class="{
      hovered: isHovered,
      focused: isFocused,
      'blocked-selection': isSelectionBlocked
    }"
    :transform="`translate(${visuals.posX}, ${visuals.posY})`"
    :opacity="visuals.opacity"
  >
    <!-- FOCUS/CONTEXT -->
    <!-- distance fading focus circle
    <circle
      v-if="!isNaN(this.distanceFromFocusedNode)"
      class="node-focus-circle"
      :r="
        visCircleRadius +
          visuals.groupNodeRadiusDifference +
          3 / (0.9 + Math.log(this.distanceFromFocusedNode / 2))
      "
      :opacity="Math.min(1, 1 / (1 + Math.log(this.distanceFromFocusedNode) ** 10))"
    />-->
    <circle
      v-if="isFocused || isPermanentlyFocused || isGloballyHighlighted"
      class="node-focus-circle"
      :class="{ permanent: isPermanentlyFocused }"
      :r="visCircleRadius + 8 + (node.type === 'group' ? visuals.groupNodeRadiusDifference : 0)"
    />

    <!-- TAGS -->
    <circle
      v-for="(t, i) in visibleTags"
      :key="t.uuid"
      :cy="t.properties.get('groupNodeId') === node.uuid ? 35 : 30"
      :cx="i * 12"
      :r="5"
      :fill="t.properties.get('color')"
      :stroke="t.properties.get('stroke')"
    />

    <!-- LABEL -->
    <g
      v-if="node.properties.get('label').length > 0 && !visuals.inDebug && !hasMiniatureStyle"
      class="node-label"
    >
      <rect
        class="node-label-box"
        :class="{ 'user-defined': hasUserDefinedStyle }"
        :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.uuid}`"
        class="node-label-text"
        dx="24"
        dy="4"
        :opacity="visuals.opacity"
        :x="node.type === 'group' ? visuals.groupNodeRadiusDifference : 0"
      >
        {{
          node.properties.get('label') && node.properties.get('label').length >= 18
            ? node.properties.get('label').substring(0, 15) + '...'
            : node.properties.get('label')
        }}
      </text>
    </g>

    <g
      v-dragged="nodeDragged"
      class="node-icon"
      @click="onClick"
      @dblclick="onDoubleClick"
      @mousedown.prevent
      @mouseover="onMouseover"
      @mouseout="onMouseout"
      @click.right.prevent="onRightClick($event)"
    >
      <circle
        v-if="node.type === 'group'"
        class="node-circle white-filled"
        :class="{
          'user-defined': hasUserDefinedStyle,
          selected: isSelected,
          hovered: isHovered
        }"
        :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 white-filled"
        :class="{
          'user-defined': hasUserDefinedStyle,
          selected: isSelected,
          hovered: isHovered
        }"
        :cx="-visCircleRadius / 2.0"
        :cy="-visCircleRadius / 2.0"
        :r="visCircleRadius"
      />

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

      <circle
        :r="visCircleRadius"
        fill="white"
      /><!-- fixes transparency from fading in focus-context -->

      <circle
        :id="`node-circle-${node.uuid}`"
        class="node-circle"
        :class="{
          selected: isSelected,
          detailed: isDetailed,
          hovered: isHovered,
          'user-defined': hasUserDefinedStyle
        }"
        :r="visCircleRadius"
        :fill="mainCircleFillColor"
      />

      <!-- icon -->
      <image
        v-if="!visuals.inDebug && !hasMiniatureStyle"
        :x="-visCircleRadius"
        :y="-visCircleRadius"
        :height="2 * visCircleRadius"
        :width="2 * visCircleRadius"
        :xlink:href="iconSrc"
        draggable="false"
      />

      <!-- debug -->
      <text v-if="visuals.inDebug" class="node-label-id" dx="-6" dy="8">
        {{ node.uuid }}
      </text>
      <text v-if="visuals.inDebug" class="node-label-uuid" dx="0" dy="30">
        {{ node.uuid.substring(0, 5) }}
      </text>
      <text v-if="visuals.inDebug" class="node-label-uuid" dx="0" dy="45">
        {{ node.properties.get('label').substring(0, 11) + '...' }}
      </text>
    </g>
  </g>
</template>

<script>
import Vue from 'vue'
import { debounce, map } from 'lodash'
import VDragged from 'v-dragged'
import { getBundleSize, hasVisAttr } from '@/store/modules/data/helpers'
Vue.use(VDragged)

export default {
  name: 'GraphNode',

  props: {
    node: { type: Object, required: true },
    tags: { type: Array, default: () => [] },
    visuals: { type: Object, required: true },
    moveCallback: { type: Function, default: () => {} },
    isFocused: { type: Boolean, default: false },
    isPermanentlyFocused: { type: Boolean, default: false },
    isGloballyHighlighted: { type: Boolean, default: false },
    isDetailed: { type: Boolean, default: false },
    isSelected: { type: Boolean, default: false },
    isHovered: { type: Boolean, default: false },
    isSelectionBlocked: { type: Boolean, default: false },
    distanceFromFocusedNode: { type: Number, default: NaN },
    focusVisuals: { type: Object, default: () => {} },
    isHighlightedInDiff: { type: Boolean, default: false }
  },

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

  computed: {
    bundleSize() {
      return getBundleSize(this.node)
    },

    hasMiniatureStyle() {
      return hasVisAttr(this.node, 'isMiniature') && !this.isGloballyHighlighted && !this.isSelected
    },
    hasUserDefinedStyle() {
      return (
        hasVisAttr(this.node, 'isUserDefined') || hasVisAttr(this.node, 'aggregatesUserDefined')
      )
    },
    visCircleRadius() {
      if (this.hasMiniatureStyle) return 6
      //if (this.isDetailed && this.isSelected) return this.visuals.baseNodeRadius + 1
      return this.visuals.baseNodeRadius
    },
    visBundleCircleRadius() {
      return Math.max(this.visCircleRadius / 3.0, 7)
    },
    visBundleCircleDistance() {
      return (
        this.visCircleRadius -
        3 +
        (['group', 'alias'].includes(this.node.type) ? this.visuals.groupNodeRadiusDifference : 0)
      )
    },
    visibleTags() {
      // TODO set isVisualized to false in tags store when color is none etc. instead
      return this.tags.filter(
        t => t.properties.get('isVisualized') && t.properties.get('color') !== 'none' //&& t.groupNodeId != this.node.uuid
      )
    },
    mainCircleFillColor() {
      if (this.isHighlightedInDiff) return this.visuals.diffHighlightColor
      if (this.hasMiniatureStyle) return `#787878`
      if (this.isFocused || isNaN(this.distanceFromFocusedNode)) return `white`
      if (this.distanceFromFocusedNode > 3) return `white`
      return `rgba(30, 45, 255, ${this.clampNumber(
        this.focusVisuals.c1 /
          (this.focusVisuals.c2 + Math.log(this.distanceFromFocusedNode / this.focusVisuals.c3)),
        0,
        0.5
      )})`
    },
    iconSrc() {
      try {
        return require(`@/assets/icons/${this.node.properties.get('icon')}.svg`)
      } catch (err) {
        return require('@/assets/icons/question-mark.svg')
      }
    }
  },

  watch: {
    node: {
      handler: newNode => {
        //FIXME: this may be unnnecessary
        // 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 (!newNode && this.vm) this.vm.$destroy()
      },
      deep: true
    }
  },

  methods: {
    clampNumber(val, min, max) {
      if (val > max) return max
      if (val < min) return min
      return val
    },

    nodeDragged({ deltaX, deltaY, first, last, offsetX, offsetY }) {
      if (first) {
        this.$emit('node-dragstart')
        this.isDragging = true
        this.overrideMouseEvents = true
        this.currentDragLength = 0
        return
      }
      if (last) {
        this.$emit('node-dragend')
        this.isDragging = false
        this.overrideMouseEvents = false

        // if user held the node without dragging, interpret is as a selection
        if (!this.isSelectionBlocked && this.currentDragLength < 4) {
          this.$emit('node-click', this.node.uuid)
        }
        return
      }
      this.currentDragLength = Math.abs(offsetX) + Math.abs(offsetY)
      this.moveCallback({ uuid: this.node.uuid, x: deltaX, y: deltaY })
    },

    onClick(e) {
      console.log(e)
      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.$emit('node-doubleclick', this.node.uuid)
    },

    onMouseover() {
      if (!this.overrideMouseEvents) this.$emit('node-mouseover', this.node.uuid)
    },

    onMouseout() {
      if (!this.overrideMouseEvents) this.$emit('node-mouseout', this.node.uuid)
    },

    onRightClick(event) {
      this.$emit('context-menu', event, this.node.uuid)
    }
  }
}
</script>

<style lang="sass" scoped>
.node-icon
  cursor: pointer

.hovered .node-circle
  stroke: #979797

.node-circle
  stroke-width: 1.5px
  stroke: #d8d8d8
  &.white-filled
    fill: white
  &.user-defined
    stroke-width: 1px
    stroke: #bfbfbf
    stroke-dasharray: 5
    stroke-linecap: round
  &.hovered
    stroke: #979797
  &.selected
    stroke: #1e2dff
    stroke-width: 2.2px
  &.detailed
    stroke: #757575

.node-focus-circle
  fill: rgba(0, 0, 0, 0.5)
  &.permanent
    fill: rgba(30, 45, 255, 0.5)

.node-label-box
  display: inline
  fill: rgba(220, 220, 220, 0.85) // #f2f2f2
  &.user-defined
    fill: rgba(248, 248, 248, 0.85)
    stroke: rgba(0, 0, 0, 0.15)
    stroke-dasharray: 5

.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

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

.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

.blocked-selection, .blocked-selection .node-icon
  cursor: not-allowed !important
</style>
