// (C) 2007-2020 GoodData Corporation
import noop = require("lodash/noop");
import isString = require("lodash/isString");
import set = require("lodash/set");
import get = require("lodash/get");
import merge = require("lodash/merge");
import map = require("lodash/map");
import partial = require("lodash/partial");
import isEmpty = require("lodash/isEmpty");
import compact = require("lodash/compact");
import cloneDeep = require("lodash/cloneDeep");
import every = require("lodash/every");
import isNil = require("lodash/isNil");
import pickBy = require("lodash/pickBy");
import * as numberJS from "@gooddata/numberjs";
import * as cx from "classnames";

import { styleVariables } from "../../styles/variables";
import { supportedDualAxesChartTypes, supportedTooltipFollowPointerChartTypes } from "../chartOptionsBuilder";
import { VisualizationTypes, ChartType } from "../../../../constants/visualizationTypes";
import {
    IDataLabelsVisible,
    IChartConfig,
    IAxis,
    IChartOptions,
    ISeriesItem,
    IPointData,
} from "../../../../interfaces/Config";
import { percentFormatter } from "../../../../helpers/utils";
import { formatAsPercent, getLabelStyle, getLabelsVisibilityConfig, isInPercent } from "./dataLabelsHelpers";
import { HOVER_BRIGHTNESS, MINIMUM_HC_SAFE_BRIGHTNESS } from "./commonConfiguration";
import { AXIS_LINE_COLOR, getLighterColor } from "../../utils/color";
import {
    isBarChart,
    isColumnChart,
    isBulletChart,
    isOneOfTypes,
    isAreaChart,
    isRotationInRange,
    isHeatmap,
    isScatterPlot,
    isBubbleChart,
    isComboChart,
    isInvertedChartType,
} from "../../utils/common";
import { TOOLTIP_MAX_WIDTH, isTooltipShownInFullScreen, getTooltipContentWidth } from "../../chart/tooltip";
import { shouldFollowPointer } from "../../../visualizations/chart/highcharts/helpers";
import {
    shouldStartOnTick,
    shouldEndOnTick,
    shouldXAxisStartOnTickOnBubbleScatter,
    shouldYAxisStartOnTickOnBubbleScatter,
} from "../highcharts/helpers";

import getOptionalStackingConfiguration from "./getOptionalStackingConfiguration";
import { IDrillConfig } from "../../../../interfaces/DrillEvents";
import { getZeroAlignConfiguration } from "./getZeroAlignConfiguration";
import { canComboChartBeStackedInPercent } from "../chartOptions/comboChartOptions";
import { getAxisNameConfiguration } from "./getAxisNameConfiguration";
import { getChartAlignmentConfiguration } from "./getChartAlignmentConfiguration";
import { getAxisLabelConfigurationForDualBarChart } from "./getAxisLabelConfigurationForDualBarChart";

const { stripColors, numberFormat }: any = numberJS;

const EMPTY_DATA: any = { categories: [], series: [] };

const ALIGN_LEFT = "left";
const ALIGN_RIGHT = "right";
const ALIGN_CENTER = "center";

const TOOLTIP_ARROW_OFFSET = 23;
const TOOLTIP_INVERTED_CHART_VERTICAL_OFFSET = 5;
const TOOLTIP_VERTICAL_OFFSET = 14;
export const TOOLTIP_PADDING = 24; // padding of tooltip container - defined by CSS
export const TOOLTIP_VIEWPORT_MARGIN_TOP = 20;
const BAR_COLUMN_TOOLTIP_TOP_OFFSET = 8;
const BAR_COLUMN_TOOLTIP_LEFT_OFFSET = 5;
const HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET = 16;

const escapeAngleBrackets = (str: any) => str && str.replace(/</g, "&lt;").replace(/>/g, "&gt;");

function getTitleConfiguration(chartOptions: IChartOptions) {
    const { yAxes = [], xAxes = [] } = chartOptions;
    const yAxis = yAxes.map((axis: IAxis) =>
        axis
            ? {
                  title: {
                      text: escapeAngleBrackets(get(axis, "label", "")),
                  },
              }
            : {},
    );

    const xAxis = xAxes.map((axis: IAxis) =>
        axis
            ? {
                  title: {
                      text: escapeAngleBrackets(get(axis, "label", "")),
                  },
              }
            : {},
    );

    return {
        yAxis,
        xAxis,
    };
}

export function formatOverlappingForParentAttribute(category: any) {
    // category is passed from 'grouped-categories' which is npm highcharts plug-in
    if (!category) {
        return formatOverlapping.call(this);
    }

    const categoriesCount = get(this, "axis.categoriesTree", []).length;
    if (categoriesCount === 1) {
        // Let the width be auto to make sure "this.value" is displayed on screen
        return `<div style="overflow: hidden; text-overflow: ellipsis">${this.value}</div>`;
    }
    const chartHeight = get(this, "axis.chart.chartHeight", 1);
    const width = Math.floor(chartHeight / categoriesCount);
    const pixelOffset = 40; // parent attribute should have more space than its children

    const finalWidth = Math.max(0, width - pixelOffset);

    return `<div style="width: ${finalWidth}px; overflow: hidden; text-overflow: ellipsis">${
        this.value
    }</div>`;
}

export function formatOverlapping() {
    const categoriesCount = get(this, "axis.categories", []).length;
    if (categoriesCount === 1) {
        // Let the width be auto to make sure "this.value" is displayed on screen
        return `<div align="center" style="overflow: hidden; text-overflow: ellipsis">${this.value}</div>`;
    }
    const chartHeight = get(this, "chart.chartHeight", 1);
    const width = Math.floor(chartHeight / categoriesCount);
    const pixelOffset = 20;

    const finalWidth = Math.max(0, width - pixelOffset);

    return (
        `<div align="center" style="width: ${finalWidth}px; overflow: hidden; text-overflow: ellipsis">` +
        this.value +
        "</div>"
    );
}

function hideOverlappedLabels(chartOptions: IChartOptions) {
    const rotation = Number(get(chartOptions, "xAxisProps.rotation", "0"));

    // Set only for bar chart and labels are rotated by 90
    const isInvertedChart = isInvertedChartType(chartOptions.type);
    if (isInvertedChart && isRotationInRange(rotation, 75, 105)) {
        const { xAxes = [], isViewByTwoAttributes } = chartOptions;

        return {
            xAxis: xAxes.map((axis: any) =>
                axis
                    ? {
                          labels: {
                              useHTML: true,
                              formatter: isViewByTwoAttributes
                                  ? formatOverlappingForParentAttribute
                                  : formatOverlapping,
                          },
                      }
                    : {},
            ),
        };
    }

    return {};
}

function getShowInPercentConfiguration(chartOptions: IChartOptions) {
    const { yAxes = [], xAxes = [] } = chartOptions;
    const percentageFormatter = partial(formatAsPercent, 100);

    const xAxis = xAxes.map((axis: any) =>
        axis && isInPercent(axis.format)
            ? {
                  labels: {
                      formatter: percentageFormatter,
                  },
              }
            : {},
    );

    const yAxis = yAxes.map((axis: any) =>
        axis && isInPercent(axis.format)
            ? {
                  labels: {
                      formatter: percentageFormatter,
                  },
              }
            : {},
    );

    return {
        xAxis,
        yAxis,
    };
}

function getArrowAlignment(arrowPosition: any, chartWidth: any) {
    const minX = -TOOLTIP_ARROW_OFFSET;
    const maxX = chartWidth + TOOLTIP_ARROW_OFFSET;

    if (arrowPosition + TOOLTIP_MAX_WIDTH / 2 > maxX && arrowPosition - TOOLTIP_MAX_WIDTH / 2 > minX) {
        return ALIGN_RIGHT;
    }

    if (arrowPosition - TOOLTIP_MAX_WIDTH / 2 < minX && arrowPosition + TOOLTIP_MAX_WIDTH / 2 < maxX) {
        return ALIGN_LEFT;
    }

    return ALIGN_CENTER;
}

const getTooltipHorizontalStartingPosition = (arrowPosition: any, chartWidth: any, tooltipWidth: any) => {
    switch (getArrowAlignment(arrowPosition, chartWidth)) {
        case ALIGN_RIGHT:
            return arrowPosition - tooltipWidth + TOOLTIP_ARROW_OFFSET;
        case ALIGN_LEFT:
            return arrowPosition - TOOLTIP_ARROW_OFFSET;
        default:
            return arrowPosition - tooltipWidth / 2;
    }
};

function getArrowHorizontalPosition(chartType: any, stacking: any, dataPointEnd: any, dataPointHeight: any) {
    if (isBarChart(chartType) && stacking) {
        return dataPointEnd - dataPointHeight / 2;
    }

    return dataPointEnd;
}

function getDataPointEnd(chartType: any, isNegative: any, endPoint: any, height: any, stacking: any) {
    return isBarChart(chartType) && isNegative && stacking ? endPoint + height : endPoint;
}

function getDataPointStart(chartType: any, isNegative: any, endPoint: any, height: any, stacking: any) {
    return isColumnChart(chartType) && isNegative && stacking ? endPoint - height : endPoint;
}

function getTooltipVerticalOffset(chartType: any, stacking: any, point: any) {
    if (isColumnChart(chartType) && (stacking || point.negative)) {
        return 0;
    }

    if (isInvertedChartType(chartType)) {
        return TOOLTIP_INVERTED_CHART_VERTICAL_OFFSET;
    }

    return TOOLTIP_VERTICAL_OFFSET;
}

export function getTooltipPositionInChartContainer(
    chartType: string,
    stacking: string,
    labelWidth: number,
    labelHeight: number,
    point: IPointData,
) {
    const dataPointEnd = getDataPointEnd(chartType, point.negative, point.plotX, point.h, stacking);
    const arrowPosition = getArrowHorizontalPosition(chartType, stacking, dataPointEnd, point.h);
    const chartWidth = this.chart.plotWidth;

    const tooltipHorizontalStartingPosition = getTooltipHorizontalStartingPosition(
        arrowPosition,
        chartWidth,
        labelWidth,
    );

    const verticalOffset = getTooltipVerticalOffset(chartType, stacking, point);

    const dataPointStart = getDataPointStart(chartType, point.negative, point.plotY, point.h, stacking);

    return {
        x: this.chart.plotLeft + tooltipHorizontalStartingPosition,
        y: this.chart.plotTop + dataPointStart - (labelHeight + verticalOffset),
    };
}

function getHighchartTooltipTopOffset(chartType: string): number {
    if (
        isBarChart(chartType) ||
        isColumnChart(chartType) ||
        isBulletChart(chartType) ||
        isComboChart(chartType)
    ) {
        return BAR_COLUMN_TOOLTIP_TOP_OFFSET;
    }
    return HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET;
}

function getHighchartTooltipLeftOffset(chartType: string): number {
    if (
        isBarChart(chartType) ||
        isColumnChart(chartType) ||
        isBulletChart(chartType) ||
        isComboChart(chartType)
    ) {
        return BAR_COLUMN_TOOLTIP_LEFT_OFFSET;
    }
    return HIGHCHARTS_TOOLTIP_TOP_LEFT_OFFSET;
}

export function getTooltipPositionInViewPort(
    chartType: string,
    stacking: string,
    labelWidth: number,
    labelHeight: number,
    point: IPointData,
) {
    const { x, y } = getTooltipPositionInChartContainer.call(
        this,
        chartType,
        stacking,
        labelWidth,
        labelHeight,
        point,
    );
    const { top: containerTop, left: containerLeft } = this.chart.container.getBoundingClientRect();
    const leftOffset = pageXOffset + containerLeft - getHighchartTooltipLeftOffset(chartType);
    const topOffset = pageYOffset + containerTop - getHighchartTooltipTopOffset(chartType);

    const posX = isTooltipShownInFullScreen() ? leftOffset : leftOffset + x;
    const posY = topOffset + y;

    const minPosY = TOOLTIP_VIEWPORT_MARGIN_TOP - TOOLTIP_PADDING + pageYOffset;
    const posYLimited = posY < minPosY ? minPosY : posY;

    return {
        x: posX,
        y: posYLimited,
    };
}

function formatTooltip(tooltipCallback: any) {
    const { chart } = this.series;
    const { color: pointColor } = this.point;
    const chartWidth = chart.spacingBox.width;
    const isFullScreenTooltip = isTooltipShownInFullScreen();
    const maxTooltipContentWidth = getTooltipContentWidth(isFullScreenTooltip, chartWidth, TOOLTIP_MAX_WIDTH);

    // when brushing, do not show tooltip
    if (chart.mouseIsDown) {
        return false;
    }

    const strokeStyle = pointColor ? `border-top-color: ${pointColor};` : "";
    const tooltipStyle = isFullScreenTooltip ? `width: ${maxTooltipContentWidth}px;` : "";

    // null disables whole tooltip
    const tooltipContent: string = tooltipCallback(this.point, maxTooltipContentWidth, this.percentage);

    return tooltipContent !== null
        ? `<div class="hc-tooltip gd-viz-tooltip" style="${tooltipStyle}">
            <span class="stroke gd-viz-tooltip-stroke" style="${strokeStyle}"></span>
            <div class="content gd-viz-tooltip-content" style="max-width: ${maxTooltipContentWidth}px;">
                ${tooltipContent}
            </div>
        </div>`
        : null;
}

function formatLabel(value: any, format: any, config: IChartConfig = {}) {
    // no labels for missing values
    if (isNil(value)) {
        return null;
    }

    const stripped = stripColors(format || "");
    const { separators } = config;
    return escapeAngleBrackets(String(numberFormat(value, stripped, undefined, separators)));
}

function labelFormatter(config?: IChartConfig) {
    return formatLabel(this.y, get(this, "point.format"), config);
}

export function percentageDataLabelFormatter(config?: IChartConfig): string {
    // suppose that chart has one Y axis by default
    const isSingleAxis = get(this, "series.chart.yAxis.length", 1) === 1;
    const isPrimaryAxis = !get(this, "series.yAxis.opposite", false);

    // only format data labels to percentage for
    //  * left or right axis on single axis chart, or
    //  * primary axis on dual axis chart
    if (this.percentage && (isSingleAxis || isPrimaryAxis)) {
        return percentFormatter(this.percentage);
    }

    return labelFormatter.call(this, config);
}

function labelFormatterHeatmap(options: any) {
    return formatLabel(this.point.value, options.formatGD, options.config);
}

function level1LabelsFormatter(config?: IChartConfig) {
    return `${get(this, "point.name")} (${formatLabel(
        get(this, "point.node.val"),
        get(this, "point.format"),
        config,
    )})`;
}

function level2LabelsFormatter(config?: IChartConfig) {
    return `${get(this, "point.name")} (${formatLabel(
        get(this, "point.value"),
        get(this, "point.format"),
        config,
    )})`;
}

function labelFormatterBubble(config?: IChartConfig) {
    const value = get(this, "point.z");
    if (isNil(value) || isNaN(value)) {
        return null;
    }

    const xAxisMin = get(this, "series.xAxis.min");
    const xAxisMax = get(this, "series.xAxis.max");
    const yAxisMin = get(this, "series.yAxis.min");
    const yAxisMax = get(this, "series.yAxis.max");

    if (
        (!isNil(xAxisMax) && this.x > xAxisMax) ||
        (!isNil(xAxisMin) && this.x < xAxisMin) ||
        (!isNil(yAxisMax) && this.y > yAxisMax) ||
        (!isNil(yAxisMin) && this.y < yAxisMin)
    ) {
        return null;
    } else {
        return formatLabel(value, get(this, "point.format"), config);
    }
}

function labelFormatterScatter() {
    const name = get(this, "point.name");
    if (name) {
        return escapeAngleBrackets(name);
    }
    return null;
}

// check whether series contains only positive values, not consider nulls
function hasOnlyPositiveValues(series: any, x: any) {
    return every(series, (seriesItem: any) => {
        const dataPoint = seriesItem.yData[x];
        return dataPoint !== null && dataPoint >= 0;
    });
}

function stackLabelFormatter(config?: IChartConfig) {
    // show labels: always for negative,
    // without negative values or with non-zero total for positive
    const showStackLabel =
        this.isNegative || hasOnlyPositiveValues(this.axis.series, this.x) || this.total !== 0;
    return showStackLabel
        ? formatLabel(this.total, get(this, "axis.userOptions.defaultFormat"), config)
        : null;
}

function getTooltipConfiguration(chartOptions: IChartOptions) {
    const tooltipAction = get(chartOptions, "actions.tooltip");
    const chartType = chartOptions.type;
    const { stacking } = chartOptions;

    const followPointer = isOneOfTypes(chartType, supportedTooltipFollowPointerChartTypes)
        ? { followPointer: shouldFollowPointer(chartOptions) }
        : {};

    return tooltipAction
        ? {
              tooltip: {
                  borderWidth: 0,
                  borderRadius: 0,
                  shadow: false,
                  useHTML: true,
                  outside: true,
                  positioner: partial(getTooltipPositionInViewPort, chartType, stacking),
                  formatter: partial(formatTooltip, tooltipAction),
                  ...followPointer,
              },
          }
        : {};
}

function getTreemapLabelsConfiguration(
    isMultiLevel: boolean,
    style: any,
    config?: IChartConfig,
    labelsConfig?: object,
) {
    const smallLabelInCenter = {
        dataLabels: {
            enabled: true,
            padding: 2,
            formatter: partial(level2LabelsFormatter, config),
            allowOverlap: false,
            style,
            ...labelsConfig,
        },
    };
    if (isMultiLevel) {
        return {
            dataLabels: {
                ...labelsConfig,
            },
            levels: [
                {
                    level: 1,
                    dataLabels: {
                        enabled: true,
                        align: "left",
                        verticalAlign: "top",
                        padding: 5,
                        style: {
                            ...style,
                            fontSize: "14px",
                        },
                        formatter: partial(level1LabelsFormatter, config),
                        allowOverlap: false,
                        ...labelsConfig,
                    },
                },
                {
                    level: 2,
                    ...smallLabelInCenter,
                },
            ],
        };
    } else {
        return {
            dataLabels: {
                ...labelsConfig,
            },
            levels: [
                {
                    level: 1,
                    ...smallLabelInCenter,
                },
            ],
        };
    }
}

function getLabelsConfiguration(chartOptions: IChartOptions, _config: any, chartConfig?: IChartConfig) {
    const { stacking, yAxes = [], type } = chartOptions;

    const labelsVisible: IDataLabelsVisible = get(chartConfig, "dataLabels.visible");

    const labelsConfig = getLabelsVisibilityConfig(labelsVisible);

    const style = getLabelStyle(type, stacking);

    const yAxis = yAxes.map((axis: any) => ({
        defaultFormat: get(axis, "format"),
    }));

    const series: ISeriesItem[] = get(chartOptions, "data.series", []);
    const canStackInPercent = canComboChartBeStackedInPercent(series);
    const { stackMeasuresToPercent = false } = chartConfig || {};

    // only applied to bar, column, dual axis and area chart
    const dataLabelFormatter =
        stackMeasuresToPercent && canStackInPercent ? percentageDataLabelFormatter : labelFormatter;

    const DEFAULT_LABELS_CONFIG = {
        formatter: partial(labelFormatter, chartConfig),
        style,
        allowOverlap: false,
        ...labelsConfig,
    };

    return {
        plotOptions: {
            gdcOptions: {
                dataLabels: {
                    visible: labelsVisible,
                },
            },
            bar: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    formatter: partial(dataLabelFormatter, chartConfig),
                },
            },
            column: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    formatter: partial(dataLabelFormatter, chartConfig),
                },
            },
            heatmap: {
                dataLabels: {
                    formatter: labelFormatterHeatmap,
                    config: chartConfig,
                    ...labelsConfig,
                },
            },
            treemap: {
                ...getTreemapLabelsConfiguration(!!stacking, style, chartConfig, labelsConfig),
            },
            line: {
                dataLabels: DEFAULT_LABELS_CONFIG,
            },
            area: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    formatter: partial(dataLabelFormatter, chartConfig),
                },
            },
            scatter: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    formatter: partial(labelFormatterScatter, chartConfig),
                },
            },
            bubble: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    formatter: partial(labelFormatterBubble, chartConfig),
                },
            },
            pie: {
                dataLabels: {
                    ...DEFAULT_LABELS_CONFIG,
                    verticalAlign: "middle",
                },
            },
        },
        yAxis,
    };
}

function getStackingConfiguration(chartOptions: IChartOptions, _config: any, chartConfig?: IChartConfig) {
    const { stacking, yAxes = [], type } = chartOptions;
    let labelsConfig = {};

    if (isColumnChart(type)) {
        const labelsVisible: IDataLabelsVisible = get(chartConfig, "dataLabels.visible");
        labelsConfig = getLabelsVisibilityConfig(labelsVisible);
    }

    const yAxis = yAxes.map(() => ({
        stackLabels: {
            ...labelsConfig,
            formatter: partial(stackLabelFormatter, chartConfig),
        },
    }));

    let connectNulls = {};
    if (stacking && isAreaChart(type)) {
        connectNulls = {
            connectNulls: true,
        };
    }

    return stacking
        ? {
              plotOptions: {
                  series: {
                      stacking, // this stacking config will be applied to all series
                      ...connectNulls,
                  },
              },
              yAxis,
          }
        : {};
}

function getSeries(series: any) {
    return series.map((seriesItem: any) => {
        const item = cloneDeep(seriesItem);
        // Escaping is handled by highcharts so we don't want to provide escaped input.
        // With one exception, though. Highcharts supports defining styles via
        // for example <b>...</b> and parses that from series name.
        // So to avoid this parsing, escape only < and > to &lt; and &gt;
        // which is understood by highcharts correctly
        item.name = item.name && escapeAngleBrackets(item.name);

        // Escape data items for pie chart
        item.data = item.data.map((dataItem: any) => {
            if (!dataItem) {
                return dataItem;
            }

            return {
                ...dataItem,
                name: escapeAngleBrackets(dataItem.name),
            };
        });

        return item;
    });
}

function getHeatmapDataConfiguration(chartOptions: IChartOptions) {
    const data = chartOptions.data || EMPTY_DATA;
    const series = data.series;
    const categories = data.categories;

    return {
        series,
        xAxis: [
            {
                categories: categories[0] || [],
            },
        ],
        yAxis: [
            {
                categories: categories[1] || [],
            },
        ],
        colorAxis: {
            dataClasses: get(chartOptions, "colorAxis.dataClasses", []),
        },
    };
}

export function escapeCategories(dataCategories: any) {
    return map(dataCategories, (category: any) => {
        return isString(category)
            ? escapeAngleBrackets(category)
            : {
                  name: escapeAngleBrackets(category.name),
                  categories: map(category.categories, escapeAngleBrackets),
              };
    });
}

function getDataConfiguration(chartOptions: IChartOptions) {
    const data = chartOptions.data || EMPTY_DATA;
    const series = getSeries(data.series);
    const { type } = chartOptions;

    switch (type) {
        case VisualizationTypes.SCATTER:
        case VisualizationTypes.BUBBLE:
            return {
                series,
            };
        case VisualizationTypes.HEATMAP:
            return getHeatmapDataConfiguration(chartOptions);
    }

    const categories = escapeCategories(data.categories);

    return {
        series,
        xAxis: [
            {
                categories,
            },
        ],
    };
}

function lineSeriesMapFn(seriesOrig: any) {
    const series = cloneDeep(seriesOrig);
    if (series.isDrillable) {
        set(series, "marker.states.hover.fillColor", getLighterColor(series.color, HOVER_BRIGHTNESS));
    } else {
        set(series, "states.hover.halo.size", 0);
    }

    return series;
}

function barSeriesMapFn(seriesOrig: any) {
    const series = cloneDeep(seriesOrig);

    set(series, "states.hover.brightness", HOVER_BRIGHTNESS);
    set(series, "states.hover.enabled", series.isDrillable);

    return series;
}

function getHeatMapHoverColor(config: any) {
    const dataClasses = get(config, ["colorAxis", "dataClasses"], null);
    let resultColor = "rgb(210,210,210)";

    if (dataClasses) {
        if (dataClasses.length === 1) {
            resultColor = dataClasses[0].color;
        } else if (dataClasses.length > 1) {
            resultColor = dataClasses[1].color;
        }
    }

    return getLighterColor(resultColor, 0.2);
}

function getHoverStyles({ type }: any, config: any) {
    let seriesMapFn = noop;

    switch (type) {
        default:
            throw new Error(`Undefined chart type "${type}".`);

        case VisualizationTypes.LINE:
        case VisualizationTypes.SCATTER:
        case VisualizationTypes.AREA:
        case VisualizationTypes.BUBBLE:
            seriesMapFn = lineSeriesMapFn;
            break;

        case VisualizationTypes.BAR:
        case VisualizationTypes.COLUMN:
        case VisualizationTypes.BULLET:
        case VisualizationTypes.FUNNEL:
            seriesMapFn = barSeriesMapFn;
            break;
        case VisualizationTypes.HEATMAP:
            seriesMapFn = (seriesOrig, config) => {
                const series = cloneDeep(seriesOrig);
                const color = getHeatMapHoverColor(config);

                set(series, "states.hover.color", color);
                set(series, "states.hover.enabled", series.isDrillable);

                return series;
            };
            break;

        case VisualizationTypes.COMBO:
        case VisualizationTypes.COMBO2:
            seriesMapFn = seriesOrig => {
                const { type } = seriesOrig;

                if (type === "line") {
                    return lineSeriesMapFn(seriesOrig);
                }
                return barSeriesMapFn(seriesOrig);
            };
            break;

        case VisualizationTypes.PIE:
        case VisualizationTypes.DONUT:
        case VisualizationTypes.TREEMAP:
            seriesMapFn = seriesOrig => {
                const series = cloneDeep(seriesOrig);

                return {
                    ...series,
                    data: series.data.map((dataItemOrig: any) => {
                        const dataItem = cloneDeep(dataItemOrig);
                        const drilldown = get(dataItem, "drilldown");

                        set(
                            dataItem,
                            "states.hover.brightness",
                            drilldown ? HOVER_BRIGHTNESS : MINIMUM_HC_SAFE_BRIGHTNESS,
                        );

                        if (!drilldown) {
                            set(dataItem, "halo.size", 0); // see plugins/pointHalo.js
                        }

                        return dataItem;
                    }),
                };
            };
            break;
    }
    return {
        series: config.series.map((item: any) => seriesMapFn(item, config)),
        plotOptions: {
            ...[
                VisualizationTypes.LINE,
                VisualizationTypes.AREA,
                VisualizationTypes.SCATTER,
                VisualizationTypes.BUBBLE,
            ].reduce(
                (conf: any, key) => ({
                    ...conf,
                    [key]: {
                        point: {
                            events: {
                                // Workaround
                                // from Highcharts 5.0.0 cursor can be set by using 'className' for individual data items
                                mouseOver() {
                                    if (this.drilldown) {
                                        this.graphic.element.style.cursor = "pointer";
                                    }
                                },
                            },
                        },
                    },
                }),
                {},
            ),
        },
    };
}

function getGridConfiguration(chartOptions: IChartOptions) {
    const gridEnabled = get(chartOptions, "grid.enabled", true);
    const { yAxes = [], xAxes = [] } = chartOptions;

    const config = gridEnabled ? { gridLineWidth: 1, gridLineColor: "#ebebeb" } : { gridLineWidth: 0 };

    const yAxis = yAxes.map(() => config);

    const bothAxesGridlineCharts = [VisualizationTypes.BUBBLE, VisualizationTypes.SCATTER];
    let xAxis = {};
    if (isOneOfTypes(chartOptions.type, bothAxesGridlineCharts)) {
        xAxis = xAxes.map(() => config);
    }

    return {
        yAxis,
        xAxis,
    };
}

export function areAxisLabelsEnabled(
    chartOptions: IChartOptions,
    axisPropsName: string,
    shouldCheckForEmptyCategories: boolean,
) {
    const data = chartOptions.data || EMPTY_DATA;

    const { type } = chartOptions;
    const categories = isHeatmap(type) ? data.categories : escapeCategories(data.categories);

    const visible = get(chartOptions, `${axisPropsName}.visible`, true);

    const labelsEnabled = get(chartOptions, `${axisPropsName}.labelsEnabled`, true);

    const categoriesFlag = shouldCheckForEmptyCategories ? !isEmpty(compact(categories)) : true;

    return {
        enabled: categoriesFlag && visible && labelsEnabled,
    };
}

function shouldExpandYAxis(chartOptions: IChartOptions) {
    const min = get(chartOptions, "xAxisProps.min", "");
    const max = get(chartOptions, "xAxisProps.max", "");
    return min === "" && max === "" ? {} : { getExtremesFromAll: true };
}

function getAxisLineConfiguration(chartType: ChartType, isAxisVisible: boolean) {
    let lineWidth;

    // tslint:disable-next-line prefer-conditional-expression
    if (isAxisVisible === false) {
        lineWidth = 0;
    } else {
        lineWidth = isScatterPlot(chartType) || isBubbleChart(chartType) ? 1 : undefined;
    }

    return pickBy({ AXIS_LINE_COLOR, lineWidth }, (item: any) => item !== undefined);
}

function getXAxisTickConfiguration(chartOptions: IChartOptions) {
    const { type } = chartOptions;
    if (isBubbleChart(type) || isScatterPlot(type)) {
        return {
            startOnTick: shouldXAxisStartOnTickOnBubbleScatter(chartOptions),
            endOnTick: false,
        };
    }

    return {};
}

function getYAxisTickConfiguration(chartOptions: IChartOptions, axisPropsKey: string) {
    const { type, yAxes } = chartOptions;
    if (isBubbleChart(type) || isScatterPlot(type)) {
        return {
            startOnTick: shouldYAxisStartOnTickOnBubbleScatter(chartOptions),
        };
    }

    if (isOneOfTypes(type, supportedDualAxesChartTypes) && yAxes.length > 1) {
        // disable { startOnTick, endOnTick } to make gridline sync in both axes
        return {};
    }

    return {
        startOnTick: shouldStartOnTick(chartOptions, axisPropsKey),
        endOnTick: shouldEndOnTick(chartOptions, axisPropsKey),
    };
}

function getAxesConfiguration(chartOptions: IChartOptions) {
    const { forceDisableDrillOnAxes = false } = chartOptions;
    const type = chartOptions.type as ChartType;

    return {
        plotOptions: {
            series: {
                ...shouldExpandYAxis(chartOptions),
            },
        },
        yAxis: get(chartOptions, "yAxes", []).map((axis: any) => {
            if (!axis) {
                return {
                    visible: false,
                };
            }

            const opposite = get(axis, "opposite", false);
            const axisType: string = axis.opposite ? "secondary" : "primary";
            const className: string = cx(`s-highcharts-${axisType}-yaxis`, {
                "gd-axis-label-drilling-disabled": forceDisableDrillOnAxes,
            });
            const axisPropsKey = opposite ? "secondary_yAxisProps" : "yAxisProps";

            // For bar chart take x axis options
            const min = get(chartOptions, `${axisPropsKey}.min`, "");
            const max = get(chartOptions, `${axisPropsKey}.max`, "");
            const visible = get(chartOptions, `${axisPropsKey}.visible`, true);

            const maxProp = max ? { max: Number(max) } : {};
            const minProp = min ? { min: Number(min) } : {};

            const rotation = get(chartOptions, `${axisPropsKey}.rotation`, "auto");
            const rotationProp = rotation !== "auto" ? { rotation: -Number(rotation) } : {};

            const shouldCheckForEmptyCategories = isHeatmap(type) ? true : false;
            const labelsEnabled = areAxisLabelsEnabled(
                chartOptions,
                axisPropsKey,
                shouldCheckForEmptyCategories,
            );

            const tickConfiguration = getYAxisTickConfiguration(chartOptions, axisPropsKey);

            return {
                ...getAxisLineConfiguration(type, visible),
                labels: {
                    ...labelsEnabled,
                    style: {
                        color: styleVariables.gdColorStateBlank,
                        font: '12px Avenir, "Helvetica Neue", Arial, sans-serif',
                    },
                    ...rotationProp,
                },
                title: {
                    enabled: visible,
                    margin: 15,
                    style: {
                        color: styleVariables.gdColorLink,
                        font: '14px Avenir, "Helvetica Neue", Arial, sans-serif',
                    },
                },
                opposite,
                className,
                ...maxProp,
                ...minProp,
                ...tickConfiguration,
            };
        }),

        xAxis: get(chartOptions, "xAxes", []).map((axis: any) => {
            if (!axis) {
                return {
                    visible: false,
                };
            }

            const opposite = get(axis, "opposite", false);
            const axisPropsKey = opposite ? "secondary_xAxisProps" : "xAxisProps";
            const className: string = cx({
                "gd-axis-label-drilling-disabled": forceDisableDrillOnAxes,
            });

            const min = get(chartOptions, axisPropsKey.concat(".min"), "");
            const max = get(chartOptions, axisPropsKey.concat(".max"), "");

            const maxProp = max ? { max: Number(max) } : {};
            const minProp = min ? { min: Number(min) } : {};

            const isViewByTwoAttributes = get(chartOptions, "isViewByTwoAttributes", false);
            const visible = get(chartOptions, axisPropsKey.concat(".visible"), true);
            const rotation = get(chartOptions, axisPropsKey.concat(".rotation"), "auto");
            const rotationProp = rotation !== "auto" ? { rotation: -Number(rotation) } : {};

            const shouldCheckForEmptyCategories = isScatterPlot(type) || isBubbleChart(type) ? false : true;
            const labelsEnabled = areAxisLabelsEnabled(
                chartOptions,
                axisPropsKey,
                shouldCheckForEmptyCategories,
            );

            const tickConfiguration = getXAxisTickConfiguration(chartOptions);

            // for bar chart take y axis options
            return {
                ...getAxisLineConfiguration(type, visible),

                // hide ticks on x axis
                minorTickLength: 0,
                tickLength: 0,

                // padding of maximum value
                maxPadding: 0.05,

                labels: {
                    ...labelsEnabled,
                    style: {
                        color: styleVariables.gdColorStateBlank,
                        font: '12px Avenir, "Helvetica Neue", Arial, sans-serif',
                    },
                    autoRotation: [-90],
                    ...rotationProp,
                },
                title: {
                    // should disable X axis title when 'View By 2 attributes'
                    enabled: visible && !isViewByTwoAttributes,
                    margin: 10,
                    style: {
                        textOverflow: "ellipsis",
                        color: styleVariables.gdColorLink,
                        font: '14px Avenir, "Helvetica Neue", Arial, sans-serif',
                    },
                },
                className,
                ...maxProp,
                ...minProp,
                ...tickConfiguration,
            };
        }),
    };
}

function getTargetCursorConfigurationForBulletChart(chartOptions: IChartOptions) {
    const { type, data } = chartOptions;

    if (!isBulletChart(type)) {
        return {};
    }

    const isTargetDrillable = data.series.some(
        (series: ISeriesItem) => series.type === "bullet" && series.isDrillable,
    );

    return isTargetDrillable ? { plotOptions: { bullet: { cursor: "pointer" } } } : {};
}

export function getCustomizedConfiguration(
    chartOptions: IChartOptions,
    chartConfig?: IChartConfig,
    drillConfig?: IDrillConfig,
) {
    const configurators = [
        getAxesConfiguration,
        getTitleConfiguration,
        getStackingConfiguration,
        hideOverlappedLabels,
        getShowInPercentConfiguration,
        getDataConfiguration,
        getTooltipConfiguration,
        getHoverStyles,
        getGridConfiguration,
        getLabelsConfiguration,
        // should be after 'getDataConfiguration' to modify 'series'
        // and should be after 'getStackingConfiguration' to get stackLabels config
        getOptionalStackingConfiguration,
        getZeroAlignConfiguration,
        getAxisNameConfiguration,
        getChartAlignmentConfiguration,
        getAxisLabelConfigurationForDualBarChart,
        getTargetCursorConfigurationForBulletChart,
    ];

    const commonData = configurators.reduce((config: any, configurator: any) => {
        return merge(config, configurator(chartOptions, config, chartConfig, drillConfig));
    }, {});

    return merge({}, commonData);
}
