import castArray from 'lodash/castArray';
import merge from 'lodash/merge';
import { BLUE_500 } from '../../tokens/build/js/tokens';
import { GlBreakpointInstance } from '../breakpoints';
import { areDatesEqual } from '../datetime_utility';
import { engineeringNotation } from '../number_utils';
import { hexToRgba } from '../utils';
import { arrowSymbol, ANNOTATIONS_SERIES_NAME, CHART_TYPE_BAR, CHART_TYPE_LINE } from './constants';

const defaultAreaOpacity = 0.2;
const defaultFontSize = 12;
const defaultHeight = 400;
const defaultWidth = 300;
const validRenderers = ['canvas', 'svg'];
const toolboxHeight = 14;
const axes = {
  name: 'Value',
  type: 'value',
  nameLocation: 'center'
};
const xAxis = merge({}, axes, {
  boundaryGap: false,
  splitLine: {
    show: false
  }
});
const yAxis = merge({}, axes, {
  nameGap: 50,
  axisLabel: {
    formatter: num => engineeringNotation(num, 2)
  }
});
const grid = {
  top: 16,
  bottom: 44,
  left: 64,
  right: 32
};

/**
 * Options for charts where the y-axis is the metrics axis.
 */
const defaultChartOptions = {
  grid,
  xAxis,
  yAxis,
  legend: {
    show: false
  }
};
const gridWithSecondaryYAxis = {
  ...grid,
  right: 64
};
const lineStyle = {
  symbol: 'circle',
  type: 'line',
  width: 2
};

/**
 * Annotations series consists of annotations lines
 * along with markers. Annotations co-exist with data
 * series but have their own virtual coords so that they stay put
 * irrespective of data series extents.
 */
const annotationsYAxisCoords = {
  min: 0,
  pos: 3,
  // 3% height of chart's grid
  max: 100,
  show: false
};
const symbolSize = 6;

/**
 * These comparison operators are currently in monitoring
 * charts that have alerting related data.
 *
 * {Array} Possible values for greater than
 */
const GREATER_THAN = ['>', '&gt;'];

/**
 * These comparison operators are currently in monitoring
 * charts that have alerting related data.
 *
 * {Array} Possible values for less than
 */
const LESS_THAN = ['<', '&lt;'];

/**
 * All default dataZoom configs will have slider & inside
 * (for reference, see https://gitlab.com/gitlab-org/gitlab-ui/issues/240)
 * Inside is disabled for larger viewports (lg and xl)
 * and is specifically to enable touch zoom for mobile devices
 * @param {Object} options
 */
const getDataZoomConfig = function () {
  let {
    filterMode = 'none'
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const disabledBreakpoints = ['lg', 'xl'];
  const disabled = disabledBreakpoints.includes(GlBreakpointInstance.getBreakpointSize());
  const minSpan = filterMode === 'none' ? 0.01 : null;
  return {
    grid: {
      bottom: 81
    },
    xAxis: {
      nameGap: 67
    },
    dataZoom: [{
      type: 'slider',
      bottom: 22,
      filterMode,
      minSpan
    }, {
      type: 'inside',
      filterMode,
      minSpan,
      disabled
    }]
  };
};

// All chart options can be merged but series
// needs to be concatenated.
// Series can be an object for single series or
// an array of objects.
const mergeSeriesToOptions = function (options) {
  let series = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
  const {
    series: optSeries = []
  } = options;
  return {
    ...options,
    series: [...castArray(series), ...castArray(optSeries)]
  };
};

/**
 * If an annotation series exists, the chart options should have an
 * array of yAxis settings so that the series can exist in its own
 * coordinate system without interfering with the data series
 *
 * @param {Object} options options to merge annotation series yAxis with
 * @param {Boolean} hasAnnotations if annotation series yAxis should be merged
 * @returns {Object} options
 */
const mergeAnnotationAxisToOptions = function (options) {
  let hasAnnotations = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  return {
    ...options,
    ...(hasAnnotations && {
      yAxis: [options.yAxis, annotationsYAxisCoords]
    })
  };
};
const dataZoomAdjustments = dataZoom => {
  // handle cases where dataZoom is array and object.
  const useSlider = dataZoom && Array.isArray(dataZoom) ? dataZoom.length : Boolean(dataZoom);
  return useSlider ? getDataZoomConfig({
    filterMode: 'weakFilter'
  }) : [];
};

/**
 * Generate eCharts markArea arrays for thresholds and annotations.
 *
 * This method purposefully has no knowledge of comparison
 * operators used in thresholds as it is not necessary and instead
 * expects explict value bounds
 *
 * Examples:
 * { min: 7, max: 10 } => markArea from 7 to 10
 * { min: 1, max: 7 } => markArea from 1 to 7
 *
 * If min and max are equal it would be markLine and would be
 * generated by `generateMarkLines`
 *
 * @param {Object} threshold Threshold/Annotation object with min and max values
 * @param {String} axis markArea is generated against this axis
 * @returns {Array}
 */
const generateMarkArea = function (_ref) {
  let {
    min,
    max
  } = _ref;
  let axis = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yAxis';
  return [{
    [axis]: min
  }, {
    [axis]: max
  }];
};

/**
 * Generate eCharts markLine objects for thresholds and annotations.
 *
 * This method purposefully has no knowledge of comparison
 * operators used in thresholds as it is not necessary and instead
 * expects explict value bounds
 *
 * In order to continue supporting existing thresholds format, min
 * is passed as undefined so the correct markLine object is generated.
 *
 * For annotations, min and max will be the same value.
 *
 * Threshold Examples:
 * { max: 7 } => markLine at 7
 *
 * Annotation Examples:
 * { min: 7, max: 7 } => markLine at 7
 *
 * @param {Object} threshold Threshold/Annotation object with min and max values
 * @param {String} axis markLine is generated against this axis
 * @returns {Object}
 */
const generateMarkLines = function (_ref2) {
  let {
    min,
    max
  } = _ref2;
  let axis = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yAxis';
  if (min) {
    return {
      [axis]: min
    };
  }
  return {
    [axis]: max
  };
};

/**
 * Generates markPoints that are placed under the markLines.
 *
 * These are used only in annotation lines. For annotation lines,
 * both min and max are same values so only one is enough to generate
 * the markPoint.
 *
 * @param {Object} annotation object
 * @return {Object}
 */
const generateMarkPoints = _ref3 => {
  let {
    min,
    tooltipData
  } = _ref3;
  return {
    name: 'annotations',
    xAxis: min,
    yAxis: 0,
    tooltipData
  };
};

/**
 * Generate set of markAreas and markLines to draw on charts
 * as alert thresholds.
 *
 * Alert thresholds always have a markLine associated with a markArea
 *
 * @param {Array} thresholds Array of alert thresholds
 * @returns {Object} markAreas and markLines
 */
function getThresholdConfig(thresholds) {
  if (!thresholds.length) {
    return {};
  }
  const data = thresholds.reduce((acc, alert) => {
    const {
      threshold,
      operator
    } = alert;
    if (GREATER_THAN.includes(operator)) {
      acc.areas.push(generateMarkArea({
        min: threshold,
        max: Infinity
      }));
    } else if (LESS_THAN.includes(operator)) {
      acc.areas.push(generateMarkArea({
        min: Number.NEGATIVE_INFINITY,
        max: threshold
      }));
    }
    acc.lines.push(generateMarkLines({
      max: threshold
    }));
    return acc;
  }, {
    lines: [],
    areas: []
  });
  return {
    markLine: {
      data: data.lines
    },
    markArea: {
      data: data.areas,
      zlevel: -1
    }
  };
}

/**
 * This method is only for testing both markLines and markAreas
 * that are used for annotations.
 *
 * `getAnnotationsConfig` as of %12.10 supports only markLines.
 * But this method can generate lines, points and areas.
 *
 * @param {Array} annotations Array of annotation objects
 * @returns {Object} { areas, lines, points }
 */
const parseAnnotations = annotations => annotations.reduce((acc, annotation) => {
  // because only markLines are supported all cases will
  // satisfy this condition. This is more of a sanity check
  // until markAreas are supported.
  // https://gitlab.com/gitlab-org/gitlab/-/issues/212910
  if (areDatesEqual(annotation.min, annotation.max)) {
    acc.lines.push(generateMarkLines(annotation, 'xAxis'));
    acc.points.push(generateMarkPoints(annotation));
    return acc;
  }
  acc.areas.push(generateMarkArea(annotation, 'xAxis'));
  return acc;
}, {
  areas: [],
  lines: [],
  points: []
});

/**
 * Generate set of markAreas and markLines to draw on charts
 * as annotations.
 *
 * Annotations as of %12.10 will only be markLines.
 * markAreas are not supported yet. They are generated by
 * `parseAnnotations` but not rendered.
 *
 * @param {Array} annotations Array of annotations
 * @returns {Object} { markLines }
 */
const getAnnotationsConfig = annotations => {
  if (!annotations.length) {
    return {};
  }

  // annotations parsing is moved out so that it can be tested
  // for markLines and markAreas.
  const {
    lines,
    points
  } = parseAnnotations(annotations);
  return {
    markLine: {
      lineStyle: {
        color: BLUE_500
      },
      silent: true,
      data: lines
    },
    markPoint: {
      itemStyle: {
        color: BLUE_500
      },
      symbol: arrowSymbol,
      symbolSize: '8',
      symbolOffset: [0, ' 60%'],
      data: points
    }
  };
};

/**
 * Given thresholds and annotations options, this method generates
 * an annotation series that co-exists along with the data series.
 *
 * yAxis option is useful in cases where multiple yAxis settings
 * are used in a chart. Currently, all of our charts have single
 * yAxis settings.
 *
 * @param {Object} params Thresholds, annotations and yAxis options
 * @returns {Object} Annotation series
 */
const generateAnnotationSeries = function (annotations) {
  let yAxisIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
  if (!annotations.length) {
    return null;
  }
  return merge({
    name: ANNOTATIONS_SERIES_NAME,
    yAxisIndex,
    type: 'scatter',
    data: []
  }, getAnnotationsConfig(annotations));
};

/**
 * This method generates the data series and relevant defaults for a bar chart
 *
 * @param {Object} options
 * @param {string} options.name - xAxis name for the chart
 * @param {string} options.color - color to render the data series
 * @param {Array} options.data - data set to be rendered
 * @param {string} [options.stack] - controls how the stacked charts should render either `stacked` or `tiled`
 * @param {number} [options.yAxisIndex] - specifies the yAxis to use (if there are multiple)
 * @returns {Object} Bar chart series
 */
const generateBarSeries = _ref4 => {
  let {
    name,
    color,
    stack,
    data = [],
    yAxisIndex = 0
  } = _ref4;
  return {
    type: CHART_TYPE_BAR,
    name,
    data,
    stack,
    barMaxWidth: '50%',
    yAxisIndex,
    itemStyle: {
      color: hexToRgba(color, 0.2),
      borderColor: color,
      borderWidth: 1
    },
    emphasis: {
      itemStyle: {
        color: hexToRgba(color, 0.4)
      }
    }
  };
};

/**
 * This method generates the data series and relevant defaults for a line chart
 *
 * @param {Object} options
 * @param {string} options.name - xAxis name for the chart
 * @param {string} options.color - color to render the data series
 * @param {Array} options.data - data set to be rendered
 * @param {number} [options.yAxisIndex] - specifies the yAxis to use (if there are multiple)
 * @returns {Object} Line chart series
 */
const generateLineSeries = _ref5 => {
  let {
    name,
    color,
    data = [],
    yAxisIndex = 0
  } = _ref5;
  return {
    name,
    data,
    type: CHART_TYPE_LINE,
    yAxisIndex,
    lineStyle: {
      color
    },
    itemStyle: {
      color
    }
  };
};
const getTooltipAxisConfig = dimensionAxis => {
  if (!['xAxis', 'yAxis'].includes(dimensionAxis)) {
    throw new Error(`\`dimensionAxis\` must be "xAxis" or "yAxis", received ${dimensionAxis}`);
  }
  if (dimensionAxis === 'xAxis') {
    return {
      dimensionIndex: 0,
      metricIndex: 1,
      valueAxis: 'yAxis'
    };
  }
  return {
    dimensionIndex: 1,
    metricIndex: 0,
    valueAxis: 'xAxis'
  };
};
const getTooltipTitle = function () {
  let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  let titleAxisName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  let dimensionIndex = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  if (!params) return '';
  const title = params.seriesData.reduce((acc, _ref6) => {
    let {
      value
    } = _ref6;
    const dimension = value[dimensionIndex];
    if (acc.includes(dimension)) {
      return acc;
    }
    return [...acc, dimension];
  }, []).join(', ');
  return titleAxisName ? `${title} (${titleAxisName})` : title;
};
const getTooltipContent = function () {
  let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  let valueAxisName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  let metricIndex = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
  if (!params) {
    return {};
  }
  const {
    seriesData
  } = params;
  if (seriesData.length === 1) {
    const {
      value,
      seriesName
    } = seriesData[0];
    return {
      [valueAxisName || seriesName]: {
        value: value[metricIndex],
        color: '' // ignore color when showing a single series
      }
    };
  }
  return seriesData.reduce((acc, _ref7) => {
    let {
      value,
      seriesName,
      color
    } = _ref7;
    acc[seriesName] = {
      value: value[metricIndex],
      color
    };
    return acc;
  }, {});
};

/**
 * Enriches tooltip parameters with stack information for proper series ordering.
 *
 * In stacked column charts, series are grouped by their `stack` property to determine
 * visual stacking and tooltip display order. ECharts doesn't provide stack data
 * in tooltip params by default, so this function manually adds it by mapping
 * series names to their configured stack values.
 *
 * @param {Object} params - Tooltip parameters from ECharts
 * @param {Object} options - Chart configuration with series definitions
 * @returns {Object} Enhanced tooltip parameters with stack data
 */
const getTooltipParams = function () {
  let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
  let options = arguments.length > 1 ? arguments[1] : undefined;
  if (!params) return null;
  const {
    series: optionsSeries = []
  } = options !== null && options !== void 0 ? options : {};
  const seriesStacks = Object.fromEntries(optionsSeries.map(_ref8 => {
    let {
      name,
      stack
    } = _ref8;
    return [name, stack];
  }));
  return {
    ...params,
    seriesData: params.seriesData.map(series => ({
      ...series,
      stack: seriesStacks[series.seriesName]
    }))
  };
};

/**
 * The method works well if tooltip content should be against y-axis values.
 * However, for bar charts, the tooltip should be against x-axis values.
 * This method should be updated to work with all types of visualizations.
 * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/674
 *
 * @param {Object} params series data
 * @param {String} yAxisTitle y-axis title
 * @returns {Object} tooltip title and content
 * @deprecated Use getTooltipContent and getTooltipContent to obtain the tooltip
 */
const getDefaultTooltipContent = function (params) {
  let yAxisTitle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  const seriesDataLength = params.seriesData.length;
  const {
    xLabels,
    tooltipContent
  } = params.seriesData.reduce((acc, chartItem) => {
    const [title, value] = chartItem.value || [];
    // Let's use the y axis title as series name when only one series exists
    // This way, TooltipDefaultFormat will display the y axis title as label
    const seriesName = seriesDataLength === 1 && yAxisTitle ? yAxisTitle : chartItem.seriesName;
    const color = seriesDataLength === 1 ? '' : chartItem.color;
    acc.tooltipContent[seriesName] = {
      value,
      color
    };
    if (!acc.xLabels.includes(title)) {
      acc.xLabels.push(title);
    }
    return acc;
  }, {
    xLabels: [],
    tooltipContent: {}
  });
  return {
    xLabels,
    tooltipContent
  };
};

export { annotationsYAxisCoords, axes, dataZoomAdjustments, defaultAreaOpacity, defaultChartOptions, defaultFontSize, defaultHeight, defaultWidth, generateAnnotationSeries, generateBarSeries, generateLineSeries, getAnnotationsConfig, getDataZoomConfig, getDefaultTooltipContent, getThresholdConfig, getTooltipAxisConfig, getTooltipContent, getTooltipParams, getTooltipTitle, grid, gridWithSecondaryYAxis, lineStyle, mergeAnnotationAxisToOptions, mergeSeriesToOptions, parseAnnotations, symbolSize, toolboxHeight, validRenderers, xAxis, yAxis };
