<template>
  <svg
    width="100%" height="100%"
    ref="svg" :id="id"
  >
    <template v-if="ready">
      <path
        v-for="(item, index) in links"
        :key="`link_${index}`"
        :d="lineGenerator([item.source, item.target])"
        :class="['link', item.linkType || item.target.data.linkType]"
      />

      <template v-for="(node, index) in nodes">
        <circle
          :key="'circle' + index"
          :cx="node.x"
          :cy="node.y"
          :class="node.data.class + (node.data.first ? ' first' : '')"
          :r="node.children ? 7 : 5"
          tabindex="0"
          @click="onNodeClick(node.data)"
          @keyup.enter="onNodeClick(node.data)"
        >
          <title>{{ node.data.title }}</title>
        </circle>

        <text
          v-if="showId"
          :key="'text' + index" :x="node.x + 7" :y="node.y-7"
        >{{ node.data.id }}</text>
      </template>
    </template>
  </svg>
</template>

<script>
import { line } from 'd3-shape'
import { selectAll } from 'd3-selection'
import { drag } from 'd3-drag'
import {
  forceSimulation, forceLink, forceManyBody,
  forceX, forceY, forceCenter
} from 'd3-force'


export default {
  /*
    This component uses vue as the svg renderer and the rest is handle with d3 functions
    (events, data computing, etc.)
  */
  name: 'NodeMap',

  props: {
    nodes: { type: Array, required: true },
    links: { type: Array, required: true },
    id: { type: String, default: 'node-map' },
    showId: { type: Boolean, default: true }
  },

  data () {
    return {
      ready: false,
      width: 100,
      height: 100,
      simulation: forceSimulation(),
      lineGenerator: line().x(node => node.x).y(node => node.y)
    }
  },

  methods: {
    updateSize () {
      const { width, height } = this.$el.getBoundingClientRect()
      Object.assign(this.$data, { width, height })
    },

    init () {
      this.updateSize()

      this.nodes.forEach(node => {
        this.$set(node, 'x', this.width * 0.5)
        this.$set(node, 'y', this.height * 0.5)
      })

      this.setupForces()
      // Wait for DOM update so d3 can select
      this.$nextTick(this.initListeners)
      this.ready = true
    },

    initListeners () {
      // TODO replace with vue events ?

      selectAll(`#${this.id} circle`)
        .data(this.nodes)
        .call(drag()
          .on('start', this.onNodeDragStart)
          .on('drag', this.onNodeDrag)
          .on('end', this.onNodeDragEnd)
        )
    },

    setupForces () {
      this.simulation
        .nodes(this.nodes)
        .force('link', forceLink(this.links).id(d => d.id).distance(50))
        .force('charge', forceManyBody().strength(-150))
        .force('x', forceX())
        .force('y', forceY())
        .force('center', forceCenter(this.width * 0.5, this.height * 0.5))
    },

    // D3 DRAG EVENT HANDLERS

    onNodeDragStart (e, node) {
      if (!e.active) {
        this.simulation.alphaTarget(0.8).restart()
      }
      node.fx = node.x
      node.fy = node.y
    },

    onNodeDrag (e, node) {
      node.fx = e.x
      node.fy = e.y
    },

    onNodeDragEnd (e, node) {
      if (!e.active) {
        this.simulation.alphaTarget(0)
      }
      node.fx = null
      node.fy = null
    },

    // BASIC HANDLERS

    onNodeClick (nodeData) {
      const { id, parents } = nodeData
      if (parents) {
        this.$emit('open', parents[0].id, id)
      } else {
        this.$emit('open', id)
      }
    }
  },

  async mounted () {
    this.init()
  }
}
</script>

<style lang="scss" scoped>
.link {
  stroke: grey;

  &.siblings {
    stroke-dasharray: 4;
  }

  &.parents {
    stroke: red;
    opacity: .3;
  }
}

text {
  font-size: 0.7rem;
  user-select: none;
}

@each $id in map-keys($families){
  .family-#{$id} {
    fill: setColorFromId($id);
    @if $id == 9 {
      stroke: black;
    }
  }
}

.first {
  stroke-width: 2px;
}
</style>
