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

      <circle
        v-for="(node, index) in nodes"
        :key="'circle' + index"
        :cx="node.x"
        :cy="node.y"
        class="svg-dot-btn"
        :class="['svg-dot-btn-' + node.data.variant, { 'origin': node.data.isOrigin }]"
        tabindex="0"
        @click="onNodeClick(node.data)"
        @keyup.enter="onNodeClick(node.data)"
        @mouseenter="activeNode = node"
      />

      <g>
        <!-- rect for forcing full render -->
        <rect
          x="-50%" y="-50%" fill="transparent"
          width="100%" height="100%" style="pointer-events: none;"
        />
        <foreignObject
          v-if="activeNode"
          :x="activeNode.x"
          :y="activeNode.y"
        >
          <dot-button
            :variant="activeNode.data.variant"
            class="active"
            @click="onNodeClick(activeNode.data)"
            @mouseleave="activeNode = null"
          >
            <template v-if="activeNode.data.variant !== 'depart'">
              {{ activeNode.data.families[0].name }}<br>
            </template>
            {{ toCommaList(activeNode.data.authors) }},<br>
            <span v-if="activeNode.data.field_titre_regular" v-html="activeNode.data.field_titre_regular + ','" />
            <span v-html="activeNode.data.field_titre_italique || '<em>pas de titre ital</em>'" />
          </dot-button>
        </foreignObject>
      </g>
    </g>
  </svg>
</template>

<script>
import { line } from 'd3-shape'
import { selectAll } from 'd3-selection'
import { select, zoom } from 'd3'
import { drag } from 'd3-drag'
import {
  forceSimulation, forceLink, forceManyBody,
  forceX, forceY
} 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' }
  },

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

  computed: {
    viewBox () {
      const { width, height } = this
      return `-${width / 2} -${height / 2} ${width} ${height}`
    }
  },

  methods: {
    toCommaList (arr) {
      // FIXME TEMP some texts doesn't have authors
      try {
        return arr.map(({ name }) => name).join(', ')
      } catch {
        return 'pas d\'auteur⋅rices'
      }
    },

    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 ?
      const svg = select('#' + this.id)
      const map = select('#' + this.id + '-map')
      svg.call(zoom()
        .extent([[0, 0], [this.width, this.height]])
        .scaleExtent([0.3, 1])
        .on('zoom', ({ transform }) => {
          map.attr('transform', transform)
        }))

      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('charge', forceManyBody().strength((d) => {
          if (d.data.linkType === 'parents') return -2000
          if (d.data.linkType === 'siblings') return -500
          return -1000
        }))
        .force('link', forceLink(this.links).id(d => d.id).distance((d) => {
          if (d.linkType === 'parents') return 200
          if (d.linkType === 'siblings') return 100
          return 0
        }).strength(0.5))
        .force('x', forceX())
        .force('y', forceY())
    },

    // 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)
      }
    }
  },

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

<style lang="scss" scoped>
.link {
  stroke: grey;
  vector-effect: non-scaling-stroke;

  &.siblings {
    stroke-dasharray: 4;
  }

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

.svg-dot-btn {
  r: 9.5px;

  @each $color, $value in $theme-colors {
    &-#{$color} {
      fill: $value;

      @if $color == 'depart' {
        stroke: $black;
        stroke-width: 3px;
        vector-effect: non-scaling-stroke;
      }
    }
  }

  &.origin {
    r: 15px;
  }
}

foreignObject {
  height: 1px;
  width: 1px;
  overflow: visible;
}

.dot-btn {
  vertical-align: top;
  transform: translate(-50%, -50%);
}
</style>
