/*
 * Copyright (c) 2008 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.analysis.reflection;

import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.Context;
import com.ibm.wala.ipa.callgraph.ContextSelector;
import com.ibm.wala.ipa.callgraph.propagation.ConstantKey;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.ReceiverInstanceContext;
import com.ibm.wala.ssa.IR;
import com.ibm.wala.ssa.SSAAbstractInvokeInstruction;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.ssa.SymbolTable;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.intset.EmptyIntSet;
import com.ibm.wala.util.intset.IntSet;
import com.ibm.wala.util.intset.IntSetUtil;

/**
 * A {@link ContextSelector} to intercept calls to reflective method invocations such as
 * Constructor.newInstance and Method.invoke
 */
class ReflectiveInvocationSelector implements ContextSelector {

  public ReflectiveInvocationSelector() {}

  /**
   * Creates a callee target based on the following criteria:
   *
   * <ol>
   *   <li>If the method being invoked through reflection is definitely static, then do not create a
   *       callee target for any {@code callee} method that is not static. In this case, return
   *       {@code null}.
   *   <li>If the method being invoked through reflection takes a constant number of parameters,
   *       {@code n}, then do not create a callee target for any {@code callee} method that takes a
   *       number of parameters different from {@code n}. In this case, return {@code null}.
   *   <li>Otherwise, return a new {@link ReceiverInstanceContext} for {@code receiver}.
   * </ol>
   */
  @Override
  public Context getCalleeTarget(
      CGNode caller, CallSiteReference site, IMethod callee, InstanceKey[] receiver) {
    if (receiver == null || receiver.length == 0 || !mayUnderstand(callee, receiver[0])) {
      return null;
    }
    IR ir = caller.getIR();
    SSAAbstractInvokeInstruction[] invokeInstructions = ir.getCalls(site);
    if (invokeInstructions.length != 1) {
      return new ReceiverInstanceContext(receiver[0]);
    }
    SymbolTable st = ir.getSymbolTable();
    @SuppressWarnings("unchecked")
    ConstantKey<IMethod> receiverConstantKey = (ConstantKey<IMethod>) receiver[0];
    IMethod m = receiverConstantKey.getValue();
    boolean isStatic = m.isStatic();
    boolean isConstructor = isConstructorConstant(receiver[0]);

    // If the method being invoked through reflection is not a constructor and is definitely static,
    // then
    // we should not create a callee target for any method that is not static
    if (!isConstructor) {
      int recvUse = invokeInstructions[0].getUse(1);
      if (st.isNullConstant(recvUse) && !isStatic) {
        return null;
      }
    }

    // If the method being invoked through reflection is being passed n parameters,
    // then we should not create a callee target for any method that takes a number
    // of parameters different from n
    int numberOfParams = isStatic ? m.getNumberOfParameters() : m.getNumberOfParameters() - 1;
    // instruction[0] is a call to Method.invoke(), where the receiver is a specific method,
    // the first parameter is the receiver of the method invocation, and the second parameter
    // is an array of objects corresponding to the parameters passed to the method.
    int paramIndex = isConstructor ? 1 : 2;
    int paramUse = invokeInstructions[0].getUse(paramIndex);
    SSAInstruction instr = caller.getDU().getDef(paramUse);
    if (!(instr instanceof SSANewInstruction)) {
      return new ReceiverInstanceContext(receiver[0]);
    }
    SSANewInstruction newInstr = (SSANewInstruction) instr;
    if (!newInstr.getConcreteType().isArrayType()) {
      return null;
    }
    int vn = newInstr.getUse(0);
    try {
      int arrayLength = st.getIntValue(vn);
      if (arrayLength == numberOfParams) {
        return new ReceiverInstanceContext(receiver[0]);
      } else {
        return new IllegalArgumentExceptionContext();
      }
    } catch (IllegalArgumentException e) {
      return new ReceiverInstanceContext(receiver[0]);
    }
  }

  /** This object may understand a dispatch to Constructor.newInstance(). */
  private static boolean mayUnderstand(IMethod targetMethod, InstanceKey instance) {
    if (instance instanceof ConstantKey) {
      if (targetMethod.getReference().equals(ReflectiveInvocationInterpreter.METHOD_INVOKE)
          || isConstructorConstant(instance)
              && targetMethod
                  .getReference()
                  .equals(ReflectiveInvocationInterpreter.CTOR_NEW_INSTANCE)) {
        return true;
      }
    }
    return false;
  }

  private static boolean isConstructorConstant(InstanceKey instance) {
    if (instance instanceof ConstantKey) {
      ConstantKey<?> c = (ConstantKey<?>) instance;
      if (c.getConcreteType().getReference().equals(TypeReference.JavaLangReflectConstructor)) {
        return true;
      }
    }
    return false;
  }

  private static final IntSet thisParameter = IntSetUtil.make(new int[] {0});

  @Override
  public IntSet getRelevantParameters(CGNode caller, CallSiteReference site) {
    if (site.getDeclaredTarget().equals(ReflectiveInvocationInterpreter.METHOD_INVOKE)
        || site.getDeclaredTarget().equals(ReflectiveInvocationInterpreter.CTOR_NEW_INSTANCE)) {
      return thisParameter;
    } else {
      return EmptyIntSet.instance;
    }
  }
}
