import { breakingSchemaChangesError, graphqlTypeNameKey } from "../constants";
import { Expression, Logs, Value } from "../types";
import nullThrows from "./nullThrows";
import { getExpressionEvaluationCountLogs, mergeLogs } from "./reductionLogs";

export const complexFormExpressionEvaluationError = `After evaluating your expression, the result was still in a complex form. ${breakingSchemaChangesError}`;

/**
 * Expression evaluation takes a fully reduced expression in "normal form" and
 * converts it to a JSON value. It returns this value with "logs" that track
 * all of the expressions we've needed to evaluate to compute this final value.
 */

// TODO: Pass in Expr<Expr> instead of Expr<Expr | null> to avoid null checks?
export default function evaluate(
  expression: Expression // Must be a fully reduced expression
): {
  value: Value;
  logs: Logs;
  shouldLogEvaluation: boolean;
} {
  const thisLogs = mergeLogs(
    expression.logs,
    getExpressionEvaluationCountLogs(expression)
  );

  switch (expression.type) {
    case "NoOpExpression":
      return { value: true, logs: thisLogs, shouldLogEvaluation: false };

    case "BooleanExpression":
    case "IntExpression":
    case "FloatExpression":
    case "StringExpression":
    case "RegexExpression":
    case "EnumExpression":
      return {
        value: expression.value,
        logs: thisLogs,
        shouldLogEvaluation: true,
      };

    case "ObjectExpression": {
      const fieldEvaluations = Object.fromEntries(
        Object.keys(expression.fields).map((fieldName) => [
          fieldName,
          evaluate(
            nullThrows(expression.fields[fieldName], "null object field")
          ),
        ])
      );

      const value: Value = Object.fromEntries([
        [graphqlTypeNameKey, expression.objectTypeName],
        ...Object.entries(fieldEvaluations).map(([fieldName, evaluation]) => [
          fieldName,
          evaluation.value,
        ]),
      ]);

      const logs = mergeLogs(
        thisLogs,
        ...Object.values(fieldEvaluations).map((evaluation) => evaluation.logs)
      );

      return { value, logs, shouldLogEvaluation: true };
    }

    case "ListExpression": {
      const itemEvaluations = expression.items.map((item) =>
        evaluate(nullThrows(item, "null list item"))
      );

      const value: Value = itemEvaluations.map(
        (evaluation) => evaluation.value
      );

      const logs = mergeLogs(
        thisLogs,
        ...itemEvaluations.map((evaluation) => evaluation.logs)
      );

      return { value, logs, shouldLogEvaluation: true };
    }

    case "GetFieldExpression":
    case "UpdateObjectExpression":
    case "SwitchExpression":
    case "EnumSwitchExpression":
    case "ComparisonExpression":
    case "ArithmeticExpression":
    case "RoundNumberExpression":
    case "StringifyNumberExpression":
    case "StringConcatExpression":
    case "GetUrlQueryParameterExpression":
    case "SplitExpression":
    case "LogEventExpression":
    case "FunctionExpression":
    case "VariableExpression":
    case "ApplicationExpression":
      throw new Error(complexFormExpressionEvaluationError);

    default: {
      const neverExpression: never = expression;
      throw new Error(
        `unexpected expression: ${JSON.stringify(neverExpression)}`
      );
    }
  }
}
