/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.eval;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.eval.ICodeSnippetRequestor;
import org.eclipse.jdt.core.eval.IEvaluationContext;
import org.eclipse.jdt.debug.core.IEvaluationRunnable;
import org.eclipse.jdt.debug.core.IJavaClassObject;
import org.eclipse.jdt.debug.core.IJavaClassType;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaStackFrame;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;
import org.eclipse.jdt.debug.core.JDIDebugModel;
import org.eclipse.jdt.debug.eval.IClassFileEvaluationEngine;
import org.eclipse.jdt.debug.eval.IEvaluationEngine;
import org.eclipse.jdt.debug.eval.IEvaluationListener;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.JavaDebugUtils;
import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget;
import org.eclipse.jdt.internal.debug.core.model.JDIValue;

import com.sun.jdi.InvocationException;
import com.sun.jdi.ObjectReference;

/**
 * An evaluation engine that deploys class files locally
 */

public class LocalEvaluationEngine implements IClassFileEvaluationEngine,
		ICodeSnippetRequestor, IEvaluationRunnable {

	private static final String CODE_SNIPPET_NAME = "CodeSnippet.class"; //$NON-NLS-1$

	/**
	 * A count of the number of engines created. Count is incremented on
	 * instantiation and decremented on dispose. When the count == 0, the
	 * special CodeSnippet.class is deleted as this class file is shared by all.
	 */
	private static int ENGINE_COUNT = 0;

	/**
	 * The Java project context in which to compile snippets.
	 */
	private IJavaProject fJavaProject;

	/**
	 * The debug target on which to execute snippets
	 */
	private IJavaDebugTarget fDebugTarget;

	/**
	 * The location in which to deploy snippet class files
	 */
	private File fOutputDirectory;

	/**
	 * The listener to notify when the current evaluation is complete.
	 */
	private IEvaluationListener fListener;

	/**
	 * The stack frame context for the current evaluation or <code>null</code>
	 * if there is no stack frame context.
	 */
	private IJavaStackFrame fStackFrame;

	/**
	 * The result of this evaluation
	 */
	private EvaluationResult fResult;

	/**
	 * Collection of deployed snippet class files
	 */
	private List<File> fSnippetFiles;

	/**
	 * Collection of directories created by this evaluation engine.
	 */
	private List<File> fDirectories;

	/**
	 * Evaluation context for the Java project associated with this evaluation
	 * engine.
	 */
	private IEvaluationContext fEvaluationContext;

	/**
	 * Array of modifier constants for visible local variables in the current
	 * evaluation.
	 *
	 * XXX: constants should be 'default' or 'final'. Where are these constants
	 * defined.
	 */
	private int[] fLocalVariableModifiers;

	/**
	 * Array of names of visible local variables in the current evaluation.
	 */
	private String[] fLocalVariableNames;

	/**
	 * Array of type names of visible local variables in the current evaluation.
	 */
	private String[] fLocalVariableTypeNames;

	/**
	 * The 'this' object for the current evaluation or <code>null</code> if
	 * there is no 'this' context (static method, or not context)
	 */
	private IJavaObject fThis;

	/**
	 * Whether this engine has been disposed.
	 */
	private boolean fDisposed = false;

	/**
	 * The number of evaluations currently being performed.
	 */
	private int fEvaluationCount = 0;

	/**
	 * The name of the code snippet class to instantiate
	 */
	private String fCodeSnippetClassName = null;

	/**
	 * Whether to hit breakpoints in the evaluation thread
	 */
	private boolean fHitBreakpoints = false;

	/**
	 * Constant for empty array of <code>java.lang.String</code>
	 */
	private static final String[] EMPTY_STRING_ARRAY = new String[0];

	/**
	 * Constant for empty array of <code>int</code>
	 */
	private static final int[] EMPTY_INT_ARRAY = new int[0];

	/**
	 * Constructs a new evaluation engine for the given VM in the context of the
	 * specified project. Class files required for the evaluation will be
	 * deployed to the specified directory (which must be on the class path of
	 * the VM in order for evaluation to work).
	 *
	 * @param project
	 *            context in which to compile snippets
	 * @param vm
	 *            debug target in which to evaluate snippets
	 * @param directory
	 *            location where snippet class files will be deployed for
	 *            execution. The directory must exist
	 */
	public LocalEvaluationEngine(IJavaProject project, IJavaDebugTarget vm,
			File directory) {
		setJavaProject(project);
		setDebugTarget(vm);
		setOutputDirectory(directory);
		ENGINE_COUNT++;
	}

	/**
	 * @see ICodeSnippetRequestor#acceptClassFiles(byte[][], String[][], String)
	 */
	@Override
	public boolean acceptClassFiles(byte[][] classFileBytes,
			String[][] classFileCompoundNames, String codeSnippetClassName) {
		try {
			deploy(classFileBytes, classFileCompoundNames);
		} catch (DebugException e) {
			getResult().setException(e);
			return false;
		}
		if (codeSnippetClassName != null) {
			setCodeSnippetClassName(codeSnippetClassName);
			try {
				getThread().runEvaluation(this, null, DebugEvent.EVALUATION,
						getHitBreakpoints());
			} catch (DebugException e) {
				// exception handling is in evaluation runnable
			}
		}
		return true;
	}

	@Override
	public void run(IJavaThread thread, IProgressMonitor monitor) {
		IJavaObject codeSnippetInstance = null;
		try {
			codeSnippetInstance = newInstance(getCodeSnippetClassName());
			initializeLocals(codeSnippetInstance);
			codeSnippetInstance.sendMessage(RUN_METHOD,	"()V", null, getThread(), false); //$NON-NLS-1$
			restoreLocals(codeSnippetInstance);

			// now retrieve the description of the result
			IVariable[] fields = codeSnippetInstance.getVariables();
			IJavaVariable resultValue = null;
			IJavaVariable resultType = null;
			for (IVariable field : fields) {
				if (field.getName().equals(RESULT_TYPE_FIELD)) {
					resultType = (IJavaVariable) field;
				}
				if (field.getName().equals(RESULT_VALUE_FIELD)) {
					resultValue = (IJavaVariable) field;
				}
			}
			IJavaValue result = convertResult((IJavaClassObject) resultType.getValue(), (IJavaValue) resultValue.getValue());
			getResult().setValue(result);
		} catch (DebugException e) {
			getResult().setException(e);

			Throwable underlyingException = e.getStatus().getException();
			if (underlyingException instanceof InvocationException) {
				ObjectReference theException = ((InvocationException) underlyingException)
						.exception();
				if (theException != null) {
					try {
						try {
							IJavaObject v = (IJavaObject) JDIValue.createValue(
									(JDIDebugTarget) getDebugTarget(),
									theException);
							v.sendMessage(
									"printStackTrace", "()V", null, getThread(), false); //$NON-NLS-2$ //$NON-NLS-1$
						} catch (DebugException de) {
							JDIDebugPlugin.log(de);
						}
					} catch (RuntimeException re) {
						JDIDebugPlugin.log(re);
					}
				}
			}
		}

	}

	/**
	 * Initializes the value of instance variables in the 'code snippet object'
	 * that are used as place-holders for locals and 'this' in the current stack
	 * frame.
	 *
	 * @param object
	 *            instance of code snippet class that will be run
	 * @exception DebugException
	 *                if an exception is thrown accessing the given object
	 */
	protected void initializeLocals(IJavaObject object) throws DebugException {
		IJavaVariable[] locals = null;
		IJavaObject thisObject = getThis();
		if (getStackFrame() != null) {
			locals = getStackFrame().getLocalVariables();
		}
		if (locals != null) {
			for (IJavaVariable local : locals) {
				IJavaVariable field = object.getField(
						LOCAL_VAR_PREFIX + local.getName(), false);
				// internal error if field is not found
				if (field == null) {
					throw new DebugException(
							new Status(
									IStatus.ERROR,
									JDIDebugModel.getPluginIdentifier(),
									DebugException.REQUEST_FAILED,
									EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__4,
									null));
				}
				field.setValue(local.getValue());
			}
		}
		if (thisObject != null) {
			IJavaVariable field = object.getField(DELEGATE_THIS, false);
			// internal error if field is not found
			if (field == null) {
				throw new DebugException(
						new Status(
								IStatus.ERROR,
								JDIDebugModel.getPluginIdentifier(),
								DebugException.REQUEST_FAILED,
								EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize___this___context__5,
								null));
			}
			field.setValue(thisObject);
		}
	}

	/**
	 * Restores the value local variables from the instance variables in the
	 * 'code snippet object' that are used as place-holders for locals in the
	 * current stack frame.
	 *
	 * @param object
	 *            instance of code snippet class that was run
	 * @exception DebugException
	 *                if an exception is thrown accessing the given object
	 */
	protected void restoreLocals(IJavaObject object) throws DebugException {
		IJavaVariable[] locals = null;
		if (getStackFrame() != null) {
			locals = getStackFrame().getLocalVariables();
		}
		if (locals != null) {
			for (IJavaVariable local : locals) {
				IJavaVariable field = object.getField(
						LOCAL_VAR_PREFIX + local.getName(), false);
				// internal error if field is not found
				if (field == null) {
					throw new DebugException(
							new Status(
									IStatus.ERROR,
									JDIDebugModel.getPluginIdentifier(),
									DebugException.REQUEST_FAILED,
									EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__6,
									null));
				}
				local.setValue(field.getValue());
			}
		}
	}

	/**
	 * @see ICodeSnippetRequestor#acceptProblem(IMarker, String, int)
	 */
	@Override
	public void acceptProblem(IMarker problemMarker, String fragmentSource,
			int fragmentKind) {
		if (problemMarker.getAttribute(IMarker.SEVERITY, -1) != IMarker.SEVERITY_ERROR) {
			return;
		}
		getResult().addError(problemMarker.getAttribute(IMarker.MESSAGE, "")); //$NON-NLS-1$
	}

	/**
	 * @see IEvaluationEngine#getDebugTarget()
	 */
	@Override
	public IJavaDebugTarget getDebugTarget() {
		return fDebugTarget;
	}

	/**
	 * Sets the debug target in which snippets are executed.
	 *
	 * @param debugTarget
	 *            the debug target in which snippets are executed
	 */
	private void setDebugTarget(IJavaDebugTarget debugTarget) {
		fDebugTarget = debugTarget;
	}

	/**
	 * @see IEvaluationEngine#getJavaProject()
	 */
	@Override
	public IJavaProject getJavaProject() {
		return fJavaProject;
	}

	/**
	 * Sets the Java project in which snippets are compiled.
	 *
	 * @param javaProject
	 *            the Java project in which snippets are compiled
	 */
	private void setJavaProject(IJavaProject javaProject) {
		fJavaProject = javaProject;
	}

	/**
	 * Returns the directory in which snippet class files are deployed.
	 *
	 * @return the directory in which snippet class files are deployed.
	 */
	public File getOutputDirectory() {
		return fOutputDirectory;
	}

	/**
	 * Sets the directory in which snippet class files are deployed.
	 *
	 * @param outputDirectory
	 *            location to deploy snippet class files
	 */
	private void setOutputDirectory(File outputDirectory) {
		fOutputDirectory = outputDirectory;
	}

	/**
	 * @see IClassFileEvaluationEngine#evaluate(String, IJavaThread,
	 *      IEvaluationListener)
	 */
	@Override
	public void evaluate(String snippet, IJavaThread thread,
			IEvaluationListener listener, boolean hitBreakpoints)
			throws DebugException {
		checkDisposed();
		checkEvaluating();
		try {
			evaluationStarted();
			setListener(listener);
			setHitBreakpoints(hitBreakpoints);
			setResult(new EvaluationResult(this, snippet, thread));
			checkThread();
			// no receiver/stack frame context
			setThis(null);
			setLocalVariableNames(EMPTY_STRING_ARRAY);
			setLocalVariableTypeNames(EMPTY_STRING_ARRAY);
			setLocalVariableModifiers(EMPTY_INT_ARRAY);

			// do the evaluation in a different thread
			Runnable r = new Runnable() {
				@Override
				public void run() {
					try {
						LocalEvaluationEngine.this
								.getEvaluationContext()
								.evaluateCodeSnippet(
										LocalEvaluationEngine.this.getSnippet(),
										LocalEvaluationEngine.this, null);
					} catch (JavaModelException e) {
						LocalEvaluationEngine.this.getResult().setException(
								new DebugException(e.getStatus()));
					} finally {
						LocalEvaluationEngine.this.evaluationComplete();
					}
				}
			};

			Thread t = new Thread(r);
			t.setDaemon(true);
			t.start();
		} catch (DebugException d) {
			evaluationAborted();
			throw d;
		}

	}

	/**
	 * @see IEvaluationEngine#evaluate(String, IJavaStackFrame,
	 *      IEvaluationListener, int)
	 */
	@Override
	public void evaluate(String snippet, IJavaStackFrame frame,
			IEvaluationListener listener, int evaluationDetail,
			boolean hitBreakpoints) throws DebugException {
		checkDisposed();
		checkEvaluating();
		try {
			evaluationStarted();
			setListener(listener);
			setStackFrame(frame);
			setHitBreakpoints(hitBreakpoints);
			setResult(new EvaluationResult(this, snippet,
					(IJavaThread) frame.getThread()));
			checkThread();

			// set up local variables and 'this' context for evaluation
			IJavaVariable[] locals = frame.getLocalVariables();

			List<String> typeNames = new ArrayList<>(locals.length);
			List<String> varNames = new ArrayList<>(locals.length);

			for (IJavaVariable var : locals) {
				String typeName = getTranslatedTypeName(var
						.getReferenceTypeName());
				if (typeName != null) {
					typeNames.add(typeName);
					varNames.add(var.getName());
				}
			}

			setLocalVariableTypeNames(typeNames
					.toArray(new String[typeNames.size()]));
			setLocalVariableNames(varNames
					.toArray(new String[varNames.size()]));
			int[] modifiers = new int[typeNames.size()];
			// cannot determine if local is final, so specify as default
			Arrays.fill(modifiers, 0);
			setLocalVariableModifiers(modifiers);
			setThis(frame.getThis());

			final boolean isStatic = frame.isStatic();
			final boolean isConstructor = frame.isConstructor();
			final IType receivingType = JavaDebugUtils
					.resolveDeclaringType(frame);
			validateReceivingType(receivingType);

			// do the evaluation in a different thread
			Runnable r = new Runnable() {
				@Override
				public void run() {
					try {
						LocalEvaluationEngine.this
								.getEvaluationContext()
								.evaluateCodeSnippet(
										LocalEvaluationEngine.this.getSnippet(),
										LocalEvaluationEngine.this
												.getLocalVariableTypeNames(),
										LocalEvaluationEngine.this
												.getLocalVariableNames(),
										LocalEvaluationEngine.this
												.getLocalVariableModifiers(),
										receivingType, isStatic, isConstructor,
										LocalEvaluationEngine.this, null);
					} catch (JavaModelException e) {
						LocalEvaluationEngine.this.getResult().setException(
								new DebugException(e.getStatus()));
					} finally {
						LocalEvaluationEngine.this.evaluationComplete();
					}
				}
			};

			Thread t = new Thread(r);
			t.setDaemon(true);
			t.start();
		} catch (DebugException d) {
			evaluationAborted();
			throw d;
		} catch (CoreException e) {
			evaluationAborted();
			throw new DebugException(e.getStatus());
		}
	}

	/**
	 * Verifies the receiving type was resolved and is not an inner type.
	 *
	 * @param receivingType
	 * @throws DebugException
	 */
	private void validateReceivingType(final IType receivingType)
			throws DebugException {
		if (receivingType == null) {
			throw new DebugException(
					new Status(
							IStatus.ERROR,
							JDIDebugModel.getPluginIdentifier(),
							DebugException.REQUEST_FAILED,
							EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_determine_receiving_type_context__18,
							null));
		}

		if (receivingType.getDeclaringType() != null) {
			throw new DebugException(
					new Status(
							IStatus.ERROR,
							JDIDebugModel.getPluginIdentifier(),
							DebugException.REQUEST_FAILED,
							EvaluationMessages.LocalEvaluationEngine_Evaluation_in_context_of_inner_type_not_supported__19,
							null));
		}
	}

	/**
	 * @see IEvaluationEngine#evaluate(String, IJavaObject, IJavaThread,
	 *      IEvaluationListener, int)
	 */
	@Override
	public void evaluate(String snippet, IJavaObject thisContext,
			IJavaThread thread, IEvaluationListener listener,
			int evaluationDetail, boolean hitBreakpoints) throws DebugException {
		checkDisposed();
		checkEvaluating();
		try {
			evaluationStarted();
			setListener(listener);
			setHitBreakpoints(hitBreakpoints);
			setResult(new EvaluationResult(this, snippet, thread));
			checkThread();

			// no locals
			setLocalVariableTypeNames(new String[0]);
			setLocalVariableNames(new String[0]);
			setLocalVariableModifiers(new int[0]);

			setThis(thisContext);

			final boolean isStatic = false;
			final boolean isConstructor = false;
			final IType receivingType = JavaDebugUtils.resolveType(thisContext
					.getJavaType());
			validateReceivingType(receivingType);

			// do the evaluation in a different thread
			Runnable r = new Runnable() {
				@Override
				public void run() {
					try {
						LocalEvaluationEngine.this
								.getEvaluationContext()
								.evaluateCodeSnippet(
										LocalEvaluationEngine.this.getSnippet(),
										LocalEvaluationEngine.this
												.getLocalVariableTypeNames(),
										LocalEvaluationEngine.this
												.getLocalVariableNames(),
										LocalEvaluationEngine.this
												.getLocalVariableModifiers(),
										receivingType, isStatic, isConstructor,
										LocalEvaluationEngine.this, null);
					} catch (JavaModelException e) {
						LocalEvaluationEngine.this.getResult().setException(
								new DebugException(e.getStatus()));
					} finally {
						LocalEvaluationEngine.this.evaluationComplete();
					}
				}
			};

			Thread t = new Thread(r);
			t.setDaemon(true);
			t.start();
		} catch (DebugException d) {
			evaluationAborted();
			throw d;
		} catch (CoreException e) {
			evaluationAborted();
			throw new DebugException(e.getStatus());
		}
	}

	/**
	 * Throws an exception if this engine has already been disposed.
	 *
	 * @exception DebugException
	 *                if this engine has been disposed
	 */
	protected void checkDisposed() throws DebugException {
		if (isDisposed()) {
			throw new DebugException(
					new Status(
							IStatus.ERROR,
							JDIDebugModel.getPluginIdentifier(),
							DebugException.REQUEST_FAILED,
							EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___evaluation_context_has_been_disposed__7,
							null));
		}
	}

	/**
	 * Throws an exception if this engine is already in an evaluation.
	 *
	 * @exception DebugException
	 *                if this engine is currently performing an evaluation
	 */
	protected void checkEvaluating() throws DebugException {
		if (isEvaluating()) {
			throw new DebugException(new Status(IStatus.ERROR,
					JDIDebugModel.getPluginIdentifier(),
					DebugException.REQUEST_FAILED,
					"Cannot perform nested evaluations.", null) //$NON-NLS-1$
			);
		}
	}

	/**
	 * Throws an exception if this engine's current evaluation thread is not
	 * suspended.
	 *
	 * @exception DebugException
	 *                if this engine's current evaluation thread is not
	 *                suspended
	 */
	protected void checkThread() throws DebugException {
		if (!getThread().isSuspended()) {
			throw new DebugException(
					new Status(
							IStatus.ERROR,
							JDIDebugModel.getPluginIdentifier(),
							DebugException.REQUEST_FAILED,
							EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___evaluation_thread_must_be_suspended__8,
							null));
		}
	}

	/**
	 * Deletes deployed class files, and clears state.
	 *
	 * @see IEvaluationEngine#dispose()
	 */
	@Override
	public void dispose() {
		fDisposed = true;
		ENGINE_COUNT--;
		if (isEvaluating()) {
			// cannot dispose if in an evaluation, must
			// wait for evaluation to complete
			return;
		}
		List<File> snippetFiles = getSnippetFiles();
		Iterator<File> iter = snippetFiles.iterator();
		while (iter.hasNext()) {
			File file = iter.next();
			if (file.exists()) {
				if (CODE_SNIPPET_NAME.equals(file.getName())
						&& ENGINE_COUNT > 0) {
					continue; // do not delete the common file for other engines
				}
				if (!file.delete()) {
					JDIDebugPlugin
							.log(new Status(
									IStatus.ERROR,
									JDIDebugModel.getPluginIdentifier(),
									DebugException.REQUEST_FAILED,
									MessageFormat
											.format("Unable to delete temporary evaluation class file {0}.", new Object[] { file.getAbsolutePath() }), null) //$NON-NLS-1$
							);
				}
			}
		}
		List<File> directories = getDirectories();
		// remove directories in bottom up order
		int i = directories.size() - 1;
		while (i >= 0) {
			File dir = directories.get(i);
			String[] listing = dir.list();
			if (dir.exists() && listing != null && listing.length == 0
					&& !dir.delete()) {
				JDIDebugPlugin
						.log(new Status(
								IStatus.ERROR,
								JDIDebugModel.getPluginIdentifier(),
								DebugException.REQUEST_FAILED,
								MessageFormat
										.format("Unable to delete temporary evaluation directory {0}.", new Object[] { dir.getAbsolutePath() }), null) //$NON-NLS-1$
						);
			}
			i--;
		}
		reset();
		setJavaProject(null);
		setDebugTarget(null);
		setOutputDirectory(null);
		setResult(null);
		setEvaluationContext(null);
	}

	/**
	 * Resets this engine for another evaluation.
	 */
	private void reset() {
		setThis(null);
		setStackFrame(null);
		setListener(null);
	}

	/**
	 * Returns the listener to notify when the current evaluation is complete.
	 *
	 * @return the listener to notify when the current evaluation is complete
	 */
	protected IEvaluationListener getListener() {
		return fListener;
	}

	/**
	 * Sets the listener to notify when the current evaluation is complete.
	 *
	 * @param listener
	 *            the listener to notify when the current evaluation is complete
	 */
	private void setListener(IEvaluationListener listener) {
		fListener = listener;
	}

	/**
	 * Returns the stack frame context for the current evaluation, or
	 * <code>null</code> if none.
	 *
	 * @return the stack frame context for the current evaluation, or
	 *         <code>null</code> if none
	 */
	protected IJavaStackFrame getStackFrame() {
		return fStackFrame;
	}

	/**
	 * Sets the stack frame context for the current evaluation.
	 *
	 * @param stackFrame
	 *            stack frame context or <code>null</code> if none
	 */
	private void setStackFrame(IJavaStackFrame stackFrame) {
		fStackFrame = stackFrame;
	}

	/**
	 * Returns the thread in which the current evaluation is to be executed.
	 *
	 * @return the thread in which the current evaluation is to be executed
	 */
	protected IJavaThread getThread() {
		return getResult().getThread();
	}

	/**
	 * Returns the code snippet being evaluated.
	 *
	 * @return the code snippet being evaluated.
	 */
	protected String getSnippet() {
		return getResult().getSnippet();
	}

	/**
	 * Returns the current evaluation result.
	 *
	 * @return the current evaluation result
	 */
	protected EvaluationResult getResult() {
		return fResult;
	}

	/**
	 * Sets the current evaluation result.
	 *
	 * @param result
	 *            the current evaluation result
	 */
	private void setResult(EvaluationResult result) {
		fResult = result;
	}

	/**
	 * Deploys the given class files to this engine's output location, and adds
	 * the files to this engines list of temporary files to be deleted when
	 * disposed.
	 *
	 * @exception DebugException
	 *                if this fails due to a lower level exception.
	 */
	protected void deploy(byte[][] classFiles, String[][] classFileNames)
			throws DebugException {
		for (int i = 0; i < classFiles.length; i++) {
			String[] compoundName = classFileNames[i];
			// create required folders
			File dir = LocalEvaluationEngine.this.getOutputDirectory();
			try {
				String pkgDirName = dir.getCanonicalPath();
				for (int j = 0; j < (compoundName.length - 1); j++) {
					pkgDirName += File.separator + compoundName[j];
					File pkgDir = new File(pkgDirName);
					if (!pkgDir.exists()) {
						pkgDir.mkdir();
						addDirectory(pkgDir);
					}
				}
				String name = compoundName[compoundName.length - 1] + ".class"; //$NON-NLS-1$
				File classFile = new File(pkgDirName + File.separator + name);
				if (!classFile.exists()) {
					classFile.createNewFile();
				}
				try (FileOutputStream stream = new FileOutputStream(classFile)) {
					stream.write(classFiles[i]);
				}
				LocalEvaluationEngine.this.addSnippetFile(classFile);
			} catch (IOException e) {
				throw new DebugException(
						new Status(
								IStatus.ERROR,
								JDIDebugModel.getPluginIdentifier(),
								DebugException.REQUEST_FAILED,
								MessageFormat
										.format(EvaluationMessages.LocalEvaluationEngine__0__occurred_deploying_class_file_for_evaluation_9,
												new Object[] { e.toString() }),
								e));
			}
		}
	}

	/**
	 * Adds the given file to this engine's collection of deployed snippet class
	 * files, which are to be deleted when this engine is disposed.
	 *
	 * @param File
	 *            snippet class file
	 */
	private void addSnippetFile(File file) {
		if (fSnippetFiles == null) {
			fSnippetFiles = new ArrayList<>();
		}
		fSnippetFiles.add(file);
	}

	/**
	 * Adds the given file to this engine's collection of created directories,
	 * which are to be deleted when this engine is disposed.
	 *
	 * @param file
	 *            directory created for class file deployment
	 */
	private void addDirectory(File file) {
		if (fDirectories == null) {
			fDirectories = new ArrayList<>();
		}
		fDirectories.add(file);
	}

	/**
	 * Returns an evaluation context for this evaluation engine. An evaluation
	 * context is associated with a specific Java project. The evaluation context
	 * is created lazily on the first access.
	 *
	 * @return evaluation context
	 */
	protected IEvaluationContext getEvaluationContext() {
		if (fEvaluationContext == null) {
			fEvaluationContext = getJavaProject().newEvaluationContext();
		}
		return fEvaluationContext;
	}

	/**
	 * Sets the evaluation context for this evaluation engine.
	 *
	 * @param context
	 *            evaluation context
	 */
	private void setEvaluationContext(IEvaluationContext context) {
		fEvaluationContext = context;
	}

	/**
	 * Returns a collection of snippet class file deployed by this evaluation
	 * engine, possibly empty.
	 *
	 * @return deployed class files
	 */
	protected List<File> getSnippetFiles() {
		if (fSnippetFiles == null) {
			return Collections.EMPTY_LIST;
		}
		return fSnippetFiles;
	}

	/**
	 * Returns a collection of directories created by this evaluation engine,
	 * possibly empty.
	 *
	 * @return directories created when deploying class files
	 */
	protected List<File> getDirectories() {
		if (fDirectories == null) {
			return Collections.EMPTY_LIST;
		}
		return fDirectories;
	}

	/**
	 * Returns whether this evaluation engine has been disposed.
	 *
	 * @return whether this evaluation engine has been disposed
	 */
	protected boolean isDisposed() {
		return fDisposed;
	}

	/**
	 * The evaluation is complete. Notify the current listener and reset for the
	 * next evaluation.
	 */
	protected void evaluationComplete() {
		// only notify if plug-in not yet shutdown (bug# 8693)
		if (JDIDebugPlugin.getDefault() != null) {
			getListener().evaluationComplete(getResult());
		}
		evaluationEnded();
		reset();
		if (isDisposed()) {
			// if the engine was disposed during an evaluation
			// do the cleanup now
			dispose();
		}
	}

	/**
	 * Increments the evaluation counter.
	 */
	private void evaluationStarted() {
		fEvaluationCount++;
	}

	/**
	 * Decrements the evaluation counter.
	 */
	private void evaluationEnded() {
		if (fEvaluationCount > 0) {
			fEvaluationCount--;
		}
	}

	/**
	 * Returns whether this engine is currently in the midst of an evaluation.
	 */
	protected boolean isEvaluating() {
		return fEvaluationCount > 0;
	}

	/**
	 * Called when an evaluation is aborted due to an exception. Decrements the
	 * evaluation count, and disposes this engine if the target VM disconnected
	 * or terminated during the evaluation attempt.
	 */
	private void evaluationAborted() {
		evaluationEnded();
		if (isDisposed()) {
			// if the engine was disposed during an evaluation
			// do the cleanup now
			dispose();
		}
	}

	/**
	 * Constructs and returns a new instance of the specified class on the
	 * target VM.
	 *
	 * @param className
	 *            fully qualified class name
	 * @return a new instance on the target, as an <code>IJavaValue</code>
	 * @exception DebugException
	 *                if creation fails
	 */
	protected IJavaObject newInstance(String className) throws DebugException {
		IJavaObject object = null;
		IJavaClassType clazz = null;
		IJavaType[] types = getDebugTarget().getJavaTypes(className);
		if (types != null && types.length > 0) {
			clazz = (IJavaClassType) types[0];
		}
		if (clazz == null) {
			// The class is not loaded on the target VM.
			// Force the load of the class.
			types = getDebugTarget().getJavaTypes("java.lang.Class"); //$NON-NLS-1$
			IJavaClassType classClass = null;
			if (types != null && types.length > 0) {
				classClass = (IJavaClassType) types[0];
			}
			if (classClass == null) {
				// unable to load the class
				throw new DebugException(
						new Status(
								IStatus.ERROR,
								JDIDebugModel.getPluginIdentifier(),
								DebugException.REQUEST_FAILED,
								EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_instantiate_code_snippet_class__11,
								null));
			}
			IJavaValue[] args = new IJavaValue[] { getDebugTarget().newValue(
					className) };
			IJavaObject classObject = (IJavaObject) classClass
					.sendMessage(
							"forName", "(Ljava/lang/String;)Ljava/lang/Class;", args, getThread()); //$NON-NLS-2$ //$NON-NLS-1$
			object = (IJavaObject) classObject
					.sendMessage(
							"newInstance", "()Ljava/lang/Object;", null, getThread(), false); //$NON-NLS-2$ //$NON-NLS-1$
		} else {
			object = clazz.newInstance("<init>", null, getThread()); //$NON-NLS-1$
		}
		return object;
	}

	/**
	 * Interprets and returns the result of the running the snippet class file.
	 * The type of the result is described by an instance of
	 * <code>java.lang.Class</code>. The value is interpreted based on the
	 * result type.
	 * <p>
	 * Objects as well as primitive data types (boolean, int, etc.), have class
	 * objects, which are created by the VM. If the class object represents a
	 * primitive data type, then the associated value is stored in an instance
	 * of its "object" class. For example, when the result type is the class
	 * object for <code>int</code>, the result object is an instance of
	 * <code>java.lang.Integer</code>, and the actual <code>int</code> is stored
	 * in the </code>intValue()</code>. When the result type is the class object
	 * for <code>java.lang.Integer</code> the result object is an instance of
	 * <code>java.lang.Integer</code>, to be interpreted as a
	 * <code>java.lang.Integer</code>.
	 * </p>
	 *
	 * @param resultType
	 *            the class of the result
	 * @param resultValue
	 *            the value of the result, to be interpreted based on
	 *            resultType
	 * @return the result of running the code snippet class file
	 */
	protected IJavaValue convertResult(IJavaClassObject resultType,
			IJavaValue result) throws DebugException {
		if (resultType == null) {
			// there was an exception or compilation problem - no result
			return null;
		}

		// check the type of the result - if a primitive type, convert it
		String sig = resultType.getInstanceType().getSignature();
		if (sig.equals("V") || sig.equals("Lvoid;")) { //$NON-NLS-2$ //$NON-NLS-1$
			// void
			return getDebugTarget().voidValue();
		}

		if (result.getJavaType() == null) {
			// null result
			return result;
		}

		if (sig.length() == 1) {
			// primitive type - find the instance variable with the
			// signature of the result type we are looking for
			IVariable[] vars = result.getVariables();
			IJavaVariable var = null;
			for (IVariable var2 : vars) {
				IJavaVariable jv = (IJavaVariable) var2;
				if (!jv.isStatic() && jv.getSignature().equals(sig)) {
					var = jv;
					break;
				}
			}
			if (var != null) {
				return (IJavaValue) var.getValue();
			}
		} else {
			// an object
			return result;
		}
		throw new DebugException(
				new Status(
						IStatus.ERROR,
						JDIDebugModel.getPluginIdentifier(),
						DebugException.REQUEST_FAILED,
						EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___internal_error_retreiving_result__17,
						null));
	}

	/**
	 * Returns the modifiers of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @return array of modifiers
	 */
	private int[] getLocalVariableModifiers() {
		return fLocalVariableModifiers;
	}

	/**
	 * Sets the modifiers of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @param localVariableModifiers
	 *            array of modifiers
	 */
	private void setLocalVariableModifiers(int[] localVariableModifiers) {
		fLocalVariableModifiers = localVariableModifiers;
	}

	/**
	 * Returns the names of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @param array
	 *            of names
	 */
	private String[] getLocalVariableNames() {
		return fLocalVariableNames;
	}

	/**
	 * Sets the names of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @param localVariableNames
	 *            array of names
	 */
	private void setLocalVariableNames(String[] localVariableNames) {
		fLocalVariableNames = localVariableNames;
	}

	/**
	 * Returns the type names of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @param array
	 *            of type names
	 */
	private String[] getLocalVariableTypeNames() {
		return fLocalVariableTypeNames;
	}

	/**
	 * Sets the type names of the local variables visible in this evaluation,
	 * possibly empty.
	 *
	 * @param localVariableTypeNames
	 *            array of type names
	 */
	private void setLocalVariableTypeNames(String[] localVariableTypeNames) {
		fLocalVariableTypeNames = localVariableTypeNames;
	}

	/**
	 * Sets the receiver context for the associated evaluation, possibly
	 * <code>null</code> if the evaluation is in the context of a static method
	 * or there is no object context.
	 *
	 * @param thisObject
	 *            the receiver content of the associated evaluation, or
	 *            <code>null</code>
	 */
	private void setThis(IJavaObject thisObject) {
		fThis = thisObject;
	}

	/**
	 * Returns the receiver context for the associated evaluation, or
	 * <code>null</code> if the evaluation is in the context of a static method
	 * or there is no object context.
	 *
	 * @return the receiver context of the associated evaluation or
	 *         <code>null</code>
	 */
	private IJavaObject getThis() {
		return fThis;
	}

	/**
	 * Returns a copy of the type name with '$' replaced by '.', or returns
	 * <code>null</code> if the given type name refers to an anonymous inner
	 * class.
	 *
	 * @param typeName
	 *            a fully qualified type name
	 * @return a copy of the type name with '$' replaced by '.', or returns
	 *         <code>null</code> if the given type name refers to an anonymous
	 *         inner class.
	 */
	protected String getTranslatedTypeName(String typeName) {
		int index = typeName.lastIndexOf('$');
		if (index == -1) {
			return typeName;
		}
		if (index + 1 > typeName.length()) {
			// invalid name
			return typeName;
		}
		String last = typeName.substring(index + 1);
		try {
			Integer.parseInt(last);
			return null;
		} catch (NumberFormatException e) {
			return typeName.replace('$', '.');
		}
	}

	/**
	 * Returns an array of simple type names that are part of the given type's
	 * qualified name. For example, if the given name is <code>x.y.A$B</code>,
	 * an array with <code>["A", "B"]</code> is returned.
	 *
	 * @param typeName
	 *            fully qualified type name
	 * @return array of nested type names
	 */
	protected String[] getNestedTypeNames(String typeName) {
		int index = typeName.lastIndexOf('.');
		if (index >= 0) {
			typeName = typeName.substring(index + 1);
		}
		index = typeName.indexOf('$');
		ArrayList<String> list = new ArrayList<>(1);
		while (index >= 0) {
			list.add(typeName.substring(0, index));
			typeName = typeName.substring(index + 1);
			index = typeName.indexOf('$');
		}
		list.add(typeName);
		return list.toArray(new String[list.size()]);
	}

	/**
	 * @see IClassFileEvaluationEngine#getImports()
	 */
	@Override
	public String[] getImports() {
		return getEvaluationContext().getImports();
	}

	/**
	 * @see IClassFileEvaluationEngine#setImports(String[])
	 */
	@Override
	public void setImports(String[] imports) {
		getEvaluationContext().setImports(imports);
	}

	/**
	 * Sets the name of the code snippet to instantiate to run the current
	 * evaluation.
	 *
	 * @param name
	 *            the name of the deployed code snippet to instantiate and run
	 */
	private void setCodeSnippetClassName(String name) {
		fCodeSnippetClassName = name;
	}

	/**
	 * Returns the name of the code snippet to instantiate to run the current
	 * evaluation.
	 *
	 * @return the name of the deployed code snippet to instantiate and run
	 */
	protected String getCodeSnippetClassName() {
		return fCodeSnippetClassName;
	}

	/**
	 * @see ICodeSnippetRequestor#isRequestingClassFiles()
	 */
	public boolean isRequestingClassFiles() {
		return true;
	}

	/**
	 * Returns whether to hit breakpoints in the evaluation thread.
	 *
	 * @return whether to hit breakpoints in the evaluation thread
	 */
	protected boolean getHitBreakpoints() {
		return fHitBreakpoints;
	}

	/**
	 * Sets whether to hit breakpoints in the evaluation thread.
	 *
	 * @param hit
	 *            whether to hit breakpoints in the evaluation thread
	 */
	private void setHitBreakpoints(boolean hit) {
		fHitBreakpoints = hit;
	}

}
