/*
 * Copyright (c) 2002 - 2006 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.ipa.summaries;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.Language;
import com.ibm.wala.classLoader.SyntheticMethod;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.MethodTargetSelector;
import com.ibm.wala.ipa.callgraph.impl.ClassHierarchyMethodTargetSelector;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSAInstructionFactory;
import com.ibm.wala.types.MemberReference;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeName;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.strings.Atom;

/**
 * "Non-standard" bypass rules to use during call graph construction.
 * 
 * Normally, the method bypass rules replace the IMethod that is resolved by other means, via the getBypass() method. However, the
 * bypass rules can be invoked even before resolving the target of a call, by checking the intercept rules.
 * 
 * @author sfink
 */
public class BypassMethodTargetSelector implements MethodTargetSelector {

  static final boolean DEBUG = false;

  /**
   * Method summaries collected for methods. Mapping Object -&gt; MethodSummary where Object is either a
   * <ul>
   * <li>MethodReference
   * <li>TypeReference
   * <li>Atom (package name)
   * </ul>
   */
  private final Map<MethodReference, MethodSummary> methodSummaries;

  /**
   * Set of Atoms representing package names whose methods should be treated as no-ops
   */
  private final Set<Atom> ignoredPackages;

  /**
   * Governing class hierarchy.
   */
  protected final IClassHierarchy cha;

  /**
   * target selector to use for non-bypassed calls
   */
  protected final MethodTargetSelector parent;

  /**
   * for checking method target resolution via CHA
   */
  private final ClassHierarchyMethodTargetSelector chaMethodTargetSelector;

  /**
   * Mapping from MethodReference -&gt; SyntheticMethod We may call syntheticMethod.put(m,null) .. in which case we use containsKey()
   * to check for having already considered m.
   */
  final private HashMap<MethodReference, SummarizedMethod> syntheticMethods = HashMapFactory.make();

  public BypassMethodTargetSelector(MethodTargetSelector parent, Map<MethodReference, MethodSummary> methodSummaries,
      Set<Atom> ignoredPackages, IClassHierarchy cha) {
    this.methodSummaries = methodSummaries;
    this.ignoredPackages = ignoredPackages;
    this.parent = parent;
    this.cha = cha;
    this.chaMethodTargetSelector = new ClassHierarchyMethodTargetSelector(cha);
  }

  /**
   * Check to see if a particular call site should be bypassed, before checking normal resolution of the receiver.
   * 
   * @throws IllegalArgumentException if site is null
   */
  @Override
  public IMethod getCalleeTarget(CGNode caller, CallSiteReference site, IClass dispatchType) {

    if (site == null) {
      throw new IllegalArgumentException("site is null");
    }
    // first, see if we'd like to bypass the CHA-based target for the site
    MethodReference ref = site.getDeclaredTarget();    
    IMethod chaTarget = chaMethodTargetSelector.getCalleeTarget(caller, site, dispatchType);
    IMethod target = (chaTarget == null) ? findOrCreateSyntheticMethod(ref, site.isStatic()) : findOrCreateSyntheticMethod(chaTarget,
        site.isStatic());

    // try synthetic method that matches receiver type
    if (dispatchType != null) {
      ref = MethodReference.findOrCreate(dispatchType.getReference(), ref.getSelector());
      chaTarget = chaMethodTargetSelector.getCalleeTarget(caller, site, dispatchType);
      target = (chaTarget == null) ? findOrCreateSyntheticMethod(ref, site.isStatic()) : findOrCreateSyntheticMethod(chaTarget,
          site.isStatic());      
    }
    
    if (DEBUG) {
      System.err.println("target is initially " + target);
    }

    if (target != null) {
      return target;
    } else {
      // didn't bypass the CHA target; check if we should bypass the parent target
      if (canIgnore(site.getDeclaredTarget())) {
        // we want to generate a NoOpSummary for this method.
        return findOrCreateSyntheticMethod(site.getDeclaredTarget(), site.isStatic());
      }

      // not using if (instanceof ClassHierarchyMethodTargetSelector) because
      // we want to make sure that getCalleeTarget() is still called if 
      // parent is a subclass of ClassHierarchyMethodTargetSelector
      if (parent.getClass() == ClassHierarchyMethodTargetSelector.class) {
        // already checked this case and decided not to bypass
        return chaTarget;
      }
      target = parent.getCalleeTarget(caller, site, dispatchType);

      if (DEBUG) {
        System.err.println("target becomes " + target);
      }

      if (target != null) {
        IMethod bypassTarget = findOrCreateSyntheticMethod(target, site.isStatic());

        if (DEBUG)
          System.err.println("bypassTarget is " + target);

        return (bypassTarget == null) ? target : bypassTarget;
      } else
        return target;
    }
  }

  /**
   * @param m a method reference
   * @return a SyntheticMethod corresponding to m; or null if none is available.
   */
  protected SyntheticMethod findOrCreateSyntheticMethod(MethodReference m, boolean isStatic) {
    if (syntheticMethods.containsKey(m)) {
      return syntheticMethods.get(m);
    } else {
      MethodSummary summ = null;
      if (canIgnore(m)) {
        TypeReference T = m.getDeclaringClass();
        IClass C = cha.lookupClass(T);
        if (C == null) {
          // did not load class; don't try to create a synthetic method
          syntheticMethods.put(m, null);
          return null;
        }
        summ = generateNoOp(m, isStatic);
      } else {
        summ = findSummary(m);
      }
      if (summ != null) {
        TypeReference T = m.getDeclaringClass();
        IClass C = cha.lookupClass(T);
        if (C == null) {
          syntheticMethods.put(m, null);
          return null;
        }
        SummarizedMethod n = new SummarizedMethodWithNames(m, summ, C);
        syntheticMethods.put(m, n);
        return n;
      } else {
        syntheticMethods.put(m, null);
        return null;
      }
    }
  }

  /**
   * @param m a method reference
   * @return a SyntheticMethod corresponding to m; or null if none is available.
   */
  protected SyntheticMethod findOrCreateSyntheticMethod(IMethod m, boolean isStatic) {
    MethodReference ref = m.getReference();
    if (syntheticMethods.containsKey(ref)) {
      return syntheticMethods.get(ref);
    } else {
      MethodSummary summ = null;
      if (canIgnore(ref)) {
        summ = generateNoOp(ref, isStatic);
      } else {
        summ = findSummary(ref);
      }
      if (summ != null) {
        SummarizedMethod n = new SummarizedMethod(ref, summ, m.getDeclaringClass());
        syntheticMethods.put(ref, n);
        return n;
      } else {
        syntheticMethods.put(ref, null);
        return null;
      }
    }
  }

  /**
   * Generate a {@link MethodSummary} which is the "standard" representation of a method 
   * that does nothing.
   */
  public static MethodSummary generateStandardNoOp(Language l, MethodReference m, boolean isStatic) {
    return new NoOpSummary(l, m, isStatic);
  }
  
  /**
   * Generate a {@link MethodSummary} which is the "standard" representation of a method 
   * that does nothing.  Subclasses may override this method to implement alternative semantics
   * concerning what "do nothing" means.
   */
  public MethodSummary generateNoOp(MethodReference m, boolean isStatic) {
    Language l = cha.resolveMethod(m).getDeclaringClass().getClassLoader().getLanguage();
    return new NoOpSummary(l, m, isStatic);
  }
  

  private static class NoOpSummary extends MethodSummary {

    private final Language l;
    
    public NoOpSummary(Language l, MethodReference method, boolean isStatic) {
      super(method);
      setStatic(isStatic);
      this.l = l;
    }

    /*
     * @see com.ibm.wala.ipa.summaries.MethodSummary#getStatements()
     */
    @Override
    public SSAInstruction[] getStatements() {
      if (getReturnType().equals(TypeReference.Void)) {
        return NO_STATEMENTS;
      } else {
        int nullValue = getNumberOfParameters() + 1;
        SSAInstruction[] result = new SSAInstruction[1];
        SSAInstructionFactory insts = l.instructionFactory();
        result[0] = insts.ReturnInstruction(0, nullValue, getReturnType().isPrimitiveType());
        return result;
      }
    }

  }

  /**
   * @return true iff we can treat m as a no-op method
   */
  protected boolean canIgnore(MemberReference m) {
    TypeReference T = m.getDeclaringClass();
    TypeName n = T.getName();
    Atom p = n.getPackage();
    return (ignoredPackages.contains(p));
  }

  private MethodSummary findSummary(MemberReference m) {
    return methodSummaries.get(m);
  }

  protected IClassHierarchy getClassHierarchy() {
    return cha;
  }  
}
