import { CompiledFormula } from "../../formulas/compiler";
import { astToFormula } from "../../formulas/formula_formatter";
import { toScalar } from "../../functions/helper_matrices";
import { PositionMap } from "../../helpers/cells/position_map";
import { deepCopy, deepEquals, getUniqueText, isDefined } from "../../helpers/misc";
import {
  getNumberOfPivotFunctions,
  getPivotFunctions,
} from "../../helpers/pivot/pivot_composer_helpers";
import { domainToColRowDomain } from "../../helpers/pivot/pivot_domain_helpers";
import { getPivotStyleFromFnArgs } from "../../helpers/pivot/pivot_helpers";
import withPivotPresentationLayer from "../../helpers/pivot/pivot_presentation";
import { pivotRegistry } from "../../helpers/pivot/pivot_registry";
import { resetMapValueDimensionDate } from "../../helpers/pivot/spreadsheet_pivot/date_spreadsheet_pivot";
import { EMPTY_PIVOT_CELL } from "../../helpers/pivot/table_spreadsheet_pivot";
import { _t } from "../../translation";
import { CellValue, CellValueType, FormulaCell } from "../../types/cells";
import {
  AddPivotCommand,
  CoreCommand,
  EvaluationCommand,
  UpdatePivotCommand,
  invalidateEvaluationCommands,
  isCoreCommand,
} from "../../types/commands";
import {
  CellPosition,
  FunctionResultObject,
  Matrix,
  SortDirection,
  UID,
  isMatrix,
} from "../../types/misc";
import { PivotCoreMeasure, PivotStyle, PivotTableCell } from "../../types/pivot";
import { Pivot } from "../../types/pivot_runtime";
import { EvaluationPlugin, EvaluationPluginConfig } from "../evaluation_plugin";
import { UIPluginConfig } from "../ui_plugin";

export const UNDO_REDO_PIVOT_COMMANDS = ["ADD_PIVOT", "UPDATE_PIVOT", "REMOVE_PIVOT"];

function isPivotCommand(cmd: CoreCommand): cmd is AddPivotCommand | UpdatePivotCommand {
  return UNDO_REDO_PIVOT_COMMANDS.includes(cmd.type);
}

interface PivotEvaluatedArgs {
  functionName: string;
  args: (CellValue | Matrix<CellValue> | undefined)[];
}

export class PivotUIPlugin extends EvaluationPlugin {
  static getters = [
    "getPivot",
    "getFirstPivotFunction",
    "getPivotCellSortDirection",
    "getPivotIdFromPosition",
    "getPivotIdsFromPosition",
    "getPivotCellFromPosition",
    "generateNewCalculatedMeasureName",
    "isPivotUnused",
    "isSpillPivotFormula",
    "getAllPivotArrayFormulas",
    "getPivotStyleAtPosition",
  ] as const;

  private pivots: Record<UID, Pivot> = {};
  private unusedPivotsInFormulas?: UID[];
  private custom: UIPluginConfig["custom"];
  private pivotPositionCache: PositionMap<UID[]> = new PositionMap();
  private shouldInvalidateCache: boolean = false;

  constructor(config: EvaluationPluginConfig) {
    super(config);
    this.custom = config.custom;
  }

  beforeHandle(cmd: EvaluationCommand) {
    switch (cmd.type) {
      case "START":
        for (const pivotId of this.getters.getPivotIds()) {
          this.setupPivot(pivotId);
        }
    }
  }

  handle(cmd: EvaluationCommand) {
    if (isCoreCommand(cmd) || cmd.type === "UNDO" || cmd.type === "REDO") {
      this.unusedPivotsInFormulas = undefined;
    }

    if (invalidateEvaluationCommands.has(cmd.type)) {
      this.shouldInvalidateCache = true;
      for (const pivotId of this.getters.getPivotIds()) {
        this.setupPivot(pivotId, { recreate: true });
      }
    }
    if (cmd.type === "UPDATE_CELL") {
      this.shouldInvalidateCache = true;
    }
    switch (cmd.type) {
      case "REFRESH_PIVOT":
        this.refreshPivot(cmd.id);
        break;
      case "ADD_PIVOT": {
        this.unusedPivotsInFormulas?.push(cmd.pivotId);
        this.setupPivot(cmd.pivotId);
        break;
      }
      case "DUPLICATE_PIVOT": {
        this.unusedPivotsInFormulas?.push(cmd.newPivotId);
        this.setupPivot(cmd.newPivotId);
        break;
      }
      case "UPDATE_PIVOT": {
        this.setupPivot(cmd.pivotId, { recreate: true });
        break;
      }
      case "UNDO":
      case "REDO": {
        const pivotCommands = cmd.commands.filter(isPivotCommand);

        for (const cmd of pivotCommands) {
          const pivotId = cmd.pivotId;
          if (!this.getters.isExistingPivot(pivotId)) {
            continue;
          }
          this.setupPivot(pivotId, { recreate: true });
        }
        break;
      }
      case "UPDATE_LOCALE":
        /**
         * Reset the cache of the date/datetime pivot values, as it depends on
         * the locale. (e.g. the first day of the week)
         */
        resetMapValueDimensionDate();
        break;
    }
  }

  finalize() {
    if (this.shouldInvalidateCache) {
      this.pivotPositionCache = new PositionMap();
      this.shouldInvalidateCache = false;
    }
  }

  // ---------------------------------------------------------------------
  // Getters
  // ---------------------------------------------------------------------

  /**
   * Get the id of the first pivot in the formula at the given position. Returns undefined if there
   * is no pivot at this position
   */
  getPivotIdFromPosition(position: CellPosition): UID | undefined {
    return this.getPivotIdsFromPosition(position)[0];
  }

  /**
   * Get all of the ids of the pivot present in the formula at the given position.
   */
  getPivotIdsFromPosition(position: CellPosition): UID[] {
    if (!this.pivotPositionCache.has(position)) {
      const cell = this.getters.getCorrespondingFormulaCell(position);
      if (cell && cell.isFormula) {
        const pivotIds = this.getPivotIdsFromFormula(cell.compiledFormula);
        this.pivotPositionCache.set(position, pivotIds);
      }
    }
    return this.pivotPositionCache.get(position) || [];
  }

  private getPivotIdsFromFormula(formula: CompiledFormula): UID[] {
    return this.getPivotFunctions(formula.sheetId, formula)
      .map((pivotFunction) => {
        const pivotId = pivotFunction.args[0]?.toString();
        return pivotId && this.getters.getPivotId(pivotId);
      })
      .filter(isDefined);
  }

  isSpillPivotFormula(position: CellPosition) {
    const cell = this.getters.getCorrespondingFormulaCell(position);
    if (cell && cell.isFormula) {
      const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula);
      return pivotFunction?.functionName === "PIVOT";
    }
    return false;
  }

  private getPivotFunctions(sheetId: UID, formula: CompiledFormula): PivotEvaluatedArgs[] {
    const pivotFunctions = getPivotFunctions(formula, this.getters);
    if (!pivotFunctions.length) {
      return [];
    }
    const evaluatedPivotFunctions: PivotEvaluatedArgs[] = [];
    for (const pivotFunction of pivotFunctions) {
      const { functionName, args } = pivotFunction;
      const evaluatedArgs = args.map((argAst) => {
        if (argAst.type === "EMPTY") {
          return undefined;
        } else if (
          argAst.type === "STRING" ||
          argAst.type === "BOOLEAN" ||
          argAst.type === "NUMBER"
        ) {
          return argAst.value;
        }
        const argsString = astToFormula(argAst);
        return this.getters.evaluateFormula(sheetId, argsString);
      });
      evaluatedPivotFunctions.push({ functionName, args: evaluatedArgs });
    }
    return evaluatedPivotFunctions;
  }

  getFirstPivotFunction(sheetId: UID, formula: CompiledFormula): PivotEvaluatedArgs | undefined {
    return this.getPivotFunctions(sheetId, formula)[0];
  }

  /**
   * Returns the domain args of a pivot formula from a position.
   * For all those formulas:
   *
   * =PIVOT.VALUE(1,"expected_revenue","stage_id",2,"city","Brussels")
   * =PIVOT.HEADER(1,"stage_id",2,"city","Brussels")
   * =PIVOT.HEADER(1,"stage_id",2,"city","Brussels","measure","expected_revenue")
   *
   * the result is the same: ["stage_id", 2, "city", "Brussels"]
   *
   * If the cell is the result of PIVOT, the result is the domain of the cell
   * as if it was the individual pivot formula
   */
  getPivotCellFromPosition(position: CellPosition): PivotTableCell {
    const cell = this.getters.getCorrespondingFormulaCell(position);
    if (
      !cell ||
      !cell.isFormula ||
      getNumberOfPivotFunctions(cell.compiledFormula, this.getters) === 0
    ) {
      return EMPTY_PIVOT_CELL;
    }
    const mainPosition = this.getters.getCellPosition(cell.id);
    const result = this.getters.getFirstPivotFunction(position.sheetId, cell.compiledFormula);
    if (!result) {
      return EMPTY_PIVOT_CELL;
    }
    let { functionName, args } = result;
    const formulaId = args[0];
    if (!formulaId) {
      return EMPTY_PIVOT_CELL;
    }
    const pivotId = this.getters.getPivotId(formulaId.toString());
    if (!pivotId) {
      return EMPTY_PIVOT_CELL;
    }
    const pivot = this.getPivot(pivotId);
    if (!pivot.isValid()) {
      return EMPTY_PIVOT_CELL;
    }
    if (functionName === "PIVOT" && !this.isMainFunctionPivotSpreadFunction(cell)) {
      return EMPTY_PIVOT_CELL;
    }
    try {
      if (functionName === "PIVOT") {
        const pivotStyle = getPivotStyleFromFnArgs(
          this.getters.getPivotCoreDefinition(pivotId),
          toScalar(args[1]),
          toScalar(args[2]),
          toScalar(args[3]),
          toScalar(args[4]),
          toScalar(args[5]),
          this.getters.getLocale()
        );
        const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(pivotStyle);
        const pivotCol = position.col - mainPosition.col;
        const pivotRow = position.row - mainPosition.row;
        return pivotCells[pivotCol][pivotRow];
      }
      const offsetRow = position.row - mainPosition.row;
      const offsetCol = position.col - mainPosition.col;
      args = args.map((arg) => (isMatrix(arg) ? arg[offsetCol][offsetRow] : arg));
      if (functionName === "PIVOT.HEADER" && args.at(-2) === "measure") {
        const domain = pivot.parseArgsToPivotDomain(
          args.slice(1, -2).map((value) => ({ value } as FunctionResultObject))
        );
        return {
          type: "MEASURE_HEADER",
          domain,
          measure: args.at(-1)?.toString() || "",
        };
      } else if (functionName === "PIVOT.HEADER") {
        const domain = pivot.parseArgsToPivotDomain(
          args.slice(1).map((value) => ({ value } as FunctionResultObject))
        );
        const colRowDomain = domainToColRowDomain(pivot, domain);
        return {
          type: "HEADER",
          domain,
          dimension: colRowDomain.colDomain.length ? "COL" : "ROW",
        };
      }
      const [measure, ...domainArgs] = args.slice(1);
      const domain = pivot.parseArgsToPivotDomain(
        domainArgs.map((value) => ({ value } as FunctionResultObject))
      );
      return {
        type: "VALUE",
        domain,
        measure: measure?.toString() || "",
      };
    } catch (_) {
      return EMPTY_PIVOT_CELL;
    }
  }

  generateNewCalculatedMeasureName(measures: PivotCoreMeasure[]) {
    const existingMeasures = measures.map((m) => m.fieldName);
    return getUniqueText(_t("Calculated measure 1"), existingMeasures, {
      compute: (name, i) => _t("Calculated measure %s", i),
    });
  }

  getPivot(pivotId: UID) {
    if (!this.getters.isExistingPivot(pivotId)) {
      throw new Error(`pivot ${pivotId} not found`);
    }
    return this.pivots[pivotId];
  }

  isPivotUnused(pivotId: UID) {
    const { type } = this.getters.getPivot(pivotId);
    return (
      this._getUnusedPivotsInFormulas().includes(pivotId) &&
      pivotRegistry.get(type).isPivotUnused(this.getters, pivotId)
    );
  }

  getPivotCellSortDirection(position: CellPosition): SortDirection | "none" | undefined {
    const pivotId = this.getters.getPivotIdFromPosition(position);
    const pivotCell = this.getters.getPivotCellFromPosition(position);
    if (
      pivotCell.type === "EMPTY" ||
      pivotCell.type === "HEADER" ||
      pivotCell.type === "ROW_GROUP_NAME" ||
      !pivotId
    ) {
      return undefined;
    }
    const pivot = this.getters.getPivot(pivotId);
    const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
    const sortedColumn = pivot.definition.sortedColumn;
    if (sortedColumn?.measure === pivotCell.measure && deepEquals(sortedColumn.domain, colDomain)) {
      return sortedColumn.order;
    }
    return "none";
  }
  // ---------------------------------------------------------------------
  // Private
  // ---------------------------------------------------------------------

  /**
   * Refresh the cache of a pivot
   */
  private refreshPivot(pivotId: UID) {
    const pivot = this.getters.getPivot(pivotId);
    pivot.init({ reload: true });
  }

  setupPivot(pivotId: UID, { recreate } = { recreate: false }) {
    const definition = deepCopy(this.getters.getPivotCoreDefinition(pivotId));
    if (!(pivotId in this.pivots)) {
      const Pivot = withPivotPresentationLayer(pivotRegistry.get(definition.type).ui);
      this.pivots[pivotId] = new Pivot(pivotId, this.custom, { definition, getters: this.getters });
    } else if (recreate) {
      this.pivots[pivotId].onDefinitionChange(definition);
    }
  }

  private _getUnusedPivotsInFormulas(): UID[] {
    if (this.unusedPivotsInFormulas !== undefined) {
      return this.unusedPivotsInFormulas;
    }
    const unusedPivots = new Set(this.getters.getPivotIds());
    for (const formula of this.getters.getAllFormulas()) {
      const pivotIds = this.getPivotIdsFromFormula(formula);
      for (const pivotId of pivotIds) {
        unusedPivots.delete(pivotId);
        if (!unusedPivots.size) {
          this.unusedPivotsInFormulas = [];
          return [];
        }
      }
    }

    this.unusedPivotsInFormulas = [...unusedPivots];
    return this.unusedPivotsInFormulas;
  }

  getAllPivotArrayFormulas(): {
    position: CellPosition;
    pivotStyle: Required<PivotStyle>;
    pivotId: UID;
  }[] {
    const result: { position: CellPosition; pivotStyle: Required<PivotStyle>; pivotId: UID }[] = [];
    for (const cellId of this.getters.getCellsWithTrackedFormula("PIVOT")) {
      const position = this.getters.getCellPosition(cellId);
      const evaluatedCell = this.getters.getEvaluatedCell(position);
      if (evaluatedCell.type === CellValueType.error) {
        continue;
      }
      const pivotInfo = this.getPivotStyleAtPosition(position);
      if (pivotInfo) {
        result.push({ position, ...pivotInfo });
      }
    }
    return result;
  }

  getPivotStyleAtPosition(
    position: CellPosition
  ): { pivotStyle: Required<PivotStyle>; pivotId: UID } | undefined {
    position = this.getters.getArrayFormulaSpreadingOn(position) || position;
    const cell = this.getters.getCell(position);
    if (!cell || !cell.isFormula || !this.isMainFunctionPivotSpreadFunction(cell)) {
      return undefined;
    }
    const pivotFunction = this.getFirstPivotFunction(position.sheetId, cell.compiledFormula);
    if (!pivotFunction || pivotFunction.functionName !== "PIVOT") {
      return undefined;
    }
    const pivotIdArg = pivotFunction.args[0];
    if (!pivotIdArg) {
      return undefined;
    }
    const pivotId = this.getters.getPivotId(pivotIdArg.toString());
    if (!pivotId) {
      return undefined;
    }
    const pivotStyle = getPivotStyleFromFnArgs(
      this.getters.getPivotCoreDefinition(pivotId),
      toScalar(pivotFunction.args[1]),
      toScalar(pivotFunction.args[2]),
      toScalar(pivotFunction.args[3]),
      toScalar(pivotFunction.args[4]),
      toScalar(pivotFunction.args[5]),
      this.getters.getLocale()
    );
    return { pivotStyle, pivotId };
  }

  private isMainFunctionPivotSpreadFunction(cell: FormulaCell): boolean {
    return cell.compiledFormula.isFirstNonWhitespaceSymbol("PIVOT");
  }
}
