/*
 * Copyright 2008 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.jjs.impl;

import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JLongLiteral;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JPostfixOperation;
import com.google.gwt.dev.jjs.ast.JPrefixOperation;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JThisRef;
import com.google.gwt.dev.jjs.ast.JUnaryOperator;
import com.google.gwt.dev.jjs.ast.js.JMultiExpression;

/**
 * <p>
 * Replace problematic compound assignments with a sequence of simpler
 * operations, all of which are either simple assignments or are non-assigning
 * operations. When doing so, be careful that side effects happen exactly once
 * and that the order of any side effects is preserved. The choice of which
 * assignments to replace is made in subclasses; they must override the three
 * <code>shouldBreakUp()</code> methods.
 * </p>
 *
 * <p>
 * Note that because AST nodes are mutable, they cannot be reused in different
 * parts of the same tree. Instead, the node must be cloned before each
 * insertion into a tree other than the first.
 * </p>
 */
public abstract class CompoundAssignmentNormalizer {
  /**
   * Breaks apart certain complex assignments.
   */
  private class BreakupAssignOpsVisitor extends JModVisitorWithTemporaryVariableCreation {

    public BreakupAssignOpsVisitor(OptimizerContext optimizerCtx) {
      super(optimizerCtx);
    }

    /**
     * Replaces side effects in lvalue.
     */
    private class ReplaceSideEffectsInLvalue extends JModVisitor {

      private final JMultiExpression multi;

      ReplaceSideEffectsInLvalue(JMultiExpression multi) {
        this.multi = multi;
      }

      public JMultiExpression getMultiExpr() {
        return multi;
      }

      @Override
      public boolean visit(JArrayRef x, Context ctx) {
        JExpression newInstance = possiblyReplace(x.getInstance());
        JExpression newIndexExpr = possiblyReplace(x.getIndexExpr());
        if (newInstance != x.getInstance() || newIndexExpr != x.getIndexExpr()) {
          JArrayRef newExpr = new JArrayRef(x.getSourceInfo(), newInstance, newIndexExpr);
          ctx.replaceMe(newExpr);
        }
        return false;
      }

      @Override
      public boolean visit(JFieldRef x, Context ctx) {
        if (x.getInstance() != null) {
          JExpression newInstance = possiblyReplace(x.getInstance());
          if (newInstance != x.getInstance()) {
            JFieldRef newExpr =
                new JFieldRef(x.getSourceInfo(), newInstance, x.getField(), x.getEnclosingType());
            ctx.replaceMe(newExpr);
          }
        }
        return false;
      }

      @Override
      public boolean visit(JLocalRef x, Context ctx) {
        return false;
      }

      @Override
      public boolean visit(JParameterRef x, Context ctx) {
        return false;
      }

      @Override
      public boolean visit(JThisRef x, Context ctx) {
        return false;
      }

      private JExpression possiblyReplace(JExpression x) {
        if (!x.hasSideEffects()) {
          return x;
        }

        // Create a temp local
        JLocal tempLocal = createTempLocal(x.getSourceInfo(), x.getType(), TEMP_LOCAL_NAME);

        // Create an assignment for this temp and add it to multi.
        JLocalRef tempRef = tempLocal.makeRef(x.getSourceInfo());
        JBinaryOperation asg =
            new JBinaryOperation(x.getSourceInfo(), x.getType(), JBinaryOperator.ASG, tempRef, x);
        multi.addExpressions(asg);
        // Update me with the temp
        return cloner.cloneExpression(tempRef);
      }
    }

    @Override
    public void endVisit(JBinaryOperation x, Context ctx) {
      JBinaryOperator op = x.getOp();
      if (op.getNonAssignmentOf() == null) {
        return;
      }
      if (!shouldBreakUp(x)) {
        return;
      }

      /*
       * Convert to an assignment and binary operation. Since the left hand size
       * must be computed twice, we have to replace any left-hand side
       * expressions that could have side effects with temporaries, so that they
       * are only run once.
       */
      ReplaceSideEffectsInLvalue replacer =
          new ReplaceSideEffectsInLvalue(new JMultiExpression(x.getSourceInfo()));
      JExpression newLhs = replacer.accept(x.getLhs());

      JExpression operation =
          new JBinaryOperation(x.getSourceInfo(), newLhs.getType(), op.getNonAssignmentOf(),
              newLhs, x.getRhs());
      operation = modifyResultOperation((JBinaryOperation) operation);

      // newLhs is cloned below because it was used in operation
      JBinaryOperation asg =
          new JBinaryOperation(x.getSourceInfo(), newLhs.getType(), JBinaryOperator.ASG, cloner
              .cloneExpression(newLhs), operation);

      JMultiExpression multiExpr = replacer.getMultiExpr();
      if (multiExpr.isEmpty()) {
        // just use the split assignment expression
        ctx.replaceMe(asg);
      } else {
        // add the assignment as the last item in the multi
        multiExpr.addExpressions(asg);
        ctx.replaceMe(multiExpr);
      }
    }

    @Override
    public void endVisit(JPostfixOperation x, Context ctx) {
      JUnaryOperator op = x.getOp();
      if (!op.isModifying()) {
        return;
      }
      if (!shouldBreakUp(x)) {
        return;
      }

      // Convert into a comma operation, such as:
      // (t = x, x += 1, t)

      // First, replace the arg with a non-side-effect causing one.
      JMultiExpression multi = new JMultiExpression(x.getSourceInfo());
      ReplaceSideEffectsInLvalue replacer = new ReplaceSideEffectsInLvalue(multi);
      JExpression newArg = replacer.accept(x.getArg());

      JExpression expressionReturn = expressionToReturn(newArg);

      // Now generate the appropriate expressions.
      JLocal tempLocal =
          createTempLocal(x.getSourceInfo(), expressionReturn.getType(), TEMP_LOCAL_NAME);

      // t = x
      JLocalRef tempRef = tempLocal.makeRef(x.getSourceInfo());
      JBinaryOperation asg =
          new JBinaryOperation(x.getSourceInfo(), x.getType(), JBinaryOperator.ASG, tempRef,
              expressionReturn);
      multi.addExpressions(asg);

      // x += 1
      asg = createAsgOpFromUnary(newArg, op);
      // Break the resulting asg op before adding to multi.
      multi.addExpressions(accept(asg));

      // t
      tempRef = tempLocal.makeRef(x.getSourceInfo());
      multi.addExpressions(tempRef);

      ctx.replaceMe(multi);
    }

    @Override
    public void endVisit(JPrefixOperation x, Context ctx) {
      JUnaryOperator op = x.getOp();
      if (!op.isModifying()) {
        return;
      }
      if (!shouldBreakUp(x)) {
        return;
      }

      // Convert into the equivalent binary assignment operation, such as:
      // x += 1
      JBinaryOperation asg = createAsgOpFromUnary(x.getArg(), op);

      // Visit the result to break it up even more.
      ctx.replaceMe(accept(asg));
    }

    private JBinaryOperation createAsgOpFromUnary(JExpression arg, JUnaryOperator op) {
      JBinaryOperator newOp;
      if (op == JUnaryOperator.INC) {
        newOp = JBinaryOperator.ASG_ADD;
      } else if (op == JUnaryOperator.DEC) {
        newOp = JBinaryOperator.ASG_SUB;
      } else {
        throw new InternalCompilerException("Unexpected modifying unary operator: "
            + String.valueOf(op.getSymbol()));
      }

      JExpression one;
      if (arg.getType() == JPrimitiveType.LONG) {
        // use an explicit long, so that LongEmulationNormalizer does not get
        // confused
        one = JLongLiteral.get(1);
      } else {
        // int is safe to add to all other types
        one = JIntLiteral.get(1);
      }
      // arg is cloned below because the caller is allowed to use it somewhere
      JBinaryOperation asg =
          new JBinaryOperation(arg.getSourceInfo(), arg.getType(), newOp, cloner
              .cloneExpression(arg), one);
      return asg;
    }
  }

  private final CloneExpressionVisitor cloner = new CloneExpressionVisitor();

  public int accept(JNode node) {
    BreakupAssignOpsVisitor breaker =
        new BreakupAssignOpsVisitor(OptimizerContext.NULL_OPTIMIZATION_CONTEXT);
    breaker.accept(node);
    return breaker.getNumMods();
  }

  // Name to assign to temporaries. All temporaries are created with the same name, which is
  // not a problem as they are referred to by reference.
  // {@link GenerateJavaScriptAst.FixNameClashesVisitor} will resolve into unique names when
  // needed.
  private static final String TEMP_LOCAL_NAME = "$tmp";

  /**
   * Decide what expression to return when breaking up a compound assignment of
   * the form <code>lhs op= rhs</code>. By default the <code>lhs</code> is
   * returned.
   */
  protected JExpression expressionToReturn(JExpression lhs) {
    return lhs;
  }

  /**
   * Decide what expression to return when breaking up a compound assignment of
   * the form <code>lhs op= rhs</code>. The breakup creates an expression of the
   * form <code>lhs = lhs op rhs</code>, and the right hand side of the newly
   * created expression is passed to this method.
   */
  protected JExpression modifyResultOperation(JBinaryOperation op) {
    return op;
  }

  protected abstract boolean shouldBreakUp(JBinaryOperation x);

  protected abstract boolean shouldBreakUp(JPostfixOperation x);

  protected abstract boolean shouldBreakUp(JPrefixOperation x);
}
