/*
 * 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.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.ibm.wala.analysis.reflection.JavaTypeContext;
import com.ibm.wala.analysis.typeInference.TypeAbstraction;
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.CallGraph;
import com.ibm.wala.ipa.callgraph.Context;
import com.ibm.wala.ipa.callgraph.ContextKey;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.ReceiverInstanceContext;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.shrikeBT.IInvokeInstruction;
import com.ibm.wala.types.MethodReference;
import com.ibm.wala.util.CancelException;
import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.collections.HashSetFactory;
import com.ibm.wala.util.collections.Iterator2Iterable;
import com.ibm.wala.util.collections.NonNullSingletonIterator;
import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.debug.UnimplementedError;
import com.ibm.wala.util.graph.AbstractNumberedGraph;
import com.ibm.wala.util.graph.NumberedNodeManager;
import com.ibm.wala.util.graph.impl.DelegatingNumberedNodeManager;
import com.ibm.wala.util.graph.impl.NodeWithNumber;
import com.ibm.wala.util.graph.traverse.DFS;

/**
 * Basic data structure support for a call graph.
 */
public abstract class BasicCallGraph<T> extends AbstractNumberedGraph<CGNode> implements CallGraph {

  private static final boolean DEBUG = false;

  private final DelegatingNumberedNodeManager<CGNode> nodeManager = new DelegatingNumberedNodeManager<>();

  /**
   * A fake root node for the graph
   */
  private CGNode fakeRoot;

  /**
   * A node which handles all calls to class initializers
   */
  private CGNode fakeWorldClinit;

  /**
   * An object that handles context interpreter functions
   */
  private T interpreter;

  /**
   * Set of nodes that are entrypoints for this analysis
   */
  private final Set<CGNode> entrypointNodes = HashSetFactory.make();

  /**
   * A mapping from Key to NodeImpls in the graph. Note that each node is created on demand. This Map does not include the root
   * node.
   */
  final private Map<Key, CGNode> nodes = HashMapFactory.make();

  /**
   * A mapping from MethodReference to Set of nodes that represent this methodReference.
   * 
   * TODO: rhs of mapping doesn't have to be a set if it's a singleton; could be a node instead.
   * 
   * TODO: this is a bit redundant with the nodes Map. Restructure these data structures for space efficiency.
   */
  protected final Map<MethodReference, Set<CGNode>> mr2Nodes = HashMapFactory.make();

  public BasicCallGraph() {
    super();
  }

  @SuppressWarnings("deprecation")
  public void init() throws CancelException {
    fakeRoot = makeFakeRootNode();
    Key k = new Key(fakeRoot.getMethod(), fakeRoot.getContext());
    registerNode(k, fakeRoot);
    fakeWorldClinit = makeFakeWorldClinitNode();
    if (fakeWorldClinit != null) {
      k = new Key(fakeWorldClinit.getMethod(), fakeWorldClinit.getContext());
      registerNode(k, fakeWorldClinit);

      // add a call from fakeRoot to fakeWorldClinit
      CallSiteReference site = CallSiteReference.make(1, fakeWorldClinit.getMethod().getReference(),
          IInvokeInstruction.Dispatch.STATIC);
      // note that the result of addInvocation is a different site, with a different program counter!
      site = ((AbstractRootMethod) fakeRoot.getMethod()).addInvocation(null, site).getCallSite();
      fakeRoot.addTarget(site, fakeWorldClinit);
    }
  }

  protected abstract CGNode makeFakeRootNode() throws CancelException;

  protected abstract CGNode makeFakeWorldClinitNode() throws CancelException;

  /**
   * Use with extreme care.
   * 
   * @throws CancelException TODO
   */
  public abstract CGNode findOrCreateNode(IMethod method, Context C) throws CancelException;

  protected void registerNode(Key K, CGNode N) {
    nodes.put(K, N);
    addNode(N);
    Set<CGNode> s = findOrCreateMr2Nodes(K.m);
    s.add(N);
    if (DEBUG) {
      System.err.println(("registered Node: " + N + " for key " + K));
      System.err.println(("now size = " + getNumberOfNodes()));
    }
  }

  private Set<CGNode> findOrCreateMr2Nodes(IMethod method) {
    Set<CGNode> result = mr2Nodes.get(method.getReference());
    if (result == null) {
      result = HashSetFactory.make(3);
      mr2Nodes.put(method.getReference(), result);
    }
    return result;
  }

  protected CGNode getNode(Key K) {
    return nodes.get(K);
  }

  @Override
  public CGNode getFakeRootNode() {
    return fakeRoot;
  }

  @Override
  public CGNode getFakeWorldClinitNode() {
    return fakeWorldClinit;
  }

  /**
   * record that a node is an entrypoint
   */
  public void registerEntrypoint(CGNode node) {
    entrypointNodes.add(node);
  }

  /**
   * Note: not all successors of the root node are entrypoints
   */
  @Override
  public Collection<CGNode> getEntrypointNodes() {
    return entrypointNodes;
  }

  /**
   * A class that represents the a normal node in a call graph.
   */
  public abstract class NodeImpl extends NodeWithNumber implements CGNode {

    /**
     * The method this node represents.
     */
    protected final IMethod method;

    /**
     * The context this node represents.
     */
    private final Context context;

    protected NodeImpl(IMethod method, Context C) {
      this.method = method;
      this.context = C;
      if (method != null && !method.isWalaSynthetic() && method.isAbstract()) {
        assert !method.isAbstract() : "Abstract method " + method;
      }
      assert C != null;
    }

    @Override
    public IMethod getMethod() {
      return method;
    }

    /**
     * @see java.lang.Object#equals(Object)
     */
    @Override
    public abstract boolean equals(Object obj);

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public abstract int hashCode();

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
      return "Node: " + method.toString() + " Context: " + context.toString();
    }

    @Override
    public Context getContext() {
      return context;
    }

    @SuppressWarnings("deprecation")
    @Override
    public abstract boolean addTarget(CallSiteReference reference, CGNode target);

    @Override
    public IClassHierarchy getClassHierarchy() {
      return method.getClassHierarchy();
    }
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    for (CGNode n : Iterator2Iterable.make(DFS.iterateDiscoverTime(this, new NonNullSingletonIterator<>(getFakeRootNode())))) {
      result.append(nodeToString(this, n)).append('\n');
    }
    return result.toString();
  }

  public static String nodeToString(CallGraph CG, CGNode n) {
    StringBuilder result = new StringBuilder(n.toString() + '\n');
     if (n.getMethod() != null) {
      for (CallSiteReference site : Iterator2Iterable.make(n.iterateCallSites())) {
        Iterator<CGNode> targets = CG.getPossibleTargets(n, site).iterator();
        if (targets.hasNext()) {
          result.append(" - ").append(site).append('\n');
        }
        for (CGNode target : Iterator2Iterable.make(targets)) {
          result.append("     -> ").append(target).append('\n');
        }
      }
    }
    return result.toString();
  }

  @Override
  public void removeNodeAndEdges(CGNode N) throws UnimplementedError {
    Assertions.UNREACHABLE();
  }

  /**
   * @return NodeImpl, or null if none found
   */
  @Override
  public CGNode getNode(IMethod method, Context C) {
    Key key = new Key(method, C);
    return getNode(key);
  }

  protected final static class Key {
    private final IMethod m;

    private final Context C;

    public Key(IMethod m, Context C) {
      assert m != null : "null method";
      assert C != null : "null context";
      this.m = m;
      this.C = C;
    }

    @Override
    public int hashCode() {
      return 17 * m.hashCode() + C.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      assert o instanceof Key;
      Key other = (Key) o;
      return (m.equals(other.m) && C.equals(other.C));
    }

    @Override
    public String toString() {
      return "{" + m + ',' + C + '}';
    }

  }

  @Override
  public Set<CGNode> getNodes(MethodReference m) {
    IMethod im = getClassHierarchy().resolveMethod(m);
    if (im != null) {
      m = im.getReference();
    }
    Set<CGNode> result = mr2Nodes.get(m);
    return (result == null) ? Collections.emptySet() : result;
  }

  /**
   * @param node a call graph node we want information about
   * @return an object that knows how to interpret information about the node
   */
  protected T getInterpreter(CGNode node) {
    if (interpreter == null) {
      throw new IllegalStateException("must register an interpreter for this call graph");
    }
    return interpreter;
  }

  /**
   * We override this since this class supports remove() on nodes, but the superclass doesn't.
   * 
   * @see com.ibm.wala.util.graph.Graph#getNumberOfNodes()
   */
  @Override
  public int getNumberOfNodes() {
    return nodes.size();
  }

  /**
   * We override this since this class supports remove() on nodes, but the superclass doesn't.
   * 
   * @see com.ibm.wala.util.graph.Graph#iterator()
   */
  @Override
  public Iterator<CGNode> iterator() {
    return nodes.values().iterator();
  }

  /**
   * This implementation is necessary because the underlying SparseNumberedGraph may not support node membership tests.
   * 
   * @throws IllegalArgumentException if N is null
   * 
   */
  @Override
  public boolean containsNode(CGNode N) {
    if (N == null) {
      throw new IllegalArgumentException("N is null");
    }
    return getNode(N.getMethod(), N.getContext()) != null;
  }

  public void setInterpreter(T interpreter) {
    this.interpreter = interpreter;
  }

  @Override
  protected NumberedNodeManager<CGNode> getNodeManager() {
    return nodeManager;
  }
  
  public void summarizeByPackage() {
   Map<String, Integer> packages = HashMapFactory.make();
   for(CGNode n : this) {
     final StringBuilder nmBuilder = new StringBuilder(n.getMethod().getDeclaringClass().getName().toString())
             .append('/').append(n.getMethod().getName()).append('/').append(n.getContext().getClass().toString());

     if (n.getContext().isA(ReceiverInstanceContext.class)) {
       nmBuilder.append('/').append(((InstanceKey)n.getContext().get(ContextKey.RECEIVER)).getConcreteType().getName());
     } else if (n.getContext() instanceof JavaTypeContext) {
       nmBuilder.append('/').append(((TypeAbstraction) n.getContext().get(ContextKey.RECEIVER)).getTypeReference().getName());
     }
     String nm = nmBuilder.toString();

     do {
       if (packages.containsKey(nm)) {
         packages.put(nm, 1 + packages.get(nm));
       } else {
         packages.put(nm, 1);
       }
       
       if (nm.indexOf('/') < 0) {
         break;
       }
       
       nm = nm.substring(0, nm.lastIndexOf('/'));
     } while (true);
   }
   
   System.err.println("dump of CG");
   for(Map.Entry<String, Integer> e : packages.entrySet()) {
     System.err.println(e.getValue().intValue() + " " + e.getKey());
   }
  }

}
