import { CellPosition, Color, Dimension, Format, HeaderIndex, Style, UID, Zone } from "../..";
import { DEFAULT_STYLE } from "../../constants";
import { getItemId, ItemsDic } from "../../helpers/data_normalization";
import {
  deepCopy,
  defaultDict,
  groupConsecutive,
  isObjectEmptyRecursive,
} from "../../helpers/misc";
import { recomputeZones } from "../../helpers/recompute_zones";
import { getZoneArea } from "../../helpers/zones";
import { CoreCommand } from "../../types/commands";
import { ExcelWorkbookData, WorkbookData } from "../../types/workbook_data";
import { CorePlugin } from "../core_plugin";

export type defaultValue<T> = {
  sheetDefault?: T | undefined;
  colDefault?: (T | undefined)[];
  rowDefault?: (T | undefined)[];
};
export type sparseDefaultValue<T> = {
  sheetDefault?: T | undefined;
  colDefault?: Record<number, T | undefined>;
  rowDefault?: Record<number, T | undefined>;
};
export type defaultValues<T> = Record<UID, defaultValue<T> | undefined>;
export type defaultStyle = { [J in keyof Style]: defaultValue<Style[J]> };
export type defaultStyles = Record<UID, defaultStyle | undefined>;

interface defaultState {
  readonly style: defaultStyles;
  readonly format: defaultValues<Format>;
}

export class DefaultPlugin extends CorePlugin<defaultState> implements defaultState {
  static getters = [
    "getSheetDefaultStyle",
    "getSheetDefaultFormat",
    "getCellDefaultStyle",
    "getDefaultStyle",
    "getCellDefaultStyleValue",
    "getCellDefaultFormat",
    "getDefaultFormat",
    "getDefaultStyleColors",
  ] as const;
  public readonly style: defaultStyles = {};
  public readonly format: defaultValues<Format> = {};

  handle(cmd: CoreCommand): void {
    switch (cmd.type) {
      case "SET_FORMATTING":
        if (cmd.style !== undefined) {
          this.setStyle(cmd.sheetId, cmd.target, cmd.style);
        }
        if (cmd.format !== undefined) {
          this.setFormat(cmd.sheetId, cmd.target, cmd.format);
        }
        break;
      case "CLEAR_FORMATTING":
        this.setStyle(cmd.sheetId, cmd.target, DEFAULT_STYLE);
        this.setFormat(cmd.sheetId, cmd.target, null);
        break;
      case "ADD_COLUMNS_ROWS":
        const startingIdx = cmd.position === "before" ? cmd.base : cmd.base + 1;
        this.moveColRows(cmd.sheetId, cmd.dimension, startingIdx, cmd.quantity);
        const indexToCopy = cmd.position === "before" ? cmd.base + cmd.quantity : cmd.base;
        for (let index = startingIdx; index < startingIdx + cmd.quantity; index++) {
          this.copyColRow(cmd.sheetId, cmd.dimension, indexToCopy, index);
        }
        break;
      case "REMOVE_COLUMNS_ROWS":
        for (const el of groupConsecutive(cmd.elements).toReversed()) {
          for (const i of el) {
            this.clearColRows(cmd.sheetId, cmd.dimension, i);
          }
          this.moveColRows(cmd.sheetId, cmd.dimension, el[0], -el.length);
        }
        break;
      case "DUPLICATE_SHEET":
        this.history.update("style", cmd.sheetIdTo, deepCopy(this.style[cmd.sheetId]));
        this.history.update("format", cmd.sheetIdTo, deepCopy(this.format[cmd.sheetId]));
        break;
    }
  }

  private clearColRows(sheetId: UID, colRow: Dimension, index: HeaderIndex) {
    const colRowDefault = colRow === "COL" ? "colDefault" : "rowDefault";
    for (const key in this.style[sheetId]) {
      this.history.update("style", sheetId, key as keyof Style, colRowDefault, index, undefined);
    }
    this.history.update("format", sheetId, colRowDefault, index, undefined);
  }

  private moveColRows(sheetId: UID, colRow: Dimension, start: HeaderIndex, quantity: number) {
    const colRowDefault = colRow === "COL" ? "colDefault" : "rowDefault";
    for (const key in this.style[sheetId]) {
      const positionValue = Object.entries(
        this.style[sheetId][key as keyof Style]?.[colRowDefault] ?? []
      );
      if (quantity > 0) {
        positionValue.reverse();
      }
      for (const [headerIndex, value] of positionValue) {
        const header = parseInt(headerIndex);
        if (header < start) {
          continue;
        }
        this.history.update(
          "style",
          sheetId,
          key as keyof Style,
          colRowDefault,
          header + quantity,
          value
        );
        this.history.update("style", sheetId, key as keyof Style, colRowDefault, header, undefined);
      }
    }
    const positionValue = Object.entries(this.format[sheetId]?.[colRowDefault] ?? []);
    if (quantity > 0) {
      positionValue.reverse();
    }
    for (const [headerIndex, value] of positionValue) {
      const header = parseInt(headerIndex);
      if (header < start) {
        continue;
      }
      this.history.update("format", sheetId, colRowDefault, header + quantity, value);
      this.history.update("format", sheetId, colRowDefault, header, undefined);
    }
  }

  private copyColRow(sheetId: UID, colRow: Dimension, copyFrom: HeaderIndex, copyTo: HeaderIndex) {
    const colRowDefault = colRow === "COL" ? "colDefault" : "rowDefault";
    for (const key in this.style[sheetId]) {
      const value = this.style[sheetId][key]?.[colRowDefault]?.[copyFrom];
      this.history.update("style", sheetId, key as keyof Style, colRowDefault, copyTo, value);
    }
    this.history.update(
      "format",
      sheetId,
      colRowDefault,
      copyTo,
      this.format[sheetId]?.[colRowDefault]?.[copyFrom]
    );
  }

  // ---------------------------------------------------------------------------
  // Format
  // ---------------------------------------------------------------------------

  private setFormat(sheetId: UID, zones: Zone[], format: Format | null) {
    zones = recomputeZones(zones);
    const { numberOfCols, numberOfRows } = this.getters.getSheetSize(sheetId);
    const sheetArea = numberOfCols * numberOfRows;
    for (const zone of zones) {
      const defaultCol = zone.bottom - zone.top + 1 > numberOfRows / 2;
      const defaultRow = zone.right - zone.left + 1 > numberOfCols / 2;
      if (defaultRow && defaultCol && getZoneArea(zone) > sheetArea / 2) {
        this.setSheetFormat(sheetId, zone, format);
      } else if (defaultCol) {
        this.setColsFormat(sheetId, zone, format ?? "");
      } else if (defaultRow) {
        this.setRowsFormat(sheetId, zone, format ?? "");
      }
    }
  }

  private setSheetFormat(sheetId: UID, zone: Zone, format: Format | null) {
    this.history.update("format", sheetId, "sheetDefault", format ?? undefined);
    const rows = Object.keys(this.format[sheetId]?.rowDefault ?? {});
    for (const rowIdx of rows) {
      const row = parseInt(rowIdx);
      if (zone.top <= row && row <= zone.bottom) {
        this.history.update("format", sheetId, "rowDefault", row, undefined);
      }
    }
    const cols = Object.keys(this.format[sheetId]?.colDefault ?? {});
    for (const colIdx of cols) {
      const col = parseInt(colIdx);
      if (zone.left <= col && col <= zone.right) {
        this.history.update("format", sheetId, "colDefault", col, undefined);
      }
    }
  }

  private setColsFormat(sheetId: UID, zone: Zone, format: Format) {
    const colFormat = format !== (this.format[sheetId]?.sheetDefault ?? "") ? format : undefined;
    for (let col = zone.left; col <= zone.right; col++) {
      this.history.update("format", sheetId, "colDefault", col, colFormat);
    }
  }

  private setRowsFormat(sheetId: UID, zone: Zone, format: Format) {
    for (let row = zone.top; row <= zone.bottom; row++) {
      this.history.update("format", sheetId, "rowDefault", row, format);
    }
  }

  // ---------------------------------------------------------------------------
  // Style
  // ---------------------------------------------------------------------------

  private setStyle(sheetId: UID, zones: Zone[], style: Style) {
    zones = recomputeZones(zones);
    const { numberOfCols, numberOfRows } = this.getters.getSheetSize(sheetId);
    const sheetArea = numberOfCols * numberOfRows;
    for (const zone of zones) {
      const defaultCol = zone.bottom - zone.top + 1 > numberOfRows / 2;
      const defaultRow = zone.right - zone.left + 1 > numberOfCols / 2;
      if (defaultRow && defaultCol && getZoneArea(zone) > sheetArea / 2) {
        this.setSheetStyle(sheetId, zone, style);
      } else if (defaultCol) {
        this.setColsStyle(sheetId, zone, style);
      } else if (defaultRow) {
        this.setRowsStyle(sheetId, zone, style);
      }
    }
  }

  private setSheetStyle(sheetId: UID, zone: Zone, style: Style) {
    for (const key in style) {
      if (style[key] !== DEFAULT_STYLE[key]) {
        this.history.update("style", sheetId, key as keyof Style, "sheetDefault", style[key]);
      } else {
        this.history.update("style", sheetId, key as keyof Style, "sheetDefault", undefined);
      }
      const rows = Object.keys(this.style[sheetId]?.[key]?.rowDefault ?? {});
      for (const rowIdx of rows) {
        const row = parseInt(rowIdx);
        if (zone.top <= row && row <= zone.bottom) {
          this.history.update("style", sheetId, key as keyof Style, "rowDefault", row, undefined);
        }
      }
      const cols = Object.keys(this.style[sheetId]?.[key]?.colDefault ?? {});
      for (const colIdx of cols) {
        const col = parseInt(colIdx);
        if (zone.left <= col && col <= zone.right) {
          this.history.update("style", sheetId, key as keyof Style, "colDefault", col, undefined);
        }
      }
    }
  }

  private setColsStyle(sheetId: UID, zone: Zone, style: Style) {
    for (const key in style) {
      const colStyle =
        style[key] !== (this.style[sheetId]?.[key]?.sheetDefault ?? DEFAULT_STYLE[key])
          ? style[key]
          : undefined;
      for (let col = zone.left; col <= zone.right; col++) {
        this.history.update("style", sheetId, key as keyof Style, "colDefault", col, colStyle);
      }
    }
  }

  private setRowsStyle(sheetId: UID, zone: Zone, style: Style) {
    for (const key in style) {
      const hasColStyle = Object.keys(this.style[sheetId]?.[key]?.colDefault ?? {}).length !== 0;
      for (let row = zone.top; row <= zone.bottom; row++) {
        if (
          hasColStyle ||
          style[key] !== (this.style[sheetId]?.[key]?.sheetDefault ?? DEFAULT_STYLE[key])
        ) {
          this.history.update("style", sheetId, key as keyof Style, "rowDefault", row, style[key]);
        } else {
          this.history.update("style", sheetId, key as keyof Style, "rowDefault", row, undefined);
        }
      }
    }
  }

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

  getSheetDefaultStyle(sheetId: UID): defaultStyle | undefined {
    return this.style[sheetId];
  }

  getSheetDefaultFormat(sheetId: UID): defaultValue<Format> | undefined {
    return this.format[sheetId];
  }

  getCellDefaultStyle(position: CellPosition): Style {
    const style: Style = {};
    const styleSheet = this.style[position.sheetId];
    if (!styleSheet) {
      return style;
    }
    for (const key in styleSheet) {
      const defaults = styleSheet[key];
      if (!defaults) {
        continue;
      }
      const styleValue =
        defaults.rowDefault?.[position.row] ??
        defaults.colDefault?.[position.col] ??
        defaults.sheetDefault;

      if (styleValue !== undefined) {
        style[key] = styleValue;
      }
    }
    return style;
  }

  getCellDefaultStyleValue<J extends keyof Style>(position: CellPosition, key: J): Style[J] {
    const styleSheet = this.style[position.sheetId]?.[key];
    return (
      styleSheet?.rowDefault?.[position.row] ??
      styleSheet?.colDefault?.[position.col] ??
      styleSheet?.sheetDefault ??
      DEFAULT_STYLE[key]
    );
  }

  getCellDefaultFormat(position: CellPosition): Format | undefined {
    const formatSheet = this.format[position.sheetId];
    return (
      formatSheet?.rowDefault?.[position.row] ??
      formatSheet?.colDefault?.[position.col] ??
      formatSheet?.sheetDefault
    );
  }

  getDefaultStyle<J extends keyof Style, D extends "COL" | "ROW" | "SHEET">(
    sheetId: UID,
    key: J,
    dimension: D,
    index: D extends "COL" | "ROW" ? HeaderIndex : undefined
  ): Style[J] {
    if (dimension === "SHEET") {
      return this.style[sheetId]?.[key]?.sheetDefault;
    } else if (dimension === "COL") {
      return this.style[sheetId]?.[key]?.colDefault?.[index as HeaderIndex];
    } else {
      return this.style[sheetId]?.[key]?.rowDefault?.[index as HeaderIndex];
    }
  }

  getDefaultStyleColors(): Color[] {
    const colors = new Set<Color>();
    for (const sheetId in this.style) {
      const styleSheet = this.style[sheetId];
      if (!styleSheet) {
        continue;
      }
      for (const key of ["fillColor", "textColor"]) {
        const defaults: defaultValue<Color> = styleSheet[key];
        if (!defaults) {
          continue;
        }
        if (defaults.sheetDefault) {
          colors.add(defaults.sheetDefault);
        }
        for (const value of Object.values(defaults.colDefault ?? [])) {
          if (value) {
            colors.add(value);
          }
        }
        for (const value of Object.values(defaults.rowDefault ?? [])) {
          if (value) {
            colors.add(value);
          }
        }
      }
    }
    return [...colors];
  }

  getDefaultFormat<D extends "COL" | "ROW" | "SHEET">(
    sheetId: UID,
    dimension: D,
    index: D extends "COL" | "ROW" ? HeaderIndex : undefined
  ): Format | undefined {
    if (dimension === "SHEET") {
      return this.format[sheetId]?.sheetDefault;
    } else if (dimension === "COL") {
      return this.format[sheetId]?.colDefault?.[index as HeaderIndex];
    } else {
      return this.format[sheetId]?.rowDefault?.[index as HeaderIndex];
    }
  }

  // ---------------------------------------------------------------------------
  // Export
  // ---------------------------------------------------------------------------

  mapToId<T>(defaults: defaultValue<T>, dict: ItemsDic<T>): sparseDefaultValue<number> {
    const defaultsIds: sparseDefaultValue<number> = {
      colDefault: {},
      rowDefault: {},
    };
    if (defaults.sheetDefault) {
      defaultsIds.sheetDefault = getItemId(defaults.sheetDefault, dict);
    }
    for (const colIndex in defaults.colDefault) {
      defaultsIds.colDefault![colIndex] = getItemId(defaults.colDefault[colIndex], dict);
    }
    for (const rowIndex in defaults.rowDefault) {
      defaultsIds.rowDefault![rowIndex] = getItemId(defaults.rowDefault[rowIndex], dict);
    }
    return defaultsIds;
  }

  mapToValue<T>(defaultIds: sparseDefaultValue<number>, dict: ItemsDic<T>): defaultValue<T> {
    const defaults: defaultValue<T> = {
      colDefault: [],
      rowDefault: [],
    };
    if (defaultIds.sheetDefault) {
      defaults.sheetDefault = dict[defaultIds.sheetDefault];
    }
    for (const colIndex in defaultIds.colDefault) {
      defaults.colDefault![colIndex] = dict[defaultIds.colDefault[colIndex]];
    }
    for (const rowIndex in defaultIds.rowDefault) {
      defaults.rowDefault![rowIndex] = dict[defaultIds.rowDefault[rowIndex]];
    }
    return defaults;
  }

  mapStyleToId(defaults: defaultStyle, dict: ItemsDic<Style>): sparseDefaultValue<number> {
    const colDefaultDict = defaultDict({});
    const rowDefaultDict = defaultDict({});
    const sheetDefault = {};

    for (const key in defaults) {
      const keyDefault = defaults[key];
      if (keyDefault.sheetDefault) {
        sheetDefault[key] = keyDefault.sheetDefault;
      }
      for (const colIndex in keyDefault.colDefault) {
        colDefaultDict.get(colIndex)[key] = keyDefault.colDefault[colIndex];
      }
      for (const rowIndex in keyDefault.rowDefault) {
        rowDefaultDict.get(rowIndex)[key] = keyDefault.rowDefault[rowIndex];
      }
    }

    const colDefault = [];
    for (const [col, value] of Object.entries(colDefaultDict.state)) {
      colDefault[col] = value;
    }
    const rowDefault = [];
    for (const [row, value] of Object.entries(rowDefaultDict.state)) {
      rowDefault[row] = value;
    }

    return this.mapToId(
      {
        colDefault,
        rowDefault,
        sheetDefault,
      },
      dict
    );
  }

  export(data: WorkbookData) {
    for (const sheet of data.sheets) {
      const sheetFormat = this.format[sheet.id];
      sheet.defaultFormat =
        sheetFormat && !isObjectEmptyRecursive(sheetFormat)
          ? this.mapToId(sheetFormat, data.formats)
          : undefined;
      const sheetStyle = this.style[sheet.id];
      sheet.defaultStyle =
        sheetStyle && !isObjectEmptyRecursive(sheetStyle)
          ? this.mapStyleToId(sheetStyle, data.styles)
          : undefined;
    }
  }

  import(data: WorkbookData) {
    for (const sheet of data.sheets) {
      this.history.update(
        "format",
        sheet.id,
        sheet.defaultFormat && this.mapToValue(sheet.defaultFormat, data.formats)
      );
      if (sheet.defaultStyle) {
        const defaultStyle = this.mapToValue(sheet.defaultStyle, data.styles);
        for (const key in defaultStyle.sheetDefault) {
          this.history.update(
            "style",
            sheet.id,
            key as keyof Style,
            "sheetDefault",
            defaultStyle.sheetDefault[key]
          );
        }
        for (const colIndex in defaultStyle.colDefault ?? []) {
          const colInt = parseInt(colIndex);
          const colStyle = defaultStyle.colDefault?.[colIndex];
          for (const key in colStyle) {
            this.history.update(
              "style",
              sheet.id,
              key as keyof Style,
              "colDefault",
              colInt,
              colStyle[key]
            );
          }
        }
        for (const rowIndex in defaultStyle.rowDefault ?? []) {
          const rowInt = parseInt(rowIndex);
          const rowStyle = defaultStyle.rowDefault?.[rowIndex];
          for (const key in rowStyle) {
            this.history.update(
              "style",
              sheet.id,
              key as keyof Style,
              "rowDefault",
              rowInt,
              rowStyle[key]
            );
          }
        }
      }
    }
  }

  exportForExcel(data: ExcelWorkbookData) {
    this.export(data);
  }
}
