package edu.mit.ai.psg.traveler.jevaHooks;

import edu.mit.ai.psg.jeva.*;
import edu.mit.ai.psg.traveler.*;
import java.lang.reflect.*;
import java.util.*;
// main (example of use):
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import edu.mit.ai.psg.utilities.*;
import edu.mit.ai.psg.jevaUI.*;
import edu.mit.ai.psg.strings.Stringify;

/** A Jeva EvalHook which calls Trace methods to record method call and
    instance creation expressions, and records activities which use
    interpreted synchronized methods and blocks in the biography for
    the synchronizing object.

    @see EvalMethods
    @see Trace

    @author CarlManning, caroma@ai.mit.edu<br>
    Copyright (c) 1999 Massachusetts Institute of Technology
**/
public class TraceCallsEvalHook extends JevaVisitorBase {
  public TraceCallsEvalHook() {}
  /** default -- do nothing **/
  public Object visit(IJevaNode node, Object data) { 
    return null; }

  /** {@link Trace} calls to compiled methods,
      (Calls to interpreted methods are traced by
      {@link #visit(JNDMethodDeclaration,Object)}). **/
  public Object visit(JNEMethodInvocationExpression node, Object data) {
    IMethod iMethodSignature = node.getMethodSignature();
    if (data instanceof EvalMethods.PreApplyData) {
      Object[] values = ((EvalMethods.PreApplyData)data).getValues();
      if (Modifier.isStatic(iMethodSignature.getModifiers())) { 
	if (iMethodSignature instanceof WrappedMethod) {
	  Method method = ((WrappedMethod)iMethodSignature).getWrappedMethod();
	  pushRecord(Trace.invokingStatic(method, values));
	} else { // else visit(JNDMethodDeclaration will record it.
	  pushRecord(null); // did not create record
	}
      } else { //instance method
	Object target = values[0]; // target can't be null?
	Method invokedCompiledMethod =
	  (null == target?
	   null : findInvokedCompiledMethod(target, iMethodSignature));
	if (invokedCompiledMethod != null) { 
	  Object[] argValues = new Object[values.length - 1];
	  System.arraycopy(values, 1, argValues, 0, argValues.length);
	  pushRecord(Trace.invoking(target, invokedCompiledMethod, argValues));
	} else {// else visit(JNDMethodDeclaration will record it.
	  pushRecord(null);
	}
      }
    } else if (data instanceof EvalMethods.EvalHookPostData) {
      CallRecord record = (CallRecord) popRecord();
      if (record != null) { // exit record created earlier
	if (data instanceof EvalMethods.ResultData) {
	  if (record.getMember() instanceof Method &&
	      void.class.equals(((Method)record.getMember()).getReturnType()))
	    Trace.returningVoid(record);
	  else 
	    Trace.returning(record,((EvalMethods.ResultData)data).getResult());
	} else if (data instanceof EvalMethods.AbruptData) {
	  Trace.throwing(record, ((EvalMethods.AbruptData)data).getThrown());
	}
      }
    }
    return null;
  }
  /** {@link Trace} calls received by interpreted methods.
      (Calls invoking compiled methods are traced by
      {@link #visit(JNEMethodInvocationExpression,Object)} **/
  public Object visit(JNDMethodDeclaration node, Object data) {
    if (data instanceof EvalMethods.EnteredBodyData) {
      Object target = ((EvalMethods.BodyData) data).getTarget();
      Method method = node.getMethod().getProxyMethod();
      Object[] parameters =
	node.getParametersNode().getParameterValues(((EvalMethods.BodyData)
						     data).getEnv());
      pushRecord(Trace.received(target, method, parameters));
    } else if (data instanceof EvalMethods.ExittingBodyData) {
      CallRecord record = (CallRecord) popRecord();
      if (data instanceof EvalMethods.ExittingBodyAbruptlyData) {
	AbruptCompletionException completionException =
	  ((EvalMethods.ExittingBodyAbruptlyData)data).getThrown();
	if (completionException instanceof ReturnException) {
	  if (TypeMethods.voidIClass.equals(node.getMethod().getReturnType()))
	    Trace.returningVoid(record);
	  else Trace.returning(record, ((ReturnException)
					completionException).getValue());
	} else if (completionException instanceof ThrowException) {
	  Trace.throwing(record, ((ThrowException)
				  completionException).getThrown());
	} else throw new Error("Shouldn't happen --- "+
			       "Illegal abrupt completion from method: "+
			       completionException);
      } else { // ran thru end of void method
	Trace.returningVoid(record);
      }
    }
    return null;
  }
  /** {@link Trace} calls to compiled constructors.
      (calls received by interpreted constructors are recorded by
      {@link #visit(JNDConstructorDeclaration,Object)}. */
  public Object visit(JNEInstanceCreationExpression node, Object data) {
    if (data instanceof EvalMethods.PreApplyData) {
      Object[] values = ((EvalMethods.PreApplyData)data).getValues();
      IConstructor iConstructor = node.getConstructor();
      if (iConstructor instanceof WrappedConstructor) { 
	Constructor constructor =
	  ((WrappedConstructor)iConstructor).getWrappedConstructor();
	pushRecord(Trace.invokingStatic(constructor, values));
      } else pushRecord(null); // will be recorded by receiver.
    } else if (data instanceof EvalMethods.EvalHookPostData) {
      CallRecord record = (CallRecord) popRecord();
      if (record != null) { 
	if (data instanceof EvalMethods.ResultData) {
	  Trace.returning(record, ((EvalMethods.ResultData)data).getResult());
	} else if (data instanceof EvalMethods.AbruptData) {
	  Trace.throwing((CallRecord) Trace.getCurrentRecord(),
			 ((EvalMethods.AbruptData)data).getThrown());
	}
      }
    }
    return null;
  }
  /** {@link Trace} calls received by interpreted constructors.
      (Calls invoking compiled methods are traced by
      {@link #visit(JNEInstanceCreationExpression,Object)} **/
  public Object visit(JNDConstructorDeclaration node, Object data) {
    if (data instanceof EvalMethods.EnteredBodyData) {
      Object target = ((EvalMethods.BodyData) data).getTarget();
      Constructor constructor = node.getConstructor().getProxyConstructor();
      Object[] parameters =
	node.getParametersNode().getParameterValues(((EvalMethods.BodyData)
						     data).getEnv());
      pushRecord(Trace.received(target, constructor, parameters));
    } else if (data instanceof EvalMethods.ExittingBodyData) {
      CallRecord record = (CallRecord) popRecord();
      if (data instanceof EvalMethods.ExittingBodyAbruptlyData) {
	AbruptCompletionException completionException =
	  ((EvalMethods.ExittingBodyAbruptlyData)data).getThrown();
	if (completionException instanceof ReturnException) {
	  Trace.returningVoid(record);
	} else if (completionException instanceof ThrowException) {
	  Trace.throwing(record, ((ThrowException)
				  completionException).getThrown());
	} else throw new Error("Shouldn't happen --- "+
			       "Illegal abrupt completion from constructor: "+
			       completionException);
      } else { // ran thru end of constructor
	Trace.returningVoid(record);
      }
    }
    return null;
  }


  /** {@link Trace} entry and exit from synchronized blocks. **/
  public Object visit(JNSSynchronizedStatement node, Object data) {
    if (data instanceof EvalMethods.EnteredBodyData) {
      Object target = ((EvalMethods.BodyData)data).getTarget();
      // find method or constructor enclosing this synchronzed block
      Node parent = node.jjtGetParent();
      while (parent != null &&
	     ! (parent instanceof JNDMethodDeclaration) &&
	     ! (parent instanceof JNDConstructorDeclaration))
	parent = parent.jjtGetParent();
      Member member = 
	(parent instanceof JNDMethodDeclaration ? 
	 (Member) ((JNDMethodDeclaration)parent).getMethod().getProxyMethod() :
	 parent instanceof JNDConstructorDeclaration ?
	 (Member) ((JNDConstructorDeclaration)
		   parent).getConstructor().getProxyConstructor() : null );
      pushRecord(Trace.beganSynchronization(target, member));
    } else if (data instanceof EvalMethods.ExittingBodyData) {
      Trace.exitting(popRecord());
    }
    return null;
  }


  /** map class -> (map methodSignature -> method) **/
  public static Map invokedMethodCache =
    Collections.synchronizedMap(new WeakHashMap());
  /** returns null if interpreted method is invoked, else returns
      compiled method that is invoked for target (may override compile time
      signature) **/
  public static Method findInvokedCompiledMethod(Object target,
						 IMethod iMethodSignature){
    if (iMethodSignature instanceof InterpretedMethod) {
      // iMethodSignature is interpreted.  Interpreted classes and interfaces
      // can't have compiled implementations (no compiled
      // extending/implementing classes)
      return null;
    } else {
      // iMethodSignature is of compiled class.  It must be implemented by
      // compiled method if it cannot be overridden.
      int modifiers = iMethodSignature.getModifiers();
      if (Modifier.isStatic(modifiers) || Modifier.isPrivate(modifiers) ||
	  Modifier.isFinal(modifiers)) {
	return ((WrappedMethod)iMethodSignature).getWrappedMethod();
      } else {
	// iMethodSignature's implementation depends on target's class.
	// The actual method must be found in the target's class hierarchy.
	Class targetClass = target.getClass();
	synchronized (invokedMethodCache) { 
	  // First look in cache.
	  Map classMap = (Map) invokedMethodCache.get(targetClass);
	  if (classMap != null) {
	    Object methodEntry = classMap.get(iMethodSignature);
	    if (methodEntry instanceof Method) return (Method) methodEntry;
	    else if (methodEntry == Boolean.FALSE) return null;
	    // else no entry for signature... fall thru
	  } else { // no classMap ... prepare for adding new entry
	    classMap = new WeakHashMap(10);
	    invokedMethodCache.put(targetClass, classMap);
	  }
	  // entry not found, so compute new entry.
	  Method methodSignature =
	    ((WrappedMethod)iMethodSignature).getWrappedMethod();
	  String methodName = methodSignature.getName();
	  Class[] methodParameterTypes = methodSignature.getParameterTypes();
	  boolean foundInterpretedMethod = false;
	  Method invokedMethod = null;
	  for (Class aClass = targetClass; aClass != null;
	       aClass = aClass.getSuperclass()) {
	    try { //throws if not found
	      invokedMethod =
		aClass.getDeclaredMethod(methodName, methodParameterTypes);
	      foundInterpretedMethod = 
		IInterpretedObject.class.isAssignableFrom(aClass);
	      break;
	    } catch(NoSuchMethodException e){}
	  }
	  if (invokedMethod == null)
	    throw new Error("Shouldn't happen:  "+targetClass+
			    "has no implementation for "+methodSignature);
	  // if found method was Interpreted, put FALSE, else store method
	  classMap.put(iMethodSignature,
		       foundInterpretedMethod?
		       // cast works around jikes bug
		       (Object) Boolean.FALSE : invokedMethod);
	  return foundInterpretedMethod? null : invokedMethod;
	}
      }
    }
  }

  /** thread local stack of data created by this evalhook, so invocations of 
      hook can pass data to later invocation.  **/
  private ThreadLocal localStack = new ThreadLocal() {
    public Object initialValue() { return new Stack(); }};
  /** save data on a thread-local stack **/
  private void pushRecord(ActivityRecord record) { 
    ((Stack)localStack.get()).push(record);
  }
  /** retrieve data from a thread-local stack **/
  private ActivityRecord popRecord() { 
    return (ActivityRecord) ((Stack)localStack.get()).pop();
  }
  /** Given a class name for a class in classpath, 
      runs its public static void main(...) method with a TraceCallsEvalHook.
      Jeva will run compiled .class file if it exists, so delete it first.
      @see EvalMethods
  **/
  public static void main(String[] arguments) throws FileNotFoundException {
    int argIndex = 0;
    boolean doExamine = false, doRepl = false;
    if (arguments.length == 0) usageExit();
    for (; argIndex < arguments.length &&
	   arguments[argIndex].length() > 0 &&
	   arguments[argIndex].charAt(0) == '+'; argIndex++) {
      if ("+examine".equalsIgnoreCase(arguments[argIndex])) doExamine = true;
      else if ("+repl".equalsIgnoreCase(arguments[argIndex])) doRepl = true;
      else usageExit();
    }
    final String className = arguments[argIndex++];
    final String[] programArguments = new String[arguments.length - argIndex];
    System.arraycopy(arguments,argIndex, programArguments,0,
		     programArguments.length);
    ActivityRecord record = Trace.beginning("TracingCalls");

    EvalMethods.setEvalHook(new TraceCallsEvalHook());
    CompileUnit.setTempRoot(new File("./proxies")); //reuse proxy classes (opt)

    Thread programThread = new Thread(new Runnable() {
      public void run() { 
	try { 
	  Jeva.runClass(className, programArguments);
	}
	catch (ThrowException e) { } // continue on program exceptions
	catch (ParseException e) { System.err.println(e); return; }
	catch (ClassNotFoundException e) { System.err.println(e); return; }
	catch (Throwable e) { e.printStackTrace(); return; }
      }});
    programThread.start();
    // ensure at least one first level child is available when first displayed
    while(record.getChildren().isEmpty() && programThread.isAlive())
      Thread.yield();
    if (doExamine) { 
      TravelerOutliner.ensureInitialized();
      java.awt.Window frame =
	TravelerOutliner.makeFrame("Recording. (close this window to exit)",
				   TravelerOutliner.makeOutlineNode(record));
      frame.addWindowListener(new WindowAdapter() {
				public void windowClosing(WindowEvent e) {
				  System.exit(0); }});
      frame.show();
    }
    if (doRepl) { 
      try { 
	JevaGUI.repl
	  (0,
	   ("Record of "+className+
	    ".main("+Stringify.printToString(programArguments)+")"),
	   "record: "+record,
	   null, null, 
	   Jeva.extendEnv
	   (ActivityRecord.class, "record", record,
	    Jeva.makeDefaultEnv()));
      } catch (Throwable t) {}
    }
  }
  private static void usageExit() { 
    System.err.println
      ("Use: java edu.mit.ai.psg.traveler.jevaHooks.TraceCallsEvalHook "+
       "[+examine] [+repl] ClassName [args]");
    System.exit(1);
  } 
}
