import {
  DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
  DEFAULT_SCORECARD_BASELINE_COLOR_UP,
  DEFAULT_SCORECARD_BASELINE_MODE,
} from "../../../constants";
import { CellValueType, EvaluatedCell } from "../../../types/cells";
import { BarChartDefinition } from "../../../types/chart/bar_chart";
import { ChartDefinition } from "../../../types/chart/chart";
import { LineChartDefinition } from "../../../types/chart/line_chart";
import { Getters } from "../../../types/getters";
import { Zone } from "../../../types/misc";
import { isDateTimeFormat } from "../../format/format";
import { getZoneArea, getZonesByColumns } from "../../zones";
import {
  dataset,
  getUnboundRange,
  isDatasetTitled,
  rangeSource,
} from "./chart_data_source_helpers";

type ColumnType = "number" | "text" | "date" | "percentage" | "boolean" | "empty";

const DEFAULT_BAR_CHART_CONFIG: BarChartDefinition = {
  type: "bar",
  title: {},
  dataSource: { type: "range", dataSets: [], dataSetsHaveTitle: false },
  dataSetStyles: {},
  legendPosition: "none",
  stacked: false,
  humanize: true,
};

const DEFAULT_LINE_CHART_CONFIG: LineChartDefinition = {
  type: "line",
  title: {},
  dataSource: { type: "range", dataSets: [], dataSetsHaveTitle: false },
  dataSetStyles: {},
  legendPosition: "none",
  stacked: false,
  cumulative: false,
  labelsAsText: false,
  humanize: true,
};

interface ColumnInfo {
  zone: Zone;
  type: ColumnType;
}

function detectColumnType(cells: EvaluatedCell[]): ColumnType {
  if (!cells.length) {
    return "empty";
  }
  const counts = { number: 0, text: 0, date: 0, percentage: 0, boolean: 0 };
  let max = 0;
  let detectedType: ColumnType = "empty";
  for (const cell of cells) {
    let type: ColumnType | null = null;
    if (cell.type === CellValueType.number) {
      if (cell.format && isDateTimeFormat(cell.format)) {
        type = "date";
      } else if (cell.format?.includes("%")) {
        type = "percentage";
      } else {
        type = "number";
      }
    } else if (cell.type === CellValueType.text) {
      type = "text";
    } else if (cell.type === CellValueType.boolean) {
      type = "boolean";
    }
    if (type) {
      const newCount = ++counts[type];
      if (newCount > max) {
        max = newCount;
        detectedType = type;
      }
    }
  }
  return detectedType;
}

function categorizeColumns(zones: Zone[], getters: Getters): ColumnInfo[] {
  const columns: ColumnInfo[] = [];
  for (const zone of getZonesByColumns(zones)) {
    const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
    columns.push({ zone, type: detectColumnType(cells) });
  }
  return columns;
}

function getCellStats(getters: Getters, zone: Zone) {
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
  const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
  return {
    uniqueCount: new Set(values).size,
    totalCount: values.length,
  };
}

/**
 * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
 * - If the column contains a single cell, create a scorecard.
 * - If the column type is "percentage", create a pie chart.
 * - If the column type is "text" or "boolean", create a pie chart
 * - If the column type is "date", create a calendar chart.
 * - Otherwise, create a bar chart.
 */
function buildSingleColumnChart(column: ColumnInfo, getters: Getters): ChartDefinition {
  const { type, zone } = column;
  const sheetId = getters.getActiveSheetId();
  const dataSetsHaveTitle = isDatasetTitled(getters, column.zone);
  const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });

  if (getZoneArea(zone) === 1) {
    return buildScorecard(zone, getters);
  }

  switch (type) {
    case "percentage":
      return {
        type: "pie",
        title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
        dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
        dataSetStyles: {},
        legendPosition: "none",
      };

    case "text":
    case "boolean":
      const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
      const titleCount = cells.reduce(
        (count, cell) => (cell.value === titleCell.value ? count + 1 : count),
        0
      );
      const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
      return {
        type: "pie",
        title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
        dataSource: rangeSource(
          [dataset(zone, getters)],
          hasUniqueTitle,
          getUnboundRange(getters, zone)
        ),
        dataSetStyles: {},
        aggregated: true,
        legendPosition: "top",
      };

    case "date": {
      const dataRange = getUnboundRange(getters, zone);
      return {
        type: "calendar",
        title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
        dataSource: rangeSource([], dataSetsHaveTitle, dataRange),
        dataSetStyles: {},
        legendPosition: "left",
        horizontalGroupBy: "day_of_week",
        verticalGroupBy: "month_number",
      };
    }
  }
  return {
    ...DEFAULT_BAR_CHART_CONFIG,
    title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
    dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
    dataSetStyles: {},
  };
}

/**
 * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
 * columns left to right, and is as follows:
 * - any type + percentage columns: pie chart
 * - number + number columns: scatter chart
 * - date + number columns: line chart
 * - text + number columns: treemap if repetition in labels
 * - any other combination: bar chart
 */
function buildTwoColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefinition {
  if (columns.length !== 2) {
    throw new Error("buildTwoColumnChart expects exactly two columns");
  }

  if (columns[1].type === "percentage") {
    return {
      type: "pie",
      title: {},
      dataSource: rangeSource(
        [dataset(columns[1].zone, getters)],
        isDatasetTitled(getters, columns[1].zone),
        getUnboundRange(getters, columns[0].zone)
      ),
      dataSetStyles: {},
      aggregated: true,
      legendPosition: "none",
    };
  }

  if (columns[0].type === "number" && columns[1].type === "number") {
    return {
      type: "scatter",
      title: {},
      dataSource: rangeSource(
        [dataset(columns[1].zone, getters)],
        isDatasetTitled(getters, columns[1].zone),
        getUnboundRange(getters, columns[0].zone)
      ),
      dataSetStyles: {},
      labelsAsText: false,
      legendPosition: "none",
    };
  }

  if (columns[0].type === "date" && columns[1].type === "number") {
    return {
      ...DEFAULT_LINE_CHART_CONFIG,
      type: "line",
      dataSource: rangeSource(
        [dataset(columns[1].zone, getters)],
        isDatasetTitled(getters, columns[0].zone),
        getUnboundRange(getters, columns[0].zone)
      ),
      dataSetStyles: {},
    };
  }

  if (columns[0].type === "text" && columns[1].type === "number") {
    const textColumn = columns[0];
    const numberColumn = columns[1];

    const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
    const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn.zone);

    if (uniqueCount !== totalCount) {
      return {
        type: "treemap",
        title: {},
        dataSource: rangeSource(
          [dataset(textColumn.zone, getters)],
          dataSetsHaveTitle,
          getUnboundRange(getters, numberColumn.zone)
        ),
        dataSetStyles: {},
        legendPosition: "none",
      };
    }
  }

  return {
    ...DEFAULT_BAR_CHART_CONFIG,
    dataSource: rangeSource(
      [dataset(columns[1].zone, getters)],
      isDatasetTitled(getters, columns[1].zone),
      getUnboundRange(getters, columns[0].zone)
    ),
    dataSetStyles: {},
  };
}

/**
 * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
 * the columns left to right, and is as follows:
 * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
 * - any type + multiple percentage columns: pie chart
 * - date + multiple number columns: line chart
 * - any other combination: bar chart
 */
function buildMultiColumnChart(columns: ColumnInfo[], getters: Getters): ChartDefinition {
  if (columns.length < 3) {
    throw new Error("buildMultiColumnChart expects at least three columns");
  }

  const dataSetsHaveTitle = columns.some(
    (col) => col.type !== "text" && isDatasetTitled(getters, col.zone)
  );

  const lastColumn = columns[columns.length - 1];
  const columnsExceptLast = columns.slice(0, columns.length - 1);

  if (
    (lastColumn.type === "percentage" || lastColumn.type === "number") &&
    columnsExceptLast.every((col) => col.type === "text")
  ) {
    const dataSets = columnsExceptLast.map(({ zone }, i) => dataset(zone, getters, String(i)));
    return {
      type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
      title: {},
      dataSource: rangeSource(
        dataSets,
        dataSetsHaveTitle,
        getUnboundRange(getters, lastColumn.zone)
      ),
      dataSetStyles: {},
      legendPosition: "none",
    };
  }

  const firstColumn = columns[0];
  const columnsExceptFirst = columns.slice(1);
  const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) =>
    dataset(zone, getters, String(i))
  );

  if (columnsExceptFirst.every((col) => col.type === "percentage")) {
    return {
      type: "pie",
      title: {},
      dataSource: rangeSource(
        rangesOfColumnsExceptFirst,
        dataSetsHaveTitle,
        getUnboundRange(getters, firstColumn.zone)
      ),
      dataSetStyles: {},
      aggregated: false,
      legendPosition: "top",
    };
  }

  if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
    return {
      ...DEFAULT_LINE_CHART_CONFIG,
      type: "line",
      dataSource: rangeSource(
        rangesOfColumnsExceptFirst,
        dataSetsHaveTitle,
        getUnboundRange(getters, firstColumn.zone)
      ),
      dataSetStyles: {},
      legendPosition: "top",
    };
  }

  return {
    ...DEFAULT_BAR_CHART_CONFIG,
    dataSource: rangeSource(
      rangesOfColumnsExceptFirst,
      dataSetsHaveTitle,
      getUnboundRange(getters, firstColumn.zone)
    ),
    dataSetStyles: {},
    legendPosition: "top",
  };
}

function buildScorecard(zone: Zone, getters: Getters): ChartDefinition {
  const cell = getters.getCell({
    sheetId: getters.getActiveSheetId(),
    col: zone.left,
    row: zone.top,
  });
  return {
    type: "scorecard",
    title: {},
    keyValue: `=${getUnboundRange(getters, zone)}`,
    background: cell?.style?.fillColor,
    baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
    baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
    baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
  };
}

/**
 * Analyzes selected zones and intelligently determines the most suitable chart.
 */
export function getSmartChartDefinition(zones: Zone[], getters: Getters): ChartDefinition {
  const columns = categorizeColumns(zones, getters);

  if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
    const dataSets = columns.map(({ zone }, i) => dataset(zone, getters, String(i)));
    return {
      ...DEFAULT_BAR_CHART_CONFIG,
      dataSource: rangeSource(dataSets, false),
    };
  }

  const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
  switch (nonEmptyColumns.length) {
    case 1:
      return {
        humanize: true,
        ...buildSingleColumnChart(nonEmptyColumns[0], getters),
      };
    case 2:
      return {
        humanize: true,
        ...buildTwoColumnChart(nonEmptyColumns, getters),
      };
    default:
      return {
        humanize: true,
        ...buildMultiColumnChart(nonEmptyColumns, getters),
      };
  }
}
