/**
 * MVEL 2.0
 * Copyright (C) 2007 The Codehaus
 * Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mvel2.util;

import org.mvel2.CompileException;
import org.mvel2.Operator;
import org.mvel2.ParserContext;
import org.mvel2.ast.*;
import org.mvel2.compiler.*;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.integration.impl.ClassImportResolverFactory;

import java.util.LinkedHashMap;
import java.util.Map;

import static org.mvel2.Operator.PTABLE;
import static org.mvel2.compiler.AbstractParser.getCurrentThreadParserContext;
import static org.mvel2.util.ASTBinaryTree.buildTree;
import static org.mvel2.util.ParseTools.__resolveType;
import static org.mvel2.util.ParseTools.boxPrimitive;

public class CompilerTools {
  /**
   * Finalize the payload, by reducing any stack-based-operations to dedicated nodes where possible.
   *
   * @param astLinkedList          - AST to be optimized.
   * @param secondPassOptimization - perform a second pass optimization to optimize boolean expressions.
   * @param pCtx                    - The parser context
   * @return optimized AST
   */
  public static ASTLinkedList finalizePayload(ASTLinkedList astLinkedList, boolean secondPassOptimization, ParserContext pCtx) {
    ASTLinkedList optimizedAst = new ASTLinkedList();
    ASTNode tk, tkOp, tkOp2;

    /**
     * Re-process the AST and optimize it.
     */
    while (astLinkedList.hasMoreNodes()) {
      if ((tk = astLinkedList.nextNode()).getFields() == -1) {
        optimizedAst.addTokenNode(tk);
      }
      else if (astLinkedList.hasMoreNodes()) {
        if ((tkOp = astLinkedList.nextNode()).getFields() == -1) {
          optimizedAst.addTokenNode(tk, tkOp);
        }
        else if (tkOp.isOperator() && tkOp.getOperator() < 21) {
          int op = tkOp.getOperator();
          int op2;

          if (op == -1) {
            throw new CompileException("illegal use of operator: " + tkOp.getName(), tkOp.getExpr(), tk.getStart());
          }


          ASTNode tk2 = astLinkedList.nextNode();
          BinaryOperation bo;

          if (tk.getEgressType() == Integer.class && tk2.getEgressType() == Integer.class) {
            bo = boOptimize(op, tk, tk2, pCtx);
          }
          else {
            /**
             * Let's see if we can simply the expression more.
             */
            bo = null;

            boolean inv = tkOp.isOperator(Operator.SUB);
            boolean reduc = isReductionOpportunity(tkOp, tk2);
            boolean p_inv = false;

            while (reduc) {
              ASTNode oper = astLinkedList.nextNode();
              ASTNode rightNode = astLinkedList.nextNode();

              if (rightNode == null) break;

              Object val = new BinaryOperation(oper.getOperator(), inv ?
                  new LiteralNode(signNumber(tk2.getLiteralValue()), pCtx) : tk2, rightNode, pCtx)
                  .getReducedValueAccelerated(null, null, null);

              if (!astLinkedList.hasMoreNodes() && BlankLiteral.INSTANCE.equals(val)) {
                optimizedAst.addTokenNode(tk);
                continue;
              }

              reduc = astLinkedList.hasMoreNodes()
                  && (reducacbleOperator(astLinkedList.peekNode().getOperator()))
                  && astLinkedList.peekNext().isLiteral();

              if (inv) p_inv = true;
              inv = false;

              if (!reduc) {
                bo = new BinaryOperation(tkOp.getOperator(), tk, new LiteralNode(p_inv ? signNumber(val) : val, pCtx), pCtx);
              }
              else {
                tk2 = new LiteralNode(val, pCtx);
              }
            }

            if (bo == null)
              bo = new BinaryOperation(op, tk, tk2, pCtx);
          }

          tkOp2 = null;

          /**
           * If we have a chain of math/comparitive operators then we fill them into the tree
           * right here.
           */
          while (astLinkedList.hasMoreNodes() && (tkOp2 = astLinkedList.nextNode()).isOperator()
              && tkOp2.getFields() != -1 && (op2 = tkOp2.getOperator()) != -1 && op2 < 21) {

            if (PTABLE[op2] > PTABLE[op]) {
              //       bo.setRightMost(new BinaryOperation(op2, bo.getRightMost(), astLinkedList.nextNode(), pCtx));
              bo.setRightMost(boOptimize(op2, bo.getRightMost(), astLinkedList.nextNode(), pCtx));
            }
            else if (bo.getOperation() != op2 && PTABLE[op] == PTABLE[op2]) {
              if (PTABLE[bo.getOperation()] == PTABLE[op2]) {
                //     bo = new BinaryOperation(op2, bo, astLinkedList.nextNode(), pCtx);
                bo = boOptimize(op2, bo, astLinkedList.nextNode(), pCtx);
              }
              else {
                tk2 = astLinkedList.nextNode();

                if (isIntOptimizationviolation(bo, tk2)) {
                  bo = new BinaryOperation(bo.getOperation(), bo.getLeft(), bo.getRight(), pCtx);
                }

                bo.setRight(new BinaryOperation(op2, bo.getRight(), tk2, pCtx));
              }
            }
            else if (PTABLE[bo.getOperation()] >= PTABLE[op2]) {
              bo = new BinaryOperation(op2, bo, astLinkedList.nextNode(), pCtx);
            }
            else {
              tk2 = astLinkedList.nextNode();

              if (isIntOptimizationviolation(bo, tk2)) {
                bo = new BinaryOperation(bo.getOperation(), bo.getLeft(), bo.getRight(), pCtx);
              }

              bo.setRight(new BinaryOperation(op2, bo.getRight(), tk2, pCtx));
            }

            op = op2;
            tkOp = tkOp2;
          }


          if (tkOp2 != null && tkOp2 != tkOp) {
            optimizeOperator(tkOp2.getOperator(), bo, tkOp2, astLinkedList, optimizedAst, pCtx);
          }
          else {
            optimizedAst.addTokenNode(bo);
          }
        }
        else if (tkOp.isOperator()) {
          optimizeOperator(tkOp.getOperator(), tk, tkOp, astLinkedList, optimizedAst, pCtx);
        }
        else if (!tkOp.isAssignment() && !tkOp.isOperator() && tk.getLiteralValue() instanceof Class) {
          optimizedAst.addTokenNode(new DeclTypedVarNode(tkOp.getName(), tkOp.getExpr(), tkOp.getStart(), tk.getOffset(), (Class) tk.getLiteralValue(), 0, pCtx));
        }
        else if (tkOp.isAssignment() && tk.getLiteralValue() instanceof Class) {
          tk.discard();
          optimizedAst.addTokenNode(tkOp);
        }
        else if (astLinkedList.hasMoreNodes() && tkOp.getLiteralValue() instanceof Class
            && astLinkedList.peekNode().isAssignment()) {
          tkOp.discard();
          optimizedAst.addTokenNode(tk, astLinkedList.nextNode());
        }
        else {
          astLinkedList.back();
          optimizedAst.addTokenNode(tk);
        }
      }
      else {
        optimizedAst.addTokenNode(tk);
      }
    }

    if (secondPassOptimization) {
      /**
       * Perform a second pass optimization for boolean conditions.
       */
      (astLinkedList = optimizedAst).reset();
      optimizedAst = new ASTLinkedList();

      while (astLinkedList.hasMoreNodes()) {
        if ((tk = astLinkedList.nextNode()).getFields() == -1) {
          optimizedAst.addTokenNode(tk);
        }
        else if (astLinkedList.hasMoreNodes()) {
          if ((tkOp = astLinkedList.nextNode()).getFields() == -1) {
            optimizedAst.addTokenNode(tk, tkOp);
          }
          else if (tkOp.isOperator()
              && (tkOp.getOperator() == Operator.AND || tkOp.getOperator() == Operator.OR)) {

            tkOp2 = null;
            BooleanNode bool;

            if (tkOp.getOperator() == Operator.AND) {
              bool = new And(tk, astLinkedList.nextNode(), pCtx.isStrongTyping(), pCtx);
            }
            else {
              bool = new Or(tk, astLinkedList.nextNode(), pCtx.isStrongTyping(), pCtx);
            }

            while (astLinkedList.hasMoreNodes() && (tkOp2 = astLinkedList.nextNode()).isOperator()
                && (tkOp2.isOperator(Operator.AND) || tkOp2.isOperator(Operator.OR))) {

              if ((tkOp = tkOp2).getOperator() == Operator.AND) {
                bool.setRightMost(new And(bool.getRightMost(), astLinkedList.nextNode(), pCtx.isStrongTyping(), pCtx));
              }
              else {
                bool = new Or(bool, astLinkedList.nextNode(), pCtx.isStrongTyping(), pCtx);
              }

            }

            optimizedAst.addTokenNode(bool);

            if (tkOp2 != null && tkOp2 != tkOp) {
              optimizedAst.addTokenNode(tkOp2);
            }
          }
          else {
            optimizedAst.addTokenNode(tk, tkOp);
          }
        }
        else {
          optimizedAst.addTokenNode(tk);
        }
      }
    }

    return optimizedAst;
  }

  private static BinaryOperation boOptimize(int op, ASTNode tk, ASTNode tk2, ParserContext pCtx) {
    if (tk.getEgressType() == Integer.class && tk2.getEgressType() == Integer.class) {
      switch (op) {
        case Operator.ADD:
          return new IntAdd(tk, tk2, pCtx);

        case Operator.SUB:
          return new IntSub(tk, tk2, pCtx);

        case Operator.MULT:
          return new IntMult(tk, tk2, pCtx);

        case Operator.DIV:
          return new IntDiv(tk, tk2, pCtx);

        default:
          return new BinaryOperation(op, tk, tk2, pCtx);
      }
    }
    else {
      return new BinaryOperation(op, tk, tk2, pCtx);
    }
  }

  private static boolean isReductionOpportunity(ASTNode oper, ASTNode node) {
    ASTNode n = node;
    return (n != null && n.isLiteral()
        && (n = n.nextASTNode) != null && reducacbleOperator(n.getOperator())
        && PTABLE[oper.getOperator()] <= PTABLE[n.getOperator()]
        && (n = n.nextASTNode) != null && n.isLiteral() && n.getLiteralValue() instanceof Number);
  }

  private static boolean reducacbleOperator(int oper) {
    switch (oper) {
      case Operator.ADD:
      case Operator.SUB:
        return true;

    }
    return false;
  }

  private static void optimizeOperator(int operator, ASTNode tk, ASTNode tkOp,
                                       ASTLinkedList astLinkedList,
                                       ASTLinkedList optimizedAst,
                                       ParserContext pCtx) {
    switch (operator) {
      case Operator.REGEX:
        optimizedAst.addTokenNode(new RegExMatchNode(tk, astLinkedList.nextNode(), pCtx));
        break;
      case Operator.CONTAINS:
        optimizedAst.addTokenNode(new Contains(tk, astLinkedList.nextNode(), pCtx));
        break;
      case Operator.INSTANCEOF:
        optimizedAst.addTokenNode(new Instance(tk, astLinkedList.nextNode(), pCtx));
        break;
      case Operator.CONVERTABLE_TO:
        optimizedAst.addTokenNode((new Convertable(tk, astLinkedList.nextNode(), pCtx)));
        break;
      case Operator.SIMILARITY:
        optimizedAst.addTokenNode(new Strsim(tk, astLinkedList.nextNode(), pCtx));
        break;
      case Operator.SOUNDEX:
        optimizedAst.addTokenNode(new Soundslike(tk, astLinkedList.nextNode(), pCtx));
        break;

      default:
        optimizedAst.addTokenNode(tk, tkOp);
    }
  }

  private static boolean isIntOptimizationviolation(BooleanNode bn, ASTNode bn2) {
    return (bn instanceof IntOptimized && bn2.getEgressType() != Integer.class);
  }

  public static Class getReturnType(ASTIterator input, boolean strongTyping) {
    ASTNode begin = input.firstNode();
    if (begin == null) return Object.class;
    if (input.size() == 1) return begin.getEgressType();
    return buildTree(input).getReturnType(strongTyping);
  }

  /**
   * Returns an ordered Map of all functions declared within an compiled script.
   *
   * @param compile
   * @return - ordered Map
   */
  public static Map<String, Function> extractAllDeclaredFunctions(CompiledExpression compile) {
    Map<String, Function> allFunctions = new LinkedHashMap<String, Function>();
    ASTIterator instructions = new ASTLinkedList(compile.getFirstNode());

    ASTNode n;
    while (instructions.hasMoreNodes()) {
      if ((n = instructions.nextNode()) instanceof Function) {
        allFunctions.put(n.getName(), (Function) n);
      }
    }

    return allFunctions;
  }

  public static void expectType(Accessor expression, Class type, boolean compileMode) {
    Class retType = expression.getKnownEgressType();
    if (compileMode) {
      if ((retType == null || !boxPrimitive(type).isAssignableFrom(boxPrimitive(retType))) && (!Object.class.equals(retType)
          || getCurrentThreadParserContext().isStrictTypeEnforcement())) {
        throw new CompileException("was expecting type: " + type.getName() + "; but found type: "
            + (retType != null ? retType.getName() : "<Unknown>"), new char[0], 0);
      }
    }
    else if (retType == null || !Object.class.equals(retType) && !boxPrimitive(type).isAssignableFrom(boxPrimitive(retType))) {
      throw new CompileException("was expecting type: " + type.getName() + "; but found type: "
          + (retType != null ? retType.getName() : "<Unknown>"), new char[0], 0);
    }
  }

  public static void expectType(ASTNode node, Class type, boolean compileMode) {
    Class retType = boxPrimitive(node.getEgressType());
    if (compileMode) {
      if ((retType == null || !boxPrimitive(type).isAssignableFrom(retType)) && (!Object.class.equals(retType) &&
          (getCurrentThreadParserContext().isStrictTypeEnforcement()
              || getCurrentThreadParserContext().isStrictTypeEnforcement()))) {
        throw new CompileException("was expecting type: " + type.getName() + "; but found type: "
            + (retType != null ? retType.getName() : "<Unknown>"), new char[0], 0);
      }
    }
    else if (retType == null || !Object.class.equals(retType) && !boxPrimitive(type).isAssignableFrom(retType)) {
      throw new CompileException("was expecting type: " + type.getName() + "; but found type: "
          + (retType != null ? retType.getName() : "<Unknown>"), new char[0], 0);
    }
  }

  public static Class getReturnTypeFromOp(int operation, Class left, Class right) {
    switch (operation) {
      case Operator.LETHAN:
      case Operator.LTHAN:
      case Operator.GETHAN:
      case Operator.GTHAN:
      case Operator.EQUAL:
      case Operator.NEQUAL:
      case Operator.AND:
      case Operator.OR:
      case Operator.CONTAINS:
      case Operator.CONVERTABLE_TO:
        return Boolean.class;

      case Operator.ADD:
        if (left == String.class) return String.class;
      case Operator.SUB:
      case Operator.MULT:
      case Operator.POWER:
      case Operator.MOD:
      case Operator.DIV:
            if (left == Object.class || right == Object.class)
          return Object.class;
        else
          return __resolveType(boxPrimitive(left)) < __resolveType(boxPrimitive(right)) ? right : left;

      case Operator.BW_AND:
      case Operator.BW_OR:
      case Operator.BW_XOR:
      case Operator.BW_SHIFT_RIGHT:
      case Operator.BW_SHIFT_LEFT:
      case Operator.BW_USHIFT_LEFT:
      case Operator.BW_USHIFT_RIGHT:
      case Operator.BW_NOT:
        return Integer.class;

      case Operator.STR_APPEND:
        return String.class;
    }
    return null;
  }

  public static Accessor extractAccessor(ASTNode n) {
    if (n instanceof LiteralNode) return new ExecutableLiteral(n.getLiteralValue());
    else return new ExecutableAccessor(n, n.getEgressType());
  }


  public static Map<String, Object> getInjectedImports(VariableResolverFactory factory) {
    if (factory == null) return null;
    do {
      if (factory instanceof ClassImportResolverFactory) {
        return ((ClassImportResolverFactory) factory).getImportedClasses();
      }
    }
    while ((factory = factory.getNextFactory()) != null);

    return null;
  }

  public static Number signNumber(Object number) {
    if (number instanceof Integer) {
      return -((Integer) number);
    }
    else if (number instanceof Double) {
      return -((Double) number);
    }
    else if (number instanceof Float) {
      return -((Float) number);
    }
    else if (number instanceof Short) {
      return -((Short) number);
    }
    else {
      throw new CompileException("expected a numeric type but found: " + number.getClass().getName(), new char[0], 0);
    }
  }

}
