package edu.neu.ccs.demeter.dj;

import java.util.*;
import java.lang.reflect.*;
import edu.neu.ccs.demeter.aplib.EdgeI;

public abstract class Visitor {
  /** This method is called when a traversal begins, before any nodes
      are visited.  The default behavior is to do nothing. */
  public void start() { }

  /** This method is called when a traversal ends, after all nodes
      have been visited.  The default behavior is to do nothing. */
  public void finish() { }

  /** This method is called on the first visitor (i.e. v[0]) after
      finish() has been called on all visitors in a traversal, and its
      return value is returned as the return value of the traversal.
      The default behavior is to do return null. */
  public Object getReturnValue() { return null; }

  /** This method is called when a traversal is about to traverse
      <code>obj</code> in the object graph with <code>cl</code> as the
      token.  Note that the same object might be passed to this method
      multiple times, once per class in its inheritance hierarchy.  The
      default behavior is to call
      {@link #invokeMethod(String, Object, Class)
      invokeMethod}<code>("before", obj, cl)</code>.
  */
  public void before(Object obj, Class cl) {
    invokeMethod("before", obj, cl);
  }

  /** This method is called when a traversal has finished traversing
      <code>obj</code> in the object graph with <code>cl</code> as the
      token.  Note that the same object might be passed to this method
      multiple times, once per class in its inheritance hierarchy.
      The default behavior is to call
      {@link #invokeMethod(String, Object, Class)
      invokeMethod}<code>("after", obj, cl)</code>.
  */
  public void after(Object obj, Class cl) {
    invokeMethod("after", obj, cl);
  }

  /** This method is called when a traversal is about to traverse an
      edge in the object graph from <code>source</code> to
      <code>target</code> corresponding to the class graph edge
      <code>edge</code> from <code>sourceClass</code> to
      <code>targetClass</code>.  The default behavior is to call
      {@link #invokeMethods(String, Object, Class, EdgeI, Object, Class)
      invokeMethods}<code>("before", source, sourceClass,
                           edge, target, targetClass)</code>.
  */
  public void before(Object source, Class sourceClass, EdgeI edge,
		     Object target, Class targetClass) {
    
    invokeMethods("before", source, sourceClass, edge, target, targetClass);
  }

  /** This method is called when a traversal has finished traversing an
      edge in the object graph from <code>source</code> to
      <code>target</code> corresponding to the class graph edge
      <code>edge</code> from <code>sourceClass</code> to
      <code>targetClass</code>.  The default behavior is to call
      {@link #invokeMethods(String, Object, Class, EdgeI, Object, Class)
      invokeMethods}<code>("before", source, sourceClass,
                           edge, target, targetClass)</code>.
  */
  public void after(Object source, Class sourceClass, EdgeI edge,
		    Object target, Class targetClass) {
    
    invokeMethods("after", source, sourceClass, edge, target, targetClass);
  }

  /** Invoke the methods on this class whose names start with
      <code>name</code> and whose signatures correspond to the class graph
      edge <code>edge</code>, passing <code>source</code> and
      <code>target</code> (and the edge label, if it's a construction
      edge) as arguments.

      <p>For a construction edge, the following signatures are matched,
      in order:
      <ol>
        <li> <i>n</i><code>_</code><i>l</i><code>(</code><i>S</i><code>, </code><i>T</i><code>)</code>
        <li> <i>n</i><code>_</code><i>l</i><code>(</code><i>S</i><code>, Object)</code>
        <li> <i>n</i><code>_</code><i>l</i><code>(Object, </code><i>T</i><code>)</code>
        <li> <i>n</i><code>_</code><i>l</i><code>(Object, Object)</code>
        <li> <i>n</i><code>(</code><i>S</i><code>, String, </code><i>T</i><code>)</code>
        <li> <i>n</i><code>(</code><i>S</i><code>, String, Object)</code>
        <li> <i>n</i><code>(Object, String, </code><i>T</i><code>)</code>
        <li> <i>n</i><code>(Object, String, Object)</code>
      </ol>
      where <i>n</i> is the value of <code>name</code>,
      <i>S</i> is the source type of the edge,
      <i>l</i> is the label of the edge, and
      <i>T</i> is the target type of the edge.
      For example, if an edge <code>-&gt;Employee,salary,Currency</code>
      is traversed, then first the visitor method
      <code>before_salary(Employee,Currency)</code> is invoked,
      if it exists, followed by
      <code>before_salary(Employee,Object)</code>, etc.      
  */
  public void invokeMethods(String name, Object source, Class sourceClass,
			    EdgeI edge, Object target, Class targetClass) {
    if (edge.isConstructionEdge()) {
      // FIXME: repetition edge

      String label = edge.getLabel();
      String longname = name + "_" + label;
      // FIXME: should it call all of these, or just the first match?
      // And is this the right order?
      invokeMethod(longname, source, sourceClass, target, targetClass);
      invokeMethod(longname, source, sourceClass, target, Object.class);
      invokeMethod(longname, source, Object.class, target, targetClass);
      invokeMethod(longname, source, Object.class, target, Object.class);
      invokeMethod(name, source, sourceClass, label, target, targetClass);
      invokeMethod(name, source, sourceClass, label, target, Object.class);
      invokeMethod(name, source, Object.class, label, target, targetClass);
      invokeMethod(name, source, Object.class, label, target, Object.class);
    } else {
      // FIXME: alternation or inheritance edge
    }
  }

  /** Invoke the method on this class named <code>name</code> with one
      parameter of type <code>cl</code>, passing <code>obj</code> as the
      argument. */
  public void invokeMethod(String name, Object obj, Class cl) {
    invokeMethod(name, new Object[] { obj }, new Class[] { cl });
  }

  /** Invoke the method on this class named <code>name</code> with two
      parameters of type <code>cl1</code> and <code>cl2</code>, passing
      <code>obj1</code> and <code>obj2</code> as the arguments. */
  void invokeMethod(String name, Object obj1, Class cl1,
		    Object obj2, Class cl2) {
    invokeMethod(name, new Object[] { obj1, obj2 }, new Class[] { cl1, cl2 });
  }

  /** Invoke the method on this class named <code>name</code> with
      three parameters of type <code>cl1</code>,
      <code>String</code>, and <code>cl2</code>, passing
      <code>obj1</code>, <code>str</code>, and <code>obj2</code> as the
      arguments. */
  void invokeMethod(String name, Object obj1, Class cl1,
		    String str, Object obj2, Class cl2) {
    invokeMethod(name, new Object[] { obj1, str, obj2 },
		 new Class[] { cl1, String.class, cl2 });
  }

  /** Invoke the method on this class named <code>name</code> with
      parameter types <code>paramTypes</code>, passing <code>args</code>
      as the arguments. */
  public void invokeMethod(String name, Object args[], Class paramTypes[]) {
    try {
      Method meth = getMethod(name, paramTypes);
      if (meth != null) {
	meth.setAccessible(true);
	meth.invoke(this, args);
      }
    } catch (SecurityException e) {
      // print something?
      return;
    } catch (IllegalAccessException e) {
      throw new RuntimeException("\n" + e);
    } catch (InvocationTargetException e) {
      throw new VisitorMethodException(e.getTargetException());
    }
  }

  /** Return the method on this class named <code>name</code> with
      parameter types <code>paramTypes</code>, or null if there is no such
      method. */
  public Method getMethod(final String name, final Class paramTypes[]) {
    // Memoize, using the visitor class + method name + param types as the key.

    final Class visitorClass = getClass();

    // it's easier to do equals and hashCode on Lists than arrays.
    final List paramTypesList = Arrays.asList(paramTypes);

    class Signature {
      public boolean equals(Object x) {
	if (!(x instanceof Signature)) return false;
	Signature s = (Signature) x;
	return s.getVisitorClass().equals(getVisitorClass())
	  && s.getName().equals(getName())
	  && s.getParamTypes().equals(getParamTypes());
      }
      public int hashCode() {
	return getVisitorClass().hashCode()
	  + getName().hashCode()
	  + getParamTypes().hashCode();
      }
      Class getVisitorClass() { return visitorClass; }
      String getName() { return name; }
      List getParamTypes() { return paramTypesList; }
      public String toString() {
	String paramTypesString = null;
	Iterator it = getParamTypes().iterator();
	while (it.hasNext()) {
	  String name = ((Class) it.next()).getName();
	  if (paramTypesString == null)
	    paramTypesString = name;
	  else
	    paramTypesString += ", " + name;
	}
	return getVisitorClass().getName()
	  + "." + getName()
	  + "(" + paramTypesString + ")";
      }
    }

    Signature key = new Signature();
    if (methods.containsKey(key)) return (Method) methods.get(key);

    Method method = getUnmemoizedMethod(name, paramTypes);
    methods.put(key, method);
    return method;
  }
  private static Map methods = new HashMap();

  /** Return the method on this class named <code>name</code> with
      parameter types <code>paramTypes</code>, or null if there is no such
      method. */
  protected Method getUnmemoizedMethod(String name, Class paramTypes[]) {
    // java.lang.Class.getMethod() doesn't do what we need -
    // it only returns public methods!  Bogus!

    for (Class vcl = getClass(); vcl != null; vcl = vcl.getSuperclass()) {
      // FIXME: This is expensive, because it has to fill in the stack
      // trace each time it throws an exception.  The alternative is
      // to use getDeclaredMethods and search through the list, but
      // that could be even more expensive if there are a lot of
      // methods.  Replace with hasDeclaredMethod if that's ever added
      // to Java.
      try {
	return vcl.getDeclaredMethod(name, paramTypes);
      } catch (NoSuchMethodException e) {
      }
    }
    return null;
  }
}
