<script>
import * as d3 from 'd3';
import tooltip from '../directives/tooltip';
import Icon from './icon.vue';
import SvgGradient from './svg_gradient.vue';
import {
  GRADIENT_COLORS,
  GRADIENT_OPACITY,
  INVERSE_GRADIENT_COLORS,
  INVERSE_GRADIENT_OPACITY,
} from './bar_chart_constants';

/**
 * Renders a bar chart that can be dragged(scrolled) when the number
 * of elements to renders surpasses that of the available viewport space
 * while keeping even padding and a width of 24px (customizable)
 *
 * It can render data with the following format:
 * graphData: [{
 *   name: 'element' // x domain data
 *   value: 1 // y domain data
 * }]
 *
 * Used in:
 * - Contribution analytics - all of the rows describing pushes, merge requests and issues
 */

export default {
  directives: {
    tooltip,
  },
  components: {
    Icon,
    SvgGradient,
  },
  props: {
    graphData: {
      type: Array,
      required: true,
    },
    barWidth: {
      type: Number,
      required: false,
      default: 24,
    },
    yAxisLabel: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      minX: -40,
      minY: 0,
      vbWidth: 0,
      vbHeight: 0,
      vpWidth: 0,
      vpHeight: 350,
      preserveAspectRatioType: 'xMidYMid meet',
      containerMargin: {
        leftRight: 30,
      },
      viewBoxMargin: {
        topBottom: 150,
      },
      panX: 0,
      xScale: {},
      yScale: {},
      zoom: {},
      bars: {},
      xGraphRange: 0,
      isLoading: true,
      paddingThreshold: 50,
      showScrollIndicator: false,
      showLeftScrollIndicator: false,
      isGrabbed: false,
      isPanAvailable: false,
      gradientColors: GRADIENT_COLORS,
      gradientOpacity: GRADIENT_OPACITY,
      inverseGradientColors: INVERSE_GRADIENT_COLORS,
      inverseGradientOpacity: INVERSE_GRADIENT_OPACITY,
      maxTextWidth: 72,
      rectYAxisLabelDims: {},
      xAxisTextElements: {},
      yAxisRectTransformPadding: 20,
      yAxisTextTransformPadding: 10,
      yAxisTextRotation: 90,
    };
  },
  computed: {
    svgViewBox() {
      return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`;
    },
    xAxisLocation() {
      return `translate(${this.panX}, ${this.vbHeight})`;
    },
    barTranslationTransform() {
      return `translate(${this.panX}, 0)`;
    },
    scrollIndicatorTransform() {
      return `translate(${this.vbWidth - 80}, 0)`;
    },
    activateGrabCursor() {
      return {
        'svg-graph-container-with-grab': this.isPanAvailable,
        'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed,
      };
    },
    yAxisLabelRectTransform() {
      const rectWidth =
        this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
      const yCoord = this.vbHeight / 2 - rectWidth;

      return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`;
    },
    yAxisLabelTextTransform() {
      const rectWidth =
        this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
      const yCoord = this.vbHeight / 2 + rectWidth - 5;

      return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${
        this.yAxisTextRotation
      })`;
    },
  },
  mounted() {
    if (!this.allValuesEmpty) {
      this.draw();
    }
  },
  methods: {
    draw() {
      // update viewport
      this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight;
      // update viewbox
      this.vbWidth = this.vpWidth;
      this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom;
      let padding = 0;
      if (this.graphData.length * this.barWidth > this.vbWidth) {
        this.xGraphRange = this.graphData.length * this.barWidth;
        padding = this.calculatePadding(this.barWidth);
        this.showScrollIndicator = true;
        this.isPanAvailable = true;
      } else {
        this.xGraphRange = this.vbWidth - Math.abs(this.minX);
      }

      this.xScale = d3
        .scaleBand()
        .range([0, this.xGraphRange])
        .round(true)
        .paddingInner(padding);
      this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]);

      this.xScale.domain(this.graphData.map(d => d.name));
      this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]);

      // Zoom/Panning Function
      this.zoom = d3
        .zoom()
        .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]])
        .on('zoom', this.panGraph)
        .on('end', this.removeGrabStyling);

      const xAxis = d3.axisBottom().scale(this.xScale);

      const yAxis = d3
        .axisLeft()
        .scale(this.yScale)
        .ticks(4);

      const renderedXAxis = d3
        .select(this.$refs.baseSvg)
        .select('.x-axis')
        .call(xAxis);

      this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text');

      renderedXAxis.select('.domain').remove();

      renderedXAxis
        .selectAll('text')
        .style('text-anchor', 'end')
        .attr('dx', '-.3em')
        .attr('dy', '-.95em')
        .attr('class', 'tick-text')
        .attr('transform', 'rotate(-90)');

      renderedXAxis.selectAll('line').remove();

      const { maxTextWidth } = this;
      renderedXAxis.selectAll('text').each(function formatText() {
        const axisText = d3.select(this);
        let textLength = axisText.node().getComputedTextLength();
        let textContent = axisText.text();
        while (textLength > maxTextWidth && textContent.length > 0) {
          textContent = textContent.slice(0, -1);
          axisText.text(`${textContent}...`);
          textLength = axisText.node().getComputedTextLength();
        }
      });

      const width = this.vbWidth;

      const renderedYAxis = d3
        .select(this.$refs.baseSvg)
        .select('.y-axis')
        .call(yAxis);

      renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) {
        if (i > 0) {
          d3.select(this)
            .select('line')
            .attr('x2', width)
            .attr('class', 'axis-tick');
        }
      });

      // Add the panning capabilities
      if (this.isPanAvailable) {
        d3.select(this.$refs.baseSvg)
          .call(this.zoom)
          .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel
      }

      this.isLoading = false;
      // Update the yAxisLabel coordinates
      const labelDims = this.$refs.yAxisLabel.getBBox();
      this.rectYAxisLabelDims = {
        height: labelDims.width + 10,
      };
    },
    panGraph() {
      const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold;
      const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll;
      this.isGrabbed = true;
      this.panX = d3.event.transform.x;

      if (d3.event.transform.x === 0) {
        this.showLeftScrollIndicator = false;
      } else {
        this.showLeftScrollIndicator = true;
        this.showScrollIndicator = true;
      }

      if (!graphMaxPan) {
        this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold);
        this.showScrollIndicator = false;
      }
    },
    setTooltipTitle(data) {
      return data !== null ? `${data.name}: ${data.value}` : '';
    },
    calculatePadding(desiredBarWidth) {
      const widthWithMargin = this.vbWidth - Math.abs(this.minX);
      const dividend = widthWithMargin - this.graphData.length * desiredBarWidth;
      const divisor = widthWithMargin - desiredBarWidth;

      return dividend / divisor;
    },
    removeGrabStyling() {
      this.isGrabbed = false;
    },
    barHoveredIn(index) {
      this.xAxisTextElements[index].classList.add('x-axis-text');
    },
    barHoveredOut(index) {
      this.xAxisTextElements[index].classList.remove('x-axis-text');
    },
  },
};
</script>
<template>
  <div ref="svgContainer" :class="activateGrabCursor" class="svg-graph-container">
    <svg
      ref="baseSvg"
      :width="vpWidth"
      :height="vpHeight"
      :viewBox="svgViewBox"
      :preserveAspectRatio="preserveAspectRatioType"
    >
      <g ref="xAxis" :transform="xAxisLocation" class="x-axis" />
      <g v-if="!isLoading">
        <template v-for="(data, index) in graphData">
          <rect
            :key="index"
            v-tooltip
            :width="xScale.bandwidth()"
            :x="xScale(data.name)"
            :y="yScale(data.value)"
            :height="vbHeight - yScale(data.value)"
            :transform="barTranslationTransform"
            :title="setTooltipTitle(data)"
            class="bar-rect"
            data-placement="top"
            @mouseover="barHoveredIn(index);"
            @mouseout="barHoveredOut(index);"
          />
        </template>
      </g>
      <rect :height="vbHeight + 100" transform="translate(-100, -5)" width="100" fill="#fff" />
      <g class="y-axis-label">
        <line :x1="0" :x2="0" :y1="0" :y2="vbHeight" transform="translate(-35, 0)" stroke="black" />
        <!-- Get text length and change the height of this rect accordingly -->
        <rect
          :height="rectYAxisLabelDims.height"
          :transform="yAxisLabelRectTransform"
          :width="30"
          fill="#fff"
        />
        <text ref="yAxisLabel" :transform="yAxisLabelTextTransform">{{ yAxisLabel }}</text>
      </g>
      <g class="y-axis" />
      <g v-if="showScrollIndicator">
        <rect
          :height="vbHeight + 100"
          :transform="`translate(${vpWidth - 60}, -5)`"
          width="40"
          fill="#fff"
        />
        <icon
          :x="vpWidth - 50"
          :y="vbHeight / 2"
          :width="14"
          :height="14"
          name="chevron-right"
          class="animate-flicker"
        />
      </g>
      <!-- The line that shows up when the data elements surpass the available width -->
      <g v-if="showScrollIndicator" :transform="scrollIndicatorTransform">
        <rect :height="vbHeight" x="0" y="0" width="20" fill="url(#shadow-gradient)" />
      </g>
      <!-- Left scroll indicator -->
      <g v-if="showLeftScrollIndicator" transform="translate(0, 0)">
        <rect :height="vbHeight" x="0" y="0" width="20" fill="url(#left-shadow-gradient)" />
      </g>
      <svg-gradient
        :colors="gradientColors"
        :opacity="gradientOpacity"
        identifier-name="shadow-gradient"
      />
      <svg-gradient
        :colors="inverseGradientColors"
        :opacity="inverseGradientOpacity"
        identifier-name="left-shadow-gradient"
      />
    </svg>
  </div>
</template>
