/*******************************************************************************
 * Copyright (c) 2000, 2014 IBM Corporation and others.
 * 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 org.eclipse.jdt.core.tests.eval;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import junit.framework.TestCase;

import org.eclipse.jdt.core.tests.runtime.RuntimeConstants;
import org.eclipse.jdt.internal.eval.EvaluationConstants;
import org.eclipse.jdt.internal.eval.EvaluationResult;
import org.eclipse.jdt.internal.eval.InstallException;

import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.EventRequest;

@SuppressWarnings({ "unchecked", "rawtypes" })
public class JDIStackFrame implements EvaluationConstants, RuntimeConstants {
	VirtualMachine jdiVM;
	ThreadReference jdiThread;
	String userCode;
	String breakpointClassName;
	String breakpointMethodName;
	int breakpointLine;
	long timeout;
public JDIStackFrame(VirtualMachine jdiVM, DebugEvaluationTest test, String userCode) {
	this(jdiVM, test, userCode, "_JDIStackFrame_", "foo", Integer.MAX_VALUE);
}
public JDIStackFrame(
	VirtualMachine jdiVM,
	DebugEvaluationTest test,
	String userCode,
	String breakpointClassName,
	String breakpointMethodName,
	int breakpointLine) {
	
	this(jdiVM, test, userCode, breakpointClassName, breakpointMethodName, breakpointLine, 10000/*timeout*/);
	
}
public JDIStackFrame(
	VirtualMachine jdiVM,
	DebugEvaluationTest test,
	String userCode,
	String breakpointClassName,
	String breakpointMethodName,
	int breakpointLine,
	long timeout) {
	
	this.jdiVM = jdiVM;
	this.userCode = userCode;
	this.breakpointClassName = breakpointClassName;
	this.breakpointMethodName = breakpointMethodName;
	this.breakpointLine = breakpointLine;
	this.timeout = timeout;

	test.jdiStackFrame = null;
	this.jdiThread = getDebuggedThread(test);
	test.jdiStackFrame = this;
}
public char[] declaringTypeName() {
	if (this.breakpointLine != Integer.MAX_VALUE) { // if not in a code snippet
		StackFrame frame = getStackFrame();
		return frame.location().declaringType().name().toCharArray();
	}
	return null;
}
protected ThreadReference getDebuggedThread(DebugEvaluationTest test) {
	try {
		// desintall previous breakpoints
		this.jdiVM.eventRequestManager().deleteAllBreakpoints();

		// install a breakpoint at the breakpointLine
		List classes = this.jdiVM.classesByName(this.breakpointClassName);
		if (classes.size() == 0) {
			if (this.breakpointClassName.equals("_JDIStackFrame_")) {
				// install special class
				String source =
					"public class _JDIStackFrame_ {\n" +
					"  public int foo() {\n" +
					"    return -1;\n" +
					"  }\n" +
					"}";
				test.compileAndDeploy(source, "_JDIStackFrame_");
			}

			// force load of class
			test.evaluateWithExpectedDisplayString(
				("return Class.forName(\"" + this.breakpointClassName + "\");").toCharArray(),
				("class " + this.breakpointClassName).toCharArray()
			);
			classes = this.jdiVM.classesByName(this.breakpointClassName);
			if (classes.size() == 0) {
				// workaround bug in Standard VM
				Iterator iterator = this.jdiVM.allClasses().iterator();
				while (iterator.hasNext()) {
					ReferenceType type = (ReferenceType)iterator.next();
					if (type.name().equals(this.breakpointClassName)) {
						classes = new ArrayList(1);
						classes.add(type);
						break;
					}
				}
				if (classes.size() == 0) {
					throw new Error("JDI could not retrieve class for " + this.breakpointClassName);
				}
			}
		}
		ClassType clazz = (ClassType)classes.get(0);
		Method method = clazz.methodsByName(this.breakpointMethodName).get(0);
		Location location;
		if (this.breakpointLine < 0 || this.breakpointLine == Integer.MAX_VALUE) {
			location = method.location();
		} else {
			location = method.locationsOfLine(this.breakpointLine).get(0);
		}
		BreakpointRequest request = this.jdiVM.eventRequestManager().createBreakpointRequest(location);
		request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		request.enable();

		// create a new thread that hit the breakpoint
		EvaluationTest.Requestor requestor = test.new Requestor();
		try {
			test.resetEnv();
			test.context.evaluate(
				("(new Thread() {\n" +
				"  public void run() {\n" +
				     this.userCode + "\n" +
				(this.breakpointClassName.equals("_JDIStackFrame_") ? "    new _JDIStackFrame_().foo();\n" : "") +
				"  }\n" +
				"  public String toString() {\n" +
				"    return \"my thread\";\n" +
				"  }\n" +
				"}).start();\n").toCharArray(),
				test.getEnv(),
				test.getCompilerOptions(),
				requestor,
				test.getProblemFactory());
		} catch (InstallException e) {
			TestCase.assertTrue("Target exception " + e.getMessage(), false);
		}
		EvaluationResult[] results = requestor.results;
		for (int i = 0; i < requestor.resultIndex + 1; i++){
			if (results[i].hasErrors()) {
				TestCase.assertTrue("Compilation error in user code", false);
			}
		}

		// Wait for the breakpoint event
		Event event = null;
		do {
			EventSet set = this.jdiVM.eventQueue().remove();
			Iterator iterator = set.eventIterator();
			while (iterator.hasNext()) {
				event = (Event)iterator.next();
				if (event instanceof BreakpointEvent)
					break;
			}
		} while (!(event instanceof BreakpointEvent));

		// Return the suspended thread
		return ((BreakpointEvent)event).thread();
	} catch (AbsentInformationException e) {
		e.printStackTrace();
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return null;
}
protected StackFrame getStackFrame() {
	try {
		if (this.breakpointLine == Integer.MAX_VALUE) {
			return this.jdiThread.frame(1);
		}
		return this.jdiThread.frame(0);
	} catch (IncompatibleThreadStateException e) {
		e.printStackTrace();
		return null;
	}
}
public boolean isConstructorCall() {
	if (this.breakpointLine != Integer.MAX_VALUE) { // if not in a code snippet
		StackFrame frame = getStackFrame();
		return frame.location().method().isConstructor();
	}
	return false;
}
public boolean isStatic() {
	if (this.breakpointLine != Integer.MAX_VALUE) { // if not in a code snippet
		StackFrame frame = getStackFrame();
		return frame.location().method().isStatic();
	}
	return false;
}
public int[] localVariableModifiers() {
	try {
		StackFrame frame = getStackFrame();
		List variables = frame.visibleVariables();
		int[] modifiers = new int[variables.size()];
/*		Iterator iterator = variables.iterator();
		int i = 0;
		while (iterator.hasNext()) {
			LocalVariable var = (LocalVariable)iterator.next();
			// TBD modifiers[i++] = var. ??? ;
		}
*/		return modifiers;
	} catch (AbsentInformationException e) {
		return null;
	}
}
public char[][] localVariableNames() {
	try {
		StackFrame frame = getStackFrame();
		Iterator variables = frame.visibleVariables().iterator();
		Vector names = new Vector();
		while (variables.hasNext()) {
			LocalVariable var = (LocalVariable)variables.next();
			names.addElement(var.name().toCharArray());
		}
		char[][] result = new char[names.size()][];
		names.copyInto(result);
		return result;
	} catch (AbsentInformationException e) {
		return null;
	}
}
public char[][] localVariableTypeNames() {
	try {
		StackFrame frame = getStackFrame();
		Iterator variables = frame.visibleVariables().iterator();
		Vector names = new Vector();
		while (variables.hasNext()) {
			LocalVariable var = (LocalVariable)variables.next();
			names.addElement(var.typeName().toCharArray());
		}
		char[][] result = new char[names.size()][];
		names.copyInto(result);
		return result;
	} catch (AbsentInformationException e) {
		return null;
	}
}
public boolean run(String codeSnippetClassName) {
		ClassType codeSnippetClass;
		ObjectReference codeSnippet;
		Method method;
		List arguments;
		ObjectReference codeSnippetRunner;
		try {
			// Get the code snippet class
			long start = System.currentTimeMillis();
			List classes = this.jdiVM.classesByName(codeSnippetClassName);
			while (classes.size() == 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
				classes = this.jdiVM.classesByName(codeSnippetClassName);
				if (classes.size() == 0) {
					// workaround bug in Standard VM
					Iterator iterator = this.jdiVM.allClasses().iterator();
					while (iterator.hasNext()) {
						ReferenceType type = (ReferenceType)iterator.next();
						if (type.name().equals(codeSnippetClassName)) {
							classes = new ArrayList(1);
							classes.add(type);
							break;
						}
					}
					if (classes.size() == 0 && (System.currentTimeMillis()-start) > this.timeout) {
						return false;
					}
				}
			}
			codeSnippetClass = (ClassType)classes.get(0);

			// Create a new code snippet
			Method constructor = codeSnippetClass.methodsByName("<init>").get(0);
			codeSnippet = codeSnippetClass.newInstance(this.jdiThread, constructor, new ArrayList(), ClassType.INVOKE_SINGLE_THREADED);

			// Install local variables and "this" into generated fields
			StackFrame stackFrame = getStackFrame();
			try {
				Iterator variables = stackFrame.visibleVariables().iterator();
				while (variables.hasNext()) {
					LocalVariable jdiVariable = (LocalVariable)variables.next();
					Value value = stackFrame.getValue(jdiVariable);
					Field field = codeSnippetClass.fieldByName(new String(LOCAL_VAR_PREFIX) + jdiVariable.name());
					codeSnippet.setValue(field, value);
				}
			} catch (AbsentInformationException e) {
				// No variables
			}
			Field delegateThis = codeSnippetClass.fieldByName(new String(DELEGATE_THIS));
			ObjectReference thisObject;
			if (delegateThis != null && ((thisObject = stackFrame.thisObject()) != null)) {
				codeSnippet.setValue(delegateThis, thisObject);
			}

			// Get the code snippet runner
			ClassType codeSnippetRunnerClass = (ClassType)this.jdiVM.classesByName(CODE_SNIPPET_RUNNER_CLASS_NAME).get(0);
			Field theRunner = codeSnippetRunnerClass.fieldByName(THE_RUNNER_FIELD);
			codeSnippetRunner = (ObjectReference)codeSnippetRunnerClass.getValue(theRunner);

			// Get the method 'runCodeSnippet' and its arguments
			method = codeSnippetRunnerClass.methodsByName(RUN_CODE_SNIPPET_METHOD).get(0);
			arguments = new ArrayList();
			arguments.add(codeSnippet);
		} catch (ClassNotLoadedException e) {
			e.printStackTrace();
			return false;
		} catch (IncompatibleThreadStateException e) {
			e.printStackTrace();
			return false;
		} catch (InvalidTypeException e) {
			e.printStackTrace();
			return false;
		} catch (InvocationException e) {
			e.printStackTrace();
			return false;
		}

		try {
			// Invoke runCodeSnippet(CodeSnippet)
			codeSnippetRunner.invokeMethod(this.jdiThread, method, arguments, ClassType.INVOKE_SINGLE_THREADED);

			// Retrieve values of local variables and put them back in the stack frame
			StackFrame stackFrame = getStackFrame();
			try {
				Iterator variables = stackFrame.visibleVariables().iterator();
				while (variables.hasNext()) {
					LocalVariable jdiVariable = (LocalVariable)variables.next();
					Field field = codeSnippetClass.fieldByName(new String(LOCAL_VAR_PREFIX) + jdiVariable.name());
					Value value = codeSnippet.getValue(field);
					stackFrame.setValue(jdiVariable, value);
				}
			} catch (AbsentInformationException e) {
				// No variables
			}
		} catch (ClassNotLoadedException e) {
			e.printStackTrace();
		} catch (IncompatibleThreadStateException e) {
			e.printStackTrace();
		} catch (InvalidTypeException e) {
			e.printStackTrace();
		} catch (InvocationException e) {
			e.printStackTrace();
		}
		return true;
	}
}
