/**
 * 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;

import org.mvel2.ast.ASTNode;
import org.mvel2.ast.LineLabel;
import org.mvel2.compiler.CompiledExpression;
import org.mvel2.debug.Debugger;
import org.mvel2.debug.DebuggerContext;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.util.ExecutionStack;

import static org.mvel2.Operator.*;
import static org.mvel2.util.PropertyTools.isEmpty;

/**
 * This class contains the runtime for running compiled MVEL expressions.
 */
@SuppressWarnings({"CaughtExceptionImmediatelyRethrown"})
public class MVELRuntime {
  // public static final ImmutableDefaultFactory IMMUTABLE_DEFAULT_FACTORY = new ImmutableDefaultFactory();
  private static ThreadLocal<DebuggerContext> debuggerContext;


  /**
   * Main interpreter.
   *
   * @param debugger        Run in debug mode
   * @param expression      The compiled expression object
   * @param ctx             The root context object
   * @param variableFactory The variable factory to be injected
   * @return The resultant value
   * @see org.mvel2.MVEL
   */
  public static Object execute(boolean debugger, final CompiledExpression expression, final Object ctx,
                               VariableResolverFactory variableFactory) {

    Object v1, v2;
    ExecutionStack stk = new ExecutionStack();
    variableFactory.setTiltFlag(false);

    ASTNode tk = expression.getFirstNode();
    Integer operator;

    if (tk == null) return null;
    try {
      do {
        if (tk.fields == -1) {
          /**
           * This may seem silly and redundant, however, when an MVEL script recurses into a block
           * or substatement, a new runtime loop is entered.   Since the debugger state is not
           * passed through the AST, it is not possible to forward the state directly.  So when we
           * encounter a debugging symbol, we check the thread local to see if there is are registered
           * breakpoints.  If we find them, we assume that we are debugging.
           *
           * The consequence of this of course, is that it's not ideal to compileShared expressions with
           * debugging symbols which you plan to use in a production enviroment.
           */
          if (debugger || (debugger = hasDebuggerContext())) {
            try {
              debuggerContext.get().checkBreak((LineLabel) tk, variableFactory, expression);
            }
            catch (NullPointerException e) {
              // do nothing for now.  this isn't as calus as it seems.
            }
          }
          continue;
        }
        else if (stk.isEmpty()) {
          stk.push(tk.getReducedValueAccelerated(ctx, ctx, variableFactory));
        }

        if (variableFactory.tiltFlag()) {
          return stk.pop();
        }

        switch (operator = tk.getOperator()) {
          case RETURN:
            variableFactory.setTiltFlag(true);
            return stk.pop();
          //     throw new EndWithValue(stk.pop());

          case NOOP:
            continue;

          case TERNARY:
            if (!stk.popBoolean()) {
              //noinspection StatementWithEmptyBody
              while (tk.nextASTNode != null && !(tk = tk.nextASTNode).isOperator(TERNARY_ELSE)) ;
            }
            stk.clear();
            continue;

          case TERNARY_ELSE:
            return stk.pop();

          case END_OF_STMT:
            /**
             * If the program doesn't end here then we wipe anything off the stack that remains.
             * Althought it may seem like intuitive stack optimizations could be leveraged by
             * leaving hanging values on the stack,  trust me it's not a good idea.
             */
            if (tk.nextASTNode != null) {
              stk.clear();
            }

            continue;
        }

        stk.push(tk.nextASTNode.getReducedValueAccelerated(ctx, ctx, variableFactory), operator);

        try {
          while (stk.isReduceable()) {
            if ((Integer) stk.peek() == CHOR) {
              stk.pop();
              v1 = stk.pop();
              v2 = stk.pop();
              if (!isEmpty(v2) || !isEmpty(v1)) {
                stk.clear();
                stk.push(!isEmpty(v2) ? v2 : v1);
              }
              else stk.push(null);
            }
            else {
              stk.op();
            }
          }
        }
        catch (ClassCastException e) {
          throw new CompileException("syntax error or incomptable types", new char[0], 0, e);
        }
        catch (CompileException e) {
          throw e;
        }
        catch (Exception e) {
          throw new CompileException("failed to compileShared sub expression", new char[0], 0, e);
        }
      }
      while ((tk = tk.nextASTNode) != null);

      return stk.peek();
    }
    catch (NullPointerException e) {
      if (tk != null && tk.isOperator() && tk.nextASTNode != null) {
        throw new CompileException("incomplete statement: "
            + tk.getName() + " (possible use of reserved keyword as identifier: " + tk.getName() + ")", tk.getExpr(), tk.getStart());
      }
      else {
        throw e;
      }
    }
  }

  /**
   * Register a debugger breakpoint.
   *
   * @param source - the source file the breakpoint is registered in
   * @param line   - the line number of the breakpoint
   */
  public static void registerBreakpoint(String source, int line) {
    ensureDebuggerContext();
    debuggerContext.get().registerBreakpoint(source, line);
  }

  /**
   * Remove a specific breakpoint.
   *
   * @param source - the source file the breakpoint is registered in
   * @param line   - the line number of the breakpoint to be removed
   */
  public static void removeBreakpoint(String source, int line) {
    if (hasDebuggerContext()) {
      debuggerContext.get().removeBreakpoint(source, line);
    }
  }

  /**
   * Tests whether or not a debugger context exist.
   *
   * @return boolean
   */
  private static boolean hasDebuggerContext() {
    return debuggerContext != null && debuggerContext.get() != null;
  }

  /**
   * Ensures that debugger context exists.
   */
  private static void ensureDebuggerContext() {
    if (debuggerContext == null) debuggerContext = new ThreadLocal<DebuggerContext>();
    if (debuggerContext.get() == null) debuggerContext.set(new DebuggerContext());
  }

  /**
   * Reset all the currently registered breakpoints.
   */
  public static void clearAllBreakpoints() {
    if (hasDebuggerContext()) {
      debuggerContext.get().clearAllBreakpoints();
    }
  }

  /**
   * Tests whether or not breakpoints have been declared.
   *
   * @return boolean
   */
  public static boolean hasBreakpoints() {
    return hasDebuggerContext() && debuggerContext.get().hasBreakpoints();
  }

  /**
   * Sets the Debugger instance to handle breakpoints.   A debugger may only be registered once per thread.
   * Calling this method more than once will result in the second and subsequent calls to simply fail silently.
   * To re-register the Debugger, you must call {@link #resetDebugger}
   *
   * @param debugger - debugger instance
   */
  public static void setThreadDebugger(Debugger debugger) {
    ensureDebuggerContext();
    debuggerContext.get().setDebugger(debugger);
  }

  /**
   * Reset all information registered in the debugger, including the actual attached Debugger and registered
   * breakpoints.
   */
  public static void resetDebugger() {
    debuggerContext = null;
  }
}
