import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape';
import { extent, max, sum } from 'd3-array';
import { timeMinute, timeSecond } from 'd3-time';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';

const d3 = {
  scaleLinear,
  scaleTime,
  line,
  area,
  curveLinear,
  extent,
  max,
  timeMinute,
  timeSecond,
  sum,
};

const defaultColorPalette = {
  blue: ['#1f78d1', '#8fbce8'],
  orange: ['#fc9403', '#feca81'],
  red: ['#db3b21', '#ed9d90'],
  green: ['#1aaa55', '#8dd5aa'],
  purple: ['#6666c4', '#d1d1f0'],
};

const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];

const defaultStyleOrder = ['solid', 'dashed', 'dotted'];

function queryTimeSeries(query, graphDrawData, lineStyle) {
  let usedColors = [];
  let renderCanary = false;
  const timeSeriesParsed = [];

  function pickColor(name) {
    let pick;
    if (name && defaultColorPalette[name]) {
      pick = name;
    } else {
      const unusedColors = _.difference(defaultColorOrder, usedColors);
      if (unusedColors.length > 0) {
        [pick] = unusedColors;
      } else {
        usedColors = [];
        [pick] = defaultColorOrder;
      }
    }
    usedColors.push(pick);
    return defaultColorPalette[pick];
  }

  function findByDate(series, time) {
    const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
    if (val) {
      return val.value;
    }
    return NaN;
  }

  // The timeseries data may have gaps in it
  // but we need a regularly-spaced set of time/value pairs
  // this gives us a complete range of one minute intervals
  // offset the same amount as the original data
  const [minX, maxX] = graphDrawData.xDom;
  const offset = d3.timeMinute(minX) - Number(minX);
  const datesWithoutGaps = d3.timeSecond
    .every(60)
    .range(d3.timeMinute.offset(minX, -1), maxX)
    .map(d => d - offset);

  query.result.forEach((timeSeries, timeSeriesNumber) => {
    let metricTag = '';
    let lineColor = '';
    let areaColor = '';
    let shouldRenderLegend = true;
    const timeSeriesValues = timeSeries.values.map(d => d.value);
    const maximumValue = d3.max(timeSeriesValues);
    const accum = d3.sum(timeSeriesValues);
    const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');

    if (trackName === 'Canary') {
      renderCanary = true;
    }

    const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
    const seriesCustomizationData =
      query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });

    if (seriesCustomizationData) {
      metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
      [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
      shouldRenderLegend = false;
    } else {
      metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
      [lineColor, areaColor] = pickColor();
      if (timeSeriesParsed.length > 1) {
        shouldRenderLegend = false;
      }
    }

    if (!shouldRenderLegend) {
      if (!timeSeriesParsed[0].tracksLegend) {
        timeSeriesParsed[0].tracksLegend = [];
      }
      timeSeriesParsed[0].tracksLegend.push({
        max: maximumValue,
        average: accum / timeSeries.values.length,
        lineStyle,
        lineColor,
        metricTag,
      });
    }

    const values = datesWithoutGaps.map(time => ({
      time,
      value: findByDate(timeSeries.values, time),
    }));

    timeSeriesParsed.push({
      linePath: graphDrawData.lineFunction(values),
      areaPath: graphDrawData.areaBelowLine(values),
      timeSeriesScaleX: graphDrawData.timeSeriesScaleX,
      timeSeriesScaleY: graphDrawData.timeSeriesScaleY,
      values: timeSeries.values,
      max: maximumValue,
      average: accum / timeSeries.values.length,
      lineStyle,
      lineColor,
      areaColor,
      metricTag,
      trackName,
      shouldRenderLegend,
      renderCanary,
    });
  });

  return timeSeriesParsed;
}

function xyDomain(queries) {
  const allValues = queries.reduce(
    (allQueryResults, query) =>
      allQueryResults.concat(
        query.result.reduce((allResults, result) => allResults.concat(result.values), []),
      ),
    [],
  );

  const xDom = d3.extent(allValues, d => d.time);
  const yDom = [0, d3.max(allValues.map(d => d.value))];

  return {
    xDom,
    yDom,
  };
}

export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) {
  const { xDom, yDom } = xyDomain(queries);

  const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
  const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);

  timeSeriesScaleX.domain(xDom);
  timeSeriesScaleX.ticks(d3.timeMinute, 60);
  timeSeriesScaleY.domain(yDom);

  const defined = d => !Number.isNaN(d.value) && d.value != null;

  const lineFunction = d3
    .line()
    .defined(defined)
    .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
    .x(d => timeSeriesScaleX(d.time))
    .y(d => timeSeriesScaleY(d.value));

  const areaBelowLine = d3
    .area()
    .defined(defined)
    .curve(d3.curveLinear)
    .x(d => timeSeriesScaleX(d.time))
    .y0(graphHeight - graphHeightOffset)
    .y1(d => timeSeriesScaleY(d.value));

  const areaAboveLine = d3
    .area()
    .defined(defined)
    .curve(d3.curveLinear)
    .x(d => timeSeriesScaleX(d.time))
    .y0(0)
    .y1(d => timeSeriesScaleY(d.value));

  return {
    lineFunction,
    areaBelowLine,
    areaAboveLine,
    xDom,
    yDom,
    timeSeriesScaleX,
    timeSeriesScaleY,
  };
}

export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
  const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset);

  const timeSeries = queries.reduce((series, query, index) => {
    const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
    return series.concat(queryTimeSeries(query, graphDrawData, lineStyle));
  }, []);

  return {
    timeSeries,
    graphDrawData,
  };
}
