import { DEFAULT_FONT_SIZE, NEWLINE } from "../../constants";
import { getCanonicalRepresentation } from "../../helpers/data_normalization";
import { splitReference } from "../../helpers/references";
import { isSheetNameEqual } from "../../helpers/sheet";
import { toUnboundedZone } from "../../helpers/zones";
import { ConditionalFormattingOperatorValues } from "../../types/conditional_formatting";
import { Style, UID } from "../../types/misc";
import { ExcelWorkbookData, WorkbookData } from "../../types/workbook_data";
import {
  ExtractedStyle,
  XLSXDataValidationDateOperatorType,
  XLSXDataValidationOperatorType,
  XLSXNumFormat,
  XLSXRel,
  XLSXRelFile,
  XLSXStructure,
  XLSXStyle,
  XLSXWorksheet,
} from "../../types/xlsx";
import {
  EXCEL_DEFAULT_COL_WIDTH,
  EXCEL_DEFAULT_ROW_HEIGHT,
  FIRST_NUMFMT_ID,
  HEIGHT_FACTOR,
  WIDTH_FACTOR,
} from "../constants";
import {
  V_ALIGNMENT_EXPORT_CONVERSION_MAP,
  XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING,
  XLSX_DV_DECIMAL_OPERATOR_MAPPING,
  XLSX_FORMAT_MAP,
} from "../conversion/conversion_maps";

// -------------------------------------
//            CF HELPERS
// -------------------------------------

/**
 * Convert the conditional formatting o-spreadsheet operator to
 * the corresponding excel operator.
 * */
export function convertOperator(operator: ConditionalFormattingOperatorValues): string {
  switch (operator) {
    case "isNotEmpty":
      return "notContainsBlanks";
    case "isEmpty":
      return "containsBlanks";
    case "notContainsText":
      return "notContainsBlanks";
    case "containsText":
      return "containsText";
    case "beginsWithText":
      return "beginsWith";
    case "endsWithText":
      return "endsWith";
    case "isGreaterThan":
      return "greaterThan";
    case "isGreaterOrEqualTo":
      return "greaterThanOrEqual";
    case "isLessThan":
      return "lessThan";
    case "isLessOrEqualTo":
      return "lessThanOrEqual";
    case "isBetween":
      return "between";
    case "isNotBetween":
      return "notBetween";
    case "isEqual":
      return "equal";
    case "isNotEqual":
      return "notEqual";
    case "customFormula":
      return "";
    case "dateIs":
      return "";
    case "dateIsBefore":
      return "lessThan";
    case "dateIsAfter":
      return "greaterThan";
    case "dateIsOnOrAfter":
      return "greaterThanOrEqual";
    case "dateIsOnOrBefore":
      return "lessThanOrEqual";
    case "top10":
      return "top10";
    case "uniqueValues":
      return "uniqueValues";
    case "duplicateValues":
      return "duplicateValues";
  }
}

// -------------------------------------
//        WORKSHEET HELPERS
// -------------------------------------

export function getCellType(value: number | string | boolean | null): string | undefined {
  switch (typeof value) {
    case "boolean":
      return "b";
    case "string":
      return "str";
    case "number":
      return "n";
    default:
      return undefined;
  }
}

export function convertHeightToExcel(height: number): number {
  return Math.round(HEIGHT_FACTOR * height * 100) / 100;
}

export function convertWidthToExcel(width: number): number {
  return Math.round(WIDTH_FACTOR * width * 100) / 100;
}

export function convertHeightFromExcel(height: number | undefined): number | undefined {
  if (!height) {
    return height;
  }
  return Math.round((height / HEIGHT_FACTOR) * 100) / 100;
}

export function convertWidthFromExcel(width: number | undefined): number | undefined {
  if (!width) {
    return width;
  }
  return Math.round((width / WIDTH_FACTOR) * 100) / 100;
}

export function extractStyle(
  data: WorkbookData,
  content: string | undefined,
  styleId: number | undefined,
  formatId: number | undefined,
  borderId: number | undefined
): ExtractedStyle {
  const style: Style = styleId ? data.styles[styleId] : {};
  const format = formatId ? data.formats[formatId] : undefined;
  const styles = {
    font: {
      size: style?.fontSize || DEFAULT_FONT_SIZE,
      color: { rgb: style?.textColor ? style!.textColor : "000000" },
      family: 2,
      name: "Arial",
    },
    fill: style?.fillColor
      ? {
          fgColor: { rgb: style!.fillColor },
        }
      : { reservedAttribute: "none" },
    numFmt: format ? { format, id: 0 /* id not used for export */ } : undefined,
    border: borderId || 0,
    alignment: {
      horizontal: style.align === "default" ? undefined : style.align,
      vertical: style.verticalAlign
        ? V_ALIGNMENT_EXPORT_CONVERSION_MAP[style.verticalAlign]
        : undefined,
      wrapText: style.wrapping === "wrap" || content?.includes(NEWLINE) ? true : undefined,
      textRotation: style.rotation ? rotationToXLSX(style.rotation) : undefined,
      shrinkToFit: style.wrapping === "clip" ? true : undefined,
    },
  };

  styles.font["strike"] = !!style?.strikethrough || undefined;
  styles.font["underline"] = !!style?.underline || undefined;
  styles.font["bold"] = !!style?.bold || undefined;
  styles.font["italic"] = !!style?.italic || undefined;
  return styles;
}

function rotationToXLSX(rad: number): number {
  let deg = Math.round((-rad / Math.PI) * 180) % 180;
  if (deg > 90) {
    deg -= 180;
  } else if (deg < -90) {
    deg += 180;
  }
  if (deg >= 0) {
    return deg;
  } else {
    return 90 - deg;
  }
}

export function rotationFromXLSX(deg: number): number {
  if (deg <= 90) {
    return -(deg / 180) * Math.PI;
  } else {
    return (-(90 - deg) / 180) * Math.PI;
  }
}

export function normalizeStyle(construct: XLSXStructure, styles: ExtractedStyle): number {
  // Normalize this
  const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
  const style = {
    fontId: pushElement(styles.font, construct.fonts),
    fillId: pushElement(styles.fill, construct.fills),
    borderId: styles.border,
    numFmtId,
    alignment: {
      vertical: styles.alignment.vertical,
      horizontal: styles.alignment.horizontal,
      wrapText: styles.alignment.wrapText,
      textRotation: styles.alignment.textRotation,
      shrinkToFit: styles.alignment.shrinkToFit,
    },
  } as XLSXStyle;

  return pushElement(style, construct.styles);
}

function convertFormat(
  format: XLSXNumFormat | undefined,
  numFmtStructure: XLSXNumFormat[]
): number {
  if (!format) {
    return 0;
  }
  let formatId: number | undefined = XLSX_FORMAT_MAP[format.format];
  if (!formatId) {
    formatId = pushElement(format, numFmtStructure) + FIRST_NUMFMT_ID;
  }
  return formatId;
}

/**
 * Add a relation to the given file and return its id.
 */
export function addRelsToFile(
  relsFiles: XLSXRelFile[],
  path: string,
  rel: Omit<XLSXRel, "id">
): string {
  const relsFile = relsFiles.find((file) => file.path === path);
  // the id is a one-based int casted as string
  let id: string;
  if (!relsFile) {
    id = "rId1";
    relsFiles.push({ path, rels: [{ ...rel, id }] });
  } else {
    id = `rId${(relsFile.rels.length + 1).toString()}`;
    relsFile.rels.push({
      ...rel,
      id,
    });
  }
  return id;
}

const globalReverseLookup = new WeakMap<any[], Map<string, number>>();

export function pushElement<T>(property: T, propertyList: T[]): number {
  let reverseLookup = globalReverseLookup.get(propertyList);
  if (!reverseLookup) {
    reverseLookup = new Map();
    for (let i = 0; i < propertyList.length; i++) {
      const canonical = getCanonicalRepresentation(propertyList[i]);
      reverseLookup.set(canonical, i);
    }
    globalReverseLookup.set(propertyList, reverseLookup);
  }

  const canonical = getCanonicalRepresentation(property);
  if (reverseLookup.has(canonical)) {
    return reverseLookup.get(canonical)!;
  }

  const maxId = propertyList.length;
  propertyList.push(property);
  reverseLookup.set(canonical, maxId);
  return maxId;
}

/**
 * Convert a chart o-spreadsheet id to a xlsx id which
 * are unsigned integers (starting from 1).
 */
export function convertChartId(chartId: UID, construct: XLSXStructure) {
  const xlsxId = construct.chartIds.findIndex((id) => id === chartId);
  if (xlsxId === -1) {
    construct.chartIds.push(chartId);
    return construct.chartIds.length;
  }
  return xlsxId + 1;
}

const imageIds: UID[] = [];

/**
 * Convert a image o-spreadsheet id to a xlsx id which
 * are unsigned integers (starting from 1).
 */
export function convertImageId(imageId: UID) {
  const xlsxId = imageIds.findIndex((id) => id === imageId);
  if (xlsxId === -1) {
    imageIds.push(imageId);
    return imageIds.length;
  }
  return xlsxId + 1;
}

/**
 * Convert a value expressed in dot to EMU.
 * EMU = English Metrical Unit
 * There are 914400 EMU per inch.
 *
 * /!\ A value expressed in EMU cannot be fractional.
 * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
 */
export function convertDotValueToEMU(value: number) {
  const DPI = 96;
  return Math.round((value * 914400) / DPI);
}

export function getRangeSize(
  reference: string,
  defaultSheetIndex: string,
  data: ExcelWorkbookData
) {
  let xc = reference;
  let sheetName: string | undefined = undefined;
  ({ xc, sheetName } = splitReference(reference));
  let rangeSheetIndex: number;
  if (sheetName) {
    const index = data.sheets.findIndex((sheet) => isSheetNameEqual(sheet.name, sheetName));
    if (index < 0) {
      throw new Error("Unable to find a sheet with the name " + sheetName);
    }
    rangeSheetIndex = index;
  } else {
    rangeSheetIndex = Number(defaultSheetIndex);
  }

  const zone = toUnboundedZone(xc);
  if (zone.right === undefined) {
    zone.right = data.sheets[rangeSheetIndex].colNumber;
  }
  if (zone.bottom === undefined) {
    zone.bottom = data.sheets[rangeSheetIndex].rowNumber;
  }

  return (zone.right - zone.left + 1) * (zone.bottom - zone.top + 1);
}

export function convertEMUToDotValue(value: number) {
  const DPI = 96;
  return Math.round((value * DPI) / 914400);
}

/**
 * Get the position of the start of a column in Excel (in px).
 */
export function getColPosition(colIndex: number, sheetData: XLSXWorksheet): number {
  let position = 0;
  for (let i = 0; i < colIndex; i++) {
    const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);
    if (colAtIndex?.width) {
      position += colAtIndex.width;
    } else if (sheetData.sheetFormat?.defaultColWidth) {
      position += sheetData.sheetFormat.defaultColWidth!;
    } else {
      position += EXCEL_DEFAULT_COL_WIDTH;
    }
  }
  return position / WIDTH_FACTOR;
}

/**
 * Get the position of the start of a row in Excel (in px).
 */
export function getRowPosition(rowIndex: number, sheetData: XLSXWorksheet) {
  let position = 0;
  for (let i = 0; i < rowIndex; i++) {
    const rowAtIndex = sheetData.rows.find((row) => row.index - 1 === i);
    if (rowAtIndex?.height) {
      position += rowAtIndex.height;
    } else if (sheetData.sheetFormat?.defaultRowHeight) {
      position += sheetData.sheetFormat.defaultRowHeight!;
    } else {
      position += EXCEL_DEFAULT_ROW_HEIGHT;
    }
  }
  return position / HEIGHT_FACTOR;
}

/**
 * Convert the o-spreadsheet data validation decimal
 * criterion type to the corresponding excel operator.
 */
export function convertDecimalCriterionTypeToExcelOperator(operator: string) {
  return Object.keys(XLSX_DV_DECIMAL_OPERATOR_MAPPING).find(
    (key) => XLSX_DV_DECIMAL_OPERATOR_MAPPING[key] === operator
  ) as XLSXDataValidationOperatorType;
}

/**
 * Convert the o-spreadsheet data validation date
 * criterion type to the corresponding excel operator.
 */
export function convertDateCriterionTypeToExcelOperator(operator: string) {
  return Object.keys(XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING).find(
    (key) => XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[key] === operator
  ) as XLSXDataValidationDateOperatorType;
}
