<template lang='pug'>
  v-container(fluid, grid-list-lg)
    v-layout(row wrap)
      v-flex(xs12)
        .admin-header
          img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
          .admin-header-title
            .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:pages.visualize.title')}}
            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:pages.visualize.subtitle')}}
          v-spacer
          v-select.mx-5.animated.fadeInDown.wait-p1s(
            v-if='locales.length > 0'
            v-model='currentLocale'
            :items='locales'
            style='flex: 0 1 120px;'
            solo
            dense
            hide-details
            item-value='code'
            item-text='name'
          )
          v-btn-toggle.animated.fadeInDown(v-model='graphMode', color='primary', dense, rounded)
            v-btn.px-5(value='htree')
              v-icon(left, :color='graphMode === `htree` ? `primary` : `grey darken-3`') mdi-sitemap
              span.text-none {{$t('admin:pages.visualize.hierarchialTree')}}
            v-btn.px-5(value='hradial')
              v-icon(left, :color='graphMode === `hradial` ? `primary` : `grey darken-3`') mdi-chart-donut-variant
              span.text-none {{$t('admin:pages.visualize.hierarchialRadial')}}
            v-btn.px-5(value='rradial')
              v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
              span.text-none {{$t('admin:pages.visualize.relationalRadial')}}
        .admin-pages-visualize-svg(ref='svgContainer', v-show='pages.length >= 1')
        v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
</template>

<script>
import _ from 'lodash'
import * as d3 from 'd3'
import gql from 'graphql-tag'

/* global siteConfig, siteLangs */

export default {
  data() {
    return {
      graphMode: 'htree',
      width: 800,
      radius: 400,
      pages: [],
      locales: siteLangs,
      currentLocale: siteConfig.lang
    }
  },
  watch: {
    pages () {
      this.redraw()
    },
    graphMode () {
      this.redraw()
    }
  },
  methods: {
    goToPage (d) {
      const id = d.data.id
      if (id) {
        if (d3.event.ctrlKey || d3.event.metaKey) {
          const { href } = this.$router.resolve(String(id))
          window.open(href, '_blank')
        } else {
          this.$router.push(String(id))
        }
      }
    },
    bilink (root) {
      const map = new Map(root.descendants().map(d => [d.data.path, d]))
      for (const d of root.descendants()) {
        d.incoming = []
        d.outgoing = []
        d.data.links.forEach(i => {
          const relNode = map.get(i)
          if (relNode) {
            d.outgoing.push([d, relNode])
          }
        })
      }
      for (const d of root.descendants()) {
        for (const o of d.outgoing) {
          if (o[1]) {
            o[1].incoming.push(o)
          }
        }
      }
      return root
    },
    hierarchy (pages) {
      const map = new Map(pages.map(p => [p.path, p]))
      const getPage = path => map.get(path) || {
        path: path,
        title: path.split('/').slice(-1)[0],
        links: []
      }

      function recurse (depth, [parent, descendants]) {
        const truncatePath = path => _.take(path.split('/'), depth).join('/')
        const descendantsByChild =
          Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
            .map(([childPath, descendantsGroup]) => [getPage(childPath), _.sortBy(descendantsGroup, child => child.path)])
            .map(([child, descendantsGroup]) =>
              [child, _.filter(descendantsGroup, d => d.path !== child.path)])
        return {
          ...parent,
          children: descendantsByChild.map(_.partial(recurse, depth + 1))
        }
      }
      const root = { path: this.currentLocale, title: this.currentLocale, links: [] }
      // start at depth=2 because we're taking {locale} as the root and
      // all paths start with {locale}/
      return recurse(2, [root, pages])
    },
    /**
     * Relational Radial
     */
    drawRelations () {
      const data = this.hierarchy(this.pages)

      const line = d3.lineRadial()
        .curve(d3.curveBundle.beta(0.85))
        .radius(d => d.y)
        .angle(d => d.x)

      const tree = d3.cluster()
        .size([2 * Math.PI, this.radius - 100])

      const root = tree(this.bilink(d3.hierarchy(data)
        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))

      const svg = d3.create('svg')
        .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])

      const g = svg.append('g')

      svg.call(d3.zoom().on('zoom', function() {
        g.attr('transform', d3.event.transform)
      }))

      const link = g.append('g')
        .attr('stroke', '#CCC')
        .attr('fill', 'none')
        .selectAll('path')
        .data(root.descendants().flatMap(leaf => leaf.outgoing))
        .join('path')
        .style('mix-blend-mode', 'multiply')
        .attr('d', ([i, o]) => line(i.path(o)))
        .each(function(d) { d.path = this })

      g.append('g')
        .attr('font-family', 'sans-serif')
        .attr('font-size', 10)
        .selectAll('g')
        .data(root.descendants())
        .join('g')
        .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
        .append('text')
        .attr('dy', '0.31em')
        .attr('x', d => d.x < Math.PI ? 6 : -6)
        .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
        .attr('cursor', 'pointer')
        .text(d => d.data.title)
        .each(function(d) { d.text = this })
        .on('mouseover', overed)
        .on('mouseout', outed)
        .on('click', d => this.goToPage(d))
        .call(text => text.append('title').text(d => `${d.data.path}
          ${d.outgoing.length} outgoing
          ${d.incoming.length} incoming`))
        .clone(true).lower()
        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')

      function overed(d) {
        link.style('mix-blend-mode', null)
        d3.select(this).attr('font-weight', 'bold')
        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', '#2196F3').raise()
        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', '#2196F3').attr('font-weight', 'bold')
        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', '#E91E63').raise()
        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', '#E91E63').attr('font-weight', 'bold')
      }

      function outed(d) {
        link.style('mix-blend-mode', 'multiply')
        d3.select(this).attr('font-weight', null)
        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null)
        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null)
        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null)
        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null)
      }

      this.$refs.svgContainer.appendChild(svg.node())
    },
    /**
     * Hierarchical Tree
     */
    drawTree () {
      const data = this.hierarchy(this.pages)

      const treeRoot = d3.hierarchy(data)
      treeRoot.dx = 10
      treeRoot.dy = this.width / (treeRoot.height + 1)
      const root = d3.tree().nodeSize([treeRoot.dx, treeRoot.dy])(treeRoot)

      let x0 = Infinity
      let x1 = -x0
      root.each(d => {
        if (d.x > x1) x1 = d.x
        if (d.x < x0) x0 = d.x
      })

      const svg = d3.create('svg')
        .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])

      // this extra level is necessary because the element that we
      // apply the zoom tranform to must be above the element where
      // we apply the translation (`g`), or else zoom is wonky
      const gZoom = svg.append('g')

      svg.call(d3.zoom().on('zoom', function() {
        gZoom.attr('transform', d3.event.transform)
      }))

      const g = gZoom.append('g')
        .attr('font-family', 'sans-serif')
        .attr('font-size', 10)
        .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)

      g.append('g')
        .attr('fill', 'none')
        .attr('stroke', this.$vuetify.theme.dark ? '#999' : '#555')
        .attr('stroke-opacity', 0.4)
        .attr('stroke-width', 1.5)
        .selectAll('path')
        .data(root.links())
        .join('path')
        .attr('d', d3.linkHorizontal()
          .x(d => d.y)
          .y(d => d.x))

      const node = g.append('g')
        .attr('stroke-linejoin', 'round')
        .attr('stroke-width', 3)
        .selectAll('g')
        .data(root.descendants())
        .join('g')
        .attr('transform', d => `translate(${d.y},${d.x})`)

      node.append('circle')
        .attr('fill', d => d.children ? '#555' : '#999')
        .attr('r', 2.5)

      node.append('text')
        .attr('dy', '0.31em')
        .attr('x', d => d.children ? -6 : 6)
        .attr('text-anchor', d => d.children ? 'end' : 'start')
        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
        .attr('cursor', 'pointer')
        .text(d => d.data.title)
        .on('click', d => this.goToPage(d))
        .clone(true).lower()
        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')

      this.$refs.svgContainer.appendChild(svg.node())
    },
    /**
     * Hierarchical Radial
     */
    drawRadialTree () {
      const data = this.hierarchy(this.pages)

      const tree = d3.tree()
        .size([2 * Math.PI, this.radius])
        .separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)

      const root = tree(d3.hierarchy(data)
        .sort((a, b) => d3.ascending(a.data.title, b.data.title)))

      const svg = d3.create('svg')
        .style('font', '10px sans-serif')

      const g = svg.append('g')

      svg.call(d3.zoom().on('zoom', function () {
        g.attr('transform', d3.event.transform)
      }))

      // eslint-disable-next-line no-unused-vars
      const link = g.append('g')
        .attr('fill', 'none')
        .attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
        .attr('stroke-opacity', 0.4)
        .attr('stroke-width', 1.5)
        .selectAll('path')
        .data(root.links())
        .join('path')
        .attr('d', d3.linkRadial()
          .angle(d => d.x)
          .radius(d => d.y))

      const node = g.append('g')
        .attr('stroke-linejoin', 'round')
        .attr('stroke-width', 3)
        .selectAll('g')
        .data(root.descendants().reverse())
        .join('g')
        .attr('transform', d => `
          rotate(${d.x * 180 / Math.PI - 90})
          translate(${d.y},0)
        `)

      node.append('circle')
        .attr('fill', d => d.children ? '#555' : '#999')
        .attr('r', 2.5)

      node.append('text')
        .attr('dy', '0.31em')
        /* eslint-disable no-mixed-operators */
        .attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)
        .attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')
        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
        /* eslint-enable no-mixed-operators */
        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
        .attr('cursor', 'pointer')
        .text(d => d.data.title)
        .on('click', d => this.goToPage(d))
        .clone(true).lower()
        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')

      this.$refs.svgContainer.appendChild(svg.node())

      function autoBox() {
        const {x, y, width, height} = this.getBBox()
        return [x, y, width, height]
      }

      svg.attr('viewBox', autoBox)
    },
    redraw () {
      while (this.$refs.svgContainer.firstChild) {
        this.$refs.svgContainer.firstChild.remove()
      }
      if (this.pages.length > 0) {
        switch (this.graphMode) {
          case 'rradial':
            this.drawRelations()
            break
          case 'htree':
            this.drawTree()
            break
          case 'hradial':
            this.drawRadialTree()
            break
        }
      }
    }
  },
  apollo: {
    pages: {
      query: gql`
        query ($locale: String!) {
          pages {
            links(locale: $locale) {
              id
              path
              title
              links
            }
          }
        }
      `,
      variables () {
        return {
          locale: this.currentLocale
        }
      },
      fetchPolicy: 'network-only',
      update: (data) => data.pages.links,
      watchLoading (isLoading) {
        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
      }
    }
  }
}
</script>

<style lang='scss'>
.admin-pages-visualize-svg {
  text-align: center;
  // 100vh - header - title section - footer - content padding
  height: calc(100vh - 64px - 92px - 32px - 16px);

  > svg {
    height: 100%;
    width: 100%;
  }
}
</style>
