package com.hammurapi.common;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * This class creates a proxy which record all invocations of 
 * the methods of the target object with stack traces, calling threads, timestamps, and arguments.
 * These records can be used for troubleshooing of concurrency problems.
 * @author Pavel Vlasov
 *
 */
public class InvocationRecordingProxyFactory {
	
	/**
	 * Callback interface to receive problem notifications.
	 * @author Pavel Vlasov
	 *
	 */
	public interface ProblemListener {
		
		void onProblem(Throwable th, List<InvocationRecord> records);
	}
	
	public static class InvocationRecord {
		
		@Override
		public String toString() {
			return "InvocationRecord("+method.getName()+") [thread=" + thread + ", method=" + method
					+ ", timestamp=" + timestamp + ", arguments="
					+ Arrays.toString(arguments) + ", target=" + target + "]";
		}

		private Thread thread;
		private StackTraceElement[] stackTrace;
		private Method method;
		private long timestamp;
		private Object[] arguments;
		private Object target;
				
		InvocationRecord(Thread thread, StackTraceElement[] stackTrace, Method method, long timestamp, Object[] arguments, Object target) {
			super();
			this.thread = thread;
			this.stackTrace = stackTrace;
			this.method = method;
			this.timestamp = timestamp;
			if (arguments != null) {
				this.arguments = Arrays.copyOf(arguments, arguments.length);
			}
			this.target = target;
		}

		public Thread getThread() {
			return thread;
		}
		
		public StackTraceElement[] getStackTrace() {
			return stackTrace;
		}
		
		public Method getMethod() {
			return method;
		}
		
		public long getTimestamp() {
			return timestamp;
		}
		
		Object[] getArguments() {
			return arguments;
		}
		
		Object getTarget() {
			return target;
		}
		
	}
	
	private static class RecordingInvocationHandler implements InvocationHandler {
		
		private ProblemListener problemListener;

		RecordingInvocationHandler(Object target, ProblemListener problemListener, LinkedList<InvocationRecord> records) {
			this.target = target;
			this.problemListener = problemListener;
			this.records = records;
		}
		
		private Object target;
		
		private LinkedList<InvocationRecord> records;

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			
			try {
				synchronized (records) {
					Thread currentThread = Thread.currentThread();
					records.addFirst(new InvocationRecord(currentThread, currentThread.getStackTrace(), method, System.currentTimeMillis(), args, target));
				}
				Object ret = method.invoke(target, args);
				Class<?> returnType = method.getReturnType();
				if (returnType.isInterface()) {
					return Proxy.newProxyInstance(returnType.getClass().getClassLoader(), new Class[] {returnType}, new RecordingInvocationHandler(ret, problemListener, records));
				} 
				return ret;
			} catch (Throwable th) {
				if (problemListener!=null) {
					problemListener.onProblem(th, records);
				}
				throw th;
			}
		}
		
		List<InvocationRecord> getRecords() {
			return records;
		}
		
	}

	/**
	 * Wraps target into a proxy for method call recording.
	 * @param <I>
	 * @param <T>
	 * @param target
	 * @param problemListener 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <I, T extends I> I wrap(Class<I> proxyType, T target, ProblemListener problemListener) {
		return (I) Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[] {proxyType}, new RecordingInvocationHandler(target, problemListener, new LinkedList<InvocationRecord>()));
	}
	
	public static List<InvocationRecord> getInvocationRecords(Object proxy) {
		return ((RecordingInvocationHandler) Proxy.getInvocationHandler(proxy)).getRecords();		
	}
	
}
