/*
 * Copyright 2009 Google Inc.
 *
 * 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 com.google.gwt.dev.js;

import com.google.gwt.dev.cfg.ConfigurationProperties;
import com.google.gwt.dev.cfg.PermutationProperties;
import com.google.gwt.dev.jjs.HasSourceInfo;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.ast.HasArguments;
import com.google.gwt.dev.js.ast.HasName;
import com.google.gwt.dev.js.ast.JsArrayAccess;
import com.google.gwt.dev.js.ast.JsArrayLiteral;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsBooleanLiteral;
import com.google.gwt.dev.js.ast.JsCatch;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFor;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsNew;
import com.google.gwt.dev.js.ast.JsNode;
import com.google.gwt.dev.js.ast.JsNullLiteral;
import com.google.gwt.dev.js.ast.JsNumberLiteral;
import com.google.gwt.dev.js.ast.JsPostfixOperation;
import com.google.gwt.dev.js.ast.JsPrefixOperation;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsPropertyInitializer;
import com.google.gwt.dev.js.ast.JsReturn;
import com.google.gwt.dev.js.ast.JsRootScope;
import com.google.gwt.dev.js.ast.JsStatement;
import com.google.gwt.dev.js.ast.JsStringLiteral;
import com.google.gwt.dev.js.ast.JsThrow;
import com.google.gwt.dev.js.ast.JsTry;
import com.google.gwt.dev.js.ast.JsUnaryOperation;
import com.google.gwt.dev.js.ast.JsUnaryOperator;
import com.google.gwt.dev.js.ast.JsVars;
import com.google.gwt.dev.js.ast.JsVars.JsVar;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.dev.js.ast.JsWhile;
import com.google.gwt.dev.util.collect.Lists;
import com.google.gwt.dev.util.collect.Maps;

import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * Emulates the JS stack in order to provide useful stack traces on browsers that
 * do not provide useful stack information.
 *
 * @see com.google.gwt.core.client.impl.StackTraceCreator
 */
public class JsStackEmulator {

  /**
   * Resets the global stack depth to the local stack index and top stack frame
   * after calls to Exceptions.toJava. This is created by
   * {@link EntryExitVisitor#visit(JsCatch, JsContext)}.
   */
  private class CatchStackReset extends JsModVisitor {

    /**
     * The local stackIndex variable in the function.
     */
    private final EntryExitVisitor eeVisitor;

    public CatchStackReset(EntryExitVisitor eeVisitor) {
      this.eeVisitor = eeVisitor;
    }

    @Override
    public void endVisit(JsExprStmt x, JsContext ctx) {
      if (!isExceptionWrappingCode(x)) {
        return;
      }

      // $stackDepth = stackIndex
      SourceInfo info = x.getSourceInfo();
      JsBinaryOperation reset = new JsBinaryOperation(info,
          JsBinaryOperator.ASG, stackDepth.makeRef(info),
          eeVisitor.stackIndexRef(info));

      ctx.insertAfter(reset.makeStmt());
    }
  }

  private boolean isExceptionWrappingCode(JsExprStmt x) {
    // Looking for e = Exceptions.toJava(e);
    JsExpression expr = x.getExpression();

    if (!(expr instanceof JsBinaryOperation)) {
      return false;
    }

    JsBinaryOperation op = (JsBinaryOperation) expr;
    if (!(op.getArg2() instanceof JsInvocation)) {
      return false;
    }

    JsInvocation i = (JsInvocation) op.getArg2();
    JsExpression q = i.getQualifier();
    if (!(q instanceof JsNameRef)) {
      return false;
    }

    JsName name = ((JsNameRef) q).getName();
    if (name == null) {
      return false;
    }

    // caughtFunction is the JsFunction translated from Exceptions.toJava
    if (name != wrapFunctionName) {
      return false;
    }
    return true;
  }

  /**
   * The EntryExitVisitor handles pushing and popping frames onto the emulated
   * stack. It will operate on exactly one JsFunction. The basic transformation
   * is to add a push operation at every function entry, and then a pop
   * operation for every statement that might be the final statement executed by
   * the function.
   * <p>
   * General stack depth entry/exit code:
   *
   * <pre>
   * function foo() {
   *   var stackIndex;
   *   $stack[stackIndex = ++$stackDepth] = foo;
   *
   *   ... do stuff ..
   *
   *   $stackDepth = stackIndex - 1;
   * }
   * </pre>
   * <p>
   * For more complicated control flows involving return statements in try
   * blocks with as associated finally block, it is necessary to introduce a
   * local variable to indicate if control flow is expected to terminate
   * normally at the end of the finally block:
   *
   * <pre>
   * var exitingEarly;
   * try {
   *   if (...) {
   *     return (exitingEarly = true, new Foo());
   *   }
   *   ...
   * } finally {
   *   ... existing finally code ..
   *   exitingEarly && $stackDepth = stackIndex - 1;
   * }
   * </pre>
   * A separate local variable is used for each try/finally nested within a
   * finally block.
   * <p>
   * Try statements without a catch block will have a catch block added to them
   * so that catch blocks are the only places where flow-control may jump to.
   * All catch blocks are altered so that the global $stackDepth variable is
   * reset to the local stack index value. This allows browser-native exceptions
   * to be created with the correct stack trace before the finally code is
   * executed with a correct stack depth.
   *
   * <pre>
   * try {
   *   foo();
   * } finally {
   *   bar();
   * }
   * </pre>
   *
   * becomes
   *
   * <pre>
   * try {
   *   foo();
   * } catch (e) {
   *   e = Exceptions.toJava(e);
   *   $stackDepth = stackIndex;
   *   throw e;
   * } finally {
   *   bar();
   * }
   * <p>
   * Note that there is no specific handling for explicit throw statements, as
   * the stack instrumentation must also handle browser-generated exceptions
   * (e.g. <code>null.a()</code>).
   */
  private class EntryExitVisitor extends JsModVisitor {

    /**
     * The name of a function-local variable to hold the invocation's slot in
     * the stack.
     */
    protected JsName stackIndex;

    private final JsFunction currentFunction;

    /**
     * Maps finally blocks to the local variable name which is used to indicate
     * if that finally block will exit the function early. This is a map and not
     * a single value because a finally block might be nested in another exit
     * block.
     */
    private Map<JsBlock, JsName> finallyBlocksToExitVariables = Maps.create();

    /**
     * This variable will indicate the finally block that contains the last
     * statement that will be executed if an unconditional flow control change
     * were to occur within the associated try block.
     */
    private JsBlock outerFinallyBlock;

    /**
     * Used if a return statement's expression could potentially trigger an
     * exception.
     */
    private JsName returnTemp;

    /**
     * Final cleanup for any new local variables that need to be created.
     */
    private List<JsVar> varsToAdd = Lists.create();

    public EntryExitVisitor(JsFunction currentFunction) {
      this.currentFunction = currentFunction;
    }

    /**
     * If the visitor is exiting the current function's block, add additional
     * local variables and the final stack-pop instructions.
     */
    @Override
    public void endVisit(JsBlock x, JsContext ctx) {
      if (x == currentFunction.getBody()) {

        // Add the entry code
        List<JsStatement> statements = x.getStatements();
        int idx = statements.isEmpty()
            || !(statements.get(0) instanceof JsVars) ? 0 : 1;

        // Add push and pop statements
        statements.add(idx, push(currentFunction));
        addPopAtEndOfBlock(x, false);

        // Add any needed variables
        JsVars vars;
        if (statements.get(0) instanceof JsVars) {
          vars = (JsVars) statements.get(0);
        } else {
          vars = new JsVars(currentFunction.getSourceInfo());
          statements.add(0, vars);
        }
        for (JsVar var : varsToAdd) {
          vars.add(var);
        }
      }
    }

    @Override
    public void endVisit(JsReturn x, JsContext ctx) {
      if (outerFinallyBlock != null) {
        // There is a finally block, so we need to set the early-exit flag
        JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(),
            JsBinaryOperator.ASG, earlyExitRef(outerFinallyBlock),
            JsBooleanLiteral.get(true));
        if (x.getExpr() == null) {
          if (ctx.canInsert()) {
            // exitingEarly = true; return;
            ctx.insertBefore(asg.makeStmt());
          } else {
            // {exitingEarly = true; return;}
            JsBlock block = new JsBlock(x.getSourceInfo());
            block.getStatements().add(asg.makeStmt());
            block.getStatements().add(x);
            ctx.replaceMe(block);
          }
        } else {
          // return (exitingEarly = true, expr);
          JsBinaryOperation op = new JsBinaryOperation(x.getSourceInfo(),
              JsBinaryOperator.COMMA, asg, x.getExpr());
          x.setExpr(op);
        }
      } else {
        if (x.getExpr() != null && x.getExpr().hasSideEffects()) {
          // temp = expr; pop(); return temp;
          SourceInfo info = x.getSourceInfo();
          JsBinaryOperation asg = new JsBinaryOperation(info,
              JsBinaryOperator.ASG, returnTempRef(info), x.getExpr());
          x.setExpr(returnTempRef(info));
          pop(x, asg, ctx);
        } else {
          // Otherwise, pop the stack frame
          pop(x, null, ctx);
        }
      }
    }

    /**
     * We want to look at unaltered versions of the catch block, so this is a
     * <code>visit<code> and not a <code>endVisit</code>.
     */
    @Override
    public boolean visit(JsCatch x, JsContext ctx) {
      // Reset the stack depth to the local index
      new CatchStackReset(this).accept(x);
      return true;
    }

    @Override
    public boolean visit(JsFunction x, JsContext ctx) {
      // Will be taken care of by the Bootstrap visitor
      return false;
    }

    @Override
    public boolean visit(JsTry x, JsContext ctx) {

      /*
       * Only the outermost finally block needs special treatment; try/finally
       * block within try blocks do not receive special treatment.
       */
      JsBlock finallyBlock = x.getFinallyBlock();
      if (finallyBlock != null && outerFinallyBlock == null) {
        outerFinallyBlock = finallyBlock;

        // Manual traversal
        accept(x.getTryBlock());

        if (x.getCatches().isEmpty()) {
          JsCatch c = makeSyntheticCatchBlock(x);
          x.getCatches().add(c);
        }
        assert x.getCatches().size() >= 1;
        acceptList(x.getCatches());

        // Exceptions in the finally block just exit the function
        assert outerFinallyBlock == finallyBlock;
        outerFinallyBlock = null;
        accept(finallyBlock);

        // Stack-pop instruction
        addPopAtEndOfBlock(finallyBlock, true);

        // Clean up entry after adding pop instruction
        finallyBlocksToExitVariables = Maps.remove(
            finallyBlocksToExitVariables, finallyBlock);
        return false;
      }

      // Normal visit
      return true;
    }

    /**
     * Create a reference to the function-local stack index variable, possibly
     * allocating it.
     */
    protected JsNameRef stackIndexRef(SourceInfo info) {
      if (stackIndex == null) {
        stackIndex = currentFunction.getScope().declareName(
            "JsStackEmulator_stackIndex", "stackIndex");

        JsVar var = new JsVar(info, stackIndex);
        varsToAdd = Lists.add(varsToAdd, var);
      }
      return stackIndex.makeRef(info);
    }

    /**
     * Code-gen function for generating the stack-pop statement at the end of a
     * block. A no-op if the last statement is a <code>throw</code> or
     * <code>return</code> statement, since it will have already caused a pop
     * statement to have been added.
     *
     * @param checkEarlyExit if <code>true</code>, generates
     *          <code>earlyExit && pop()</code>
     */
    private void addPopAtEndOfBlock(JsBlock x, boolean checkEarlyExit) {
      JsStatement last = x.getStatements().isEmpty() ? null
          : x.getStatements().get(x.getStatements().size() - 1);
      if (last instanceof JsReturn || last instanceof JsThrow) {
        /*
         * Don't need a pop after a throw or break statement. This is an
         * optimization for the common case of returning a value as the last
         * statement, but doesn't cover all flow-control cases.
         */
        return;
      } else if (checkEarlyExit && !finallyBlocksToExitVariables.containsKey(x)) {
        /*
         * No early-exit variable was ever allocated for this block. This means
         * that the variable can never be true, and thus the stack-popping
         * expression will never be executed.
         */
        return;
      }

      // pop()
      SourceInfo info = x.getSourceInfo();
      JsExpression op = pop(info);

      if (checkEarlyExit) {
        // earlyExit && pop()
        op = new JsBinaryOperation(info, JsBinaryOperator.AND, earlyExitRef(x),
            op);
      }

      x.getStatements().add(op.makeStmt());
    }

    /**
     * Generate a name reference to the early-exit variable for a given block,
     * possibly allocating a new variable.
     */
    private JsNameRef earlyExitRef(JsBlock x) {
      JsName earlyExitName = finallyBlocksToExitVariables.get(x);
      if (earlyExitName == null) {
        earlyExitName = currentFunction.getScope().declareName(
            "JsStackEmulator_exitingEarly"
                + finallyBlocksToExitVariables.size(), "exitingEarly");

        finallyBlocksToExitVariables = Maps.put(finallyBlocksToExitVariables,
            x, earlyExitName);
        JsVar var = new JsVar(x.getSourceInfo(), earlyExitName);
        varsToAdd = Lists.add(varsToAdd, var);
      }
      return earlyExitName.makeRef(x.getSourceInfo());
    }

    private JsCatch makeSyntheticCatchBlock(JsTry x) {
      /*
       * catch (e) { e = Exceptions.toJava(e); throw Exceptions.toJs(e); }
       */
      SourceInfo info = x.getSourceInfo();

      JsCatch c = new JsCatch(info, currentFunction.getScope(), "e");
      JsName paramName = c.getParameter().getName();

      // Exceptiobs.toJava(e)
      JsInvocation wrapCall = new JsInvocation(info, wrapFunctionName.makeRef(info),
          paramName.makeRef(info));

      // e = Exceptions.toJava(e)
      JsBinaryOperation asg = new JsBinaryOperation(info, JsBinaryOperator.ASG,
          paramName.makeRef(info), wrapCall);

      // Exceptions.toJs(e)
      JsInvocation unwrapCall =
          new JsInvocation(info, unwrapFunctionName.makeRef(info), paramName.makeRef(info));

      // throw Exceptions.toJs(e)
      JsThrow throwStatement = new JsThrow(info, unwrapCall);

      JsBlock body = new JsBlock(info);
      body.getStatements().add(asg.makeStmt());
      body.getStatements().add(throwStatement);
      c.setBody(body);
      return c;
    }

    /**
     * Pops the stack frame.
     */
    private void pop(JsStatement x, JsExpression expr, JsContext ctx) {
      // $stackDepth = stackIndex - 1
      SourceInfo info = x.getSourceInfo();

      JsExpression op = pop(info);

      if (ctx.canInsert()) {
        if (expr != null) {
          ctx.insertBefore(expr.makeStmt());
        }
        ctx.insertBefore(op.makeStmt());
      } else {
        JsBlock block = new JsBlock(info);
        if (expr != null) {
          block.getStatements().add(expr.makeStmt());
        }
        block.getStatements().add(op.makeStmt());
        block.getStatements().add(x);
        ctx.replaceMe(block);
      }
    }

    /**
     * Decrement the $stackDepth variable.
     */
    private JsExpression pop(SourceInfo info) {
      JsBinaryOperation sub = new JsBinaryOperation(info, JsBinaryOperator.SUB,
          stackIndexRef(info), new JsNumberLiteral(info, 1));
      JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG,
          stackDepth.makeRef(info), sub);
      return op;
    }

    /**
     * Create the function-entry code.
     */
    private JsStatement push(HasSourceInfo x) {
      SourceInfo info = x.getSourceInfo();

      JsNameRef stackRef = stack.makeRef(info);
      JsNameRef stackDepthRef = stackDepth.makeRef(info);
      JsExpression currentFunctionRef;
      if (currentFunction.getName() == null) {
        // Anonymous
        currentFunctionRef = JsNullLiteral.INSTANCE;
      } else {
        currentFunctionRef = currentFunction.getName().makeRef(info);
      }

      // ++stackDepth
      JsUnaryOperation inc = new JsPrefixOperation(info, JsUnaryOperator.INC,
          stackDepthRef);

      // stackIndex = ++stackDepth
      JsBinaryOperation stackIndexOp = new JsBinaryOperation(info,
          JsBinaryOperator.ASG, stackIndexRef(info), inc);

      // stack[stackIndex = ++stackDepth]
      JsArrayAccess access = new JsArrayAccess(info, stackRef, stackIndexOp);

      // stack[stackIndex = ++stackDepth] = currentFunction
      JsBinaryOperation op = new JsBinaryOperation(info, JsBinaryOperator.ASG,
          access, currentFunctionRef);

      return op.makeStmt();
    }

    private JsNameRef returnTempRef(SourceInfo info) {
      if (returnTemp == null) {
        returnTemp = currentFunction.getScope().declareName(
            "JsStackEmulator_returnTemp", "returnTemp");

        JsVar var = new JsVar(info, returnTemp);
        varsToAdd = Lists.add(varsToAdd, var);
      }
      return returnTemp.makeRef(info);
    }
  }

  /**
   * Creates a visitor to instrument each JsFunction in the jsProgram.
   */
  private class InstrumentAllFunctions extends JsVisitor {

    @Override
    public void endVisit(JsFunction x, JsContext ctx) {
      if (x.getBody().getStatements().isEmpty() ||
          !shouldInstrumentFunction(x)) {
        return;
      }

      if (recordLineNumbers) {
        (new LocationVisitor(x)).accept(x.getBody());
      } else {
        (new EntryExitVisitor(x)).accept(x.getBody());
      }
    }
  }

  /**
   * Extends EntryExit visitor to record location information in the AST. This
   * visitor will modify every JsExpression that can potentially result in a
   * change of flow control with file and line number data.
   * <p>
   * This simply generates code to set entries in the <code>$location</code>
   * stack, parallel to <code>$stack</code>:
   *
   * <pre>
   * ($location[stackIndex] = 'Foo.java:' + 42, expr);
   * </pre>
   *
   * Inclusion of file names is dependent on the value of the
   * {@link JsStackEmulator#recordFileNames} field.
   */
  private class LocationVisitor extends EntryExitVisitor {
    private String lastFile;
    private int lastLine;

    /**
     * Nodes in this set are used in a context that expects a reference, not
     * just an arbitrary expression. For example, <code>delete</code> takes a
     * reference. These are tracked because it wouldn't be safe to rewrite
     * <code>delete foo.bar</code> to <code>delete (line='123',foo).bar</code>.
     */
    private final Set<JsNode> nodesInRefContext = new HashSet<JsNode>();

    public LocationVisitor(JsFunction function) {
      super(function);
      clearLocation();
    }

    @Override
    public void endVisit(JsArrayAccess x, JsContext ctx) {
      record(x, ctx);
    }

    @Override
    public void endVisit(JsBinaryOperation x, JsContext ctx) {
      if (x.getOperator().isAssignment()) {
        record(x, ctx);
      }
    }

    @Override
    public void endVisit(JsInvocation x, JsContext ctx) {
      nodesInRefContext.remove(x.getQualifier());

      // Record the location as close as possible to calling the function.
      List<JsExpression> args = x.getArguments();
      if (!args.isEmpty()) {
        recordAfterLastArg(x);
        return;
      }

      JsNameRef qualifier = getPossibleMethod(x);
      if (qualifier == null) {
        record(x, ctx);
        return;
      }

      // This is a call using a qualified name like foo.bar()
      // Record the location after evaluating foo.
      // (Doing it after evaluating .bar causes lots of tests to fail.)

      SourceInfo locationToRecord = x.getSourceInfo();
      if (sameAsLastLocation(locationToRecord)) {
        return;
      }

      qualifier.setQualifier(recordAfter(qualifier.getQualifier(), locationToRecord));
      setLastLocation(locationToRecord);
      didChange = true;
    }

    @Override
    public void endVisit(JsNameRef x, JsContext ctx) {
      record(x, ctx);
    }

    @Override
    public void endVisit(JsNew x, JsContext ctx) {
      nodesInRefContext.remove(x.getConstructorExpression());

      // Record the location as close as possible to calling the constructor.

      if (!x.getArguments().isEmpty()) {
        recordAfterLastArg(x);
      } else {
        record(x, ctx);
      }
    }

    @Override
    public void endVisit(JsPostfixOperation x, JsContext ctx) {
      record(x, ctx);
    }

    @Override
    public void endVisit(JsPrefixOperation x, JsContext ctx) {
      record(x, ctx);
      nodesInRefContext.remove(x.getArg());
    }

    @Override
    public boolean visit(JsExprStmt x, JsContext ctx) {
      if (isExceptionWrappingCode(x)) {
        // Don't instrument exception wrapping code.
        return false;
      }
      return true;
    }
    /**
     * This is essentially a hacked-up version of JsFor.traverse to account for
     * flow control differing from visitation order. It resets lastFile and
     * lastLine before the condition and increment expressions in the for loop
     * so that location data will be recorded correctly.
     */
    @Override
    public boolean visit(JsFor x, JsContext ctx) {
      if (x.getInitExpr() != null) {
        x.setInitExpr(accept(x.getInitExpr()));
      } else if (x.getInitVars() != null) {
        x.setInitVars(accept(x.getInitVars()));
      }

      if (x.getCondition() != null) {
        clearLocation();
        x.setCondition(accept(x.getCondition()));
      }

      if (x.getIncrExpr() != null) {
        clearLocation();
        x.setIncrExpr(accept(x.getIncrExpr()));
      }

      accept(x.getBody());
      return false;
    }

    @Override
    public boolean visit(JsInvocation x, JsContext ctx) {
      nodesInRefContext.add(x.getQualifier());
      return true;
    }

    @Override
    public boolean visit(JsNew x, JsContext ctx) {
      nodesInRefContext.add(x.getConstructorExpression());
      return true;
    }

    @Override
    public boolean visit(JsPrefixOperation x, JsContext ctx) {
      if (x.getOperator() == JsUnaryOperator.DELETE
          || x.getOperator() == JsUnaryOperator.TYPEOF) {
        nodesInRefContext.add(x.getArg());
      }
      return true;
    }

    @Override
    public boolean visit(JsPropertyInitializer x, JsContext ctx) {
      // do not instrument left hand side of initializer.
      x.setValueExpr(accept(x.getValueExpr()));
      return false;
    }

    /**
     * Similar to JsFor, this resets the current location information before
     * evaluating the condition.
     */
    @Override
    public boolean visit(JsWhile x, JsContext ctx) {
      clearLocation();
      x.setCondition(accept(x.getCondition()));
      accept(x.getBody());
      return false;
    }

    /**
     * If the invocation might be a method call, return its NameRef.
     * Otherwise, return null.
     */
    private JsNameRef getPossibleMethod(JsInvocation x) {
      if (!(x.getQualifier() instanceof JsNameRef)) {
        return null;
      }
      JsNameRef ref = (JsNameRef) x.getQualifier();
      if (ref.getQualifier() == null) {
        return null;
      }
      return ref;
    }

    /**
     * Strips off the final name segment.
     */
    private String baseName(String fileName) {
      // Try the system path separator
      int lastIndex = fileName.lastIndexOf(File.separator);
      if (lastIndex == -1) {
        // Otherwise, try URL path separator
        lastIndex = fileName.lastIndexOf('/');
      }
      if (lastIndex != -1) {
        return fileName.substring(lastIndex + 1);
      } else {
        return fileName;
      }
    }

    /**
     * Given an expression and its context, record the location before
     * evaluating the expression, under the following conditions:
     *
     * - We are in a context where this is allowed.
     * - we have not previously called record() with the same location.
     *
     * Note that record() must be called in the same order that the expressions
     * will be evaluated at runtime. When this isn't true, {@link #clearLocation}
     * must be called first.
     *
     * Side-effect: updates lastLine and possibly lastFile.
     */
    private void record(JsExpression x, JsContext ctx) {

      if (ctx.isLvalue()) {
        // Assignments to comma expressions aren't legal
        return;
      } else if (nodesInRefContext.contains(x)) {
        // Don't modify references into non-references
        return;
      }

      SourceInfo locationToRecord = x.getSourceInfo();
      if (sameAsLastLocation(locationToRecord)) {
        return; // no change
      }

      JsBinaryOperation comma = new JsBinaryOperation(locationToRecord, JsBinaryOperator.COMMA,
          assignLocation(locationToRecord), x);
      ctx.replaceMe(comma);

      setLastLocation(locationToRecord);
    }

    /**
     * Records the position after evaluating the last argument.
     * This must be called after visiting the arguments.
     *
     * Side-effect: updates lastLine and possibly lastFile.
     */
    private <T extends JsExpression & HasArguments> void recordAfterLastArg(T x) {
      SourceInfo locationToRecord = x.getSourceInfo();
      if (sameAsLastLocation(locationToRecord)) {
        return; // no change
      }
      List<JsExpression> args = x.getArguments();
      JsExpression last = args.get(args.size() - 1);
      args.set(args.size() - 1, recordAfter(last, locationToRecord));
      setLastLocation(locationToRecord);
      didChange = true;
    }

    /**
     * Sets the last location recorded. (Used to avoid repeating the same location
     * in the next call to {@link #record}.)
     */
    private void setLastLocation(SourceInfo recordedLocation) {
      lastLine = recordedLocation.getStartLine();
      if (recordFileNames) {
        lastFile = recordedLocation.getFileName();
      }
    }

    /**
     * Ensures that the next call to record() will record the location.
     */
    private void clearLocation() {
      lastFile = "";
      lastLine = -1;
    }

    private boolean sameAsLastLocation(SourceInfo info) {
      return info.getStartLine() == lastLine
          && (!recordFileNames || info.getFileName().equals(lastFile));
    }

    /**
     * Wrap an expression so that we record a location after evaluating it.
     * (Requires a temporary variable.)
     */
    private JsExpression recordAfter(JsExpression x, SourceInfo locationToRecord) {
      // ($tmp = x, $locations[stackIndex] = "{fileName}:" + "{lineNumber}", $tmp)
      SourceInfo info = x.getSourceInfo();
      JsExpression setTmp = new JsBinaryOperation(info, JsBinaryOperator.ASG, tmp.makeRef(info), x);
      return new JsBinaryOperation(info, JsBinaryOperator.COMMA,
          new JsBinaryOperation(info, JsBinaryOperator.COMMA, setTmp,
              assignLocation(locationToRecord)),
          tmp.makeRef(info));
    }

    /**
     * Returns an expression that assigns the location.
     */
    private JsExpression assignLocation(SourceInfo info) {
      // If filenames are on:
      //   $locations[stackIndex] = "{fileName}:" + "{lineNumber}";
      // Otherwise:
      //   $locations[stackIndex] = "{lineNumber}";

      JsExpression location = new JsStringLiteral(info, String.valueOf(info.getStartLine()));
      if (recordFileNames) {
        // 'fileName:' + lineNumber
        JsStringLiteral stringLit = new JsStringLiteral(info, baseName(info.getFileName()) + ":");
        location = new JsBinaryOperation(info, JsBinaryOperator.ADD, stringLit, location);
      }

      JsArrayAccess access = new JsArrayAccess(info, lineNumbers.makeRef(info),
          stackIndexRef(info));
      return new JsBinaryOperation(info, JsBinaryOperator.ASG, access, location);
    }
  }

  /**
   * The StackTraceCreator code refers to identifiers defined in JsRootScope,
   * which are unobfuscatable. This visitor replaces references to those symbols
   * with references to our locally-defined, obfuscatable names.
   */
  private class ReplaceUnobfuscatableNames extends JsModVisitor {
    // See JsRootScope for the definition of these names
    private final JsName rootLineNumbers =
        JsRootScope.INSTANCE.findExistingUnobfuscatableName("$location");
    private final JsName rootStack =
        JsRootScope.INSTANCE.findExistingUnobfuscatableName("$stack");
    private final JsName rootStackDepth =
        JsRootScope.INSTANCE.findExistingUnobfuscatableName("$stackDepth");

    @Override
    public void endVisit(JsNameRef x, JsContext ctx) {
      JsName name = x.getName();
      JsNameRef newRef = null;

      if (name == rootStack) {
        newRef = stack.makeRef(x.getSourceInfo());
      } else if (name == rootStackDepth) {
        newRef = stackDepth.makeRef(x.getSourceInfo());
      } else if (name == rootLineNumbers) {
        newRef = lineNumbers.makeRef(x.getSourceInfo());
      }

      if (newRef == null) {
        return;
      }

      assert x.getQualifier() == null;
      ctx.replaceMe(newRef);
    }
  }

  /**
   * Corresponds to property compiler.stackMode in EmulateJsStack.gwt.xml
   * module.
   */
  public enum StackMode {
    STRIP, NATIVE, EMULATED
  }

  public static void exec(JProgram jprogram, JsProgram jsProgram, PermutationProperties properties,
      JavaToJavaScriptMap jjsmap) {
    if (getStackMode(properties) == StackMode.EMULATED) {
      new JsStackEmulator(jprogram, jsProgram, jjsmap, properties.getConfigurationProperties())
          .execImpl();
    }
  }

  public static StackMode getStackMode(PermutationProperties properties) {
    String value = properties.mustGetString("compiler.stackMode");
    return StackMode.valueOf(value.toUpperCase(Locale.ROOT));
  }

  private JsName wrapFunctionName;
  private JsName unwrapFunctionName;
  private JsName lineNumbers;
  private JProgram jprogram;
  private final JsProgram jsProgram;
  private JavaToJavaScriptMap jjsmap;
  private final boolean recordFileNames;
  private final boolean recordLineNumbers;
  private JsName stack;
  private JsName stackDepth;
  private JsName tmp;
  private JDeclaredType exceptionsClass;

  private JsStackEmulator(JProgram jprogram, JsProgram jsProgram,
      JavaToJavaScriptMap jjsmap, ConfigurationProperties config) {
    this.jprogram = jprogram;
    this.jsProgram = jsProgram;
    this.jjsmap = jjsmap;
    this.exceptionsClass = jprogram.getFromTypeMap("com.google.gwt.lang.Exceptions");

    recordFileNames = config.getBoolean("compiler.emulatedStack.recordFileNames", false);
    recordLineNumbers = recordFileNames ||
        config.getBoolean("compiler.emulatedStack.recordLineNumbers", false);
  }

  private boolean shouldInstrumentFunction(JsExpression functionExpression) {
    if (!(functionExpression instanceof HasName)) {
      return true;
    }
    /**
     * Do not instrument function in the Exceptions class (those are in involved in the
     * exception handling machinery) nor immortal codegen types as their code is executed
     * for setup and the stack emulation variables may have not been defined yet.
     */
    JMethod method = jjsmap.nameToMethod(((HasName) functionExpression).getName());
    return method == null || method.getEnclosingType() != exceptionsClass
        || jprogram.immortalCodeGenTypes.contains(method.getEnclosingType());
  }

  private void execImpl() {
    wrapFunctionName =
        JsUtils.getJsNameForMethod(jjsmap, jprogram, RuntimeConstants.EXCEPTIONS_TO_JAVA);
    unwrapFunctionName =
        JsUtils.getJsNameForMethod(jjsmap, jprogram, RuntimeConstants.EXCEPTIONS_TO_JS);
    if (wrapFunctionName == null) {
      // No exceptions caught? Weird, but possible.
      return;
    }
    assert unwrapFunctionName != null;
    initNames();
    makeVars();
    (new ReplaceUnobfuscatableNames()).accept(jsProgram);
    (new InstrumentAllFunctions()).accept(jsProgram);
  }

  private void initNames() {
    stack = jsProgram.getScope().declareName("$JsStackEmulator_stack", "$stack");
    stackDepth = jsProgram.getScope().declareName("$JsStackEmulator_stackDepth",
        "$stackDepth");
    lineNumbers = jsProgram.getScope().declareName("$JsStackEmulator_location",
        "$location");
    tmp = jsProgram.getScope().declareName("$JsStackEmulator_tmp", "$tmp");
  }

  private void makeVars() {
    SourceInfo info = jsProgram.createSourceInfoSynthetic(getClass());
    JsVar stackVar = new JsVar(info, stack);
    stackVar.setInitExpr(new JsArrayLiteral(info));
    JsVar stackDepthVar = new JsVar(info, stackDepth);
    stackDepthVar.setInitExpr(new JsNumberLiteral(info, (-1)));
    JsVar lineNumbersVar = new JsVar(info, lineNumbers);
    lineNumbersVar.setInitExpr(new JsArrayLiteral(info));
    JsVar tmpVar = new JsVar(info,tmp);

    JsVars vars;
    JsStatement first = jsProgram.getGlobalBlock().getStatements().get(0);
    if (first instanceof JsVars) {
      vars = (JsVars) first;
    } else {
      vars = new JsVars(info);
      jsProgram.getGlobalBlock().getStatements().add(0, vars);
    }
    vars.add(stackVar);
    vars.add(stackDepthVar);
    vars.add(lineNumbersVar);
    vars.add(tmpVar);
  }
}
