package com.oguerreiro.resilient.expression;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.mvel2.CompileException;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;

import com.oguerreiro.resilient.error.ExpressionEvaluationException;
import com.oguerreiro.resilient.expression.error.ResilientExpressionFunctionException;

public class ExpressionUtils {
  public static Serializable compileExpression(String expression, ExpressionContext context) {
    try {
      // Create a ParserContext
      ParserContext pContext = new ParserContext();
      pContext.addInputs(context.getClassInputs());
      for (Entry<String, Class> importClass : context.getClassImports().entrySet()) {
        pContext.addImport(importClass.getKey(), importClass.getValue());
      }
      for (Entry<String, Method> importMethod : context.getMethodImports().entrySet()) {
        pContext.addImport(importMethod.getKey(), importMethod.getValue());
      }

      // Compile expression
      Serializable compiledExpression = MVEL.compileExpression(expression, pContext); // Check syntax
      return compiledExpression;
    } catch (CompileException e) {
      throw e;
    }
  }

  public static Object executeExpression(Serializable compiledExpression, ExpressionFunctions expressionFunctions,
      Map<String, Object> vars) {
    // Evaluate the compiled expression with the context
    Object result = null;
    if (expressionFunctions != null) {
      result = MVEL.executeExpression(compiledExpression, expressionFunctions, vars);
    } else {
      result = MVEL.executeExpression(compiledExpression, vars);
    }

    return result;
  }

  public static Object evalExpression(String expression, ExpressionContext context)
      throws ExpressionEvaluationException {
    try {
      Serializable compiledExpression = compileExpression(expression, context);
      Object result = executeExpression(compiledExpression, context.getExpressionFunctions(), context.getValueInputs());
      return result;
    } catch (Throwable t) {
      ResilientExpressionFunctionException functionException = null;

      //Catch any error. And then, evaluate the cause.
      if ((t instanceof ArithmeticException) && t.getMessage().contains("Division undefined")) {
        // This is a HACK to overcome problems with output formula configuration.
        // If a divide by ZERO is encoutered, just return ZERO to prevent errors.
        System.out.println("************************************************************************");
        System.out.println("CRITICAL FORMULA ERROR WITH EXPRESSION : " + expression);
        System.out.println("************************************************************************");

        return BigDecimal.ZERO;
      } else if (!(t instanceof ResilientExpressionFunctionException)) {
        // The MVEL will obfuscate the original exception, must drill-down to target
        Throwable cause = t.getCause();
        if (cause != null) {
          if (cause instanceof ResilientExpressionFunctionException) {
            functionException = (ResilientExpressionFunctionException) cause;
          } else if (cause instanceof InvocationTargetException) {
            InvocationTargetException invocationTargetException = (InvocationTargetException) cause;
            Throwable targetException = invocationTargetException.getTargetException();

            if (targetException != null && targetException instanceof ResilientExpressionFunctionException) {
              functionException = (ResilientExpressionFunctionException) targetException;
            }
          }
        }
      } else {
        functionException = (ResilientExpressionFunctionException) t;
      }

      if (functionException != null) {
        // This is a managed Resilient exception throwed by a custom function.
        throw new ExpressionEvaluationException(expression, functionException);
      } else {
        // Rethrow original exception
        throw new ExpressionEvaluationException(expression, t);
      }
    }

  }

  /**
   * Validations are limited. Validates syntax. And identifies the usage of Inputs, not provided by the context.
   * It DOESN'T checks the usage of invalid methods in classes (imports). This will be done in runtime only.
   * 
   * @param expression
   * @param expressionContext
   */
  public static void validateExpression(String expression, ExpressionContext expressionContext) {
    try {
      // Create a ParserContext for analysis
      ParserContext analysisContext = new ParserContext();
      // Add inputs
      analysisContext.addInputs(expressionContext.getClassInputs());
      // Add imports
      for (Entry<String, Class> importClass : expressionContext.getClassImports().entrySet()) {
        analysisContext.addImport(importClass.getKey(), importClass.getValue());
      }

      /*
       * Invoke MVEL.analysisCompile with an empty pContext. The analysisCompile method will :
       *  - Check syntax
       *  - Fill pContext with inputs[] and variables[]
       * With this strategy, I now can compare the analysisContext with the given expressionContext.getClassInputs(), that contains 
       * the inputs available for this expression.
       */
      MVEL.analysisCompile(expression, analysisContext);

      /*
       * Compare the used inputs with the inputs available in the give expressionContext
       */
      Set<String> availableInputsKeys = expressionContext.getClassInputs().keySet();
      Set<String> analysisInputsKeys = analysisContext.getInputs().keySet(); // Inputs the expression actually uses

      // Find used inputs that are NOT available in expresisonContext
      Set<String> missingInputs = new HashSet<>(analysisInputsKeys);
      missingInputs.removeAll(availableInputsKeys);

      if (!missingInputs.isEmpty()) {
        throw new RuntimeException("Missing inputs in the context: " + missingInputs);
      }
    } catch (CompileException e) {
      throw new RuntimeException("Expression is invalid: " + e.getMessage());
    } catch (SecurityException e) {
      throw new RuntimeException("Security failure in expression evaluation: " + e.getMessage());
    }
  }

  public static BigDecimal toBigDecimal(Object value, Integer scale) {
    BigDecimal bigDecimalValue = toBigDecimal(value);

    if (bigDecimalValue == null || scale == null)
      return bigDecimalValue;

    return bigDecimalValue.setScale(scale, RoundingMode.HALF_EVEN);
  }

  public static BigDecimal toBigDecimal(Object value) {
    if (value == null)
      return null;

    if (value.getClass().equals(BigDecimal.class)) {
      return (BigDecimal) value;
    } else if (value.getClass().equals(Integer.class)) {
      return new BigDecimal((Integer) value);
    } else if (value.getClass().equals(Long.class)) {
      return new BigDecimal((Long) value);
    } else if (value.getClass().equals(Double.class)) {
      return new BigDecimal((Double) value);
    } else {
      throw new IllegalArgumentException("Unexpected value: " + value.getClass().getTypeName());
    }
  }

}
