import { CompiledFormula } from "../../formulas/compiler";
import { isEvaluationError, toString } from "../../functions/helpers";
import {
  BooleanCell,
  Cell,
  CellValue,
  CellValueType,
  EmptyCell,
  ErrorCell,
  EvaluatedCell,
  FormulaCell,
  LiteralCell,
  NumberCell,
} from "../../types/cells";
import { Format, LocaleFormat } from "../../types/format";
import { CoreGetters } from "../../types/getters";
import { DEFAULT_LOCALE, Locale } from "../../types/locale";
import { CellPosition, FunctionResultObject, Style, UID } from "../../types/misc";
import { parseDateTime } from "../dates";
import {
  detectDateFormat,
  detectNumberFormat,
  formatValue,
  isDateTimeFormat,
  isTextFormat,
} from "../format/format";
import { detectLink } from "../links";
import { isBoolean, memoize } from "../misc";
import { isNumber, parseNumber } from "../numbers";

export function evaluateLiteral(
  literalCell: LiteralCell,
  localeFormat: LocaleFormat,
  position?: CellPosition
): EvaluatedCell {
  const value =
    isTextFormat(localeFormat.format) && literalCell.parsedValue !== null
      ? literalCell.content
      : literalCell.parsedValue;
  const functionResult = { value, format: localeFormat.format, origin: position };
  return createEvaluatedCell(functionResult, localeFormat.locale, position);
}

export function createCell(
  getters: CoreGetters,
  id: number,
  content: string,
  format: Format | undefined,
  style: Style | undefined,
  sheetId: UID,
  options?: { avoidAutomaticDateFormat: boolean }
): Cell {
  if (!content.startsWith("=")) {
    return createLiteralCell(getters, id, content, format, style, options);
  }
  return createFormulaCell(getters, id, content, format, style, sheetId);
}

export function createLiteralCell(
  getters: CoreGetters,
  id: number,
  content: string,
  format: Format | undefined,
  style: Style | undefined,
  options?: { avoidAutomaticDateFormat: boolean }
): LiteralCell {
  const locale = getters.getLocale();
  const parsedValue = parseLiteral(content, locale);

  if (format === undefined && typeof parsedValue === "number") {
    const dateFormat = detectDateFormat(content, locale);
    if (options?.avoidAutomaticDateFormat && dateFormat) {
      return {
        id,
        content,
        style,
        format,
        isFormula: false,
        parsedValue: content,
      };
    }
    format = dateFormat || detectNumberFormat(content);
  }

  if (!isTextFormat(format) && !content.startsWith("'") && !isEvaluationError(content)) {
    content = toString(parsedValue);
  }
  return {
    id,
    content,
    style,
    format,
    isFormula: false,
    parsedValue,
  };
}

export function createFormulaCell(
  getters: CoreGetters,
  id: number,
  content: string,
  format: Format | undefined,
  style: Style | undefined,
  sheetId: UID
): FormulaCell {
  const compiledFormula = CompiledFormula.Compile(content, sheetId, getters);
  return createFormulaCellFromCompiledFormula(id, compiledFormula, format, style);
}

export function createFormulaCellFromCompiledFormula(
  id: number,
  compiledFormula: CompiledFormula,
  format: Format | undefined,
  style: Style | undefined
): FormulaCell {
  return {
    id,
    format,
    style,
    isFormula: true,
    compiledFormula,
  };
}

export function parseLiteral(content: string, locale: Locale): CellValue {
  if (content.startsWith("=")) {
    throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
  }
  if (content.startsWith("'")) {
    return content.slice(1);
  }
  if (content === "") {
    return null;
  }
  if (content.includes("\n")) {
    return content;
  }
  if (isNumber(content, DEFAULT_LOCALE)) {
    return parseNumber(content, DEFAULT_LOCALE);
  }
  const internalDate = parseDateTime(content, locale);
  if (internalDate) {
    return internalDate.value;
  }
  if (isBoolean(content)) {
    return content.toUpperCase() === "TRUE";
  }
  return content;
}

export function createEvaluatedCell(
  functionResult: FunctionResultObject,
  locale: Locale = DEFAULT_LOCALE,
  position?: CellPosition,
  cellFormat?: Format,
  origin?: CellPosition
): EvaluatedCell {
  const link = detectLink(functionResult.value);
  if (!link) {
    const evaluateCell = _createEvaluatedCell(functionResult, locale, position, cellFormat);
    return addOrigin(evaluateCell, functionResult.origin ?? origin);
  }
  const value = parseLiteral(link.label, locale);
  const format =
    functionResult.format ||
    (typeof value === "number"
      ? detectDateFormat(link.label, locale) || detectNumberFormat(link.label)
      : undefined);
  const linkPayload = {
    value,
    format,
  };
  return addOrigin(
    {
      ..._createEvaluatedCell(linkPayload, locale, position, cellFormat),
      link,
    },
    functionResult.origin ?? position
  );
}

function _createEvaluatedCell(
  functionResult: FunctionResultObject,
  locale: Locale,
  position?: CellPosition,
  cellFormat?: Format
): EvaluatedCell {
  let { value, format, message, errorOriginPosition } = functionResult;
  format = cellFormat || format;

  const formattedValue = formatValue(value, { format, locale });
  if (isEvaluationError(value)) {
    return errorCell(value, message, position, errorOriginPosition);
  }
  if (value === null) {
    return emptyCell(format);
  }
  if (isTextFormat(format)) {
    // TO DO:
    // with the next line, the value of the cell is transformed depending on the format.
    // This shouldn't happen, by doing this, the formulas handling numbers are not able
    // to interpret the value as a number.
    return textCell(toString(value), format, formattedValue, position);
  }

  if (typeof value === "number") {
    if (isDateTimeFormat(format || "")) {
      return dateTimeCell(value, format, formattedValue, position);
    }
    return numberCell(value, format, formattedValue, position);
  }
  if (typeof value === "boolean") {
    return booleanCell(value, format, formattedValue, position);
  }
  return textCell(value, format, formattedValue, position);
}

export function isNumberResult(
  result: FunctionResultObject | undefined
): result is { value: number } {
  return !!result && getEvaluatedCellType(result) === CellValueType.number;
}

export function isTextResult(
  result: FunctionResultObject | undefined
): result is { value: string } {
  return !!result && getEvaluatedCellType(result) === CellValueType.text;
}

export function isBooleanResult(
  result: FunctionResultObject | undefined
): result is { value: boolean } {
  return !!result && getEvaluatedCellType(result) === CellValueType.boolean;
}

export function isEmptyResult(result: FunctionResultObject | undefined): result is { value: null } {
  return !!result && getEvaluatedCellType(result) === CellValueType.empty;
}

export function isErrorResult(
  result: FunctionResultObject | undefined
): result is { value: string } {
  return !!result && getEvaluatedCellType(result) === CellValueType.error;
}

function getEvaluatedCellType({ value, format }: FunctionResultObject): CellValueType {
  if (value === null) {
    return CellValueType.empty;
  } else if (isEvaluationError(value)) {
    return CellValueType.error;
  } else if (isTextFormat(format)) {
    return CellValueType.text;
  }
  switch (typeof value) {
    case "number":
      return CellValueType.number;
    case "boolean":
      return CellValueType.boolean;
    case "string":
      return CellValueType.text;
  }
}

function textCell(
  value: string,
  format: string | undefined,
  formattedValue: string,
  position?: CellPosition
): EvaluatedCell {
  return {
    value,
    format,
    formattedValue,
    position,
    type: CellValueType.text,
    isAutoSummable: true,
    defaultAlign: "left",
  };
}

function numberCell(
  value: number,
  format: string | undefined,
  formattedValue: string,
  position?: CellPosition
): NumberCell {
  return {
    value: value || 0, // necessary to avoid "-0" and NaN values,
    format,
    formattedValue,
    position,
    type: CellValueType.number,
    isAutoSummable: true,
    defaultAlign: "right",
  };
}

// do not create empty cells based on their position to allow memoization
const emptyCell = memoize(function emptyCell(format: string | undefined): EmptyCell {
  return {
    value: null,
    format,
    formattedValue: "",
    type: CellValueType.empty,
    isAutoSummable: true,
    defaultAlign: "left",
  };
});

function dateTimeCell(
  value: number,
  format: string | undefined,
  formattedValue: string,
  position?: CellPosition
): NumberCell {
  return {
    value,
    format,
    formattedValue,
    position,
    type: CellValueType.number,
    isAutoSummable: false,
    defaultAlign: "right",
  };
}

function booleanCell(
  value: boolean,
  format: string | undefined,
  formattedValue: string,
  position?: CellPosition
): BooleanCell {
  return {
    value,
    format,
    formattedValue,
    position,
    type: CellValueType.boolean,
    isAutoSummable: false,
    defaultAlign: "center",
  };
}

function errorCell(
  value: string,
  message?: string,
  position?: CellPosition,
  errorOriginPosition?: CellPosition
): ErrorCell {
  return {
    value,
    formattedValue: value,
    message,
    position,
    type: CellValueType.error,
    isAutoSummable: false,
    defaultAlign: "center",
    errorOriginPosition,
  };
}

function addOrigin(cell: EvaluatedCell, origin: CellPosition | undefined): EvaluatedCell {
  if (cell.value === null) {
    // ignore empty cells to allow sharing the same object instance
    return cell;
  }
  if ("origin" in cell) {
    return cell;
  }
  cell.origin = origin;
  return cell;
}
