/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: BUSL-1.1
 */

import Component from '@ember/component';
import dayjs from 'dayjs';
import Calendar from 'dayjs/plugin/calendar';

import { select, pointer } from 'd3-selection';
import { scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale';
import { schemeTableau10 } from 'd3-scale-chromatic';
import { area, stack, stackOrderReverse } from 'd3-shape';
import { max, extent, bisector } from 'd3-array';
import { set } from '@ember/object';

dayjs.extend(Calendar);

function niceTimeWithSeconds(d) {
  return dayjs(d).calendar(null, {
    sameDay: '[Today at] h:mm:ss A',
    lastDay: '[Yesterday at] h:mm:ss A',
    lastWeek: '[Last] dddd at h:mm:ss A',
    sameElse: 'MMM DD at h:mm:ss A',
  });
}

export default Component.extend({
  data: null,
  empty: false,
  actions: {
    redraw: function (evt) {
      this.drawGraphs();
    },
    change: function (evt) {
      this.set('data', evt.data.series);
      this.drawGraphs();
      this.rerender();
    },
  },

  drawGraphs: function () {
    if (!this.data) {
      set(this, 'empty', true);
      return;
    }

    let svg = (this.svg = select(this.element.querySelector('svg.sparkline')));
    svg.on('mouseover mousemove mouseout', null);
    svg.selectAll('path').remove();
    svg.selectAll('rect').remove();

    let bb = svg.node().getBoundingClientRect();
    let w = bb.width;
    let h = bb.height;

    // To be safe, filter any series that actually have no data points. This can
    // happen thanks to our current provider contract allowing empty arrays for
    // series data if there is no value.
    let maybeData = this.data || {};
    let series = maybeData.data || [];
    let labels = maybeData.labels || {};
    let unitSuffix = maybeData.unitSuffix || '';
    let keys = Object.keys(labels).filter((l) => l != 'Total');

    if (series.length == 0 || keys.length == 0) {
      // Put the graph in an error state that might get fixed if metrics show up
      // on next poll.
      set(this, 'empty', true);
      return;
    } else {
      set(this, 'empty', false);
    }

    let st = stack().keys(keys).order(stackOrderReverse);

    let stackData = st(series);

    // Sum all of the values for each point to get max range. Technically
    // stackData contains this but I didn't find reliable documentation on
    // whether we can rely on the highest stacked area to always be first/last
    // in array etc. so this is simpler.
    let summed = series.map((d) => {
      let sum = 0;
      keys.forEach((l) => {
        sum = sum + d[l];
      });
      return sum;
    });

    let x = scaleTime()
      .domain(extent(series, (d) => d.time))
      .range([0, w]);

    let y = scaleLinear()
      .domain([0, max(summed)])
      .range([h, 0]);

    let a = area()
      .x((d) => x(d.data.time))
      .y1((d) => y(d[0]))
      .y0((d) => y(d[1]));

    // Use the grey/red we prefer by default but have more colors available in
    // case user adds extra series with a custom provider.
    let colorScheme = ['#DCE0E6', '#C73445'].concat(schemeTableau10);

    if (keys.includes('Outbound')) {
      colorScheme = ['#DCE0E6', '#0E40A3'].concat(schemeTableau10);
    }
    let color = scaleOrdinal(colorScheme).domain(keys);

    svg
      .selectAll('path')
      .data(stackData)
      .join('path')
      .attr('fill', ({ key }) => color(key))
      .attr('stroke', ({ key }) => color(key))
      .attr('d', a);

    let cursor = svg
      .append('rect')
      .attr('class', 'cursor')
      .style('visibility', 'hidden')
      .attr('width', 1)
      .attr('height', h)
      .attr('x', 0)
      .attr('y', 0);

    let tooltip = select(this.element.querySelector('.tooltip'));
    tooltip.selectAll('.sparkline-tt-legend').remove();
    tooltip.selectAll('.sparkline-tt-sum').remove();

    for (var k of keys) {
      let legend = tooltip.append('div').attr('class', 'sparkline-tt-legend');

      legend
        .append('div')
        .attr('class', 'sparkline-tt-legend-color')
        .style('background-color', color(k));

      legend.append('span').text(k).append('span').attr('class', 'sparkline-tt-legend-value');
    }

    let tipVals = tooltip.selectAll('.sparkline-tt-legend-value');

    // Add a label for the summed value
    if (keys.length > 1) {
      tooltip
        .append('div')
        .attr('class', 'sparkline-tt-sum')
        .append('span')
        .text('Total')
        .append('span')
        .attr('class', 'sparkline-tt-sum-value');
    }

    let self = this;
    svg
      .on('mouseover', function (e) {
        tooltip.style('visibility', 'visible');
        cursor.style('visibility', 'visible');
        // We update here since we might redraw the graph with user's cursor
        // stationary over it. If that happens mouseover fires but not
        // mousemove but the tooltip and cursor are wrong (based on old data).
        self.updateTooltip(e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor);
      })
      .on('mousemove', function (e) {
        self.updateTooltip(e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor);
      })
      .on('mouseout', function (e) {
        tooltip.style('visibility', 'hidden');
        cursor.style('visibility', 'hidden');
      });
  },
  willDestroyElement: function () {
    this._super(...arguments);
    if (typeof this.svg !== 'undefined') {
      this.svg.on('mouseover mousemove mouseout', null);
    }
  },
  updateTooltip: function (e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor) {
    let [mouseX] = pointer(e);
    cursor.attr('x', mouseX);

    let mouseTime = x.invert(mouseX);
    var bisectTime = bisector(function (d) {
      return d.time;
    }).left;
    let tipIdx = bisectTime(series, mouseTime);

    tooltip
      // 22 px is the correction to align the arrow on the tool tip with
      // cursor.
      .style('left', mouseX - 22 + 'px')
      .select('.sparkline-time')
      .text(niceTimeWithSeconds(mouseTime));

    // Get the summed value - that's the one of the top most stack.
    tooltip.select('.sparkline-tt-sum-value').text(`${shortNumStr(summed[tipIdx])}${unitSuffix}`);

    tipVals.nodes().forEach((n, i) => {
      let val = stackData[i][tipIdx][1] - stackData[i][tipIdx][0];
      select(n).text(`${shortNumStr(val)}${unitSuffix}`);
    });
    cursor.attr('x', mouseX);
  },
});

// Duplicated in vendor/metrics-providers/prometheus.js since we want that to
// remain a standalone example of a provider that could be loaded externally.
function shortNumStr(n) {
  if (n < 1e3) {
    if (Number.isInteger(n)) return '' + n;
    if (n >= 100) {
      // Go to 3 significant figures but wrap it in Number to avoid scientific
      // notation lie 2.3e+2 for 230.
      return Number(n.toPrecision(3));
    }
    if (n < 1) {
      // Very small numbers show with limited precision to prevent long string
      // of 0.000000.
      return Number(n.toFixed(2));
    } else {
      // Two sig figs is enough below this
      return Number(n.toPrecision(2));
    }
  }
  if (n >= 1e3 && n < 1e6) return +(n / 1e3).toPrecision(3) + 'k';
  if (n >= 1e6 && n < 1e9) return +(n / 1e6).toPrecision(3) + 'm';
  if (n >= 1e9 && n < 1e12) return +(n / 1e9).toPrecision(3) + 'g';
  if (n >= 1e12) return +(n / 1e12).toFixed(0) + 't';
}
