/*******************************************************************************
 * 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
 *     Jesper S. Møller - bug 422029: [1.8] Enable debug evaluation support for default methods
 *     Jesper Steen Møller - bug 426903: [1.8] Cannot evaluate super call to default method
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.core.model;

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

import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.jdi.internal.InterfaceTypeImpl;
import org.eclipse.jdt.debug.core.IJavaFieldVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaValue;

import com.ibm.icu.text.MessageFormat;
import com.sun.jdi.ArrayType;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.InterfaceType;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.Value;

/**
 * Implementation of a value referencing an object on the target VM.
 */
public class JDIObjectValue extends JDIValue implements IJavaObject {

	private IJavaObject[] fCachedReferences;
	private int fSuspendCount;
	private long fPreviousMax;

	/**
	 * Constructs a new target object on the given target with the specified
	 * object reference.
	 */
	public JDIObjectValue(JDIDebugTarget target, ObjectReference object) {
		super(target, object);
		fSuspendCount = -1;
		fCachedReferences = null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#sendMessage(java.lang.String,
	 * java.lang.String, org.eclipse.jdt.debug.core.IJavaValue[],
	 * org.eclipse.jdt.debug.core.IJavaThread, boolean)
	 */
	@Override
	public IJavaValue sendMessage(String selector, String signature,
			IJavaValue[] args, IJavaThread thread, boolean superSend)
			throws DebugException {
		JDIThread javaThread = (JDIThread) thread;
		List<Value> arguments = null;
		if (args == null) {
			arguments = Collections.EMPTY_LIST;
		} else {
			arguments = new ArrayList<>(args.length);
			for (IJavaValue arg : args) {
				arguments.add(((JDIValue) arg).getUnderlyingValue());
			}
		}
		ObjectReference object = getUnderlyingObject();
		Method method = null;
		ReferenceType refType = getUnderlyingReferenceType();
		try {
			if (superSend) {
				// begin lookup in superclass
				refType = ((ClassType) refType).superclass();
			}
			method = concreteMethodByName(refType, selector, signature);
			if (method == null) {
				targetRequestFailed(MessageFormat.format(
						JDIDebugModelMessages.JDIObjectValue_11, selector, signature), null);
			}
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat
							.format(JDIDebugModelMessages.JDIObjectValue_exception_while_performing_method_lookup_for_selector,
									e.toString(), selector,	signature),
					e);
		}
		Value result = javaThread.invokeMethod(null, object, method, arguments,
				superSend);
		return JDIValue.createValue((JDIDebugTarget) getDebugTarget(), result);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#sendMessage(java.lang.String,
	 * java.lang.String, org.eclipse.jdt.debug.core.IJavaValue[],
	 * org.eclipse.jdt.debug.core.IJavaThread, java.lang.String)
	 */
	@Override
	public IJavaValue sendMessage(String selector, String signature,
			IJavaValue[] args, IJavaThread thread, String typeSignature)
			throws DebugException {
		JDIThread javaThread = (JDIThread) thread;
		List<Value> arguments = null;
		if (args == null) {
			arguments = Collections.EMPTY_LIST;
		} else {
			arguments = new ArrayList<>(args.length);
			for (IJavaValue arg : args) {
				arguments.add(((JDIValue) arg).getUnderlyingValue());
			}
		}
		ObjectReference object = getUnderlyingObject();
		Method method = null;
		ReferenceType refType = getUnderlyingReferenceType();
		try {
			found: while (typeSignature != null && refType != null	&& !refType.signature().equals(typeSignature)) {
				// Didin't match, could be a method from inheirited interface
				for (InterfaceType iface : ((ClassType) refType).allInterfaces()) {
					if (iface.signature().equals(typeSignature)) {
						refType = iface;
						break found;
					}
				}
				// lookup correct type through the hierarchy
				refType = ((ClassType) refType).superclass();
				if (refType == null) {
					targetRequestFailed(
							JDIDebugModelMessages.JDIObjectValueMethod_declaring_type_not_found_1,
							null);
				}
			}
			method = concreteMethodByName(refType, selector, signature);
			if (method == null) {
				targetRequestFailed(MessageFormat.format(
						JDIDebugModelMessages.JDIObjectValue_11, selector, signature), null);
			}
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat
							.format(JDIDebugModelMessages.JDIObjectValue_exception_while_performing_method_lookup_for_selector,
									e.toString(), selector, signature),
					e);
		}
		Value result = javaThread.invokeMethod(null, object, method, arguments,
				true);
		return JDIValue.createValue((JDIDebugTarget) getDebugTarget(), result);
	}

	private Method concreteMethodByName(ReferenceType refType, String selector,
			String signature) throws DebugException {
		if (refType instanceof ClassType) {
			Method m = ((ClassType) refType).concreteMethodByName(selector,
					signature);
			if (m != null) {
				return m;
			}

			for (InterfaceType iface : ((ClassType) refType).allInterfaces()) {
				List<Method> matches = iface.methodsByName(selector, signature);
				for (Method ifaceMethod : matches) {
					if (! ifaceMethod.isAbstract()) {
						return ifaceMethod;
					}
				}
			}
		}
		if (refType instanceof InterfaceTypeImpl) {
			Method m = ((InterfaceTypeImpl) refType).concreteMethodByName(selector,
					signature);
			if (m != null) {
				return m;
			}
		}
		if (refType instanceof ArrayType) {
			// the jdi spec specifies that all methods on methods return an
			// empty list for array types.
			// use a trick to get the right method from java.lang.Object
			return ((ClassType) refType.classObject().referenceType())
					.superclass().concreteMethodByName(selector, signature);
		}
		targetRequestFailed(
				MessageFormat.format(
						JDIDebugModelMessages.JDIObjectValue_method_lookup_failed_for_selector____0____with_signature____1___1,
						selector, signature), null);
		// it is not possible to return null
		return null;
	}

	/**
	 * Returns this object's the underlying object reference
	 *
	 * @return underlying object reference
	 */
	public ObjectReference getUnderlyingObject() {
		return (ObjectReference) getUnderlyingValue();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#getField(java.lang.String,
	 * boolean)
	 */
	@Override
	public IJavaFieldVariable getField(String name, boolean superField)
			throws DebugException {
		ReferenceType ref = getUnderlyingReferenceType();
		try {
			if (superField) {
				// begin lookup in superclass
				ref = ((ClassType) ref).superclass();
			}
			Field field = ref.fieldByName(name);
			if (field != null) {
				return new JDIFieldVariable((JDIDebugTarget) getDebugTarget(),
						field, getUnderlyingObject(), fLogicalParent);
			}
			Field enclosingThis = null;
			Iterator<Field> fields = ref.fields().iterator();
			while (fields.hasNext()) {
				Field fieldTmp = fields.next();
				if (fieldTmp.name().startsWith("this$")) { //$NON-NLS-1$
					enclosingThis = fieldTmp;
					break;
				}
			}

			if (enclosingThis != null) {
				return ((JDIObjectValue) (new JDIFieldVariable(
						(JDIDebugTarget) getDebugTarget(), enclosingThis,
						getUnderlyingObject(), fLogicalParent)).getValue())
						.getField(name, false);
			}
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIObjectValue_exception_retrieving_field,
							e.toString()), e);
		}
		// it is possible to return null
		return null;
	}

	@Override
	public IJavaFieldVariable getField(final String name, final String declaringTypeSignature) throws DebugException {
		ReferenceType ref = getUnderlyingReferenceType();
		try {
			Field field = null;
			Field fieldTmp = null;
			List<Field> synteticFields = new ArrayList<>();
			Iterator<Field> fields = ref.allFields().iterator();
			List<ReferenceType> superTypes = null;
			main: while (fields.hasNext()) {
				fieldTmp = fields.next();
				if (name.equals(fieldTmp.name())) {
					ReferenceType declaringType = fieldTmp.declaringType();
					String signature = declaringType.signature();
					if (declaringTypeSignature.equals(signature)) {
						field = fieldTmp;
						break;
					}
					// check if we are inside local type - Signature.createTypeSignature
					// can't create proper type name out of source field in JavaDebugHover
					// we get LDebugHoverTest$InnerClass2; instead of LDebugHoverTest$1InnerClass2;
					signature = signature.replaceFirst("\\$\\d+", "\\$"); //$NON-NLS-1$ //$NON-NLS-2$
					if (declaringTypeSignature.equals(signature)) {
						field = fieldTmp;
						break;
					}
					if (superTypes == null) {
						superTypes = superTypes(ref);
					}
					for (ReferenceType st : superTypes) {
						if (st.signature().equals(signature)) {
							field = fieldTmp;
							break main;
						}
					}
				}
				if (fieldTmp.isSynthetic()) {
					synteticFields.add(fieldTmp);
				}
			}
			JDIDebugTarget debugTarget = (JDIDebugTarget) getDebugTarget();
			if (field != null) {
				return new JDIFieldVariable(debugTarget, field, getUnderlyingObject(), fLogicalParent);
			}

			// Check possible references of variables defined in outer class
			for (Field outer : synteticFields) {
				// retrieve the reference to the "outer" object
				JDIFieldVariable syntVariable = new JDIFieldVariable(debugTarget, outer, getUnderlyingObject(), fLogicalParent);
				IValue value = syntVariable.getValue();
				if (value instanceof JDIObjectValue) {
					JDIObjectValue outerObject = (JDIObjectValue) value;
					// ask "outer" object about field probably declared within
					return outerObject.getField(name, outer.signature());
				}
			}

		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIObjectValue_exception_retrieving_field,
							e.toString()), e);
		}
		// it is possible to return null
		return null;
	}

	static List<ReferenceType> superTypes(ReferenceType type) {
		List<ReferenceType> superTypes = new ArrayList<>();
		ReferenceType t = type;
		while (t instanceof ClassType) {
			ClassType ct = (ClassType) t;
			t = ct.superclass();
			if (t == null || "java.lang.Object".equals(t.name())) { //$NON-NLS-1$
				break;
			}
			superTypes.add(t);
		}
		return superTypes;
	}

	/**
	 * Returns a variable representing the field in this object with the given
	 * name, or <code>null</code> if there is no field with the given name, or
	 * the name is ambiguous.
	 *
	 * @param name
	 *            field name
	 * @param superClassLevel
	 *            the level of the desired field in the hierarchy. Level 0
	 *            returns the field from the current type, level 1 from the
	 *            super type, etc.
	 * @return the variable representing the field, or <code>null</code>
	 * @exception DebugException
	 *                if this method fails. Reasons include:
	 *                <ul>
	 *                <li>Failure communicating with the VM. The
	 *                DebugException's status code contains the underlying
	 *                exception responsible for the failure.</li>
	 */
	public IJavaFieldVariable getField(String name, int superClassLevel)
			throws DebugException {
		ReferenceType ref = getUnderlyingReferenceType();
		try {
			for (int i = 0; i < superClassLevel; i++) {
				ref = ((ClassType) ref).superclass();
			}
			Field field = ref.fieldByName(name);
			if (field != null) {
				return new JDIFieldVariable((JDIDebugTarget) getDebugTarget(),
						field, getUnderlyingObject(), fLogicalParent);
			}
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIObjectValue_exception_retrieving_field,
							e.toString()), e);
		}
		// it is possible to return null
		return null;
	}

	/**
	 * Returns the underlying reference type for this object.
	 *
	 * @exception DebugException
	 *                if this method fails. Reasons include:
	 *                <ul>
	 *                <li>Failure communicating with the VM. The
	 *                DebugException's status code contains the underlying
	 *                exception responsible for the failure.</li>
	 */
	protected ReferenceType getUnderlyingReferenceType() throws DebugException {
		try {
			return getUnderlyingObject().referenceType();
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIObjectValue_exception_retrieving_reference_type,
							e.toString()), e);
		}
		// execution will not reach this line, as an exception will
		// be thrown.
		return null;

	}

	/**
	 * Return the enclosing object of this object at the specified level. Level
	 * 0 returns the object, level 1 returns the enclosing object, etc.
	 */
	public IJavaObject getEnclosingObject(int enclosingLevel)
			throws DebugException {
		JDIObjectValue res = this;
		for (int i = 0; i < enclosingLevel; i++) {
			ReferenceType ref = res.getUnderlyingReferenceType();
			try {
				Field enclosingThis = null, fieldTmp = null;
				Iterator<Field> fields = ref.fields().iterator();
				while (fields.hasNext()) {
					fieldTmp = fields.next();
					if (fieldTmp.name().startsWith("this$")) { //$NON-NLS-1$
						enclosingThis = fieldTmp;
					}
				}
				if (enclosingThis != null) {
					JDIDebugTarget debugTarget = (JDIDebugTarget) getDebugTarget();
					JDIFieldVariable fieldVariable = new JDIFieldVariable(
							debugTarget, enclosingThis,
							res.getUnderlyingObject(), fLogicalParent);
					res = (JDIObjectValue) fieldVariable.getValue();
				} else {
					// it is possible to return null
					return null;
				}
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIObjectValue_exception_retrieving_field,
								e.toString()), e);
			}
		}
		return res;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#getWaitingThreads()
	 */
	@Override
	public IJavaThread[] getWaitingThreads() throws DebugException {
		List<JDIThread> waiting = new ArrayList<>();
		try {
			JDIDebugTarget debugTarget = (JDIDebugTarget) getDebugTarget();
			for (ThreadReference threadReference : getUnderlyingObject().waitingThreads()) {
				JDIThread jdiThread = debugTarget.findThread(threadReference);
				if (jdiThread != null) {
					waiting.add(jdiThread);
				}
			}
		} catch (IncompatibleThreadStateException e) {
			targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_0, e);
		} catch (VMDisconnectedException e) {
			// Ignore
		} catch (RuntimeException e) {
			targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_0, e);
		}
		return waiting.toArray(new IJavaThread[waiting.size()]);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#getOwningThread()
	 */
	@Override
	public IJavaThread getOwningThread() throws DebugException {
		IJavaThread owningThread = null;
		try {
			ThreadReference thread = getUnderlyingObject().owningThread();
			JDIDebugTarget debugTarget = (JDIDebugTarget) getDebugTarget();
			if (thread != null) {
				owningThread = debugTarget.findThread(thread);
			}
		} catch (IncompatibleThreadStateException e) {
			targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_1, e);
		} catch (VMDisconnectedException e) {
			return null;
		} catch (RuntimeException e) {
			targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_1, e);
		}
		return owningThread;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.eclipse.jdt.internal.debug.core.model.JDIValue#getReferenceTypeName()
	 */
	@Override
	public String getReferenceTypeName() throws DebugException {
		try {
			return JDIReferenceType
					.getGenericName(getUnderlyingReferenceType());
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIValue_exception_retrieving_reference_type_name,
							e.toString()), e);
			// execution will not reach this line, as
			// #targetRequestFailed will thrown an exception
			return null;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#getUniqueId()
	 */
	@Override
	public long getUniqueId() throws DebugException {
		try {
			ObjectReference underlyingObject = getUnderlyingObject();
			if (underlyingObject != null) {
				return underlyingObject.uniqueID();
			}
			return -1L;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIValue_exception_retrieving_unique_id,
							e.toString()), e);
			// execution will not reach this line, as
			// #targetRequestFailed will thrown an exception
			return 0;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#getReferringObjects(long)
	 */
	@Override
	public IJavaObject[] getReferringObjects(long max) throws DebugException {
		// The cached references should be reloaded if the suspend count has
		// changed, or the maximum entries has changed
		if (fCachedReferences == null
				|| fSuspendCount < ((JDIDebugTarget) getDebugTarget())
						.getSuspendCount() || fPreviousMax != max) {
			reloadReferringObjects(max);
			fPreviousMax = max;
			fSuspendCount = ((JDIDebugTarget) getDebugTarget())
					.getSuspendCount();
		}
		return fCachedReferences;
	}

	/**
	 * Returns true if references to this object have been calculated and
	 * cached. This method will return true even if the cached references are
	 * stale.
	 *
	 * @return true is references to this object have been calculated and
	 *         cached, false otherwise
	 */
	public boolean isReferencesLoaded() {
		return fCachedReferences != null;
	}

	/**
	 * Gets the list of objects that reference this object from the VM,
	 * overwriting the cached list (if one exists).
	 *
	 * @param max
	 *            The maximum number of entries to return
	 * @throws DebugException
	 *             if the VM cannot return a list of referring objects
	 */
	protected void reloadReferringObjects(long max) throws DebugException {
		try {
			List<ObjectReference> list = getUnderlyingObject().referringObjects(max);
			IJavaObject[] references = new IJavaObject[list.size()];
			for (int i = 0; i < references.length; i++) {
				references[i] = (IJavaObject) JDIValue.createValue(
						getJavaDebugTarget(), list.get(i));
			}
			fCachedReferences = references;
		} catch (RuntimeException e) {
			fCachedReferences = null;
			targetRequestFailed(MessageFormat.format(
					JDIDebugModelMessages.JDIObjectValue_12,
					e.toString()), e);
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#disableCollection()
	 */
	@Override
	public void disableCollection() throws DebugException {
		if (getJavaDebugTarget().supportsSelectiveGarbageCollection()) {
			try {
				getUnderlyingObject().disableCollection();
			} catch (UnsupportedOperationException e) {
				// The VM does not support enable/disable GC - update target
				// capabilities and ignore (bug 246577)
				getJavaDebugTarget().setSupportsSelectiveGarbageCollection(
						false);
			} catch (RuntimeException e) {
				targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_13, e);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaObject#enableCollection()
	 */
	@Override
	public void enableCollection() throws DebugException {
		if (getJavaDebugTarget().supportsSelectiveGarbageCollection()) {
			try {
				getUnderlyingObject().enableCollection();
			} catch (RuntimeException e) {
				targetRequestFailed(JDIDebugModelMessages.JDIObjectValue_14, e);
			}
		}
	}
}