import { CompiledFormula } from "../../formulas/compiler";
import { toXC } from "../../helpers/coordinates";
import { deepCopy } from "../../helpers/misc";
import { duplicateRangeInDuplicatedSheet, getCellPositionsInRanges } from "../../helpers/range";
import { recomputeZones } from "../../helpers/recompute_zones";
import { isInside } from "../../helpers/zones";
import { criterionEvaluatorRegistry } from "../../registries/criterion_registry";
import {
  AddDataValidationCommand,
  Command,
  CommandResult,
  CoreCommand,
} from "../../types/commands";
import { DataValidationRule } from "../../types/data_validation";
import { CellPosition, RangeAdapterFunctions, Style, UID } from "../../types/misc";
import { Range } from "../../types/range";
import { ExcelWorkbookData, WorkbookData } from "../../types/workbook_data";
import { CorePlugin } from "../core_plugin";

interface DataValidationState {
  readonly rules: { [sheet: string]: DataValidationRule[] };
}

export class DataValidationPlugin
  extends CorePlugin<DataValidationState>
  implements DataValidationState
{
  static getters = [
    "cellHasListDataValidationIcon",
    "getDataValidationRule",
    "getDataValidationRules",
    "getValidationRuleForCell",
  ] as const;

  readonly rules: { [sheet: string]: DataValidationRule[] } = {};

  adaptRanges(rangeAdapters: RangeAdapterFunctions) {
    for (const sheetId in this.rules) {
      this.adaptDVRanges(sheetId, rangeAdapters);
    }
    this.adaptDVFormulas(rangeAdapters);
  }

  private adaptDVFormulas({ adaptFormulaString }: RangeAdapterFunctions) {
    for (const sheetId in this.rules) {
      const rules = this.rules[sheetId];
      for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
        const rule = this.rules[sheetId][ruleIndex];
        for (let valueIndex = 0; valueIndex < rule.criterion.values.length; valueIndex++) {
          const value = adaptFormulaString(sheetId, rule.criterion.values[valueIndex]);
          this.history.update(
            "rules",
            sheetId,
            ruleIndex,
            "criterion",
            "values",
            valueIndex,
            value
          );
        }
      }
    }
  }

  private adaptDVRanges(sheetId: UID, { applyChange }: RangeAdapterFunctions) {
    const rules = this.rules[sheetId];
    for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
      const rule = this.rules[sheetId][ruleIndex];
      for (let rangeIndex = rule.ranges.length - 1; rangeIndex >= 0; rangeIndex--) {
        const range = rule.ranges[rangeIndex];
        const change = applyChange(range);
        switch (change.changeType) {
          case "REMOVE":
            if (rule.ranges.length === 1) {
              this.removeDataValidationRule(sheetId, rule.id);
            } else {
              const copy = rule.ranges.slice();
              copy.splice(rangeIndex, 1);
              this.history.update("rules", sheetId, ruleIndex, "ranges", copy);
            }
            break;
          case "RESIZE":
          case "MOVE":
          case "CHANGE":
            this.history.update("rules", sheetId, ruleIndex, "ranges", rangeIndex, change.range);
            break;
        }
      }
    }
  }

  allowDispatch(cmd: Command) {
    switch (cmd.type) {
      case "ADD_DATA_VALIDATION_RULE":
        if (!this.getters.tryGetSheet(cmd.sheetId)) {
          return CommandResult.InvalidSheetId;
        }
        if (cmd.ranges.some((rangeData) => !this.getters.tryGetSheet(rangeData._sheetId))) {
          return CommandResult.InvalidSheetId;
        }
        return this.checkValidations(
          cmd,
          this.chainValidations(
            this.checkEmptyRange,
            this.checkValidRange,
            this.checkCriterionTypeIsValid,
            this.checkCriterionHasValidNumberOfValues,
            this.checkCriterionValuesAreValid
          )
        );
      case "REMOVE_DATA_VALIDATION_RULE":
        if (!this.getters.tryGetSheet(cmd.sheetId)) {
          return CommandResult.InvalidSheetId;
        }
        if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
          return CommandResult.UnknownDataValidationRule;
        }
        break;
    }
    return CommandResult.Success;
  }

  handle(cmd: CoreCommand) {
    switch (cmd.type) {
      case "CREATE_SHEET":
        this.history.update("rules", cmd.sheetId, []);
        break;
      case "DUPLICATE_SHEET": {
        const rules = deepCopy(this.rules[cmd.sheetId]).map((rule) => ({
          ...rule,
          ranges: rule.ranges.map((range) =>
            duplicateRangeInDuplicatedSheet(cmd.sheetId, cmd.sheetIdTo, range)
          ),
        }));
        this.history.update("rules", cmd.sheetIdTo, rules);
        break;
      }
      case "DELETE_SHEET": {
        const rules = { ...this.rules };
        delete rules[cmd.sheetId];
        this.history.update("rules", rules);
        break;
      }
      case "REMOVE_DATA_VALIDATION_RULE": {
        this.removeDataValidationRule(cmd.sheetId, cmd.id);
        break;
      }
      case "ADD_DATA_VALIDATION_RULE": {
        const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
        this.addDataValidationRule(cmd.sheetId, { ...cmd.rule, ranges });
        break;
      }
      case "DELETE_CONTENT": {
        const zones = recomputeZones(cmd.target);
        const sheetId = cmd.sheetId;
        for (const zone of zones) {
          for (let row = zone.top; row <= zone.bottom; row++) {
            for (let col = zone.left; col <= zone.right; col++) {
              const dataValidation = this.getValidationRuleForCell({ sheetId, col, row });
              if (!dataValidation) {
                continue;
              }
              const cell = this.getters.getCell({ sheetId, col, row });
              if (
                dataValidation.criterion.type === "isBoolean" ||
                (dataValidation.criterion.type === "isValueInList" &&
                  !cell?.isFormula &&
                  !cell?.content)
              ) {
                const rules = this.rules[sheetId];
                const ranges = [this.getters.getRangeFromSheetXC(sheetId, toXC(col, row))];
                const adaptedRules = this.removeRangesFromRules(sheetId, ranges, rules);
                this.history.update("rules", sheetId, adaptedRules);
              }
            }
          }
        }
      }
    }
  }

  getDataValidationRules(sheetId: UID): DataValidationRule[] {
    return this.rules[sheetId];
  }

  getDataValidationRule(sheetId: UID, id: UID): DataValidationRule | undefined {
    return this.rules[sheetId].find((rule) => rule.id === id);
  }

  getValidationRuleForCell({ sheetId, col, row }: CellPosition): DataValidationRule | undefined {
    if (!this.rules[sheetId]) {
      return undefined;
    }
    for (const rule of this.rules[sheetId]) {
      for (const range of rule.ranges) {
        if (isInside(col, row, range.zone)) {
          return rule;
        }
      }
    }
    return undefined;
  }

  cellHasListDataValidationIcon(cellPosition: CellPosition): boolean {
    const rule = this.getValidationRuleForCell(cellPosition);
    if (!rule) {
      return false;
    }

    return (
      (rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
      (rule.criterion.displayStyle === "arrow" || rule.criterion.displayStyle === "chip")
    );
  }

  private addDataValidationRule(sheetId: UID, newRule: DataValidationRule) {
    const rules = this.rules[sheetId];

    if (newRule.criterion.type === "isBoolean") {
      this.setCenterStyleToBooleanCells(newRule);
    } else if (newRule.criterion.type === "isValueInList") {
      newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
    }

    const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules, newRule.id);
    const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);

    if (ruleIndex !== -1) {
      adaptedRules[ruleIndex] = newRule;
      this.history.update("rules", sheetId, adaptedRules);
    } else {
      this.history.update("rules", sheetId, [...adaptedRules, newRule]);
    }
  }

  private removeRangesFromRules(
    sheetId: UID,
    ranges: Range[],
    rules: DataValidationRule[],
    editingRuleId?: UID
  ) {
    rules = deepCopy(rules);
    for (const rule of rules) {
      if (rule.id === editingRuleId) {
        continue; // Skip the rule being edited to preserve its place in the list
      }
      rule.ranges = this.getters.recomputeRanges(rule.ranges, ranges);
    }
    return rules.filter((rule) => rule.ranges.length > 0);
  }

  private removeDataValidationRule(sheetId: UID, ruleId: UID) {
    const rules = this.rules[sheetId];
    const newRules = rules.filter((rule) => rule.id !== ruleId);
    this.history.update("rules", sheetId, newRules);
  }

  private setCenterStyleToBooleanCells(rule: DataValidationRule) {
    for (const position of getCellPositionsInRanges(rule.ranges)) {
      const cell = this.getters.getCell(position);
      const style: Style = {
        ...cell?.style,
        align: !cell?.style?.align || cell.style.align === "default" ? "center" : cell.style.align,
        verticalAlign: cell?.style?.verticalAlign ?? "middle",
      };
      this.dispatch("UPDATE_CELL", { ...position, style });
    }
  }

  private checkEmptyRange(cmd: AddDataValidationCommand) {
    return cmd.ranges.length ? CommandResult.Success : CommandResult.EmptyRange;
  }

  import(data: WorkbookData) {
    for (const sheet of data.sheets) {
      this.rules[sheet.id] = [];
      if (!sheet.dataValidationRules) {
        continue;
      }
      for (const rule of sheet.dataValidationRules) {
        this.rules[sheet.id].push({
          ...rule,
          ranges: rule.ranges.map((range) => this.getters.getRangeFromSheetXC(sheet.id, range)),
        });
      }
    }
  }

  export(data: Partial<WorkbookData>) {
    if (!data.sheets) {
      return;
    }
    for (const sheet of data.sheets) {
      sheet.dataValidationRules = [];
      for (const rule of this.rules[sheet.id]) {
        sheet.dataValidationRules.push({
          ...rule,
          ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id)),
        });
      }
    }
  }

  exportForExcel(data: ExcelWorkbookData) {
    if (!data.sheets) {
      return;
    }
    for (const sheet of data.sheets) {
      sheet.dataValidationRules = [];
      for (const rule of this.rules[sheet.id]) {
        const excelRule = {
          ...deepCopy(rule),
          ranges: rule.ranges.map((range) =>
            this.getters.getRangeString(range, sheet.id, { useBoundedReference: true })
          ),
        };
        if (rule.criterion.type === "isValueInRange") {
          excelRule.criterion.values = rule.criterion.values.map((value) => {
            const range = this.getters.getRangeFromSheetXC(sheet.id, value);
            return this.getters.getRangeString(range, sheet.id, {
              useBoundedReference: true,
              useFixedReference: true,
            });
          });
        }
        sheet.dataValidationRules.push(excelRule);
      }
    }
  }

  private checkCriterionTypeIsValid(cmd: AddDataValidationCommand): CommandResult {
    return criterionEvaluatorRegistry.contains(cmd.rule.criterion.type)
      ? CommandResult.Success
      : CommandResult.UnknownDataValidationCriterionType;
  }

  private checkCriterionHasValidNumberOfValues(cmd: AddDataValidationCommand): CommandResult {
    const criterion = cmd.rule.criterion;
    const evaluator = criterionEvaluatorRegistry.get(criterion.type);
    const expectedNumberOfValues = evaluator.numberOfValues(criterion);
    if (
      expectedNumberOfValues !== undefined &&
      criterion.values.length !== expectedNumberOfValues
    ) {
      return CommandResult.InvalidNumberOfCriterionValues;
    }
    return CommandResult.Success;
  }

  private checkCriterionValuesAreValid(cmd: AddDataValidationCommand): CommandResult {
    const criterion = cmd.rule.criterion;
    const evaluator = criterionEvaluatorRegistry.get(criterion.type);
    const isInvalid = (value: string) => {
      if (evaluator.allowedValues === "onlyFormulas" && !value.startsWith("=")) {
        return true;
      }
      if (value.startsWith("=")) {
        return evaluator.allowedValues === "onlyLiterals" || CompiledFormula.IsBadExpression(value);
      }
      return !evaluator.isCriterionValueValid(value);
    };
    if (criterion.values.some(isInvalid)) {
      return CommandResult.InvalidDataValidationCriterionValue;
    }
    return CommandResult.Success;
  }

  private checkValidRange(cmd: AddDataValidationCommand): CommandResult {
    const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
    const stringRanges = ranges.map((range) => this.getters.getRangeString(range, cmd.sheetId));
    if (stringRanges.some((xc) => !this.getters.isRangeValid(xc))) {
      return CommandResult.InvalidRange;
    }
    return CommandResult.Success;
  }

  getFormulas(): CompiledFormula[] {
    return Object.keys(this.rules).flatMap((sheetId) =>
      this.rules[sheetId].flatMap((rule) =>
        rule.criterion.values
          .filter((value) => value.startsWith("="))
          .map((formulaString) => CompiledFormula.Compile(formulaString, sheetId, this.getters))
      )
    );
  }
}
