import { proxy, useProps } from "@odoo/owl";
import { numberToLetters } from "../../../../../helpers/coordinates";
import { createDataSets } from "../../../../../helpers/figures/charts/helpers_index";
import { getChartColorsGenerator } from "../../../../../helpers/figures/charts/runtime/chartjs_dataset";
import { isDefined } from "../../../../../helpers/misc";
import { createValidRange } from "../../../../../helpers/range";
import { splitReference } from "../../../../../helpers/references";
import { UuidGenerator } from "../../../../../helpers/uuid";
import {
  isXcRepresentation,
  mergeContiguousZones,
  toUnboundedZone,
  toZone,
  zoneToXc,
} from "../../../../../helpers/zones";
import { Component } from "../../../../../owl3_compatibility_layer";
import { chartTypeRegistry } from "../../../../../registries/chart_registry";
import { _t } from "../../../../../translation";
import {
  ChartDatasetOrientation,
  ChartDefinitionWithDataSource,
  ChartRangeDataSource as ChartRangeDataSourceType,
  DataSetStyle,
} from "../../../../../types/chart/chart";
import { CommandResult, DispatchResult } from "../../../../../types/commands";
import { UID, Zone } from "../../../../../types/misc";
import { SpreadsheetChildEnv } from "../../../../../types/spreadsheet_env";
import { types } from "../../../../props_validation";
import { ChartTerms } from "../../../../translations_terms";
import { ChartUpdateFunction } from "../../common";
import { ChartDataSeries } from "../data_series/data_series";
import { ChartLabelRange } from "../label_range/label_range";

interface ChartRangeDataSourceState {
  datasetDispatchResult?: DispatchResult;
  labelsDispatchResult?: DispatchResult;
}

export class ChartRangeDataSourceComponent extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-ChartRangeDataSource";
  static components = {
    ChartDataSeries,
    ChartLabelRange,
  };

  protected props = useProps({
    chartId: types.UID(),
    definition: types.ChartDefinitionWithDataSource(),
    dataSource: types.ChartRangeDataSource(),
    updateChart: types.function<ChartUpdateFunction<ChartDefinitionWithDataSource<string>>>(),
    canUpdateChart: types.function<ChartUpdateFunction<ChartDefinitionWithDataSource<string>>>(),
    onErrorMessagesChanged: types.function<(errorMessages: string[]) => void>().optional(),
    dataSeriesTitle: types.string().optional(),
    labelRangeTitle: types.string().optional(),
    getLabelRangeOptions: types
      .function<
        () => Array<{
          name: string;
          label: string;
          value: boolean;
          onChange: (value: boolean) => void;
        }>
      >()
      .optional(),
  });

  protected state: ChartRangeDataSourceState = proxy({
    datasetDispatchResult: undefined,
    labelsDispatchResult: undefined,
  });

  protected dataSets: ChartRangeDataSourceType<string>["dataSets"] = [];
  private labelRange: string | undefined;
  private datasetOrientation: ChartDatasetOrientation | undefined = undefined;

  protected chartTerms = ChartTerms;

  setup() {
    this.dataSets = this.props.dataSource.dataSets ?? [];
    this.labelRange = this.props.dataSource.labelRange;
    if (this.props.dataSource.type === "range") {
      this.datasetOrientation = this.computeDatasetOrientation();
    }
  }

  get errorMessages(): string[] {
    const cancelledReasons = [
      ...(this.state.datasetDispatchResult?.reasons || []),
      ...(this.state.labelsDispatchResult?.reasons || []),
    ].filter((reason) => reason !== CommandResult.NoChanges);
    return cancelledReasons.map(
      (error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected
    );
  }

  get isDatasetInvalid(): boolean {
    return !!this.state.datasetDispatchResult?.isCancelledBecause(CommandResult.InvalidDataSet);
  }

  get isLabelInvalid(): boolean {
    return !!this.state.labelsDispatchResult?.isCancelledBecause(CommandResult.InvalidLabelRange);
  }

  get dataSetsHaveTitleLabel(): string {
    return this.datasetOrientation === "rows"
      ? _t("Use col %(column_name)s as headers", {
          column_name: numberToLetters(this.calculateHeaderPosition() || 0),
        })
      : _t("Use row %(row_position)s as headers", {
          row_position: this.calculateHeaderPosition() || "",
        });
  }

  getLabelRangeOptions() {
    if (this.props.getLabelRangeOptions) {
      const options = this.props.getLabelRangeOptions();
      options.push({
        name: "dataSetsHaveTitle",
        label: this.dataSetsHaveTitleLabel,
        value: this.props.dataSource.dataSetsHaveTitle,
        onChange: this.onUpdateDataSetsHaveTitle.bind(this),
      });
      return options;
    }
    const definition = this.props.definition;
    return [
      {
        name: "aggregated",
        label: this.chartTerms.AggregatedChart,
        value: ("aggregated" in definition ? definition.aggregated : false) ?? false,
        onChange: this.onUpdateAggregated.bind(this),
      },
      {
        name: "dataSetsHaveTitle",
        label: this.dataSetsHaveTitleLabel,
        value: this.props.dataSource.dataSetsHaveTitle,
        onChange: this.onUpdateDataSetsHaveTitle.bind(this),
      },
    ];
  }

  onUpdateDataSetsHaveTitle(dataSetsHaveTitle: boolean) {
    this.props.updateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, dataSetsHaveTitle },
    });
  }

  get canChangeDatasetOrientation(): boolean {
    const sheetNames = new Set<string>();
    const datasetZones: Zone[] = [];
    const currentSheetName = this.env.model.getters.getActiveSheetName();
    const ranges = this.dataSets.map((ds) => ds.dataRange);
    if (this.labelRange) {
      ranges.push(this.labelRange);
    }
    for (const range of ranges) {
      if (!isXcRepresentation(range)) {
        return false;
      }
      const reference = splitReference(range);
      const zone = toUnboundedZone(reference.xc);
      if (zone.bottom === undefined || zone.right === undefined) {
        return false;
      }
      datasetZones.push(zone as Zone);
      sheetNames.add(reference.sheetName || currentSheetName);
      if (sheetNames.size > 1) {
        return false;
      }
    }
    const mergedZones = mergeContiguousZones(datasetZones);
    if (mergedZones.length !== 1) {
      return false;
    }
    const { left, right, top, bottom } = mergedZones[0];
    if (
      datasetZones.some(
        (zone) =>
          (zone.top !== top || zone.bottom !== bottom) &&
          (zone.left !== left || zone.right !== right)
      )
    ) {
      return false;
    }
    return true;
  }

  private computeDatasetOrientation(): ChartDatasetOrientation | undefined {
    let anyRow = false;
    let anyColumn = false;
    for (const dataSet of this.dataSets) {
      if (!isXcRepresentation(dataSet.dataRange)) {
        return undefined;
      }
      const zone = toUnboundedZone(dataSet.dataRange);
      if (zone.bottom === undefined || zone.right === undefined) {
        return undefined;
      }
      if (zone.top === zone.bottom) {
        anyRow = true;
      }
      if (zone.left === zone.right) {
        anyColumn = true;
      }
    }
    if (anyRow && !anyColumn) {
      return "rows";
    } else if (!anyRow && anyColumn) {
      return "columns";
    }
    return undefined;
  }

  setDatasetOrientation(datasetOrientation: ChartDatasetOrientation) {
    const dataSource = this.props.dataSource;
    const oldDataSets = dataSource.dataSets;
    const dataRanges = oldDataSets.map((d) => d.dataRange);
    const dataSets = this.transposeDataSet(
      [dataSource.labelRange, ...dataRanges],
      datasetOrientation
    );
    if (dataSets.length === 0) {
      return;
    }
    const labelRange = dataSets.length > 1 ? dataSets.shift()!.dataRange : "";

    this.props.updateChart(this.props.chartId, {
      dataSource: { ...dataSource, labelRange, dataSets },
    });
    this.dataSets = dataSets;
    this.labelRange = labelRange;
    this.datasetOrientation = datasetOrientation;
  }

  /**
   * Change the local dataSeriesRanges. The model should be updated when the
   * button "confirm" is clicked
   */
  onDataSeriesRangesChanged(ranges: string[]) {
    this.dataSets = ranges.map((dataRange, i) => ({
      dataSetId: this.dataSets?.[i]?.dataSetId ?? UuidGenerator.smallUuid(),
      dataRange,
    }));
    this.state.datasetDispatchResult = this.props.canUpdateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, dataSets: this.dataSets },
    });
    this.props.onErrorMessagesChanged?.(this.errorMessages);
  }

  onDataSeriesReordered(indexes: number[]) {
    const colorGenerator = getChartColorsGenerator(
      this.props.definition.dataSetStyles,
      this.dataSets
    );
    this.datasetOrientation = undefined;
    const dataSetStyles = { ...this.props.definition.dataSetStyles };
    for (const ds of this.dataSets) {
      const color = colorGenerator.next();
      dataSetStyles[ds.dataSetId] = { backgroundColor: color, ...dataSetStyles[ds.dataSetId] };
    }
    this.dataSets = indexes.map((i) => this.dataSets[i]);
    this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, dataSets: this.dataSets },
      dataSetStyles,
    });
    this.props.onErrorMessagesChanged?.(this.errorMessages);
  }

  onDataSeriesRemoved(index: number) {
    const colorGenerator = getChartColorsGenerator(
      this.props.definition.dataSetStyles,
      this.dataSets
    );
    const dataSetStyles = { ...this.props.definition.dataSetStyles };
    for (const ds of this.dataSets) {
      const color = colorGenerator.next();
      dataSetStyles[ds.dataSetId] = { backgroundColor: color, ...dataSetStyles[ds.dataSetId] };
    }
    const removedDataSetId = this.dataSets[index].dataSetId;
    delete dataSetStyles[removedDataSetId];
    this.dataSets = this.dataSets.filter((_, i) => i !== index);
    this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, dataSets: this.dataSets },
      dataSetStyles,
    });
    this.props.onErrorMessagesChanged?.(this.errorMessages);
  }

  onDataSeriesConfirmed() {
    const { dataSets, dataSetStyles } = this.splitRanges();
    this.dataSets = dataSets;
    this.datasetOrientation = this.computeDatasetOrientation();
    this.state.datasetDispatchResult = this.props.updateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, dataSets: this.dataSets },
      dataSetStyles,
    });
    if (this.state.datasetDispatchResult.isSuccessful) {
      const newDefinition = this.env.model.getters.getChartDefinition(
        this.props.chartId
      ) as ChartDefinitionWithDataSource<string>;
      if (newDefinition.dataSource.type === "range") {
        this.dataSets = newDefinition.dataSource.dataSets;
      }
    }
    this.props.onErrorMessagesChanged?.(this.errorMessages);
  }

  splitRanges() {
    const postProcessedRanges: ChartRangeDataSourceType<string>["dataSets"] = [];
    const postProcessedStyles: DataSetStyle = {};
    const definition = this.env.model.getters.getChartDefinition(
      this.props.chartId
    ) as ChartDefinitionWithDataSource<string>;
    const dataSetStyles = definition.dataSetStyles || {};
    for (const dataSet of this.dataSets) {
      const range = dataSet.dataRange;
      if (!this.env.model.getters.isRangeValid(range)) {
        postProcessedRanges.push(dataSet); // ignore invalid range
        continue;
      }

      const { sheetName } = splitReference(range);
      const sheetPrefix = sheetName ? `${sheetName}!` : "";
      const zone = toUnboundedZone(range);
      if (zone.bottom !== zone.top && zone.left !== zone.right) {
        if (this.datasetOrientation !== "rows") {
          if (zone.right !== undefined) {
            for (let j = zone.left; j <= zone.right; ++j) {
              const newDataSetId = dataSet.dataSetId + "_split" + j;
              const datasetOptions =
                j === zone.left
                  ? dataSetStyles[dataSet.dataSetId]
                  : { yAxisId: dataSetStyles[dataSet.dataSetId]?.yAxisId };
              postProcessedStyles[newDataSetId] = datasetOptions;
              postProcessedRanges.push({
                dataSetId: newDataSetId,
                dataRange: `${sheetPrefix}${zoneToXc({
                  left: j,
                  right: j,
                  top: zone.top,
                  bottom: zone.bottom,
                })}`,
              });
            }
          } else if (zone.bottom !== undefined) {
            for (let j = zone.top; j <= zone.bottom; ++j) {
              const newDataSetId = dataSet.dataSetId + "_split" + j;
              const datasetOptions =
                j === zone.top
                  ? dataSetStyles[dataSet.dataSetId]
                  : { yAxisId: dataSetStyles[dataSet.dataSetId]?.yAxisId };
              postProcessedStyles[newDataSetId] = datasetOptions;
              postProcessedRanges.push({
                dataSetId: newDataSetId,
                dataRange: `${sheetPrefix}${zoneToXc({
                  left: zone.left,
                  right: zone.right,
                  top: j,
                  bottom: j,
                })}`,
              });
            }
          }
        } else {
          if (zone.bottom !== undefined) {
            for (let j = zone.top; j <= zone.bottom; ++j) {
              const newDataSetId = dataSet.dataSetId + "_split" + j;
              const datasetOptions =
                j === zone.top
                  ? dataSetStyles[dataSet.dataSetId]
                  : { yAxisId: dataSetStyles[dataSet.dataSetId]?.yAxisId };
              postProcessedStyles[newDataSetId] = datasetOptions;
              postProcessedRanges.push({
                dataSetId: newDataSetId,
                dataRange: `${sheetPrefix}${zoneToXc({
                  left: zone.left,
                  right: zone.right,
                  top: j,
                  bottom: j,
                })}`,
              });
            }
          } else if (zone.right !== undefined) {
            for (let j = zone.left; j <= zone.right; ++j) {
              const newDataSetId = dataSet.dataSetId + "_split" + j;
              const datasetOptions =
                j === zone.left
                  ? dataSetStyles[dataSet.dataSetId]
                  : { yAxisId: dataSetStyles[dataSet.dataSetId]?.yAxisId };
              postProcessedStyles[newDataSetId] = datasetOptions;
              postProcessedRanges.push({
                dataSetId: newDataSetId,
                dataRange: `${sheetPrefix}${zoneToXc({
                  left: j,
                  right: j,
                  top: zone.top,
                  bottom: zone.bottom,
                })}`,
              });
            }
          }
        }
      } else {
        postProcessedRanges.push(dataSet);
        postProcessedStyles[dataSet.dataSetId] = dataSetStyles[dataSet.dataSetId];
      }
    }
    return {
      dataSets: postProcessedRanges,
      dataSetStyles: postProcessedStyles,
    };
  }

  getDataSeriesRanges() {
    return this.dataSets;
  }

  /**
   * Change the local labelRange. The model should be updated when the
   * button "confirm" is clicked
   */
  onLabelRangeChanged(ranges: string[]) {
    this.labelRange = ranges[0];
    this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, labelRange: this.labelRange },
    });
  }

  onLabelRangeConfirmed() {
    this.state.labelsDispatchResult = this.props.updateChart(this.props.chartId, {
      dataSource: { ...this.props.dataSource, labelRange: this.labelRange },
    });
  }

  getLabelRange(): string {
    return this.labelRange || "";
  }

  onUpdateAggregated(aggregated: boolean) {
    this.props.updateChart(this.props.chartId, {
      aggregated,
    });
  }

  calculateHeaderPosition(): number | undefined {
    if (this.isDatasetInvalid || this.isLabelInvalid) {
      return undefined;
    }
    const getters = this.env.model.getters;
    const sheetId = getters.getActiveSheetId();
    const labelRange = createValidRange(getters, sheetId, this.labelRange);
    const dataSets = createDataSets(getters, sheetId, this.props.dataSource);
    if (dataSets.length) {
      return this.datasetOrientation === "rows"
        ? dataSets[0].dataRange.zone.left
        : dataSets[0].dataRange.zone.top + 1;
    } else if (labelRange) {
      return labelRange.zone.top + 1;
    }
    return undefined;
  }

  get maxNumberOfUsedRanges(): number | undefined {
    return chartTypeRegistry.get(this.props.definition.type).dataSeriesLimit;
  }

  private transposeDataSet(
    dataRanges: (string | undefined)[],
    datasetOrientation: ChartDatasetOrientation | undefined
  ): { dataRange: string; dataSetId: UID }[] {
    const getters = this.env.model.getters;
    if (datasetOrientation === undefined) {
      return dataRanges
        .filter(isDefined)
        .map((dataRange) => ({ dataRange, dataSetId: UuidGenerator.smallUuid() }));
    }
    const zonesBySheetName = {};
    const transposedDatasets: ChartRangeDataSourceType<string>["dataSets"] = [];
    const figureId = getters.getFigureIdFromChartId(this.props.chartId);
    const figureSheetId = getters.getFigureSheetId(figureId);
    let name = getters.getActiveSheet().name;
    if (figureSheetId) {
      name = getters.getSheet(figureSheetId).name;
    }
    for (const dataRange of dataRanges) {
      if (!dataRange) {
        continue;
      }
      if (!isXcRepresentation(dataRange)) {
        return dataRanges
          .filter(isDefined)
          .map((dataRange) => ({ dataRange, dataSetId: UuidGenerator.smallUuid() }));
      }
      let { sheetName, xc } = splitReference(dataRange);
      sheetName = sheetName ?? name;
      if (!zonesBySheetName[sheetName]) {
        zonesBySheetName[sheetName] = [];
      }
      zonesBySheetName[sheetName]?.push(toZone(xc));
    }
    for (const sheetName in zonesBySheetName) {
      const zones = zonesBySheetName[sheetName];
      const contiguousZones = mergeContiguousZones(zones);
      if (datasetOrientation === "columns") {
        for (const zone of contiguousZones) {
          for (let col = zone.left; col <= zone.right; col++) {
            const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
              ...zone,
              left: col,
              right: col,
            })}`;
            transposedDatasets.push({ dataRange, dataSetId: UuidGenerator.smallUuid() });
          }
        }
      } else {
        for (const zone of contiguousZones) {
          for (let row = zone.top; row <= zone.bottom; row++) {
            const dataRange = `${sheetName === name ? "" : sheetName + "!"}${zoneToXc({
              ...zone,
              top: row,
              bottom: row,
            })}`;
            transposedDatasets.push({ dataRange, dataSetId: UuidGenerator.smallUuid() });
          }
        }
      }
    }
    return transposedDatasets;
  }
}
