package edu.neu.ccs.demeter.dj;

import edu.neu.ccs.demeter.aplib.*;
import edu.neu.ccs.demeter.aplib.cd.Part;

import java.lang.reflect.*;
import java.lang.reflect.Modifier;
import java.io.*;
import java.util.*;

/** A graph whose nodes are classes and whose edges are subclass,
    superclass, and part-of relationships between classes. */
public class ClassGraph extends edu.neu.ccs.demeter.aplib.cd.ClassGraph {
  static String version = "DJ version 0.8.6";
  /** The DJ version string. */
  public static String getVersion() { return version; }
  /** @deprecated getVersion() */
  public static String version() { return getVersion(); }

  public static boolean debug = false;
  boolean fields = true, methods = true;
  /** Make a class graph from all classes in the default package,
      including all non-static fields and non-static non-void no-argument
      methods. */
  public ClassGraph() {
    this("");
  }
  /** Make a class graph from all classes in the package named <code>pkg</code>,
      including all non-static fields and non-static non-void no-argument
      methods. */
  public ClassGraph(String pkg) {
    this(pkg, true, true);
  }
  /** Make a class graph from all classes in the default package.
      If <code>f</code> is true, include all non-static fields;
      if <code>m</code> is true, include all non-static non-void
      no-argument methods.  These settings will apply by default for all
      classes added to the class graph in the future.
  */
  public ClassGraph(boolean f, boolean m) {
    this("", f, m);
  }
  /** Make a class graph from all classes in the package named <code>pkg</code>.
      If <code>f</code> is true, include all non-static fields;
      if <code>m</code> is true, include all non-static non-void
      no-argument methods.  These settings will apply by default for all
      classes added to the class graph in the future.
  */
  public ClassGraph(String pkg, boolean f, boolean m) {
    super(); fields = f; methods = m;
    addClass(Object.class, false, false);
    addPackage(pkg);
    // FIXME: only add these if they're used?
    addClass(java.util.Collection.class, false, false);
    addRepetitionEdge("java.util.Collection", "java.lang.Object");
    addClass(Collection.class, false, false);
    addRepetitionEdge("edu.neu.ccs.demeter.dj.Collection", "java.lang.Object");
  }

  /** Make a class graph with just the classes and edges in <code>tg</code>. */
  public ClassGraph(Traversal tg) {
    ClassGraph cg = (ClassGraph) tg.getClassGraph();
    Iterator edges = tg.getEdgeSets().iterator();
    while (edges.hasNext()) {
      EdgeI edge = ((Traversal.EdgeSet) edges.next()).getEdge();
      Class cl = cg.getNodeClass(edge.getSource());
      // FIXME: call addClass, but don't add supers
      namesClasses.put(cl.getName(), cl);
      EdgeI newedge = addEdge(edge);
      if (edge instanceof Part && ((Part) edge).isDerived())
	((Part) newedge).markDerived();
    }
  }

  /** Make a class graph with just the classes and edges in <code>cg</code>
      reachable by following <code>s</code>. */
  public ClassGraph(ClassGraph cg, Strategy s) throws TraversalException {
    this(new Traversal(s, cg));
  }

  /** Make a class graph with just the classes and edges in <code>cg</code>
      reachable by following strategy <code>s</code>. */
  public ClassGraph(ClassGraph cg, String s) throws TraversalException {
    this(cg, new Strategy(s));
  }
  
  /** The set of packages that have been added. */
  protected Set packages = new HashSet();

  /** Add all classes in package <code>p</code> that can be found on the
      class path. */
  public void addPackage(String p) {
    if (!packages.add(p)) return;
    String pkgprefix = p.equals("") ? "" : p + ".";
    String pkgdir = p.replace('.', File.separatorChar);
    String classpath = System.getProperty("java.class.path");
    for (int i = 0; i < classpath.length(); i++) {
      int j = classpath.indexOf(File.pathSeparatorChar, i);
      if (j == -1) j = classpath.length();
      File path = new File(classpath.substring(i, j));
      i = j;
      if (path.isDirectory()) {
	File classdir = new File(path, pkgdir);
	if (debug) System.out.println("Searching " + classdir);
	File classfiles[] = classdir.listFiles(new FilenameFilter() {
	  public boolean accept(File dir, String name) {
	    return name.endsWith(".class");
	  }
	});
	if (classfiles != null) {
	  for (int n = 0; n < classfiles.length; n++) {
	    File classfile = classfiles[n];
	    String clname = classfile.getName();
	    clname = pkgprefix + clname.substring(0, clname.length() - 6);
	    try {
	      Class cl = Class.forName(clname);
	      if (debug) System.out.println("Adding " + cl);
	      addClass(cl);
	    } catch (ClassNotFoundException e) {
	      System.err.println("Warning: found " + classfile +
				 " but could not load class " + clname + ".");
	    }
	  }
	}
      } else {
	// FIXME: jar/zip file
      }
    }
  }

  /** A table of classes and what parts have been added.
      {@link edu.neu.ccs.demeter.aplib.cd.ClassGraph#definesClass} will be true for classes we've added on
      either end of an edge, but we want to keep track of edge sources
      in particular. */
  protected Map classesAdded = new HashMap();
  /** Values in the {@link #classesAdded} map.  <code>fields</code> is
      true if all fields have been added; <code>methods</code> is true if
      all methods have been added. */
  protected class Added { boolean fields, methods; }
  /* A table of classes indexed by name.  Includes both ends of edges
     added. */
  protected Map namesClasses = new HashMap();
  /** Add <code>cl</code> and all its non-static members to the class graph
      as construction edges, if they haven't already been added. */
  public void addClass(Class cl) {
    addClass(cl, fields, methods);
  }
  /** Add <code>cl</code> to the class graph, if it hasn't already been added.
      If <code>addFields</code> is true, add all its non-static fields as
      construction edges.  If <code>addMethods</code> is true, add all its
      non-static non-void methods with no arguments as derived construction
      edges. */
  public void addClass(Class cl, boolean addFields, boolean addMethods) {
    namesClasses.put(cl.getName(), cl);
    Added added = (Added) classesAdded.get(cl);
    if (added != null &&
	(added.fields || !addFields) &&
	(added.methods || !addMethods))
      // We already added this class and everything we needed from it.
      return;
    boolean addSupers = false;
    if (added == null) {
      added = new Added();
      classesAdded.put(cl, added);
      addSupers = true;
    }
    addFields = addFields && !added.fields;
    addMethods = addMethods && !added.methods;

    String name = cl.getName();
    // Add fields and/or methods as construction edges.
    if (addFields) {
      added.fields = true;
      Field fields[] = cl.getDeclaredFields();
      for (int i = 0; i < fields.length; i++) {
	Field field = fields[i];
	int mod = field.getModifiers();
	if (Modifier.isStatic(mod)) {
	  // FIXME: add these too?
	} else {
	  if (debug) System.out.println("Adding " + field);
	  addConstructionEdge(name, field);
	}
      }
    }
    if (addMethods) {
      added.methods = true;
      Method methods[] = cl.getDeclaredMethods();
      if (debug) System.out.println(Arrays.asList(methods));
      for (int i = 0; i < methods.length; i++) {
	Method method = methods[i];
	int mod = method.getModifiers();
	if (Modifier.isStatic(mod)) {
	  // FIXME: add these too?
	} else if (method.getParameterTypes().length == 0 &&
		   // Adding clone() to the graph is just asking for trouble...
		   // FIXME: just add methods starting with "get".
		   !method.getName().equals("clone")) {
	  if (debug) System.out.println("Adding " + method);
	  addConstructionEdge(name, method);
	}
      }
    }

    if (addSupers) {
      Class rep = getRepetitionType(cl);
      if (rep == null) {
	// Add the superclass as an alternation/inheritance pair.
	// Recurse up the superclass chain.
	Class sup = cl.getSuperclass();
	if (sup != null) {
	  String supname = sup.getName();
	  addAlternationEdge(supname, name);
	  addInheritanceEdge(name, supname);
	  addClass(sup, false, false);
	}

	// Add the implemented interfaces as alternation/inheritance pairs.
	// This includes superclasses of interfaces.  Recurse up the chain.
	Class interfaces[] = cl.getInterfaces();
	for (int i = 0; i < interfaces.length; i++) {
	  String ifname = interfaces[i].getName();
	  addAlternationEdge(ifname, name);
	  addInheritanceEdge(name, ifname);
	  addClass(interfaces[i], false, false);
	}
      } else {
	// Repetition class: just add the repetition edge, and
	// shortcut the inheritance hierarchy to avoid Collection.
	addRepetitionEdge(name, rep.getName());
	addAlternationEdge("java.lang.Object", name);
	addInheritanceEdge(name, "java.lang.Object");
      }
    }
  }

  /** Add a field as a construction edge. */
  public Part addConstructionEdge(Field field) {
    return addConstructionEdge(field.getDeclaringClass().getName(), field);
  }
  /** Add a no-args method as a construction edge. */
  public Part addConstructionEdge(Method method) {
    return addConstructionEdge(method.getDeclaringClass().getName(), method);
  }

  /** Add a field as a construction edge. */
  public Part addConstructionEdge(String source, Field field) {
    return addConstructionEdge(source, edgeLabel(field), edgeTarget(field));
  }
  /** Add a no-args method as a construction edge. */
  public Part addConstructionEdge(String source, Method method) {
    Part edge =
      addConstructionEdge(source, edgeLabel(method), edgeTarget(method));
    if (edge != null) edge.markDerived();
    return edge;
  }

  /** Add a class member as a construction edge. */
  public Part addConstructionEdge(String source, String label, Class target) {
    if (target.equals(Void.TYPE)) return null;
    while (target.isArray()) { // FIXME: aplib should allow array types?
      target = target.getComponentType();
    }
    namesClasses.put(target.getName(), target);
    Part ret = addConstructionEdge(source, label, target.getName());

    // Add the target and its superclasses/interfaces, but don't add
    // the fields or methods.  This prevents us from dragging in
    // everything from an external package; if the package later gets
    // added, then the fields and methods will get added.
    addClass(target, false, false);
    
    return ret;
  }

  static boolean isCollectionClass(Class cl) {
    // FIXME: what about Map?
    return
      Collection.class.isAssignableFrom(cl) ||
      java.util.Collection.class.isAssignableFrom(cl);
  }

  /** Check the special secret marker field to signify a repetition class. */
  static Class getRepetitionType(Class cl) {
    try {
      Field f = cl.getDeclaredField("_repeatedPart");
      int m = f.getModifiers();
      if (Modifier.isPrivate(m) && Modifier.isStatic(m))
	return f.getType();
    } catch (NoSuchFieldException e) {
    }
    return null;
  }

  /** Add a repetition edge from source to target. */
  public void addRepetitionEdge(String source, String target) {
    // The AP Library doesn't care if an edge has multiplicity, so
    // just add it as a construction edge with a distinguished label.
    addConstructionEdge(source, repetitionEdgeLabel(), target);
    // FIXME: add repetition edges to the aplib.cd package.
  }
  public static boolean isRepetitionEdge(EdgeI edge) {
    return edge.isConstructionEdge() &&
      edge.getLabel().equals(repetitionEdgeLabel());
  }
  static String repetitionEdgeLabel() { return "elements"; }

  /** The Class object corresponding to the node <code>o</code>. */
  public Class getNodeClass(Object node) {
    return (Class) namesClasses.get(String.valueOf(node));
  }

  /** The Class object corresponding to the node named <code>name</code>. */
  public Class getClassNamed(String name) {
    return (Class) namesClasses.get(name);
  }

  /** The slice of the object graph rooted at o determined by s. */
  public ObjectGraphSlice slice(Object o, Strategy s)
    { return new ObjectGraph(o, this).slice(s); }
  /** The slice of the object graph rooted at o determined by strategy s. */
  public ObjectGraphSlice slice(Object o, String s)
    { return new ObjectGraph(o, this).slice(s); }

  /** Traverse the object graph rooted at o along s, visiting v at
      each node and returning the value of v.getReturnValue() at the end
      of the traversal. */
  public Object traverse(Object o, Strategy s, Visitor v)
    { return slice(o, s).traverse(new Visitor[] { v }); }
  /** Traverse the object graph rooted at o along s, visiting the
      visitors in array v in sequence at each node and returning the value
      of v[0].getReturnValue() at the end of the traversal. */
  public Object traverse(Object o, Strategy s, Visitor[] v)
    { return slice(o, s).traverse(v); }
  /** Fetch the object in the object graph rooted at o corresponding
      to the target of s. */
  public Object fetch(Object o, Strategy s) throws FetchException
    { return slice(o, s).fetch(); }
  /** Gather into a list the objects in the object graph rooted at o
      corresponding to the target of s. */
  public List gather(Object o, Strategy s)
    { return slice(o, s).gather(); }
  /** A fixed-size List backed by the object graph rooted at o.  The
      elements of the list are the objects reachable by s in the object
      graph with the given root whose class is a target of s.  Throws
      TraversalSourceException if the type of root is not a source of the
      strategy.  Note that this list (like the List returned by
      Arrays.asList, but unlike the List returned by gather()) is
      write-through, i.e. modifying it will modify the underlying object
      graph, and vice versa. */
  public List asList(Object o, Strategy s)
    { return slice(o, s).asList(); }

  /** Traverse the object graph rooted at o along strategy s using v,
      returning the value of v.getReturnValue() at the end of the
      traversal. */
  public Object traverse(Object o, String s, Visitor v)
    { return slice(o, s).traverse(new Visitor[] { v }); }
  /** Traverse the object graph rooted at o along strategy s using
      visitors in array v in parallel, returning the value of
      v[0].getReturnValue() at the end of the traversal. */
  public Object traverse(Object o, String s, Visitor[] v)
    { return slice(o, s).traverse(v); }
  /** Fetch the object in the object graph rooted at o corresponding
      to the target of strategy s. */
  public Object fetch(Object o, String s) throws FetchException
    { return slice(o, s).fetch(); }
  /** Gather the objects in the object graph rooted at o corresponding
      to the target of strategy s. */
  public List gather(Object o, String s)
    { return slice(o, s).gather(); }
  /** A fixed-size List backed by the object graph rooted at o.  The
      elements of the list are the objects reachable by strategy s in the object
      graph with the given root whose class is a target of s.  Throws
      TraversalSourceException if the type of root is not a source of the
      strategy.  Note that this list (like the List returned by
      Arrays.asList, but unlike the List returned by gather()) is
      write-through, i.e. modifying it will modify the underlying object
      graph, and vice versa. */
  public List asList(Object o, String s)
    { return slice(o, s).asList(); }

  // EdgeI doesn't differentiate between derived and non-derived
  // edges, so we need to mangle & unmangle the label.

  static String edgeLabel(Field field)
  { return fieldEdgeLabel(field.getName()); }
  static String fieldEdgeLabel(String name)
  { return /* "f" + */ name; }
  static String edgeLabel(Method method)
  { return methodEdgeLabel(method.getName()); }
  static String methodEdgeLabel(String name)
  { return /* "m" + */ name; }
  boolean isDerivedEdge(EdgeI edge)
  { /* return edge.getLabel().startsWith("m"); */
    return edge instanceof Part && ((Part) edge).isDerived();
  }
  static String memberName(EdgeI edge)
  { return edge.getLabel() /* .substring(1) */; }

  static Class edgeTarget(Field field)
  { return field.getType(); }
  static Class edgeTarget(Method method)
  { return method.getReturnType(); }

  /** Testing stub. */
  public static void main(String args[]) {
    ClassGraph cg = new ClassGraph();
    cg.addPackage("edu.neu.ccs.demeter.dj");
    cg.addPackage("edu.neu.ccs.demeter.aplib");
    cg.addPackage("edu.neu.ccs.demeter.aplib.cd");
    System.out.println(cg);
  }
}
