<script>
import { scaleLinear, scaleTime } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis';
import _ from 'underscore';
import { max, extent } from 'd3-array';
import { select } from 'd3-selection';
import GraphAxis from './graph/axis.vue';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';

const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };

export default {
  components: {
    GraphAxis,
    GraphFlag,
    GraphDeployment,
    GraphPath,
    GraphLegend,
  },
  mixins: [MonitoringMixin],
  props: {
    graphData: {
      type: Object,
      required: true,
    },
    deploymentData: {
      type: Array,
      required: true,
    },
    hoverData: {
      type: Object,
      required: false,
      default: () => ({}),
    },
    projectPath: {
      type: String,
      required: true,
    },
    tagsPath: {
      type: String,
      required: true,
    },
    showLegend: {
      type: Boolean,
      required: false,
      default: true,
    },
    smallGraph: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
  data() {
    return {
      baseGraphHeight: 450,
      baseGraphWidth: 600,
      graphHeight: 450,
      graphWidth: 600,
      graphHeightOffset: 120,
      margin: {},
      unitOfDisplay: '',
      yAxisLabel: '',
      legendTitle: '',
      reducedDeploymentData: [],
      measurements: measurements.large,
      currentData: {
        time: new Date(),
        value: 0,
      },
      currentXCoordinate: 0,
      currentCoordinates: {},
      showFlag: false,
      showFlagContent: false,
      timeSeries: [],
      graphDrawData: {},
      realPixelRatio: 1,
      seriesUnderMouse: [],
    };
  },
  computed: {
    outerViewBox() {
      return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
    },
    innerViewBox() {
      return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
    },
    axisTransform() {
      return `translate(70, ${this.graphHeight - 100})`;
    },
    paddingBottomRootSvg() {
      return {
        paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
      };
    },
    deploymentFlagData() {
      return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
    },
    shouldRenderData() {
      return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
    },
  },
  watch: {
    hoverData() {
      this.positionFlag();
    },
  },
  mounted() {
    this.draw();
  },
  methods: {
    showDot(path) {
      return this.showFlagContent && this.seriesUnderMouse.includes(path);
    },
    draw() {
      const breakpointSize = bp.getBreakpointSize();
      const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;

      this.margin = measurements.large.margin;

      if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
        this.graphHeight = 300;
        this.margin = measurements.small.margin;
        this.measurements = measurements.small;
      }

      this.yAxisLabel = this.graphData.y_label || 'Values';
      this.graphWidth = svgWidth - this.margin.left - this.margin.right;
      this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
      this.baseGraphHeight = this.graphHeight - 50;
      this.baseGraphWidth = this.graphWidth;

      // pixel offsets inside the svg and outside are not 1:1
      this.realPixelRatio = svgWidth / this.baseGraphWidth;

      // set the legends on the axes
      const [query] = this.graphData.queries;
      this.legendTitle = query ? query.label : 'Average';
      this.unitOfDisplay = query ? query.unit : '';

      if (this.shouldRenderData) {
        this.renderAxesPaths();
        this.formatDeployments();
      }
    },
    handleMouseOverGraph(e) {
      let point = this.$refs.graphData.createSVGPoint();
      point.x = e.clientX;
      point.y = e.clientY;
      point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
      point.x += 7;

      this.seriesUnderMouse = this.timeSeries.filter(series => {
        const mouseX = series.timeSeriesScaleX.invert(point.x);
        let minDistance = Infinity;

        const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
          const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
          if (distance < minDistance) {
            minDistance = distance;
            return x;
          }
          return closest;
        });

        return series.values.find(v => v.time.toString() === closestTickMark);
      });

      const firstTimeSeries = this.seriesUnderMouse[0];
      const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
      const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
      const d0 = firstTimeSeries.values[overlayIndex - 1];
      const d1 = firstTimeSeries.values[overlayIndex];
      if (d0 === undefined || d1 === undefined) return;
      const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
      const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
      const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
      const currentDeployXPos = this.mouseOverDeployInfo(point.x);

      eventHub.$emit('hoverChanged', {
        hoveredDate,
        currentDeployXPos,
      });
    },
    renderAxesPaths() {
      ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
        this.graphData.queries,
        this.graphWidth,
        this.graphHeight,
        this.graphHeightOffset,
      ));

      if (_.findWhere(this.timeSeries, { renderCanary: true })) {
        this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
      }

      const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
      const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);

      const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
      axisXScale.domain(d3.extent(allValues, d => d.time));
      axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);

      this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
        const seriesKeys = {};
        series.values.forEach(v => {
          seriesKeys[v.time] = true;
        });
        return {
          ...obj,
          ...seriesKeys,
        };
      }, {});

      const xAxis = d3
        .axisBottom()
        .scale(axisXScale)
        .ticks(this.graphWidth / 120)
        .tickFormat(timeScaleFormat);

      const yAxis = d3
        .axisLeft()
        .scale(axisYScale)
        .ticks(measurements.yTicks);

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

      const width = this.graphWidth;
      d3.select(this.$refs.baseSvg)
        .select('.y-axis')
        .call(yAxis)
        .selectAll('.tick')
        .each(function createTickLines(d, i) {
          if (i > 0) {
            d3.select(this)
              .select('line')
              .attr('x2', width)
              .attr('class', 'axis-tick');
          } // Avoid adding the class to the first tick, to prevent coloring
        }); // This will select all of the ticks once they're rendered
    },
  },
};
</script>

<template>
  <div
    class="prometheus-graph"
    @mouseover="showFlagContent = true;"
    @mouseleave="showFlagContent = false;"
  >
    <div class="prometheus-graph-header">
      <h5 class="prometheus-graph-title">{{ graphData.title }}</h5>
      <div class="prometheus-graph-widgets"><slot></slot></div>
    </div>
    <div :style="paddingBottomRootSvg" class="prometheus-svg-container">
      <svg ref="baseSvg" :viewBox="outerViewBox">
        <g :transform="axisTransform" class="x-axis" />
        <g class="y-axis" transform="translate(70, 20)" />
        <graph-axis
          :graph-width="graphWidth"
          :graph-height="graphHeight"
          :margin="margin"
          :measurements="measurements"
          :y-axis-label="yAxisLabel"
          :unit-of-display="unitOfDisplay"
        />
        <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
          <slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
          <graph-path
            v-for="(path, index) in timeSeries"
            :key="index"
            :generated-line-path="path.linePath"
            :generated-area-path="path.areaPath"
            :line-style="path.lineStyle"
            :line-color="path.lineColor"
            :area-color="path.areaColor"
            :current-coordinates="currentCoordinates[path.metricTag]"
            :show-dot="showDot(path)"
          />
          <graph-deployment
            :deployment-data="reducedDeploymentData"
            :graph-height="graphHeight"
            :graph-height-offset="graphHeightOffset"
          />
          <rect
            ref="graphOverlay"
            :width="graphWidth - 70"
            :height="graphHeight - 100"
            class="prometheus-graph-overlay"
            transform="translate(-5, 20)"
            @mousemove="handleMouseOverGraph($event);"
          />
        </svg>
        <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
          <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
            {{ s__('Metrics|No data to display') }}
          </text>
        </svg>
      </svg>
      <graph-flag
        v-if="shouldRenderData"
        :real-pixel-ratio="realPixelRatio"
        :current-x-coordinate="currentXCoordinate"
        :current-data="currentData"
        :graph-height="graphHeight"
        :graph-height-offset="graphHeightOffset"
        :show-flag-content="showFlagContent"
        :time-series="seriesUnderMouse"
        :unit-of-display="unitOfDisplay"
        :legend-title="legendTitle"
        :deployment-flag-data="deploymentFlagData"
        :current-coordinates="currentCoordinates"
      />
    </div>
    <graph-legend v-if="showLegend" :legend-title="legendTitle" :time-series="timeSeries" />
  </div>
</template>
