import { EvaluationError, NotAvailableError } from "../types/errors";

import { _t } from "../translation";
import {
  ArgDefinition,
  ComputeFunction,
  EvalContext,
  FunctionDescription,
} from "../types/functions";
import { Arg, FunctionResultObject, isMatrix, Matrix } from "../types/misc";
import { argTargeting } from "./arguments";
import { isEvaluationError, matrixForEach } from "./helpers";

type VectorArgType = "horizontal" | "vertical" | "matrix";

/**
 * Enables a formula function to accept matrix or vector inputs instead of simple value, computing results across multiple dimensions.
 *
 * ```
 *                    /         |‾                 ‾| \        |‾                                                    ‾|
 *                   |          | [A]               |  |       | compute(A, D, E), compute(A, D, F), compute(A, D, G) |
 * applyVectorization| compute, | [B], D, [E, F, G] |  |  <=>  | compute(B, D, E), compute(B, D, F), compute(B, D, G) |
 *                   |          | [C]               |  |       | compute(C, D, E), compute(C, D, F), compute(C, D, G) |
 *                    \         |_                 _| /        |_                                                    _|
 * ```
 *
 * By default, all arguments are vectorized. To control which arguments are vectorized,
 * pass an `acceptToVectorize` boolean array of the same length as `args`:
 * - `true`  enables vectorization for that argument
 * - `false` disables vectorization for that argument
 *
 * For example, with `[true, true, false]` on previous example you get:
 *
 * ```
 * |‾                        ‾|
 * | compute(A, D, [E, F, G]) |
 * | compute(B, D, [E, F, G]) |
 * | compute(C, D, [E, F, G]) |
 * |_                        _|
 * ```
 *
 * @remarks
 * This helper is automatically applied (by default) to **all** `compute` functions
 * across the various spreadsheet formula modules:
 * - If an argument is declared **scalar** (not `"range"`), it is vectorized.
 * - If **all** arguments are declared **ranges**, no vectorization occurs.
 *   - e.g. `SUM(A1:B2)` returns a 1×1 sum over the range.
 *   - e.g. `COS(A1:B2)` over `A1:B2` returns a 2×2 element-wise result.
 * - For special behaviors (e.g. the `IF` function), you may declare all arguments
 *   as ranges and invoke this helper directly within your `compute` implementation.
 */
export function applyVectorization(
  context: EvalContext,
  descr: FunctionDescription,
  args: Arg[],
  argDefinitions: ArgDefinition[],
  vectorizedArgsIndices: number[]
): FunctionResultObject | Matrix<FunctionResultObject> {
  let countVectorizedCol = 1;
  let countVectorizedRow = 1;
  let vectorizedColLimit = Infinity;
  let vectorizedRowLimit = Infinity;

  let vectorArgsType: VectorArgType[] | undefined = undefined;

  for (const argIndex of vectorizedArgsIndices) {
    const arg = args[argIndex];

    if (isMatrix(arg)) {
      const nColumns = arg.length;
      const nRows = arg[0].length;
      if (nColumns !== 1 || nRows !== 1) {
        vectorArgsType ??= new Array(args.length);
        if (nColumns !== 1 && nRows !== 1) {
          vectorArgsType[argIndex] = "matrix";
          countVectorizedCol = Math.max(countVectorizedCol, nColumns);
          countVectorizedRow = Math.max(countVectorizedRow, nRows);
          vectorizedColLimit = Math.min(vectorizedColLimit, nColumns);
          vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows);
        } else if (nColumns !== 1) {
          vectorArgsType[argIndex] = "horizontal";
          countVectorizedCol = Math.max(countVectorizedCol, nColumns);
          vectorizedColLimit = Math.min(vectorizedColLimit, nColumns);
        } else if (nRows !== 1) {
          vectorArgsType[argIndex] = "vertical";
          countVectorizedRow = Math.max(countVectorizedRow, nRows);
          vectorizedRowLimit = Math.min(vectorizedRowLimit, nRows);
        }
      } else {
        args[argIndex] = arg[0][0];
      }
    }
  }

  if (countVectorizedCol === 1 && countVectorizedRow === 1) {
    // either this function is not vectorized or it ends up with a 1x1 dimension
    return errorHandlingCompute(descr, context, args, argDefinitions);
  }

  // Reused across every vectorized cell to avoid allocating a new args array per call.
  const argsBuffer: Arg[] = new Array(args.length);

  // Resolve each arg's access pattern once, outside the inner loop.
  type ArgGetter = (i: number, j: number) => Arg;
  const argGetters: ArgGetter[] = [];
  for (let k = 0; k < args.length; k++) {
    const arg = args[k];
    switch (vectorArgsType?.[k]) {
      case "matrix": {
        argGetters.push((i, j) => arg![i][j]);
        break;
      }
      case "horizontal": {
        argGetters.push((i) => arg![i][0]);
        break;
      }
      case "vertical": {
        argGetters.push((_i, j) => arg![0][j]);
        break;
      }
      case undefined:
        argsBuffer[k] = arg;
        break;
    }
  }
  const nbVectorized = vectorizedArgsIndices.length;

  const result: Matrix<FunctionResultObject> = new Array(countVectorizedCol);
  for (let col = 0; col < countVectorizedCol; col++) {
    const column: FunctionResultObject[] = new Array(countVectorizedRow);
    result[col] = column;
    for (let row = 0; row < countVectorizedRow; row++) {
      if (col > vectorizedColLimit - 1 || row > vectorizedRowLimit - 1) {
        column[row] = new NotAvailableError(
          _t("Array arguments to [[FUNCTION_NAME]] are of different size.")
        );
        continue;
      }
      for (let k = 0; k < nbVectorized; k++) {
        argsBuffer[vectorizedArgsIndices[k]] = argGetters[k](col, row);
      }
      const singleCellComputeResult = errorHandlingCompute(
        descr,
        context,
        argsBuffer,
        argDefinitions
      );
      // In the case where the user tries to vectorize arguments of an array formula, we will get an
      // array for every combination of the vectorized arguments, which will lead to a 3D matrix and
      // we won't be able to return the values.
      // In this case, we keep the first element of each spreading part, just as Excel does, and
      // create an array with these parts.
      // For exemple, we have MUNIT(x) that return an unitary matrix of x*x. If we use it with a
      // range, like MUNIT(A1:A2), we will get two unitary matrices (one for the value in A1 and one
      // for the value in A2). In this case, we will simply take the first value of each matrix and
      // return the array [First value of MUNIT(A1), First value of MUNIT(A2)].
      column[row] = isMatrix(singleCellComputeResult)
        ? singleCellComputeResult[0][0]
        : singleCellComputeResult;
    }
  }
  return result;
}

function errorHandlingCompute(
  descr: FunctionDescription,
  context: EvalContext,
  args: Arg[],
  argDefinitions: ArgDefinition[]
): Matrix<FunctionResultObject> | FunctionResultObject {
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    const argDefinition = argDefinitions[i];

    // Early exit if the argument is an error and the function does not accept errors.
    // We only check scalar arguments, not matrix arguments for performance reasons.
    // Casting helpers are responsible for handling errors in matrix arguments.
    if (!argDefinition.acceptErrors && !isMatrix(arg) && isEvaluationError(arg?.value)) {
      return arg;
    }
  }
  try {
    const computeFormula = descr.compute || descr.computeArray;
    let result: FunctionResultObject | Matrix<FunctionResultObject>;
    switch (args.length) {
      case 1:
        result = computeFormula.call(context, args[0]);
        break;
      case 2:
        result = computeFormula.call(context, args[0], args[1]);
        break;
      case 3:
        result = computeFormula.call(context, args[0], args[1], args[2]);
        break;
      default:
        // fallback to a generic apply for functions with more than 3 arguments
        result = computeFormula.apply(context, args);
    }
    return result;
  } catch (e) {
    return handleError(e, descr.name);
  }
}

export function getFunctionArgDefinitions(
  descr: FunctionDescription,
  argCount: number
): ArgDefinition[] {
  const argDefs: ArgDefinition[] = [];
  const argsToFocus = argTargeting(descr, argCount);
  for (let i = 0; i < argCount; i++) {
    argDefs.push(descr.args[argsToFocus[i].index]);
  }
  return argDefs;
}

export function createComputeFunction(
  descr: FunctionDescription,
  argCount: number,
  vectorizedArgsIndices?: number[]
): ComputeFunction {
  const functionName = descr.name;
  const argsToFocus = argTargeting(descr, argCount);
  const argDefinitions: ArgDefinition[] = new Array(argCount);
  for (let i = 0; i < argCount; i++) {
    const def = descr.args[argsToFocus[i].index];
    argDefinitions[i] = def;
  }
  function computeFn(evalContext: EvalContext, ...args: Arg[]) {
    let start = 0;
    if (evalContext.__timingEntries) {
      start = performance.now();
    }
    let result: FunctionResultObject | Matrix<FunctionResultObject>;

    if (vectorizedArgsIndices !== undefined && vectorizedArgsIndices.length > 0) {
      result = replaceErrorPlaceholderInResult(
        applyVectorization(evalContext, descr, args, argDefinitions, vectorizedArgsIndices),
        descr
      );
    } else {
      result = replaceErrorPlaceholderInResult(
        errorHandlingCompute(descr, evalContext, args, argDefinitions),
        descr
      );
    }

    if (evalContext.__timingEntries && evalContext.__originCellPosition) {
      const end = performance.now();
      evalContext.__timingEntries.push({
        functionName,
        position: evalContext.__originCellPosition,
        time: end - start,
      });
    }
    return result;
  }

  return computeFn;
}

function replaceErrorPlaceholderInResult(
  result: FunctionResultObject | Matrix<FunctionResultObject>,
  descr: FunctionDescription
): FunctionResultObject | Matrix<FunctionResultObject> {
  if (!isMatrix(result)) {
    replaceFunctionNamePlaceholder(result, descr.name);
  } else {
    matrixForEach(result, (result) => replaceFunctionNamePlaceholder(result, descr.name));
  }
  return result;
}

export function handleError(e: unknown, functionName: string): FunctionResultObject {
  // the error could be a user error (instance of EvaluationError)
  // or a javascript error (instance of Error)
  // we don't want block the user with an implementation error
  // so we fallback to a generic error
  if (hasStringValue(e) && isEvaluationError(e.value)) {
    if (hasStringMessage(e)) {
      replaceFunctionNamePlaceholder(e, functionName);
    }
    return e;
  }
  console.error(e);
  return new EvaluationError(
    implementationErrorMessage + (hasStringMessage(e) ? " " + e.message : "")
  );
}

function hasStringValue(obj: unknown): obj is { value: string } {
  return (
    (obj as { value: string })?.value !== undefined &&
    typeof (obj as { value: string }).value === "string"
  );
}

function replaceFunctionNamePlaceholder(
  functionResult: FunctionResultObject,
  functionName: string
) {
  // for performance reasons: change in place and only if needed
  if (functionResult.message?.includes("[[FUNCTION_NAME]]")) {
    functionResult.message = functionResult.message.replace("[[FUNCTION_NAME]]", functionName);
  }
}

export const implementationErrorMessage = _t(
  "An unexpected error occurred. Submit a support ticket at odoo.com/help."
);

function hasStringMessage(obj: unknown): obj is { message: string } {
  return (
    (obj as { message: string })?.message !== undefined &&
    typeof (obj as { message: string }).message === "string"
  );
}
