/*******************************************************************************

 * Copyright (c) 2019, 2020 Jesper Steen Møller 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:
 *     Jesper Steen Møller - initial API and implementation
 *     IBM Corporation - Bug 448473 - [1.8][debug] Cannot use lambda expressions in breakpoint properties and display/expression view
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.eval;

import static org.eclipse.jdt.core.eval.ICodeSnippetRequestor.LOCAL_VAR_PREFIX;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.core.eval.ICodeSnippetRequestor;
import org.eclipse.jdt.debug.core.IJavaArray;
import org.eclipse.jdt.debug.core.IJavaArrayType;
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.IJavaReferenceType;
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.internal.debug.core.JDIDebugPlugin;
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 to a debuggee by using Unsafe through the JDWP.
 */

public class RemoteEvaluator {

	private final LinkedHashMap<String, byte[]> classFiles;

	private final String codeSnippetClassName;

	private final List<String> variableNames;

	private IJavaClassObject loadedClass = null;

	private String enclosingTypeName;

	/**
	 * 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 classFiles
	 * @param codeSnippetClassName
	 * @param variableNames
	 * @param enclosingTypeName
	 */
	public RemoteEvaluator(LinkedHashMap<String, byte[]> classFiles, String codeSnippetClassName, List<String> variableNames, String enclosingTypeName) {
		this.classFiles = classFiles;
		this.enclosingTypeName = enclosingTypeName;
		this.codeSnippetClassName = codeSnippetClassName.replace('.', '/');
		this.variableNames = variableNames;
	}

	private IJavaClassObject loadTheClasses(IJavaThread theThread) throws DebugException {

		if (loadedClass != null) {
			return loadedClass;
		}
		JDIDebugTarget debugTarget = ((JDIDebugTarget) theThread.getDebugTarget());
		IJavaClassObject theMainClass = null;
		IJavaObject classloader = null;

		IJavaReferenceType surroundingClass = findType(this.enclosingTypeName, debugTarget);
		classloader = surroundingClass.getClassLoaderObject();

		for (Map.Entry<String, byte[]> entry : classFiles.entrySet()) {
			String className = entry.getKey();

			IJavaReferenceType existingClass = tryLoadType(className, debugTarget);
			if (existingClass != null) {
				if (codeSnippetClassName.equals(className)) {
					theMainClass = existingClass.getClassObject();
				}
			} else {
				IJavaArray byteArray = createClassBytes(theThread, debugTarget, entry);
				IJavaValue[] defineClassArgs = new IJavaValue[] { // args for defineClass
						debugTarget.newValue(className.replaceAll("/", ".")), // class name //$NON-NLS-1$ //$NON-NLS-2$
						byteArray, // classBytes,
						debugTarget.newValue(0), // offset
						debugTarget.newValue(entry.getValue().length), // length
						debugTarget.nullValue() // protection domain
				};

				IJavaClassObject theClass = (IJavaClassObject) classloader.sendMessage("defineClass", "(Ljava/lang/String;[BIILjava/security/ProtectionDomain;)Ljava/lang/Class;", defineClassArgs, theThread, false); //$NON-NLS-1$//$NON-NLS-2$
				if (codeSnippetClassName.equals(className)) {
					theMainClass = theClass;
				}
			}
		}
		return theMainClass;
	}

	private IJavaArray createClassBytes(IJavaThread theThread, JDIDebugTarget debugTarget, Map.Entry<String, byte[]> entry) throws DebugException {
		IJavaReferenceType byteArrayType = findType("byte[]", debugTarget);//$NON-NLS-1$
		byte[] classBytes = entry.getValue();
		IJavaArray byteArray = ((IJavaArrayType) byteArrayType).newInstance(classBytes.length);

		IJavaValue[] debugClassBytes = new IJavaValue[classBytes.length];
		for (int ix = 0; ix < classBytes.length; ++ix) {
			debugClassBytes[ix] = ((JDIDebugTarget) theThread.getDebugTarget()).newValue(classBytes[ix]);
		}
		byteArray.setValues(debugClassBytes);
		return byteArray;
	}

	private IJavaReferenceType findType(String typeName, IJavaDebugTarget debugTarget) throws DebugException {
		IJavaReferenceType theClass = tryLoadType(typeName, debugTarget);
		if (theClass == null) {
			// unable to load the class
			throw new DebugException(
					new Status(
							IStatus.ERROR,
							JDIDebugModel.getPluginIdentifier(),
							DebugException.REQUEST_FAILED,
							EvaluationMessages.RemoteEvaluationEngine_Evaluation_failed___unable_to_find_injected_class,
							null));
		}
		return theClass;
	}

	private IJavaReferenceType tryLoadType(String typeName, IJavaDebugTarget debugTarget) throws DebugException {
		IJavaReferenceType clazz = null;
		IJavaType[] types = debugTarget.getJavaTypes(typeName);
		if (types != null && types.length > 0) {
			clazz = (IJavaReferenceType) types[0];
		}
		return clazz;
	}

	/**
	 * Initializes the value of instance variables in the 'code snippet object' that are used as place-holders for free variables and 'this' in the
	 * current stack frame.
	 *
	 * @param object
	 *            instance of code snippet class that will be run
	 * @param boundValues
	 *            popped values which should be injected into the code snippet object.
	 * @exception DebugException
	 *                if an exception is thrown accessing the given object
	 */
	protected void initializeFreeVars(IJavaObject object, IJavaValue boundValues[]) throws DebugException {
		if (boundValues.length != this.variableNames.size()) {
			throw new DebugException(new Status(IStatus.ERROR, JDIDebugModel.getPluginIdentifier(), DebugException.REQUEST_FAILED, EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__4, null));
		}

		for (int i = 0; i < boundValues.length; ++i) {
			IJavaVariable field = object.getField(new String(LOCAL_VAR_PREFIX) + this.variableNames.get(i), false);
			if (field != null) {
				IJavaValue bound = boundValues[i];
				field.setValue(bound);
			} else {
				// System.out.print(Arrays.asList(((IJavaReferenceType) object.getJavaType()).getAllFieldNames()));
				throw new DebugException(new Status(IStatus.ERROR, JDIDebugModel.getPluginIdentifier(), DebugException.REQUEST_FAILED, EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__4, null));
			}
		}
	}

	/**
	 * 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(IJavaThread theThread) throws DebugException {
		IJavaDebugTarget debugTarget = ((IJavaDebugTarget) theThread.getDebugTarget());

		IJavaObject object = null;
		IJavaClassObject clazz = loadTheClasses(theThread);
		if (clazz == null) {
			// The class is not loaded on the target VM.
			// Force the load of the class.
			IJavaType[] types = debugTarget.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[] { debugTarget.newValue(
					this.codeSnippetClassName) };
			IJavaObject classObject = (IJavaObject) classClass
					.sendMessage(
							"forName", "(Ljava/lang/String;)Ljava/lang/Class;", args, theThread); //$NON-NLS-2$ //$NON-NLS-1$
			object = (IJavaObject) classObject
					.sendMessage(
							"newInstance", "()Ljava/lang/Object;", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
		} else {
			object = (IJavaObject) clazz.sendMessage("newInstance", "()Ljava/lang/Object;", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
			// object = clazz.newInstance("<init>", null, theThread); //$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(IJavaDebugTarget debugTarget, 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 debugTarget.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 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 codeSnippetClassName;
	}

	public IJavaValue evaluate(IJavaThread theThread, IJavaValue[] args) throws DebugException {
		IJavaObject codeSnippetInstance = null;
		IJavaDebugTarget debugTarget = ((IJavaDebugTarget) theThread.getDebugTarget());
		try {
			codeSnippetInstance = newInstance(theThread);
			initializeFreeVars(codeSnippetInstance, args);
			codeSnippetInstance.sendMessage(ICodeSnippetRequestor.RUN_METHOD, "()V", null, theThread, false); //$NON-NLS-1$

			// 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(ICodeSnippetRequestor.RESULT_TYPE_FIELD)) {
					resultType = (IJavaVariable) field;
				}
				if (field.getName().equals(ICodeSnippetRequestor.RESULT_VALUE_FIELD)) {
					resultValue = (IJavaVariable) field;
				}
			}
			IJavaValue result = convertResult(debugTarget, (IJavaClassObject) resultType.getValue(), (IJavaValue) resultValue.getValue());
			return result;
		} catch (DebugException 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) debugTarget, theException);
							v.sendMessage("printStackTrace", "()V", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
						} catch (DebugException de) {
							JDIDebugPlugin.log(de);
						}
					} catch (RuntimeException re) {
						JDIDebugPlugin.log(re);
					}
				}
			}
			throw e;
		}
	}

	public int getVariableCount() {
		return this.variableNames.size();
	}

	public String getVariableName(int i) {
		return this.variableNames.get(i);
	}
}
