/*
 * 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.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayLength;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBinaryOperator;
import com.google.gwt.dev.jjs.ast.JBlock;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JForStatement;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
import com.google.gwt.dev.jjs.ast.JLocal;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JParameterRef;
import com.google.gwt.dev.jjs.ast.JPostfixOperation;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JUnaryOperation;
import com.google.gwt.dev.jjs.ast.JUnaryOperator;
import com.google.gwt.dev.jjs.ast.JVariableRef;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;

import java.util.Collections;

/**
 * Implements JavaScript varargs calling convention by rewriting varargs calls and adding a
 * prolog to varargs JsMethods.
 * <p>
 * At the calls sites, inline array creation is replaced by array literals, which in turn will be
 * unwrapped as individual parameters at generation time.
 * <p>
 * To implement varargs methods, we analyze the usage of the varargs parameter to determine whether
 * it can be accessed directly (with possibly an offset in the index) or it has to be copied.
 */
public class ImplementJsVarargs {
  /**
   * Analyzes a method body to check whether the varargs parameter can be used directly or not.
   * <p>
   * The arguments variable cannot be used directly if is referenced without indexing or accessing
   * its length, or if it is written to.
   */
  private class NeedsArgumentsCopyAnalyzer extends JVisitor {

    private VarargsProcessingResult result = VarargsProcessingResult.SIMPLE_ACCESS;
    private JParameter varargsParameter;
    private int varargsParameterIndex;

    private NeedsArgumentsCopyAnalyzer(JMethod method) {
      assert method.isJsMethodVarargs();
      this.varargsParameter = Iterables.getLast(method.getParams());
      this.varargsParameterIndex = method.getParams().size() - 1;
      if (varargsParameterIndex != 0) {
        upgradeResult(VarargsProcessingResult.OFFSET_ACCESS);
      }
    }

    @Override
    public void endVisit(JParameterRef x, Context ctx) {
      // Any reference that is not .length or indexed means that we need to copy the varargs array.
      if (isVarargsReference(x)) {
        upgradeResult(VarargsProcessingResult.GENERAL_ACCESS);
      }
    }

    @Override
    public boolean visit(JArrayLength x, Context ctx) {
      if (isVarargsReference(x.getInstance())) {
        // This is a safe reference.
        return false;
      }
      return true;
    }

    @Override
    public boolean visit(JArrayRef x, Context ctx) {
      if (isVarargsReference(x.getInstance())) {
        // This is a safe reference, so only check the index expression.
        accept(x.getIndexExpr());
        return false;
      }

      return true;
    }

    @Override
    public boolean visit(JBinaryOperation x, Context ctx) {
      if (isModifyingVarargs(x)) {
          // The varargs parameter is written to, so upgrade to copy.
          upgradeResult(VarargsProcessingResult.GENERAL_ACCESS);
        return false;
      }
      return true;
    }

    @Override
    public boolean visit(JUnaryOperation x, Context ctx) {
      if (isModifyingVarargs(x)) {
        // The varargs parameter is written to, so upgrade to copy.
        upgradeResult(VarargsProcessingResult.GENERAL_ACCESS);
        return false;
      }
      return true;
    }

    @Override
    public boolean visit(JMethodCall x, Context ctx) {
      // Allow
      if (x.getTarget().isJsMethodVarargs() && x.getArgs().size() == 1
          && isVarargsReference(x.getArgs().get(0))) {
        // The varargs parameter is passed directly and it is the only parameter, so if it is the
        // only parameter in the current method it can be passed directly.
        upgradeResult(VarargsProcessingResult.PASS_WHOLE);
        if (x.getInstance() != null) {
          accept(x.getInstance());
        }
        for (JExpression arg : x.getArgs().subList(1, x.getArgs().size())) {
          accept(arg);
        }
        return false;
      }
      return true;
    }

    private boolean isModifyingVarargs(JBinaryOperation x) {
      if (!x.getOp().isAssignment()) {
        return false;
      }
      if (!(x.getLhs() instanceof JArrayRef)) {
        return false;
      }
      JArrayRef arrayRef = (JArrayRef) x.getLhs();
      JExpression instance = arrayRef.getInstance();
      return isVarargsReference(instance);
    }

    private boolean isModifyingVarargs(JUnaryOperation x) {
      if (!x.getOp().isModifying()) {
        return false;
      }
      if (!(x.getArg() instanceof JArrayRef)) {
        return false;
      }
      JArrayRef arrayRef = (JArrayRef) x.getArg();
      JExpression instance = arrayRef.getInstance();
      return isVarargsReference(instance);
    }

    private boolean isVarargsReference(JExpression instance) {
      if (!(instance instanceof JParameterRef)) {
        return false;
      }

      return (((JParameterRef) instance).getTarget() == varargsParameter);
    }

    private void upgradeResult(VarargsProcessingResult upgradeTo) {
      result = VarargsProcessingResult.join(result, upgradeTo);
    }
  }

  // Defines the analysis lattice
  //                            SIMPLE_ACCESS   (only arguments[i] or arguments.length, no writing)
  //                               /        \
  //  (complete passing of        /          \
  //   arguments)           PASS_WHOLE  OFFSET_ACCESS   (Simple access but needs to offset index)
  //                              \         /
  //                               \       /
  //                             GENERAL_ACCESS
  //
  private enum VarargsProcessingResult {
    SIMPLE_ACCESS, PASS_WHOLE, OFFSET_ACCESS, GENERAL_ACCESS;

    private static VarargsProcessingResult join(
        VarargsProcessingResult thisResult, VarargsProcessingResult thatResult) {
      if (thisResult.ordinal() > thatResult.ordinal()) {
        VarargsProcessingResult swap = thisResult;
        thisResult = thatResult;
        thatResult = swap;
      }

      if ((thisResult == PASS_WHOLE &&  thatResult == OFFSET_ACCESS)) {
        return GENERAL_ACCESS;
      }
      return thatResult;
    }
  }

  private VarargsProcessingResult needsVarargsProcessing(JMethod method) {
    NeedsArgumentsCopyAnalyzer analyzer =  new NeedsArgumentsCopyAnalyzer(method);
    analyzer.accept(method);
    return analyzer.result;
  }

  private abstract class VarargsReplacer {
    abstract JExpression replace(JParameterRef expression);

    JExpression replace(JArrayRef expression) {
      return new JArrayRef(expression.getSourceInfo(),
          replace((JParameterRef)  expression.getInstance()),
          expression.getIndexExpr());
    }

    JExpression replace(JArrayLength expression) {
      return new JArrayLength(expression.getSourceInfo(),
          replace((JParameterRef)  expression.getInstance()));
    }
  }

  /**
   * Replaces varargs parameter accesses with accesses to the copy.
   */
  private class ReplaceVarargsVariable extends VarargsReplacer {
    private JLocal localVariable;
    ReplaceVarargsVariable(JLocal localVariable) {
      this.localVariable = localVariable;
    }

    @Override
    public JExpression replace(JParameterRef expression) {
      return localVariable.createRef(expression.getSourceInfo());
    }
  }

  /**
   * Fixes this indexing of vararg accesses.
   */
  private class ReindexAccess extends VarargsReplacer {
    private int varargsIndex;
    ReindexAccess(int varargsIndex) {
      this.varargsIndex = varargsIndex;
    }

    @Override
    public JExpression replace(JParameterRef expression) {
      return expression;
    }

    JExpression replace(JArrayRef expression) {
      SourceInfo sourceInfo = expression.getSourceInfo();
      return new JArrayRef(expression.getSourceInfo(),
          expression.getInstance(),
          new JBinaryOperation(sourceInfo, JPrimitiveType.INT, JBinaryOperator.ADD,
              expression.getIndexExpr(), new JIntLiteral(sourceInfo, varargsIndex)));
    }

    JExpression replace(JArrayLength expression) {
      SourceInfo sourceInfo = expression.getSourceInfo();
      return new JBinaryOperation(sourceInfo, JPrimitiveType.INT, JBinaryOperator.SUB,
              expression, new JIntLiteral(sourceInfo, varargsIndex));
    }
  }

  private class VarargsMethodNormalizer extends JModVisitor {
    private JParameter varargsParameter;
    private int varargsIndex;
    private VarargsReplacer replacer;
    private JLocal argumentsCopyVariable;

    private boolean needsVarArgsPrologue(JMethod x) {
      // Native methods in general do not have bodies except for constructors; no code is generated
      // from them, safe and convenient to skip here.
      return x.isJsNative() || !x.isJsMethodVarargs();
    }

    @Override
    public boolean visit(JMethod x, Context ctx) {
      if (needsVarArgsPrologue(x)) {
        return false;
      }
      varargsParameter = Iterables.getLast(x.getParams());
      varargsIndex = x.getParams().size() - 1;

      // JsVarargs parameter can be assumed not null in the implementing method
      varargsParameter.setType(varargsParameter.getType().strengthenToNonNull());

      argumentsCopyVariable = null;
      switch (needsVarargsProcessing(x)) {
        case GENERAL_ACCESS:
          argumentsCopyVariable = JProgram.createLocal(varargsParameter.getSourceInfo(),
              varargsParameter.getName(), varargsParameter.getType(), false,
              (JMethodBody) x.getBody());
          replacer = new ReplaceVarargsVariable(argumentsCopyVariable);
          return true;
        case OFFSET_ACCESS:
          replacer = new ReindexAccess(varargsIndex);
          return true;
       default:
          return false;
      }
    }

    @Override
    public void endVisit(JParameterRef x, Context ctx) {
      if (x.getTarget() == varargsParameter) {
        maybeReplace(x, replacer.replace(x), ctx);
      }
    }

    @Override
    public void endVisit(JArrayRef x, Context ctx) {
      if (x.getInstance() instanceof JParameterRef
          && ((JParameterRef) x.getInstance()).getTarget() == varargsParameter) {
        maybeReplace(x, replacer.replace(x), ctx);
      }
    }

    @Override
    public void endVisit(JArrayLength x, Context ctx) {
      if (x.getInstance() instanceof JParameterRef
          && ((JParameterRef) x.getInstance()).getTarget() == varargsParameter) {
        maybeReplace(x, replacer.replace(x), ctx);
      }
    }

    @Override
    public void endVisit(JMethod x, Context ctx) {
      if (needsVarArgsPrologue(x)) {
        return;
      }
      // rename the varargs variable to _arguments_.
      varargsParameter.setName("_arguments_");
    }

    private void maybeReplace(JExpression x, JExpression replacement, Context ctx) {
      if (replacement != x) {
        ctx.replaceMe(replacement);
      }
    }

    @Override
    public void endVisit(JMethodBody x, Context ctx) {
      if (argumentsCopyVariable == null) {
        return;
      }

      // Needs to populate the copy; add preamble.
      //
      // {
      //   <Type>[] args = new <Type>[arguments.length - offset];
      //   for (int $i = 0; $i < arguments.length - offset; i++) {
      //     args[i] = arguments[i + offset];
      //   }
      // }

      SourceInfo sourceInfo = varargsParameter.getSourceInfo();
      JBlock preamble = new JBlock(sourceInfo);
      JArrayType varargsArrayType = (JArrayType) varargsParameter.getType().getUnderlyingType();

      // (1) varargs_ = new VarArgsType[varargs.length - varArgsParameterIndex]
      JExpression lengthMinusVarargsIndex = varargsIndex == 0
          ? new JArrayLength(sourceInfo, varargsParameter.createRef(sourceInfo))
          : new JBinaryOperation(sourceInfo, JPrimitiveType.INT, JBinaryOperator.SUB,
              new JArrayLength(sourceInfo, varargsParameter.createRef(sourceInfo)),
              new JIntLiteral(sourceInfo, varargsIndex));
      JNewArray arrayVariable = JNewArray.createArrayWithDimensionExpressions(sourceInfo,
          varargsArrayType, Collections.singletonList(lengthMinusVarargsIndex));
      arrayVariable.getLeafTypeClassLiteral().setField(
          program.getClassLiteralField(varargsArrayType.getLeafType()));
      preamble.addStmt(new JDeclarationStatement(
          sourceInfo, argumentsCopyVariable.createRef(sourceInfo), arrayVariable));

      JLocal index = JProgram.createLocal(sourceInfo, "$i", JPrimitiveType.INT, false, x);

      // (2) (copy loop body)  varargs_[i] = varargs[i + varargsIndex];
      JExpression iPlusVarargsIndex = varargsIndex == 0 ? index.createRef(sourceInfo)
          : new JBinaryOperation(sourceInfo, JPrimitiveType.INT, JBinaryOperator.ADD,
              index.createRef(sourceInfo), new JIntLiteral(sourceInfo, varargsIndex));

      JBlock block = new JBlock(sourceInfo);
      block.addStmt(new JBinaryOperation(
          sourceInfo,
          varargsArrayType.getElementType(),
          JBinaryOperator.ASG,
          new JArrayRef(sourceInfo, replacer.replace(varargsParameter.createRef(sourceInfo)),
              index.createRef(sourceInfo)),
          new JArrayRef(sourceInfo, varargsParameter.createRef(sourceInfo), iPlusVarargsIndex))
              .makeStatement());
      // (3) for (int $i = 0 ; i < arguments.length - index; i++) {
      //       varargs_[i] = varargs[i + varargsIndex];
      //     }
      preamble.addStmt(new JForStatement(sourceInfo, Collections.<JStatement>singletonList(
          new JDeclarationStatement(sourceInfo, index.createRef(sourceInfo), JIntLiteral.ZERO)),
          new JBinaryOperation(sourceInfo, JPrimitiveType.INT,JBinaryOperator.LT,
                  index.createRef(sourceInfo),
              new CloneExpressionVisitor().cloneExpression(lengthMinusVarargsIndex)),
          new JPostfixOperation(sourceInfo, JUnaryOperator.INC, index.createRef(sourceInfo)),
          block));
      x.getStatements().add(0, preamble);
    }
  }

  // Normalizes JsVarargsCalls so that
  //  (1) inline new array expressions resulting from a "regular" varargs invocation are replaced
  //      by plain array literals.
  //  (2) side effecting instances in JsVarargs instance method calls with array calling convention
  //      are hoited into temporary variables.
  private class VarargsCallsNormalizer extends JModVisitor {
    private JMethodBody currentMethodBody;

    @Override
    public boolean visit(JMethodBody x, Context ctx) {
      currentMethodBody = x;
      return true;
    }

    @Override
    public void endVisit(JMethodBody x, Context ctx) {
      currentMethodBody = null;
    }

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      JMethod method = x.getTarget();
      if (!method.isJsMethodVarargs()) {
        return;
      }

      int varargIndex = method.getParams().size() - 1;
      JExpression varargArgument = x.getArgs().get(varargIndex);
      if (varargArgument instanceof JNewArray) {
        JNewArray varargArray = (JNewArray) varargArgument;
        if (varargArray.getInitializers() != null) {
          x.setArg(varargIndex, ArrayNormalizer.getInitializerArray(varargArray));
          madeChanges();
          return;
        }
      }

      SourceInfo varargsArgumentsourceInfo = varargArgument.getSourceInfo();
      if (varargArgument.getType().canBeNull()) {
        // varargsArgument => Array.ensureNotNull(varargsArgument)
        x.setArg(varargIndex, new JMethodCall(varargsArgumentsourceInfo, null,
            program.getIndexedMethod(RuntimeConstants.ARRAY_ENSURE_NOT_NULL), varargArgument));
      }

      // Passed as an array to varargs method will result in an apply call, in which case hoist the
      // qualifier to make sure it is only evaluated once.
      JExpression instance = x.getInstance();
      if (x.getTarget().needsDynamicDispatch() && !x.isStaticDispatchOnly() && instance != null
              && !(instance instanceof JVariableRef && !instance.hasSideEffects())) {
        // Move the potentially sideffecting qualifier to a temporary variable so that
        // the code generation for calls that need .apply don't need to hande the case.
        SourceInfo sourceInfo = x.getSourceInfo();
        JLocal tempInstance = JProgram.createLocal(sourceInfo, "$instance",
            instance.getType(), false, currentMethodBody);
        // (tempInstance = instance,
        //  tempInstance.method(pars);
        ctx.replaceMe(JjsUtils.createOptimizedMultiExpression(
            new JBinaryOperation(sourceInfo, instance.getType(),
                JBinaryOperator.ASG, tempInstance.createRef(sourceInfo), instance),
            new JMethodCall(x, tempInstance.createRef(sourceInfo), x.getArgs())));
      }
    }
  }

  public static void exec(JProgram program) {
    new ImplementJsVarargs(program).execImpl();
  }

  private final JProgram program;

  private ImplementJsVarargs(JProgram program) {
    this.program = program;
  }

  private void execImpl() {
    new VarargsMethodNormalizer().accept(program);
    new VarargsCallsNormalizer().accept(program);
  }
}
