import _ from 'lodash';
import { getNiceTickValues, getTickValuesFixedDomain } from 'recharts-scale';
import * as d3Scales from 'd3-scale';
import {
  stack as shapeStack,
  stackOrderNone,
  stackOffsetExpand,
  stackOffsetNone,
  stackOffsetSilhouette,
  stackOffsetWiggle,
} from 'd3-shape';
import { ReactElement, ReactNode } from 'react';
import { isNumOrStr, uniqueId, isNumber, getPercentValue, mathSign, findEntryInArray } from './DataUtils';
import { Legend } from '../component/Legend';
import { findAllByType, findChildByType, getDisplayName } from './ReactUtils';
// TODO: Cause of circular dependency. Needs refactor.
// import { RadiusAxisProps, AngleAxisProps } from '../polar/types';
import { LayoutType, PolarLayoutType, AxisType, TickItem, BaseAxisProps, DataKey, filterProps } from './types';

export function getValueByDataKey<T>(obj: T, dataKey: DataKey<any>, defaultValue?: any) {
  if (_.isNil(obj) || _.isNil(dataKey)) {
    return defaultValue;
  }

  if (isNumOrStr(dataKey as string)) {
    return _.get(obj, dataKey as string, defaultValue);
  }

  if (_.isFunction(dataKey)) {
    return dataKey(obj);
  }

  return defaultValue;
}
/**
 * Get domain of data by key
 * @param  {Array}   data      The data displayed in the chart
 * @param  {String}  key       The unique key of a group of data
 * @param  {String}  type      The type of axis
 * @param  {Boolean} filterNil Whether or not filter nil values
 * @return {Array} Domain of data
 */
export function getDomainOfDataByKey<T>(data: Array<T>, key: string, type: string, filterNil?: boolean) {
  const flattenData = _.flatMap(data, entry => getValueByDataKey(entry, key));

  if (type === 'number') {
    const domain = flattenData.filter(entry => isNumber(entry) || parseFloat(entry));

    return domain.length ? [_.min(domain), _.max(domain)] : [Infinity, -Infinity];
  }

  const validateData = filterNil ? flattenData.filter(entry => !_.isNil(entry)) : flattenData;

  // 支持Date类型的x轴
  return validateData.map(entry => (isNumOrStr(entry) || entry instanceof Date ? entry : ''));
}

export const calculateActiveTickIndex = (
  coordinate: number,
  ticks: Array<TickItem> = [],
  unsortedTicks: Array<TickItem>,
  axis: BaseAxisProps,
) => {
  let index = -1;
  const len = ticks?.length ?? 0;

  if (len > 1) {
    if (axis && axis.axisType === 'angleAxis' && Math.abs(Math.abs(axis.range[1] - axis.range[0]) - 360) <= 1e-6) {
      const { range } = axis;
      // ticks are distributed in a circle
      for (let i = 0; i < len; i++) {
        const before = i > 0 ? unsortedTicks[i - 1].coordinate : unsortedTicks[len - 1].coordinate;
        const cur = unsortedTicks[i].coordinate;
        const after = i >= len - 1 ? unsortedTicks[0].coordinate : unsortedTicks[i + 1].coordinate;
        let sameDirectionCoord;

        if (mathSign(cur - before) !== mathSign(after - cur)) {
          const diffInterval = [];
          if (mathSign(after - cur) === mathSign(range[1] - range[0])) {
            sameDirectionCoord = after;

            const curInRange = cur + range[1] - range[0];
            diffInterval[0] = Math.min(curInRange, (curInRange + before) / 2);
            diffInterval[1] = Math.max(curInRange, (curInRange + before) / 2);
          } else {
            sameDirectionCoord = before;

            const afterInRange = after + range[1] - range[0];
            diffInterval[0] = Math.min(cur, (afterInRange + cur) / 2);
            diffInterval[1] = Math.max(cur, (afterInRange + cur) / 2);
          }
          const sameInterval = [
            Math.min(cur, (sameDirectionCoord + cur) / 2),
            Math.max(cur, (sameDirectionCoord + cur) / 2),
          ];

          if (
            (coordinate > sameInterval[0] && coordinate <= sameInterval[1]) ||
            (coordinate >= diffInterval[0] && coordinate <= diffInterval[1])
          ) {
            ({ index } = unsortedTicks[i]);
            break;
          }
        } else {
          const min = Math.min(before, after);
          const max = Math.max(before, after);

          if (coordinate > (min + cur) / 2 && coordinate <= (max + cur) / 2) {
            ({ index } = unsortedTicks[i]);
            break;
          }
        }
      }
    } else {
      // ticks are distributed in a single direction
      for (let i = 0; i < len; i++) {
        if (
          (i === 0 && coordinate <= (ticks[i].coordinate + ticks[i + 1].coordinate) / 2) ||
          (i > 0 &&
            i < len - 1 &&
            coordinate > (ticks[i].coordinate + ticks[i - 1].coordinate) / 2 &&
            coordinate <= (ticks[i].coordinate + ticks[i + 1].coordinate) / 2) ||
          (i === len - 1 && coordinate > (ticks[i].coordinate + ticks[i - 1].coordinate) / 2)
        ) {
          ({ index } = ticks[i]);
          break;
        }
      }
    }
  } else {
    index = 0;
  }

  return index;
};

/**
 * Get the main color of each graphic item
 * @param  {ReactElement} item A graphic item
 * @return {String}            Color
 */
export const getMainColorOfGraphicItem = (item: ReactElement) => {
  const {
    type: { displayName },
  } = item as any; // TODO: check if displayName is valid.
  const { stroke, fill } = item.props;
  let result;

  switch (displayName) {
    case 'Line':
      result = stroke;
      break;
    case 'Area':
    case 'Radar':
      result = stroke && stroke !== 'none' ? stroke : fill;
      break;
    default:
      result = fill;
      break;
  }

  return result;
};

// TODO: Formated -> Formatted.
interface FormatedGraphicalItem {
  props: any;
  childIndex: number;
  item: any;
}

export const getLegendProps = ({
  children,
  formatedGraphicalItems,
  legendWidth,
  legendContent,
}: {
  children: any;
  formatedGraphicalItems?: Array<FormatedGraphicalItem>;
  legendWidth: number;
  legendContent?: any;
}) => {
  const legendItem = findChildByType(children, Legend.displayName);
  if (!legendItem) {
    return null;
  }

  let legendData;
  if (legendItem.props && legendItem.props.payload) {
    legendData = legendItem.props && legendItem.props.payload;
  } else if (legendContent === 'children') {
    legendData = (formatedGraphicalItems || []).reduce((result, { item, props }) => {
      const data = props.sectors || props.data || [];

      return result.concat(
        data.map((entry: any) => ({
          type: legendItem.props.iconType || item.props.legendType,
          value: entry.name,
          color: entry.fill,
          payload: entry,
        })),
      );
    }, []);
  } else {
    legendData = (formatedGraphicalItems || []).map(({ item }) => {
      const { dataKey, name, legendType, hide } = item.props;

      return {
        inactive: hide,
        dataKey,
        type: legendItem.props.iconType || legendType || 'square',
        color: getMainColorOfGraphicItem(item),
        value: name || dataKey,
        payload: item.props,
      };
    });
  }

  return {
    ...legendItem.props,
    ...Legend.getWithHeight(legendItem, legendWidth),
    payload: legendData,
    item: legendItem,
  };
};
/**
 * Calculate the size of all groups for stacked bar graph
 * @param  {Object} stackGroups The items grouped by axisId and stackId
 * @return {Object} The size of all groups
 */
export const getBarSizeList = ({
  barSize: globalSize,
  stackGroups = {},
}: {
  barSize: number | string;
  stackGroups: any;
}) => {
  if (!stackGroups) {
    return {};
  }

  const result: Record<string, any> = {};
  const numericAxisIds = Object.keys(stackGroups);

  for (let i = 0, len = numericAxisIds.length; i < len; i++) {
    const sgs = stackGroups[numericAxisIds[i]].stackGroups;
    const stackIds = Object.keys(sgs);

    for (let j = 0, sLen = stackIds.length; j < sLen; j++) {
      const { items, cateAxisId } = sgs[stackIds[j]];

      const barItems = items.filter((item: any) => getDisplayName(item.type).indexOf('Bar') >= 0);

      if (barItems && barItems.length) {
        const { barSize: selfSize } = barItems[0].props;
        const cateId = barItems[0].props[cateAxisId];

        if (!result[cateId]) {
          result[cateId] = [];
        }

        result[cateId].push({
          item: barItems[0],
          stackList: barItems.slice(1),
          barSize: _.isNil(selfSize) ? globalSize : selfSize,
        });
      }
    }
  }

  return result;
};

/**
 * Calculate the size of each bar and the gap between two bars
 * @param  {Number} bandSize  The size of each category
 * @param  {sizeList} sizeList  The size of all groups
 * @param  {maxBarSize} maxBarSize The maximum size of bar
 * @return {Number} The size of each bar and the gap between two bars
 */
export const getBarPosition = ({
  barGap,
  barCategoryGap,
  bandSize,
  sizeList = [],
  maxBarSize,
}: {
  barGap: string | number;
  barCategoryGap: string | number;
  bandSize: number;
  sizeList: Array<any>;
  maxBarSize: number;
}) => {
  const len = sizeList.length;
  if (len < 1) return null;

  let realBarGap = getPercentValue(barGap, bandSize, 0, true);
  let result;

  // whether or not is barSize setted by user
  if (sizeList[0].barSize === +sizeList[0].barSize) {
    let useFull = false;
    let fullBarSize = bandSize / len;
    let sum = sizeList.reduce((res, entry) => res + entry.barSize || 0, 0);
    sum += (len - 1) * realBarGap;

    if (sum >= bandSize) {
      sum -= (len - 1) * realBarGap;
      realBarGap = 0;
    }
    if (sum >= bandSize && fullBarSize > 0) {
      useFull = true;
      fullBarSize *= 0.9;
      sum = len * fullBarSize;
    }

    const offset = ((bandSize - sum) / 2) >> 0;
    let prev = { offset: offset - realBarGap, size: 0 };

    result = sizeList.reduce((res, entry) => {
      const newRes = [
        ...res,
        {
          item: entry.item,
          position: {
            offset: prev.offset + prev.size + realBarGap,
            size: useFull ? fullBarSize : entry.barSize,
          },
        },
      ];

      prev = newRes[newRes.length - 1].position;

      if (entry.stackList && entry.stackList.length) {
        entry.stackList.forEach((item: any) => {
          newRes.push({ item, position: prev });
        });
      }
      return newRes;
    }, []);
  } else {
    const offset = getPercentValue(barCategoryGap, bandSize, 0, true);

    if (bandSize - 2 * offset - (len - 1) * realBarGap <= 0) {
      realBarGap = 0;
    }

    let originalSize = (bandSize - 2 * offset - (len - 1) * realBarGap) / len;
    if (originalSize > 1) {
      originalSize >>= 0;
    }
    const size = maxBarSize === +maxBarSize ? Math.min(originalSize, maxBarSize) : originalSize;

    result = sizeList.reduce((res, entry, i) => {
      const newRes = [
        ...res,
        {
          item: entry.item,
          position: {
            offset: offset + (originalSize + realBarGap) * i + (originalSize - size) / 2,
            size,
          },
        },
      ];

      if (entry.stackList && entry.stackList.length) {
        entry.stackList.forEach((item: any) => {
          newRes.push({ item, position: newRes[newRes.length - 1].position });
        });
      }
      return newRes;
    }, []);
  }

  return result;
};

export const appendOffsetOfLegend = (offset: any, items: Array<FormatedGraphicalItem>, props: any, legendBox: any) => {
  const { children, width, margin } = props;
  const legendWidth = width - (margin.left || 0) - (margin.right || 0);
  // const legendHeight = height - (margin.top || 0) - (margin.bottom || 0);
  const legendProps = getLegendProps({ children, legendWidth });
  let newOffset = offset;

  if (legendProps) {
    const box = legendBox || {};
    const { align, verticalAlign, layout } = legendProps;

    if ((layout === 'vertical' || (layout === 'horizontal' && verticalAlign === 'center')) && isNumber(offset[align])) {
      newOffset = { ...offset, [align]: newOffset[align] + (box.width || 0) };
    }

    if ((layout === 'horizontal' || (layout === 'vertical' && align === 'center')) && isNumber(offset[verticalAlign])) {
      newOffset = { ...offset, [verticalAlign]: newOffset[verticalAlign] + (box.height || 0) };
    }
  }

  return newOffset;
};

export const getDomainOfErrorBars = (data: any[], item: any, dataKey: any, axisType?: AxisType) => {
  const { children } = item.props;
  const errorBars = findAllByType(children, 'ErrorBar').filter((errorBarChild: any) => {
    const { direction } = errorBarChild.props;

    return _.isNil(direction) || _.isNil(axisType) ? true : axisType.indexOf(direction) >= 0;
  });

  if (errorBars && errorBars.length) {
    const keys = errorBars.map((errorBarChild: any) => errorBarChild.props.dataKey);

    return data.reduce(
      (result, entry) => {
        const entryValue = getValueByDataKey(entry, dataKey, 0);
        const mainValue = _.isArray(entryValue) ? [_.min(entryValue), _.max(entryValue)] : [entryValue, entryValue];
        const errorDomain = keys.reduce(
          (prevErrorArr: [number, number], k: any) => {
            const errorValue = getValueByDataKey(entry, k, 0);
            const lowerValue = mainValue[0] - Math.abs(_.isArray(errorValue) ? errorValue[0] : errorValue);
            const upperValue = mainValue[1] + Math.abs(_.isArray(errorValue) ? errorValue[1] : errorValue);

            return [Math.min(lowerValue, prevErrorArr[0]), Math.max(upperValue, prevErrorArr[1])];
          },
          [Infinity, -Infinity],
        );

        return [Math.min(errorDomain[0], result[0]), Math.max(errorDomain[1], result[1])];
      },
      [Infinity, -Infinity],
    );
  }

  return null;
};
export const parseErrorBarsOfAxis = (data: any[], items: any[], dataKey: any, axisType: AxisType) => {
  const domains = items
    .map(item => getDomainOfErrorBars(data, item, dataKey, axisType))
    .filter(entry => !_.isNil(entry));

  if (domains && domains.length) {
    return domains.reduce((result, entry) => [Math.min(result[0], entry[0]), Math.max(result[1], entry[1])], [
      Infinity,
      -Infinity,
    ]);
  }

  return null;
};
/**
 * Get domain of data by the configuration of item element
 * @param  {Array}   data      The data displayed in the chart
 * @param  {Array}   items     The instances of item
 * @param  {String}  type      The type of axis, number - Number Axis, category - Category Axis
 * @param  {Boolean} filterNil Whether or not filter nil values
 * @return {Array}        Domain
 */
export const getDomainOfItemsWithSameAxis = (data: any[], items: any[], type: string, filterNil?: boolean) => {
  const domains = items.map(item => {
    const { dataKey } = item.props;

    if (type === 'number' && dataKey) {
      return getDomainOfErrorBars(data, item, dataKey) || getDomainOfDataByKey(data, dataKey, type, filterNil);
    }
    return getDomainOfDataByKey(data, dataKey, type, filterNil);
  });

  if (type === 'number') {
    // Calculate the domain of number axis
    return domains.reduce((result, entry) => [Math.min(result[0], entry[0]), Math.max(result[1], entry[1])], [
      Infinity,
      -Infinity,
    ]);
  }

  const tag: Record<string, any> = {};
  // Get the union set of category axis
  return domains.reduce((result, entry) => {
    for (let i = 0, len = entry.length; i < len; i++) {
      if (!tag[entry[i]]) {
        tag[entry[i]] = true;

        result.push(entry[i]);
      }
    }
    return result;
  }, []);
};

export const isCategoricalAxis = (layout: LayoutType | PolarLayoutType, axisType: AxisType) =>
  (layout === 'horizontal' && axisType === 'xAxis') ||
  (layout === 'vertical' && axisType === 'yAxis') ||
  (layout === 'centric' && axisType === 'angleAxis') ||
  (layout === 'radial' && axisType === 'radiusAxis');

/**
 * Calculate the Coordinates of grid
 * @param  {Array} ticks The ticks in axis
 * @param {Number} min   The minimun value of axis
 * @param {Number} max   The maximun value of axis
 * @return {Array}       Coordinates
 */
export const getCoordinatesOfGrid = (ticks: Array<TickItem>, min: number, max: number) => {
  let hasMin, hasMax;

  const values = ticks.map(entry => {
    if (entry.coordinate === min) {
      hasMin = true;
    }
    if (entry.coordinate === max) {
      hasMax = true;
    }

    return entry.coordinate;
  });

  if (!hasMin) {
    values.push(min);
  }
  if (!hasMax) {
    values.push(max);
  }

  return values;
};

/**
 * Get the ticks of an axis
 * @param  {Object}  axis The configuration of an axis
 * @param {Boolean} isGrid Whether or not are the ticks in grid
 * @param {Boolean} isAll Return the ticks of all the points or not
 * @return {Array}  Ticks
 */
export const getTicksOfAxis = (axis: any, isGrid?: boolean, isAll?: boolean): TickItem[] => {
  if (!axis) return null;
  const { scale } = axis;
  const { duplicateDomain, type, range } = axis;
  let offset = (isGrid || isAll) && type === 'category' && scale.bandwidth ? scale.bandwidth() / 2 : 0;
  offset = axis.axisType === 'angleAxis' ? mathSign(range[0] - range[1]) * 2 * offset : offset;

  // The ticks setted by user should only affect the ticks adjacent to axis line
  if (isGrid && (axis.ticks || axis.niceTicks)) {
    return (axis.ticks || axis.niceTicks).map((entry: TickItem) => {
      const scaleContent = duplicateDomain ? duplicateDomain.indexOf(entry) : entry;

      return {
        coordinate: scale(scaleContent) + offset,
        value: entry,
        offset,
      };
    });
  }

  // When axis is a categorial axis, but the type of axis is number or the scale of axis is not "auto"
  if (axis.isCategorical && axis.categoricalDomain) {
    return axis.categoricalDomain.map((entry: any, index: number) => ({
      coordinate: scale(entry) + offset,
      value: entry,
      index,
      offset,
    }));
  }

  if (scale.ticks && !isAll) {
    return scale
      .ticks(axis.tickCount)
      .map((entry: any) => ({ coordinate: scale(entry) + offset, value: entry, offset }));
  }

  // When axis has duplicated text, serial numbers are used to generate scale
  return scale.domain().map((entry: any, index: number) => ({
    coordinate: scale(entry) + offset,
    value: duplicateDomain ? duplicateDomain[entry] : entry,
    index,
    offset,
  }));
};

/**
 * combine the handlers
 * @param  {Function} defaultHandler Internal private handler
 * @param  {Function} parentHandler  Handler function specified in parent component
 * @param  {Function} childHandler   Handler function specified in child component
 * @return {Function}                The combined handler
 */
export const combineEventHandlers = (defaultHandler: Function, parentHandler: Function, childHandler: Function) => {
  let customizedHandler: Function;

  if (_.isFunction(childHandler)) {
    customizedHandler = childHandler;
  } else if (_.isFunction(parentHandler)) {
    customizedHandler = parentHandler;
  }

  if (_.isFunction(defaultHandler) || customizedHandler) {
    return (arg1: any, arg2: any, arg3: any, arg4: any) => {
      if (_.isFunction(defaultHandler)) {
        defaultHandler(arg1, arg2, arg3, arg4);
      }
      if (_.isFunction(customizedHandler)) {
        customizedHandler(arg1, arg2, arg3, arg4);
      }
    };
  }

  return null;
};
/**
 * Parse the scale function of axis
 * @param  {Object}   axis          The option of axis
 * @param  {String}   chartType     The displayName of chart
 * @param  {Boolean}  hasBar        if it has a bar
 * @return {Function}               The scale function
 */
export const parseScale = (axis: any, chartType: string, hasBar?: boolean) => {
  const { scale, type, layout, axisType } = axis;
  if (scale === 'auto') {
    if (layout === 'radial' && axisType === 'radiusAxis') {
      return { scale: d3Scales.scaleBand(), realScaleType: 'band' };
    }
    if (layout === 'radial' && axisType === 'angleAxis') {
      return { scale: d3Scales.scaleLinear(), realScaleType: 'linear' };
    }

    if (
      type === 'category' &&
      chartType &&
      (chartType.indexOf('LineChart') >= 0 ||
        chartType.indexOf('AreaChart') >= 0 ||
        (chartType.indexOf('ComposedChart') >= 0 && !hasBar))
    ) {
      return { scale: d3Scales.scalePoint(), realScaleType: 'point' };
    }
    if (type === 'category') {
      return { scale: d3Scales.scaleBand(), realScaleType: 'band' };
    }

    return { scale: d3Scales.scaleLinear(), realScaleType: 'linear' };
  }
  if (_.isString(scale)) {
    const name = `scale${_.upperFirst(scale)}`;

    return {
      scale: ((d3Scales as Record<string, any>)[name] || d3Scales.scalePoint)(),
      realScaleType: (d3Scales as Record<string, any>)[name] ? name : 'point',
    };
  }

  return _.isFunction(scale) ? { scale } : { scale: d3Scales.scalePoint(), realScaleType: 'point' };
};
const EPS = 1e-4;
export const checkDomainOfScale = (scale: any) => {
  const domain = scale.domain();

  if (!domain || domain.length <= 2) {
    return;
  }

  const len = domain.length;
  const range = scale.range();
  const min = Math.min(range[0], range[1]) - EPS;
  const max = Math.max(range[0], range[1]) + EPS;
  const first = scale(domain[0]);
  const last = scale(domain[len - 1]);

  if (first < min || first > max || last < min || last > max) {
    scale.domain([domain[0], domain[len - 1]]);
  }
};

export const findPositionOfBar = (barPosition: any[], child: ReactNode) => {
  if (!barPosition) {
    return null;
  }

  for (let i = 0, len = barPosition.length; i < len; i++) {
    if (barPosition[i].item === child) {
      return barPosition[i].position;
    }
  }

  return null;
};

export const truncateByDomain = (value: any[], domain: any[]) => {
  if (!domain || domain.length !== 2 || !isNumber(domain[0]) || !isNumber(domain[1])) {
    return value;
  }

  const min = Math.min(domain[0], domain[1]);
  const max = Math.max(domain[0], domain[1]);

  const result = [value[0], value[1]];
  if (!isNumber(value[0]) || value[0] < min) {
    result[0] = min;
  }

  if (!isNumber(value[1]) || value[1] > max) {
    result[1] = max;
  }

  if (result[0] > max) {
    result[0] = max;
  }

  if (result[1] < min) {
    result[1] = min;
  }

  return result;
};

/* eslint no-param-reassign: 0 */
export const offsetSign = (series: any) => {
  const n = series.length;
  if (n <= 0) {
    return;
  }

  for (let j = 0, m = series[0].length; j < m; ++j) {
    let positive = 0;
    let negative = 0;

    for (let i = 0; i < n; ++i) {
      const value = _.isNaN(series[i][j][1]) ? series[i][j][0] : series[i][j][1];

      /* eslint-disable prefer-destructuring */
      if (value >= 0) {
        series[i][j][0] = positive;
        series[i][j][1] = positive + value;
        positive = series[i][j][1];
      } else {
        series[i][j][0] = negative;
        series[i][j][1] = negative + value;
        negative = series[i][j][1];
      }
      /* eslint-enable prefer-destructuring */
    }
  }
};

/* eslint no-param-reassign: 0 */
export const offsetPositive = (series: any) => {
  const n = series.length;
  if (n <= 0) {
    return;
  }

  for (let j = 0, m = series[0].length; j < m; ++j) {
    let positive = 0;

    for (let i = 0; i < n; ++i) {
      const value = _.isNaN(series[i][j][1]) ? series[i][j][0] : series[i][j][1];

      /* eslint-disable prefer-destructuring */
      if (value >= 0) {
        series[i][j][0] = positive;
        series[i][j][1] = positive + value;
        positive = series[i][j][1];
      } else {
        series[i][j][0] = 0;
        series[i][j][1] = 0;
      }
      /* eslint-enable prefer-destructuring */
    }
  }
};

const STACK_OFFSET_MAP: Record<string, any> = {
  sign: offsetSign,
  expand: stackOffsetExpand,
  none: stackOffsetNone,
  silhouette: stackOffsetSilhouette,
  wiggle: stackOffsetWiggle,
  positive: offsetPositive,
};

export const getStackedData = (data: any, stackItems: any, offsetType: string) => {
  const dataKeys = stackItems.map((item: any) => item.props.dataKey);
  const stack = shapeStack()
    .keys(dataKeys)
    .value((d, key) => +getValueByDataKey(d, key, 0))
    .order(stackOrderNone)
    .offset(STACK_OFFSET_MAP[offsetType]);

  return stack(data);
};

export const getStackGroupsByAxisId = (
  data: any,
  _items: Array<any>,
  numericAxisId: string,
  cateAxisId: string,
  offsetType: any,
  reverseStackOrder: boolean,
) => {
  if (!data) {
    return null;
  }

  // reversing items to affect render order (for layering)
  const items = reverseStackOrder ? _items.reverse() : _items;

  const stackGroups = items.reduce((result, item) => {
    const { stackId, hide } = item.props;

    if (hide) {
      return result;
    }

    const axisId = item.props[numericAxisId];
    const parentGroup = result[axisId] || { hasStack: false, stackGroups: {} };

    if (isNumOrStr(stackId)) {
      const childGroup = parentGroup.stackGroups[stackId] || {
        numericAxisId,
        cateAxisId,
        items: [],
      };

      childGroup.items.push(item);

      parentGroup.hasStack = true;

      parentGroup.stackGroups[stackId] = childGroup;
    } else {
      parentGroup.stackGroups[uniqueId('_stackId_')] = {
        numericAxisId,
        cateAxisId,
        items: [item],
      };
    }

    return { ...result, [axisId]: parentGroup };
  }, {});

  return Object.keys(stackGroups).reduce((result, axisId) => {
    const group = stackGroups[axisId];

    if (group.hasStack) {
      group.stackGroups = Object.keys(group.stackGroups).reduce((res, stackId) => {
        const g = group.stackGroups[stackId];

        return {
          ...res,
          [stackId]: {
            numericAxisId,
            cateAxisId,
            items: g.items,
            stackedData: getStackedData(data, g.items, offsetType),
          },
        };
      }, {});
    }

    return { ...result, [axisId]: group };
  }, {});
};

/**
 * get domain of ticks
 * @param  {Array} ticks Ticks of axis
 * @param  {String} type  The type of axis
 * @return {Array} domain
 */
export const calculateDomainOfTicks = (ticks: Array<TickItem>, type: string) => {
  if (type === 'number') {
    return [_.min(ticks), _.max(ticks)];
  }

  return ticks;
};

/**
 * Configure the scale function of axis
 * @param {Object} scale The scale function
 * @param {Object} opts  The configuration of axis
 * @return {Object}      null
 */
export const getTicksOfScale = (scale: any, opts: any) => {
  const { realScaleType, type, tickCount, originalDomain, allowDecimals } = opts;
  const scaleType = realScaleType || opts.scale;

  if (scaleType !== 'auto' && scaleType !== 'linear') {
    return null;
  }

  if (
    tickCount &&
    type === 'number' &&
    originalDomain &&
    (originalDomain[0] === 'auto' || originalDomain[1] === 'auto')
  ) {
    // Calculate the ticks by the number of grid when the axis is a number axis
    const domain = scale.domain();
    if (!domain.length) {
      return null;
    }
    const tickValues = getNiceTickValues(domain, tickCount, allowDecimals);

    scale.domain(calculateDomainOfTicks(tickValues, type));

    return { niceTicks: tickValues };
  }
  if (tickCount && type === 'number') {
    const domain = scale.domain();
    const tickValues = getTickValuesFixedDomain(domain, tickCount, allowDecimals);

    return { niceTicks: tickValues };
  }

  return null;
};

export const getCateCoordinateOfLine = ({
  axis,
  ticks,
  bandSize,
  entry,
  index,
  dataKey,
}: {
  axis: any;
  ticks: Array<TickItem>;
  bandSize: number;
  entry: any;
  index: number;
  dataKey?: string | number | ((obj: any) => any);
}) => {
  if (axis.type === 'category') {
    // find coordinate of category axis by the value of category
    if (!axis.allowDuplicatedCategory && axis.dataKey && !_.isNil(entry[axis.dataKey])) {
      const matchedTick = findEntryInArray(ticks, 'value', entry[axis.dataKey]);

      if (matchedTick) {
        return matchedTick.coordinate + bandSize / 2;
      }
    }

    return ticks[index] ? ticks[index].coordinate + bandSize / 2 : null;
  }

  const value = getValueByDataKey(entry, !_.isNil(dataKey) ? dataKey : axis.dataKey);

  return !_.isNil(value) ? axis.scale(value) : null;
};

export const getCateCoordinateOfBar = ({
  axis,
  ticks,
  offset,
  bandSize,
  entry,
  index,
}: {
  axis: any; // RadiusAxisProps & { dataKey?: any }; // TODO: should dataKey be included in RadiusAxisProps?
  ticks: Array<TickItem>;
  offset: any;
  bandSize: number;
  entry: any;
  index: number;
}) => {
  if (axis.type === 'category') {
    return ticks[index] ? ticks[index].coordinate + offset : null;
  }
  const value = getValueByDataKey(entry, axis.dataKey, axis.domain[index]);

  return !_.isNil(value) ? axis.scale(value) - bandSize / 2 + offset : null;
};

export const getBaseValueOfBar = ({
  numericAxis,
}: {
  numericAxis: any; // AngleAxisProps | RadiusAxisProps
}) => {
  const domain = numericAxis.scale.domain();

  if (numericAxis.type === 'number') {
    const min = Math.min(domain[0], domain[1]);
    const max = Math.max(domain[0], domain[1]);

    if (min <= 0 && max >= 0) {
      return 0;
    }
    if (max < 0) {
      return max;
    }

    return min;
  }

  return domain[0];
};

export const getStackedDataOfItem = (item: any, stackGroups: any) => {
  const { stackId } = item.props;

  if (isNumOrStr(stackId)) {
    const group = stackGroups[stackId];

    if (group && group.items.length) {
      let itemIndex = -1;

      for (let i = 0, len = group.items.length; i < len; i++) {
        if (group.items[i] === item) {
          itemIndex = i;
          break;
        }
      }
      return itemIndex >= 0 ? group.stackedData[itemIndex] : null;
    }
  }

  return null;
};

const getDomainOfSingle = (data: Array<any>) =>
  data.reduce(
    (result, entry) => [
      _.min(entry.concat([result[0]]).filter(isNumber)),
      _.max(entry.concat([result[1]]).filter(isNumber)),
    ],
    [Infinity, -Infinity],
  );

export const getDomainOfStackGroups = (stackGroups: any, startIndex: number, endIndex: number) =>
  Object.keys(stackGroups)
    .reduce(
      (result, stackId) => {
        const group = stackGroups[stackId];
        const { stackedData } = group;
        const domain = stackedData.reduce(
          (res: [number, number], entry: any) => {
            const s = getDomainOfSingle(entry.slice(startIndex, endIndex + 1));

            return [Math.min(res[0], s[0]), Math.max(res[1], s[1])];
          },
          [Infinity, -Infinity],
        );

        return [Math.min(domain[0], result[0]), Math.max(domain[1], result[1])];
      },
      [Infinity, -Infinity],
    )
    .map(result => (result === Infinity || result === -Infinity ? 0 : result));

export const MIN_VALUE_REG = /^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/;
export const MAX_VALUE_REG = /^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/;

export const parseSpecifiedDomain = (specifiedDomain: any, dataDomain: any, allowDataOverflow: boolean) => {
  if (!_.isArray(specifiedDomain)) {
    return dataDomain;
  }

  const domain = [];

  /* eslint-disable prefer-destructuring */
  if (isNumber(specifiedDomain[0])) {
    domain[0] = allowDataOverflow ? specifiedDomain[0] : Math.min(specifiedDomain[0], dataDomain[0]);
  } else if (MIN_VALUE_REG.test(specifiedDomain[0])) {
    const value = +MIN_VALUE_REG.exec(specifiedDomain[0])[1];

    domain[0] = dataDomain[0] - value;
  } else if (_.isFunction(specifiedDomain[0])) {
    domain[0] = specifiedDomain[0](dataDomain[0]);
  } else {
    domain[0] = dataDomain[0];
  }

  if (isNumber(specifiedDomain[1])) {
    domain[1] = allowDataOverflow ? specifiedDomain[1] : Math.max(specifiedDomain[1], dataDomain[1]);
  } else if (MAX_VALUE_REG.test(specifiedDomain[1])) {
    const value = +MAX_VALUE_REG.exec(specifiedDomain[1])[1];

    domain[1] = dataDomain[1] + value;
  } else if (_.isFunction(specifiedDomain[1])) {
    domain[1] = specifiedDomain[1](dataDomain[1]);
  } else {
    domain[1] = dataDomain[1];
  }
  /* eslint-enable prefer-destructuring */

  return domain;
};

/**
 * Calculate the size between two category
 * @param  {Object} axis  The options of axis
 * @param  {Array}  ticks The ticks of axis
 * @param  {Boolean} isBar if items in axis are bars
 * @return {Number} Size
 */
export const getBandSizeOfAxis = (axis: any, ticks?: Array<TickItem>, isBar?: boolean) => {
  if (axis && axis.scale && axis.scale.bandwidth) {
    const bandWidth = axis.scale.bandwidth();

    if (!isBar || bandWidth > 0) {
      return bandWidth;
    }
  }

  if (axis && ticks && ticks.length >= 2) {
    const orderedTicks = _.sortBy(ticks, o => o.coordinate);
    let bandSize = Infinity;

    for (let i = 1, len = orderedTicks.length; i < len; i++) {
      const cur = orderedTicks[i];
      const prev = orderedTicks[i - 1];

      bandSize = Math.min((cur.coordinate || 0) - (prev.coordinate || 0), bandSize);
    }

    return bandSize === Infinity ? 0 : bandSize;
  }

  return isBar ? undefined : 0;
};
/**
 * parse the domain of a category axis when a domain is specified
 * @param   {Array}        specifiedDomain  The domain specified by users
 * @param   {Array}        calculatedDomain The domain calculated by dateKey
 * @param   {ReactElement} axisChild        The axis element
 * @returns {Array}        domains
 */
export const parseDomainOfCategoryAxis = (
  specifiedDomain: Array<any>,
  calculatedDomain: Array<any>,
  axisChild: ReactElement,
) => {
  if (!specifiedDomain || !specifiedDomain.length) {
    return calculatedDomain;
  }

  if (_.isEqual(specifiedDomain, _.get(axisChild, 'type.defaultProps.domain'))) {
    return calculatedDomain;
  }

  return specifiedDomain;
};

export const getTooltipItem = (graphicalItem: any, payload: any) => {
  const { dataKey, name, unit, formatter, tooltipType, chartType } = graphicalItem.props;

  return {
    ...filterProps(graphicalItem),
    dataKey,
    unit,
    formatter,
    name: name || dataKey,
    color: getMainColorOfGraphicItem(graphicalItem),
    value: getValueByDataKey(payload, dataKey),
    type: tooltipType,
    payload,
    chartType,
  };
};
