/*
 * Copyright 2007 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.ast.Context;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JRunAsync;
import com.google.gwt.dev.jjs.ast.JType;

/**
 * Update polymorphic method calls to tighter bindings based on the type of the
 * qualifier. For a given polymorphic method call to a non-final target, see if
 * the static type of the qualifer would let us target an override instead.
 *
 * This is possible because the qualifier might have been tightened by
 * {@link com.google.gwt.dev.jjs.impl.TypeTightener}.
 *
 * For example, given the code:
 *
 * <pre>
 *   List foo = new ArrayList<String>();
 *   foo.add("bar");
 * </pre>
 *
 * The type of foo is tightened by TypeTightener from type List to be of type
 * ArrayList. This means that MethodCallTightener can analyze the polymorphic
 * call List.add() on foo and tighten it to the more specific ArrayList.add().
 */
public class MethodCallTightener {
  /**
   * Updates polymorphic method calls to tighter bindings based on the type of
   * the qualifier.
   */
  public class MethodCallTighteningVisitor extends JChangeTrackingVisitor {

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

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      // The method call is already known statically
      if (x.isVolatile() || !x.canBePolymorphic()) {
        return;
      }

      JType instanceType = x.getInstance().getType().getUnderlyingType();
      if (!(instanceType instanceof JClassType)) {
        // Cannot tighten.
        return;
      }

      JMethod mostSpecificTarget = getMostSpecificOverride(x);
      if (mostSpecificTarget.getEnclosingType().isJsNative()) {
        // Never tighten to instance methods in native types. This done because java.lang.Object
        // methods are implicitly implemented by all objects but may or may not be present in the
        // native type implementation. The dispatch for these is eventually done through a
        // trampoline {@see Devirtualizer} that makes the proper checks and invokes the native
        // implementation if present.
        assert x.getTarget().getEnclosingType().isJavaLangObject()
            || x.getTarget().getEnclosingType().isJsNative();
        return;
      }

      // Tighten the method call if a more specific override is available.
      JMethodCall newCall = maybeReplaceTargetMethod(x, mostSpecificTarget);
      maybeUpgradeToNonPolymorphicCall(newCall);

      if (newCall != x) {
        ctx.replaceMe(newCall);
      }
    }

    private JMethod getMostSpecificOverride(final JMethodCall methodCall) {
      JMethod original = methodCall.getTarget();
      JClassType underlyingType =
          (JClassType) methodCall.getInstance().getType().getUnderlyingType();

      return program.typeOracle.findMostSpecificOverride(underlyingType, original);
    }

    private JMethodCall maybeReplaceTargetMethod(JMethodCall methodCall, JMethod newTargetMethod) {
      if (methodCall.getTarget() == newTargetMethod) {
        return methodCall;
      }
      return new JMethodCall(
          methodCall.getSourceInfo(),
          methodCall.getInstance(),
          newTargetMethod,
          methodCall.getArgs());
    }

    private void maybeUpgradeToNonPolymorphicCall(JMethodCall x) {
      JReferenceType instanceType = (JReferenceType) x.getInstance().getType();

      if (!instanceType.canBeSubclass() || !hasPotentialOverride(instanceType, x.getTarget())) {
        assert getMostSpecificOverride(x) == x.getTarget();

        // Mark a call as non-polymorphic if the targeted type is guaranteed to be not a subclass
        // or there are no overriding implementations.
        x.setCannotBePolymorphic();
        madeChanges();
       }
    }

    private boolean hasPotentialOverride(JReferenceType instanceType, JMethod target) {
      if (target.isAbstract()) {
        return true;
      }

      for (JMethod override : target.getOverridingMethods()) {
        JReferenceType overrideType = override.getEnclosingType();
        if (!program.typeOracle.castFailsTrivially(instanceType, overrideType)) {
          // This call is truly polymorphic.
          return true;
        }
      }

      return false;
    }

    @Override
    public void endVisit(JNewInstance x, Context ctx) {
      // Do not tighten new operations.
    }

    @Override
    public boolean visit(JRunAsync x, Context ctx) {
      x.traverseOnSuccess(this);
      return super.visit(x, ctx);
    }
  }

  public static final String NAME = MethodCallTightener.class.getSimpleName();

  public static int exec(JProgram program, OptimizerContext optimizerCtx) {
    return new MethodCallTightener(program).execImpl(optimizerCtx);
  }

  private final JProgram program;

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

  private int execImpl(OptimizerContext optimizerCtx) {
    try (OptimizerStats stats = OptimizerStats.optimization(NAME)) {
      MethodCallTighteningVisitor tightener = new MethodCallTighteningVisitor(optimizerCtx);
      tightener.accept(program);
      stats.recordModified(tightener.getNumMods());

      JavaAstVerifier.assertProgramIsConsistent(program);
      optimizerCtx.incOptimizationStep();

      return stats.getNumMods();
    }
  }
}
