import { Point } from "chart.js";
import {
  evaluatePolynomial,
  expM,
  getMovingAverageValues,
  logM,
  polynomialRegression,
  predictLinearValues,
} from "../../../../functions/helper_statistical";
import { toNumber, tryToNumber } from "../../../../functions/helpers";
import { _t } from "../../../../translation";
import { CellValue } from "../../../../types/cells";
import { BarChartDefinition } from "../../../../types/chart/bar_chart";
import { BubbleChartDefinition } from "../../../../types/chart/bubble_chart";
import {
  CalendarChartDefinition,
  CalendarChartGranularity,
} from "../../../../types/chart/calendar_chart";
import {
  AxisType,
  ChartData,
  ChartRuntimeGenerationArgs,
  DataSetStyle,
  DatasetValues,
  GenericDefinition,
  LabelValues,
  TrendConfiguration,
} from "../../../../types/chart/chart";
import { FunnelChartDefinition } from "../../../../types/chart/funnel_chart";
import {
  GeoChartDefinition,
  GeoChartRuntimeGenerationArgs,
} from "../../../../types/chart/geo_chart";
import { LineChartDefinition } from "../../../../types/chart/line_chart";
import { PieChartDefinition } from "../../../../types/chart/pie_chart";
import { PyramidChartDefinition } from "../../../../types/chart/pyramid_chart";
import { RadarChartDefinition } from "../../../../types/chart/radar_chart";
import { SunburstChartDefinition } from "../../../../types/chart/sunburst_chart";
import { TreeMapChartDefinition } from "../../../../types/chart/tree_map_chart";
import { Format } from "../../../../types/format";
import { EvaluationGetters } from "../../../../types/getters";
import { DEFAULT_LOCALE, Locale } from "../../../../types/locale";
import { FunctionResultObject } from "../../../../types/misc";
import { Range } from "../../../../types/range";
import { ColorThemeName } from "../../../../types/rendering";
import { isNumberResult } from "../../../cells/cell_evaluation";
import { timeFormatLuxonCompatible } from "../../../chart_date";
import { DAYS, formatValue, isDateTimeFormat, MONTHS } from "../../../format/format";
import { deepCopy, findNextDefinedValue, range } from "../../../misc";
import { createDate } from "../../../pivot/spreadsheet_pivot/date_spreadsheet_pivot";
import { BubbleChartData } from "../bubble_chart";
import { getChartBackgroundColor, shouldRemoveFirstLabel } from "../chart_common";

const EMPTY = Object.freeze({ value: null });
const ZERO = Object.freeze({ value: 0 });

export function getBarChartData(
  definition: GenericDefinition<BarChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  const locale = getters.getLocale();
  let labels = labelValues.map(({ value, format }) => formatValue(value, { format, locale }));

  ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
  if (definition.aggregated) {
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
  }

  const leftAxisFormat = getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left");
  const rightAxisFormat = getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "right");
  const axisFormats = definition.horizontal
    ? { x: leftAxisFormat || rightAxisFormat }
    : { y: leftAxisFormat, y1: rightAxisFormat };

  const trendDataSetsValues: (Point[] | undefined)[] = [];
  for (const index in dataSetsValues) {
    const { data, dataSetId } = dataSetsValues[index];

    const trend = definition.dataSetStyles?.[dataSetId]?.trend;
    if (!trend?.display || definition.horizontal) {
      trendDataSetsValues.push(undefined);
      continue;
    }

    const trendDataset = getTrendDatasetForBarChart(trend, data);
    trendDataSetsValues.push(trendDataset);
  }

  return {
    dataSetsValues,
    trendDataSetsValues,
    axisFormats,
    labels,
    locale: getters.getLocale(),
    topPadding: getTopPaddingForDashboard(definition, getters),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

function getDateTimeLabel(value: number, stamp: CalendarChartGranularity): string {
  switch (stamp) {
    case "day_of_week": {
      return DAYS[value - 1].toString();
    }
    case "hour_number": {
      const hour = String(value % 12).padStart(2, "0");
      return value < 12 ? _t("%(hour)s AM", { hour }) : _t("%(hour)s PM", { hour });
    }
    case "month_number": {
      return MONTHS[value - 1].toString();
    }
    case "iso_week_number": {
      const weekNumber = String(value).padStart(2, "0");
      return _t("W%(weekNumber)s", { weekNumber });
    }
    case "quarter_number": {
      return _t("Q%(value)s", { value });
    }
    default: {
      return value.toString();
    }
  }
}

function computeValuesAndLabels(
  timeValues: FunctionResultObject[],
  values: FunctionResultObject[],
  horizontalGroupBy: CalendarChartGranularity,
  verticalGroupBy: CalendarChartGranularity,
  locale: Locale,
  countEntries: boolean
) {
  const grouping: Record<string, Record<string, { value: number }>> = {};
  const xValues: number[] = [];
  const yValues: number[] = [];
  const previousYValues: number[] = [];
  for (let i = 0; i < timeValues?.length; i++) {
    const xValue = toNumber(
      createDate(
        { granularity: horizontalGroupBy, type: "date", displayName: "date" },
        timeValues[i].value,
        DEFAULT_LOCALE
      ),
      locale
    );
    if (!(xValue in grouping)) {
      xValues.push(xValue);
      grouping[xValue] = {};
    }
    const yValue = toNumber(
      createDate(
        { granularity: verticalGroupBy, type: "date", displayName: "date" },
        timeValues[i].value,
        DEFAULT_LOCALE
      ),
      locale
    );
    if (!previousYValues.includes(yValue)) {
      yValues.push(yValue);
      previousYValues.push(yValue);
    }
    if (!(yValue in grouping[xValue])) {
      grouping[xValue][yValue] = { value: 0 };
    }
    if (countEntries) {
      grouping[xValue][yValue].value += 1;
    } else {
      const cell = values[i];
      if (isNumberResult(cell)) {
        grouping[xValue][yValue].value += cell.value;
      }
    }
  }

  xValues.sort((a, b) => a - b);
  yValues.sort((a, b) => b - a);

  const dataSetsValues = yValues.map((y) => ({
    data: xValues.map((x) => grouping?.[x]?.[y]),
    label: getDateTimeLabel(y, verticalGroupBy),
    hidden: false,
    dataSetId: "0",
  }));

  return {
    dataSetsValues,
    labels: xValues.map((v) => ({ value: getDateTimeLabel(v, horizontalGroupBy) })),
  };
}

export function getCalendarChartData(
  definition: GenericDefinition<CalendarChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  let labels = labelValues;

  const locale = getters.getLocale();
  const countEntries = dataSetsValues.length === 0;

  ({ labels, dataSetsValues } = filterInvalidCalendarDataPoints(
    labels,
    dataSetsValues,
    countEntries
  ));
  const axisFormats = {
    y: getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left"),
  };

  ({ labels, dataSetsValues } = computeValuesAndLabels(
    labels,
    dataSetsValues[0]?.data ?? [],
    definition.horizontalGroupBy ?? "day_of_week",
    definition.verticalGroupBy ?? "hour_number",
    locale,
    countEntries
  ));

  return {
    dataSetsValues,
    axisFormats,
    labels: labels.map(({ value }) => String(value ?? "")),
    locale: getters.getLocale(),
    topPadding: getTopPaddingForDashboard(definition, getters),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getPyramidChartData(
  definition: PyramidChartDefinition,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  const barChartData = getBarChartData(
    definition,
    { labelValues, dataSetsValues: dataSetsValues.slice(0, 2) },
    getters,
    colorThemeName
  );
  const barDataset = barChartData.dataSetsValues.filter((ds) => !ds.hidden);

  const pyramidDatasetValues: DatasetValues[] = [];
  if (barDataset[0]) {
    const pyramidData = barDataset[0].data.map((cell) =>
      isNumberResult(cell) && cell.value > 0 ? cell : ZERO
    );
    pyramidDatasetValues.push({ ...barDataset[0], data: pyramidData });
  }
  if (barDataset[1]) {
    const pyramidData = barDataset[1].data.map((cell) =>
      isNumberResult(cell) && cell.value > 0 ? { value: -cell.value } : ZERO
    );
    pyramidDatasetValues.push({ ...barDataset[1], data: pyramidData });
  }

  return {
    ...barChartData,
    dataSetsValues: pyramidDatasetValues,
  };
}

export function getLineChartData(
  definition: GenericDefinition<LineChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  const axisType = getChartAxisType(definition, { labelValues, dataSetsValues });
  let labels =
    axisType === "linear"
      ? labelValues.map(({ value }) => String(value ?? ""))
      : labelValues.map(({ value, format }) =>
          formatValue(value, { format, locale: getters.getLocale() })
        );

  // Area charts use non-numeric values as zero.
  const invalidDataValue = definition.fillArea ? ZERO : EMPTY;
  ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues, invalidDataValue));
  if (axisType === "time") {
    ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
  }
  if (definition.aggregated) {
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
  }
  if (definition.cumulative) {
    dataSetsValues = makeDatasetsCumulative(dataSetsValues, "asc");
  }

  const leftAxisFormat = getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left");
  const rightAxisFormat = getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "right");
  const labelsFormat = getChartLabelFormat(labelValues);
  const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat, x: labelsFormat };

  const trendDataSetsValues: (Point[] | undefined)[] = [];
  for (const index in dataSetsValues) {
    const { data, dataSetId } = dataSetsValues[index];
    const trend = definition.dataSetStyles?.[dataSetId]?.trend;
    if (!trend?.display) {
      trendDataSetsValues.push(undefined);
      continue;
    }

    trendDataSetsValues.push(
      getTrendDatasetForLineChart(trend, data, labels, axisType, getters.getLocale())
    );
  }

  return {
    dataSetsValues,
    axisFormats,
    labels,
    locale: getters.getLocale(),
    trendDataSetsValues,
    axisType,
    topPadding: getTopPaddingForDashboard(definition, getters),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getPieChartData(
  definition: GenericDefinition<PieChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  let labels = labelValues.map(({ value, format }) =>
    formatValue(value, { format, locale: getters.getLocale() })
  );

  ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));

  if (definition.aggregated) {
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
  }

  ({ dataSetsValues, labels } = keepOnlyPositiveValues(labels, dataSetsValues));

  const dataSetFormat = getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left");

  return {
    dataSetsValues,
    axisFormats: { y: dataSetFormat },
    labels,
    locale: getters.getLocale(),
    topPadding: getTopPaddingForDashboard(definition, getters),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getRadarChartData(
  definition: GenericDefinition<RadarChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  let labels = labelValues.map(({ value, format }) =>
    formatValue(value, { format, locale: getters.getLocale() })
  );

  ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
  if (definition.aggregated) {
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
  }

  const dataSetFormat =
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left") ||
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "right");
  const axisFormats = { r: dataSetFormat };

  return {
    dataSetsValues,
    axisFormats,
    labels,
    locale: getters.getLocale(),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getGeoChartData(
  definition: GeoChartDefinition,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): GeoChartRuntimeGenerationArgs {
  dataSetsValues = dataSetsValues.slice(0, 1);
  let labels = labelValues.map(({ value, format }) =>
    formatValue(value, { format, locale: getters.getLocale() })
  );
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));

  const format =
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left") ||
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "right");

  return {
    dataSetsValues,
    axisFormats: { y: format },
    labels,
    locale: getters.getLocale(),
    availableRegions: getters.getGeoChartAvailableRegions(),
    geoFeatureNameToId: getters.geoFeatureNameToId,
    getGeoJsonFeatures: getters.getGeoJsonFeatures,
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getFunnelChartData(
  definition: GenericDefinition<FunnelChartDefinition>,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  let labels = labelValues.map(({ value, format }) =>
    formatValue(value, { format, locale: getters.getLocale() })
  );

  ({ labels, dataSetsValues } = filterInvalidDataPoints(labels, dataSetsValues));
  if (definition.aggregated) {
    ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
  }
  if (definition.cumulative) {
    dataSetsValues = makeDatasetsCumulative(dataSetsValues, "desc");
  }
  const format =
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "left") ||
    getChartDatasetFormat(definition.dataSetStyles, dataSetsValues, "right");

  return {
    dataSetsValues,
    axisFormats: { x: format },
    labels,
    locale: getters.getLocale(),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getHierarchalChartData(
  definition: SunburstChartDefinition | TreeMapChartDefinition,
  { labelValues, dataSetsValues }: ChartData,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): ChartRuntimeGenerationArgs {
  // In hierarchical charts, labels are the leaf values (numbers), and the hierarchy is defined in the dataSets (strings)
  let labels = labelValues;
  ({ labels, dataSetsValues } = filterValuesWithDifferentSigns(labels, dataSetsValues));
  ({ labels, dataSetsValues } = filterInvalidHierarchicalPoints(labels, dataSetsValues));

  return {
    dataSetsValues,
    axisFormats: { y: getChartLabelFormat(labels) },
    labels: labels.map(({ value }) => String(value ?? "")),
    locale: getters.getLocale(),
    background: getChartBackgroundColor(definition, colorThemeName),
  };
}

export function getBubbleChartData(
  definition: BubbleChartDefinition<Range>,
  getters: EvaluationGetters,
  colorThemeName: ColorThemeName
): BubbleChartData {
  const chartData = getBubbleChartSourceData(definition, getters);
  const data = getLineChartData(definition, chartData, getters, colorThemeName);

  let labelValues = definition.labelRange
    ? getters.getVisibleRangeValues(definition.labelRange)
    : [];
  let sizeValues = definition.sizeRange ? getters.getVisibleRangeValues(definition.sizeRange) : [];
  if (chartData.shouldRemoveFirstLabel) {
    labelValues = labelValues.slice(1);
    sizeValues = sizeValues.slice(1);
  }

  const bubbleLabels: string[] = [];
  const bubbleSizes: (number | undefined)[] = [];
  const sizeFormat = getChartLabelFormat(sizeValues);

  for (let index = 0; index < data.dataSetsValues[0].data.length; index++) {
    const rawSize = sizeValues[index]?.value;
    const labelValue = labelValues[index];
    const label = labelValue
      ? formatValue(labelValue.value, { format: labelValue.format, locale: data.locale })
      : "";
    let sizeNumber = rawSize === null ? undefined : tryToNumber(rawSize, data.locale);
    if (sizeNumber !== undefined && sizeNumber < 0) {
      sizeNumber = undefined;
    }
    bubbleLabels.push(label ? String(label) : "");
    bubbleSizes.push(sizeNumber);
  }

  const order = range(0, bubbleSizes.length).sort((a, b) => {
    const sizeA = bubbleSizes[a];
    const sizeB = bubbleSizes[b];
    if (sizeA === undefined && sizeB === undefined) {
      return 0;
    }
    if (sizeA === undefined) {
      return 1;
    }
    if (sizeB === undefined) {
      return -1;
    }
    return sizeB - sizeA;
  });

  data.dataSetsValues[0] = {
    ...data.dataSetsValues[0],
    data: order.map((i) => data.dataSetsValues[0].data[i]),
  };

  return {
    ...data,
    axisFormats: {
      ...(data.axisFormats || {}),
      size: sizeFormat,
    },
    labels: order.map((i) => data.labels[i]),
    bubbleLabels: order.map((i) => bubbleLabels[i]),
    bubbleSizes: order.map((i) => bubbleSizes[i]),
  };
}

function getBubbleChartSourceData(
  definition: BubbleChartDefinition<Range>,
  getters: EvaluationGetters
): ChartData & { shouldRemoveFirstLabel: boolean } {
  const yRange = definition.yRanges[0];
  const labelValues = definition.xRange
    ? getters.getVisibleRangeValues(definition.xRange)
    : yRange
    ? Array.from({ length: getters.getVisibleRangeValues(yRange).length }, () => EMPTY)
    : [];
  const yValues = yRange ? getters.getVisibleRangeValues(yRange) : [];
  const removeFirstValue = shouldRemoveFirstLabel(
    labelValues.length,
    yValues.length,
    definition.dataSetsHaveTitle || false
  );
  return {
    labelValues: removeFirstValue ? labelValues.slice(1) : labelValues,
    dataSetsValues: [
      {
        dataSetId: "0",
        label: "",
        data: removeFirstValue ? yValues.slice(1) : yValues,
      },
    ],
    shouldRemoveFirstLabel: removeFirstValue,
  };
}

export function getTrendDatasetForBarChart(
  config: TrendConfiguration,
  data: FunctionResultObject[]
) {
  const filteredValues: number[] = [];
  const filteredLabels: number[] = [];
  const labels: number[] = [];
  for (let i = 0; i < data.length; i++) {
    const cell = data[i];
    if (isNumberResult(cell)) {
      filteredValues.push(cell.value);
      filteredLabels.push(i + 1);
    }
    labels.push(i + 1);
  }

  const newLabels = range(0.5, labels.length + 0.55, 0.2);
  const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);
  return newValues.length ? newValues : undefined;
}

export function getTrendDatasetForLineChart(
  config: TrendConfiguration,
  data: FunctionResultObject[],
  labels: string[],
  axisType: AxisType,
  locale: Locale
) {
  const filteredValues: number[] = [];
  const filteredLabels: number[] = [];
  const trendLabels: number[] = [];
  const datasetLength = data.length;

  if (datasetLength < 2) {
    return;
  }

  switch (axisType) {
    case "category":
      for (let i = 0; i < datasetLength; i++) {
        const cell = data[i];
        if (isNumberResult(cell)) {
          filteredValues.push(cell.value);
          filteredLabels.push(i + 1);
        }
        trendLabels.push(i + 1);
      }
      break;
    case "linear":
      for (let i = 0; i < data.length; i++) {
        const label = Number(labels[i]);
        if (isNaN(label)) {
          continue;
        }
        const cell = data[i];
        if (isNumberResult(cell)) {
          filteredValues.push(cell.value);
          filteredLabels.push(label);
        }
        trendLabels.push(label);
      }
      break;
    case "time":
      for (let i = 0; i < data.length; i++) {
        const date = toNumber({ value: labels[i] }, locale);
        const cell = data[i];
        if (isNumberResult(cell)) {
          filteredValues.push(cell.value);
          filteredLabels.push(date);
        }
        trendLabels.push(date);
      }
      break;
  }

  const xmin = Math.min(...trendLabels);
  const xmax = Math.max(...trendLabels);
  if (xmax === xmin) {
    return;
  }
  const numberOfStep = 5 * trendLabels.length;
  const step = (xmax - xmin) / numberOfStep;
  const trendNewLabels = range(xmin, xmax + step / 2, step);
  const trendValues = interpolateData(config, filteredValues, filteredLabels, trendNewLabels);
  if (!trendValues.length) {
    return;
  }
  return trendValues;
}

function interpolateData(
  config: TrendConfiguration,
  values: number[],
  labels: number[],
  newLabels: number[]
): Point[] {
  if (values.length < 2 || labels.length < 2 || newLabels.length === 0) {
    return [];
  }
  const { normalizedLabels, normalizedNewLabels } = normalizeLabels(labels, newLabels, config);
  try {
    switch (config.type) {
      case "polynomial": {
        const order = config.order;
        if (!order) {
          return newLabels.map((x) => ({ x, y: NaN }));
        }
        if (order === 1) {
          return predictLinearValues(
            [values],
            [normalizedLabels],
            [normalizedNewLabels],
            true
          )[0].map((y, i) => ({ x: newLabels[i], y }));
        }
        const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
        return normalizedNewLabels.map((x, i) => ({
          x: newLabels[i],
          y: evaluatePolynomial(coeffs, x, order),
        }));
      }
      case "exponential": {
        const positiveLogValues: number[] = [];
        const filteredLabels: number[] = [];
        for (let i = 0; i < values.length; i++) {
          if (values[i] > 0) {
            positiveLogValues.push(Math.log(values[i]));
            filteredLabels.push(normalizedLabels[i]);
          }
        }
        if (!filteredLabels.length) {
          return newLabels.map((x) => ({ x, y: NaN }));
        }
        return expM(
          predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true)
        )[0].map((y, i) => ({ x: newLabels[i], y }));
      }
      case "logarithmic": {
        return predictLinearValues(
          [values],
          logM([normalizedLabels]),
          logM([normalizedNewLabels]),
          true
        )[0].map((y, i) => ({ x: newLabels[i], y }));
      }
      case "trailingMovingAverage": {
        return getMovingAverageValues(values, labels, config.window);
      }
      default:
        return newLabels.map((x) => ({ x, y: NaN }));
    }
  } catch (e) {
    return newLabels.map((x) => ({ x, y: NaN }));
  }
}

function normalizeLabels(
  labels: number[],
  newLabels: number[],
  config: TrendConfiguration
): { normalizedLabels: number[]; normalizedNewLabels: number[] } {
  let normalizedLabels: number[] = [];
  let normalizedNewLabels: number[] = [];
  if (config.type === "logarithmic") {
    // Logarithmic trends in charts are used to visualize proportional growth or
    // relative changes. Therefore, we change the normalization technique for
    // logarithmic trend lines for a better fit. The method used here is Max Absolute
    // Scaling. This Technique is ideal for data spanning several orders of magnitude,
    // as it balances differences between small and large values by compressing larger
    // values while preserving proportionality and ensuring all values are scaled relative
    // to the largest magnitude.
    const labelMax = Math.max(...labels.map(Math.abs));
    normalizedLabels = labels.map((l) => l / labelMax);
    normalizedNewLabels = newLabels.map((l) => l / labelMax);
  } else {
    const labelMax = Math.max(...labels);
    const labelMin = Math.min(...labels);
    const labelRange = labelMax - labelMin;
    normalizedLabels = labels.map((l) => (l - labelMax) / labelRange);
    normalizedNewLabels = newLabels.map((l) => (l - labelMax) / labelRange);
  }
  return { normalizedLabels, normalizedNewLabels };
}

function getChartAxisType(
  definition: GenericDefinition<LineChartDefinition>,
  data: ChartData
): AxisType {
  if (isDateChart(definition, data) && isLuxonTimeAdapterInstalled()) {
    return "time";
  }
  if (isLinearChart(definition, data)) {
    return "linear";
  }
  return "category";
}

function isDateChart(definition: GenericDefinition<LineChartDefinition>, data: ChartData): boolean {
  return !definition.labelsAsText && canBeDateChart(data);
}

function isLinearChart(
  definition: GenericDefinition<LineChartDefinition>,
  data: ChartData
): boolean {
  return !definition.labelsAsText && canBeLinearChart(data);
}

export function canChartParseLabels(chartData: ChartData): boolean {
  return canBeDateChart(chartData) || canBeLinearChart(chartData);
}

function canBeDateChart(data: ChartData): boolean {
  if (!canBeLinearChart(data)) {
    return false;
  }
  const labelFormat = getChartLabelFormat(data.labelValues);
  return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat));
}

function canBeLinearChart(data: ChartData): boolean {
  const labels = data.labelValues;

  if (labels.some((label) => !isNumberResult(label) && label.value)) {
    return false;
  }
  if (labels.every((label) => !label.value)) {
    return false;
  }

  return true;
}

let missingTimeAdapterAlreadyWarned = false;

function isLuxonTimeAdapterInstalled() {
  if (!window.Chart) {
    return false;
  }
  const adapter = new window.Chart._adapters._date({});
  // @ts-ignore
  const isInstalled = adapter._id === "luxon";
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
    missingTimeAdapterAlreadyWarned = true;
    console.warn(
      "'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled."
    );
  }
  return isInstalled;
}

function keepOnlyPositiveValues(
  labels: readonly string[],
  datasets: readonly DatasetValues[]
): { labels: string[]; dataSetsValues: DatasetValues[] } {
  const numberOfDataPoints = Math.max(
    labels.length,
    ...datasets.map((dataset) => dataset.data?.length || 0)
  );
  const filteredIndexes = range(0, numberOfDataPoints).filter((i) =>
    datasets.some((ds) => isNumberResult(ds.data[i]) && ds.data[i].value > 0)
  );
  return {
    labels: filteredIndexes.map((i) => labels[i] || ""),
    dataSetsValues: datasets.map((ds) => ({
      ...ds,
      data: filteredIndexes.map((i) =>
        isNumberResult(ds.data[i]) && ds.data[i].value > 0 ? ds.data[i] : EMPTY
      ),
    })),
  };
}

function fixEmptyLabelsForDateCharts(
  labels: string[],
  dataSetsValues: DatasetValues[]
): { labels: string[]; dataSetsValues: DatasetValues[] } {
  if (labels.length === 0 || labels.every((label) => !label)) {
    return { labels, dataSetsValues };
  }
  const newLabels = [...labels];
  const newDatasets = deepCopy(dataSetsValues);
  for (let i = 0; i < newLabels.length; i++) {
    if (!newLabels[i]) {
      newLabels[i] = findNextDefinedValue(newLabels, i);
      for (const ds of newDatasets) {
        ds.data[i] = EMPTY;
      }
    }
  }
  return { labels: newLabels, dataSetsValues: newDatasets };
}

/**
 * Filter the data points that:
 * - have neither a label nor a value
 * - have no label and a non-numeric value
 */
function filterInvalidDataPoints(
  labels: string[],
  datasets: DatasetValues[],
  invalidDataValue: FunctionResultObject = EMPTY
): { labels: string[]; dataSetsValues: DatasetValues[] } {
  const numberOfDataPoints = Math.max(
    labels.length,
    ...datasets.map((dataset) => dataset.data?.length || 0)
  );
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
    const label = labels[dataPointIndex];
    const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
    return label || values.some(isNumberResult);
  });
  return {
    labels: dataPointsIndexes.map((i) => labels[i] || ""),
    dataSetsValues: datasets.map((dataset) => ({
      ...dataset,
      data: dataPointsIndexes.map((i) =>
        isNumberResult(dataset.data[i]) ? dataset.data[i] : invalidDataValue
      ),
    })),
  };
}

/**
 * Keep only data points that:
 * - have a valid numeric label if countEntries is true
 * - have both a valid numeric date label and a valid numeric value else
 */
function filterInvalidCalendarDataPoints(
  labels: LabelValues,
  datasets: DatasetValues[],
  countEntries: boolean
): { labels: LabelValues; dataSetsValues: DatasetValues[] } {
  const numberOfDataPoints = Math.max(
    labels.length,
    ...datasets.map((dataset) => dataset.data?.length || 0)
  );
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
    const label = labels[dataPointIndex];
    if (!isNumberResult(label)) {
      return false;
    } else if (countEntries) {
      return true;
    }
    const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
    return isNumberResult(values[0]);
  });
  return {
    labels: dataPointsIndexes.map((i) => labels[i] || ""),
    dataSetsValues: datasets.map((dataset) => ({
      ...dataset,
      data: dataPointsIndexes.map((i) =>
        isNumberResult(dataset.data[i]) ? dataset.data[i] : EMPTY
      ),
    })),
  };
}

/**
 * Filter the data points that have either no value, a negative value, no root group or null group values in the middle
 */
function filterInvalidHierarchicalPoints(
  values: LabelValues,
  hierarchy: DatasetValues[]
): { labels: LabelValues; dataSetsValues: DatasetValues[] } {
  const numberOfDataPoints = Math.max(
    values.length,
    ...hierarchy.map((dataset) => dataset.data?.length || 0)
  );
  const isEmpty = (value: CellValue) => value === undefined || value === null || value === "";
  const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
    const groups = hierarchy.map((dataset) => dataset.data?.[dataPointIndex]);
    if (isEmpty(groups[0]?.value)) {
      return false;
    }
    // Filter points with empty group in the middle
    let hasFoundEmptyGroup = false;
    for (const group of groups) {
      hasFoundEmptyGroup ||= isEmpty(group?.value);
      if (hasFoundEmptyGroup && !isEmpty(group?.value)) {
        return false;
      }
    }
    return values[dataPointIndex] && isNumberResult(values[dataPointIndex]);
  });
  return {
    labels: dataPointsIndexes.map((i) => values[i]),
    dataSetsValues: hierarchy.map((dataset) => ({
      ...dataset,
      data: dataPointsIndexes.map((i) => dataset.data[i]),
    })),
  };
}

/**
 * If the values are a mix of positive and negative values, keep only the positive ones
 */
function filterValuesWithDifferentSigns(values: LabelValues, hierarchy: DatasetValues[]) {
  const positivePointsIndexes: number[] = [];
  const negativePointsIndexes: number[] = [];

  for (let i = 0; i < values.length; i++) {
    if (Number(values[i].value) <= 0) {
      negativePointsIndexes.push(i);
    } else if (Number(values[i].value) > 0) {
      positivePointsIndexes.push(i);
    }
  }
  const indexesToKeep = positivePointsIndexes.length
    ? positivePointsIndexes
    : negativePointsIndexes;

  return {
    labels: indexesToKeep.map((i) => values[i]),
    dataSetsValues: hierarchy.map((dataset) => ({
      ...dataset,
      data: indexesToKeep.map((i) => dataset.data[i]),
    })),
  };
}

/**
 * Aggregates data based on labels
 */
function aggregateDataForLabels(
  labels: string[],
  datasets: DatasetValues[]
): { labels: string[]; dataSetsValues: DatasetValues[] } {
  const parseNumber = (value: CellValue) => (typeof value === "number" ? value : 0);
  const labelSet = new Set(labels);
  const labelMap: { [key: string]: { value: number; format?: Format }[] } = {};
  labelSet.forEach((label) => {
    labelMap[label] = new Array(datasets.length);
  });

  for (const indexOfLabel of range(0, labels.length)) {
    const label = labels[indexOfLabel];
    for (const indexOfDataset of range(0, datasets.length)) {
      const cell = datasets[indexOfDataset].data[indexOfLabel];
      if (!labelMap[label][indexOfDataset]) {
        labelMap[label][indexOfDataset] = { ...cell, value: parseNumber(cell?.value) };
      } else {
        labelMap[label][indexOfDataset].value += parseNumber(cell?.value);
      }
    }
  }

  return {
    labels: Array.from(labelSet),
    dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
      ...dataset,
      data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
    })),
  };
}

export function getChartLabelFormat(labelValues: LabelValues): Format | undefined {
  return labelValues.find(({ format }) => format !== undefined)?.format;
}

/**
 * Get the format to apply to the the dataset values. This format is defined as the first format
 * found in the dataset ranges that isn't a date format.
 */
function getChartDatasetFormat(
  dataSetStyle: DataSetStyle | undefined,
  dataSetValues: DatasetValues[],
  axis: "left" | "right"
): Format | undefined {
  const dataSets = dataSetValues.filter(
    (ds) => (axis === "right") === (dataSetStyle?.[ds.dataSetId]?.yAxisId === "y1")
  );
  for (const ds of dataSets) {
    const cell = ds.data.find(({ format }) => format !== undefined && !isDateTimeFormat(format));
    if (cell) {
      return cell.format;
    }
  }
  return undefined;
}

export function makeDatasetsCumulative(
  datasets: DatasetValues[],
  order: "asc" | "desc"
): DatasetValues[] {
  return datasets.map((dataset) => {
    const data: { value: number | null; format?: Format }[] = [];
    let accumulator = 0;
    const indexes =
      order === "asc" ? range(0, dataset.data.length) : range(0, dataset.data.length).reverse();
    for (const i of indexes) {
      const cell = dataset.data[i];
      if (isNumberResult(cell)) {
        accumulator += cell.value;
        data[i] = { ...cell, value: accumulator };
      } else {
        data[i] = EMPTY;
      }
    }
    return { ...dataset, data };
  });
}

export function getTopPaddingForDashboard(
  definition: GenericDefinition<PieChartDefinition | LineChartDefinition | BarChartDefinition>,
  getters: EvaluationGetters
) {
  const { title, legendPosition } = definition;
  const hasTitleOrLegendTop = (title && title.text) || legendPosition === "top";
  return getters.isDashboard() && !hasTitleOrLegendTop ? 30 : 0;
}
