/*******************************************************************************
 * 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.callgraph.propagation.rta;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import com.ibm.wala.analysis.reflection.ReflectionContextInterpreter;
import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.NewSiteReference;
import com.ibm.wala.fixpoint.UnaryOperator;
import com.ibm.wala.ipa.callgraph.AnalysisCache;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.ContextSelector;
import com.ibm.wala.ipa.callgraph.impl.DefaultContextSelector;
import com.ibm.wala.ipa.callgraph.impl.DelegatingContextSelector;
import com.ibm.wala.ipa.callgraph.impl.Everywhere;
import com.ibm.wala.ipa.callgraph.impl.ExplicitCallGraph;
import com.ibm.wala.ipa.callgraph.impl.FakeRootMethod;
import com.ibm.wala.ipa.callgraph.impl.FakeWorldClinitMethod;
import com.ibm.wala.ipa.callgraph.propagation.ClassBasedInstanceKeys;
import com.ibm.wala.ipa.callgraph.propagation.IPointsToSolver;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.PointerAnalysis;
import com.ibm.wala.ipa.callgraph.propagation.PointerKey;
import com.ibm.wala.ipa.callgraph.propagation.PointsToSetVariable;
import com.ibm.wala.ipa.callgraph.propagation.PropagationCallGraphBuilder;
import com.ibm.wala.ipa.callgraph.propagation.PropagationSystem;
import com.ibm.wala.ipa.callgraph.propagation.SSAContextInterpreter;
import com.ibm.wala.ipa.callgraph.propagation.StandardSolver;
import com.ibm.wala.ipa.callgraph.propagation.cfa.DefaultPointerKeyFactory;
import com.ibm.wala.ipa.callgraph.propagation.cfa.DefaultSSAInterpreter;
import com.ibm.wala.ipa.callgraph.propagation.cfa.DelegatingSSAContextInterpreter;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.shrikeBT.IInvokeInstruction;
import com.ibm.wala.ssa.SSAInvokeInstruction;
import com.ibm.wala.ssa.SSANewInstruction;
import com.ibm.wala.types.ClassLoaderReference;
import com.ibm.wala.types.FieldReference;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.CancelException;
import com.ibm.wala.util.MonitorUtil.IProgressMonitor;
import com.ibm.wala.util.collections.HashSetFactory;

/**
 * Abstract superclass of various RTA flavors
 */
public abstract class AbstractRTABuilder extends PropagationCallGraphBuilder {

  protected final static int DEBUG_LEVEL = 0;

  protected final static boolean DEBUG = (DEBUG_LEVEL > 0);

  private final static int VERBOSE_INTERVAL = 10000;

  private final static int PERIODIC_MAINTAIN_INTERVAL = 10000;

  /**
   * Should we change calls to clone() to assignments?
   */
  protected final boolean clone2Assign = true;

  /**
   * set of classes whose clinit are processed
   */
  protected final Set<IClass> clinitProcessed = HashSetFactory.make();

  /**
   * set of classes (IClass) discovered to be allocated
   */
  protected final HashSet<IClass> allocatedClasses = HashSetFactory.make();

  /**
   * set of class names that are implicitly pre-allocated Note: for performance reasons make sure java.lang.Object comes first
   */
  private final static TypeReference[] PRE_ALLOC = {
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/Object"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/ArithmeticException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/ArrayStoreException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/ClassCastException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/ClassNotFoundException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/IndexOutOfBoundsException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/NegativeArraySizeException"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/ExceptionInInitializerError"),
      TypeReference.findOrCreate(ClassLoaderReference.Primordial, "Ljava/lang/NullPointerException") };

  protected AbstractRTABuilder(IClassHierarchy cha, AnalysisOptions options, AnalysisCache cache,
      ContextSelector appContextSelector, SSAContextInterpreter appContextInterpreter) {
    super(cha, options, cache, new DefaultPointerKeyFactory());
    setInstanceKeys(new ClassBasedInstanceKeys(options, cha));
    setContextSelector(makeContextSelector(appContextSelector));
    setContextInterpreter(makeContextInterpreter(appContextInterpreter));
  }

  protected RTAContextInterpreter getRTAContextInterpreter() {
    return getContextInterpreter();
  }

  /**
   * Visit all instructions in a node, and add dataflow constraints induced by each statement relevat to RTA
   */
  @Override
  protected boolean addConstraintsFromNode(CGNode node, IProgressMonitor monitor) {

    if (haveAlreadyVisited(node)) {
      return false;
    } else {
      markAlreadyVisited(node);
    }
    if (DEBUG) {
      System.err.println(("\n\nAdd constraints from node " + node));
    }

    // add all relevant constraints
    addNewConstraints(node);
    addCallConstraints(node);
    addFieldConstraints(node);
    // conservatively assume something changed.
    return true;
  }

  /**
   * Add a constraint for each allocate
   */
  private void addNewConstraints(CGNode node) {
    for (Iterator it = getRTAContextInterpreter().iterateNewSites(node); it.hasNext();) {
      NewSiteReference n = (NewSiteReference) it.next();
      visitNew(node, n);
    }
  }

  /**
   * Add a constraint for each invoke
   */
  private void addCallConstraints(CGNode node) {
    for (Iterator it = getRTAContextInterpreter().iterateCallSites(node); it.hasNext();) {
      CallSiteReference c = (CallSiteReference) it.next();
      visitInvoke(node, c);
    }
  }

  /**
   * Handle accesses to static fields
   */
  private void addFieldConstraints(CGNode node) {
    for (Iterator it = getRTAContextInterpreter().iterateFieldsRead(node); it.hasNext();) {
      FieldReference f = (FieldReference) it.next();
      processFieldAccess(node, f);
    }
    for (Iterator it = getRTAContextInterpreter().iterateFieldsWritten(node); it.hasNext();) {
      FieldReference f = (FieldReference) it.next();
      processFieldAccess(node, f);
    }
  }

  /**
   * Is s is a getstatic or putstatic, then potentially add the relevant <clinit>to the newMethod set.
   */
  private void processFieldAccess(CGNode node, FieldReference f) {
    if (DEBUG) {
      System.err.println(("processFieldAccess: " + f));
    }
    TypeReference t = f.getDeclaringClass();
    IClass klass = getClassHierarchy().lookupClass(t);
    if (klass == null) {
    } else {
      processClassInitializer(klass);
    }
  }

  protected void processClassInitializer(IClass klass) {

    if (clinitProcessed.contains(klass)) {
      return;
    }
    clinitProcessed.add(klass);

    if (klass.getClassInitializer() != null) {
      if (DEBUG) {
        System.err.println(("process class initializer for " + klass));
      }

      // add an invocation from the fake root method to the <clinit>
      FakeWorldClinitMethod fakeWorldClinitMethod = (FakeWorldClinitMethod) callGraph.getFakeWorldClinitNode().getMethod();
      MethodReference m = klass.getClassInitializer().getReference();
      CallSiteReference site = CallSiteReference.make(1, m, IInvokeInstruction.Dispatch.STATIC);
      IMethod targetMethod = options.getMethodTargetSelector().getCalleeTarget(callGraph.getFakeRootNode(), site, null);
      if (targetMethod != null) {
        CGNode target = callGraph.getNode(targetMethod, Everywhere.EVERYWHERE);
        if (target == null) {
          SSAInvokeInstruction s = fakeWorldClinitMethod.addInvocation(null, site);
          try {
            target = callGraph.findOrCreateNode(targetMethod, Everywhere.EVERYWHERE);
            processResolvedCall(callGraph.getFakeWorldClinitNode(), s.getCallSite(), target);
          } catch (CancelException e) {
            if (DEBUG) {
              System.err.println("Could not add node for class initializer: " + targetMethod.getSignature()
                  + " due to constraints on the maximum number of nodes in the call graph.");
              return;
            }
          }
        }
      }
    }

    klass = klass.getSuperclass();
    if (klass != null && !clinitProcessed.contains(klass))
      processClassInitializer(klass);
  }

  /**
   * Add a constraint for a call instruction
   * 
   * @throws IllegalArgumentException if site is null
   */
  public void visitInvoke(CGNode node, CallSiteReference site) {

    if (site == null) {
      throw new IllegalArgumentException("site is null");
    }
    if (DEBUG) {
      System.err.println(("visitInvoke: " + site));
    }

    // if non-virtual, add callgraph edges directly
    IInvokeInstruction.IDispatch code = site.getInvocationCode();

    if (code == IInvokeInstruction.Dispatch.STATIC) {
      CGNode n = getTargetForCall(node, site, null, null);
      if (n != null) {
        processResolvedCall(node, site, n);

        // side effect of invoke: may call class initializer
        processClassInitializer(cha.lookupClass(site.getDeclaredTarget().getDeclaringClass()));
      }
    } else {

      // Add a side effect that will fire when we determine a value
      // for the receiver. This side effect will create a new node
      // and new constraints based on the new callee context.

      // TODO: special case logic for dispatch to "final" - type stuff, where
      // receiver and context can be determined a priori ... like we currently
      // do for invokestatic above.
      PointerKey selector = getKeyForSite(site);
      if (selector == null) {
        return;
      }

      if (DEBUG) {
        System.err.println(("Add side effect, dispatch to " + site));
      }
      UnaryOperator<PointsToSetVariable> dispatchOperator = makeDispatchOperator(site, node);
      system.newSideEffect(dispatchOperator, selector);
    }
  }

  protected abstract UnaryOperator<PointsToSetVariable> makeDispatchOperator(CallSiteReference site, CGNode node);

  protected abstract PointerKey getKeyForSite(CallSiteReference site);

  /**
   * Add constraints for a call site after we have computed a reachable target for the dispatch
   * 
   * Side effect: add edge to the call graph.
   */
  @SuppressWarnings("deprecation")
  void processResolvedCall(CGNode caller, CallSiteReference site, CGNode target) {

    if (DEBUG) {
      System.err.println(("processResolvedCall: " + caller + " ," + site + " , " + target));
    }
    caller.addTarget(site, target);

    if (FakeRootMethod.isFakeRootMethod(caller.getMethod().getReference())) {
      if (entrypointCallSites.contains(site)) {
        callGraph.registerEntrypoint(target);
      }
    }

    if (!haveAlreadyVisited(target)) {
      markDiscovered(target);
    }
  }

  /**
   * Add a constraint for an allocate
   * 
   * @throws IllegalArgumentException if newSite is null
   */
  public void visitNew(CGNode node, NewSiteReference newSite) {

    if (newSite == null) {
      throw new IllegalArgumentException("newSite is null");
    }
    if (DEBUG) {
      System.err.println(("visitNew: " + newSite));
    }
    InstanceKey iKey = getInstanceKeyForAllocation(node, newSite);
    if (iKey == null) {
      // something went wrong. I hope someone raised a warning.
      return;
    }
    IClass klass = iKey.getConcreteType();

    if (DEBUG) {
      System.err.println(("iKey: " + iKey + " " + system.findOrCreateIndexForInstanceKey(iKey)));
    }

    if (klass == null) {
      return;
    }
    if (allocatedClasses.contains(klass)) {
      return;
    }
    allocatedClasses.add(klass);
    updateSetsForNewClass(klass, iKey, node, newSite);

    // side effect of new: may call class initializer
    processClassInitializer(klass);
  }

  /**
   * Perform needed bookkeeping when a new class is discovered.
   * 
   * @param klass
   */
  protected abstract void updateSetsForNewClass(IClass klass, InstanceKey iKey, CGNode node, NewSiteReference ns);

  /*
   * @see com.ibm.wala.ipa.callgraph.propagation.PropagationCallGraphBuilder#customInit()
   */
  @Override
  protected void customInit() {
    super.customInit();

    FakeRootMethod m = (FakeRootMethod) getCallGraph().getFakeRootNode().getMethod();

    for (int i = 0; i < PRE_ALLOC.length; i++) {
      SSANewInstruction n = m.addAllocation(PRE_ALLOC[i]);
      // visit now to ensure java.lang.Object is visited first
      visitNew(getCallGraph().getFakeRootNode(), n.getNewSite());
    }
  }

  /**
   * @return set of IClasses determined to be allocated
   */
  @SuppressWarnings("unchecked")
  public Set<IClass> getAllocatedTypes() {
    return (Set<IClass>) allocatedClasses.clone();
  }

  /*
   * @see com.ibm.wala.ipa.callgraph.propagation.PropagationCallGraphBuilder#makeSolver()
   */
  @Override
  protected IPointsToSolver makeSolver() {
    return new StandardSolver(system, this);
  }

  protected ContextSelector makeContextSelector(ContextSelector appContextSelector) {
    ContextSelector def = new DefaultContextSelector(options, cha);
    ContextSelector contextSelector = appContextSelector == null ? def : new DelegatingContextSelector(appContextSelector, def);
    return contextSelector;
  }

  protected SSAContextInterpreter makeContextInterpreter(SSAContextInterpreter appContextInterpreter) {

    SSAContextInterpreter defI = new DefaultSSAInterpreter(getOptions(), getAnalysisCache());
    defI = new DelegatingSSAContextInterpreter(ReflectionContextInterpreter.createReflectionContextInterpreter(cha, getOptions(),
        getAnalysisCache()), defI);
    SSAContextInterpreter contextInterpreter = appContextInterpreter == null ? defI : new DelegatingSSAContextInterpreter(
        appContextInterpreter, defI);
    return contextInterpreter;
  }

  @Override
  protected boolean unconditionallyAddConstraintsFromNode(CGNode node, IProgressMonitor monitor) {
    // add all relevant constraints
    addNewConstraints(node);
    addCallConstraints(node);
    addFieldConstraints(node);
    markAlreadyVisited(node);
    return true;
  }

  @Override
  protected ExplicitCallGraph createEmptyCallGraph(IClassHierarchy cha, AnalysisOptions options) {
    return new DelegatingExplicitCallGraph(cha, options, getAnalysisCache());
  }

  /*
   * @see com.ibm.wala.ipa.callgraph.propagation.PropagationCallGraphBuilder#makeSystem(com.ibm.wala.ipa.callgraph.AnalysisOptions)
   */
  @Override
  protected PropagationSystem makeSystem(AnalysisOptions options) {
    PropagationSystem result = super.makeSystem(options);
    result.setVerboseInterval(VERBOSE_INTERVAL);
    result.setPeriodicMaintainInterval(PERIODIC_MAINTAIN_INTERVAL);
    return result;
  }

  /*
   * @see com.ibm.wala.ipa.callgraph.CallGraphBuilder#getPointerAnalysis()
   */
  @Override
  public PointerAnalysis getPointerAnalysis() {
    return TypeBasedPointerAnalysis.make(getOptions(), allocatedClasses, getCallGraph());
  }
}
