/*******************************************************************************
 * Copyright (c) 2000, 2018 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
 *     Samrat Dhillon samrat.dhillon@gmail.com - Bug 384458 - debug shows value of variable in another scope
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.core.model;

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

import org.eclipse.core.runtime.IAdaptable;
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.IRegisterGroup;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IStep;
import org.eclipse.debug.core.model.ISuspendResume;
import org.eclipse.debug.core.model.ITerminate;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdi.internal.ValueImpl;
import org.eclipse.jdi.internal.VirtualMachineImpl;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.debug.core.IJavaClassType;
import org.eclipse.jdt.debug.core.IJavaModifiers;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaStackFrame;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.logicalstructures.JDILambdaVariable;
import org.eclipse.jdt.internal.debug.core.logicalstructures.JDIReturnValueVariable;
import org.eclipse.jdt.internal.debug.core.model.MethodResult.ResultType;

import com.ibm.icu.text.MessageFormat;
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.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.Method;
import com.sun.jdi.NativeMethodException;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.Type;
import com.sun.jdi.VirtualMachine;

/**
 * Proxy to a stack frame on the target.
 */

public class JDIStackFrame extends JDIDebugElement implements IJavaStackFrame {

	/**
	 * This frame's depth in the call stack (0 == bottom of stack). A new frame
	 * is indicated by -2. An invalid frame is indicated by -1.
	 */
	private int fDepth = -2;

	/**
	 * Underlying JDI stack frame.
	 */
	private StackFrame fStackFrame;

	/**
	 * Containing thread.
	 */
	private JDIThread fThread;
	/**
	 * Visible variables.
	 */
	private List<IJavaVariable> fVariables;

	/**
	 * The underlying Object associated with this stack frame. Cached lazily on
	 * first access.
	 */
	private ObjectReference fThisObject;

	/**
	 * The name of the type of the object that received the method call
	 * associated with this stack frame. Cached lazily on first access.
	 */
	private String fReceivingTypeName;
	/**
	 * Whether the variables need refreshing
	 */
	private boolean fRefreshVariables = true;
	/**
	 * Whether this stack frame has been marked as out of synch. If set to
	 * <code>true</code> this stack frame will stop dynamically calculating its
	 * out of synch state.
	 */
	private boolean fIsOutOfSynch = false;

	/**
	 * Whether local variable information was available
	 */
	private boolean fLocalsAvailable = true;

	/**
	 * Location of this stack frame
	 */
	private Location fLocation;

	/**
	 * Whether the current stack frame is the top of the stack
	 */
	private boolean fIsTop;

	/**
	 * Creates a new stack frame in the given thread.
	 *
	 * @param thread
	 *            The parent JDI thread
	 * @param frame
	 *            underlying frame
	 * @param depth
	 *            depth on the stack (0 is bottom)
	 */
	public JDIStackFrame(JDIThread thread, StackFrame frame, int depth) {
		super((JDIDebugTarget) thread.getDebugTarget());
		setThread(thread);
		bind(frame, depth);
	}

	/**
	 * Binds this frame to the given underlying frame on the target VM or
	 * returns a new frame representing the given frame. A frame can only be
	 * re-bound to an underlying frame if it refers to the same depth on the
	 * stack in the same method.
	 *
	 * @param frame
	 *            underlying frame, or <code>null</code>
	 * @param depth
	 *            depth in the call stack, or -1 to indicate the frame should
	 *            become invalid
	 * @param return a frame to refer to the given frame or <code>null</code>
	 */
	protected JDIStackFrame bind(StackFrame frame, int depth) {
		synchronized (fThread) {
			if (fDepth == -2) {
				// first initialization
				fStackFrame = frame;
				fDepth = depth;
				fLocation = frame.location();
				return this;
			} else if (depth == -1) {
				// mark as invalid
				fDepth = -1;
				fStackFrame = null;
				fIsTop = false;
				return null;
			} else if (fDepth == depth) {
				Location location = frame.location();
				Method method = location.method();
				if (method.equals(fLocation.method())) {
					try {
						if (method.declaringType().defaultStratum()
								.equals("Java") || //$NON-NLS-1$
								equals(getSourceName(location),
										getSourceName(fLocation))) {
							// TODO: what about receiving type being the same?
							fStackFrame = frame;
							fLocation = location;
							clearCachedData();
							return this;
						}
					} catch (DebugException e) {
					}
				}
			}
			// invalidate this frame
			bind(null, -1);
			// return a new frame
			return new JDIStackFrame(fThread, frame, depth);
		}

	}

	/**
	 * @see IStackFrame#getThread()
	 */
	@Override
	public IThread getThread() {
		return fThread;
	}

	/**
	 * @see ISuspendResume#canResume()
	 */
	@Override
	public boolean canResume() {
		return getThread().canResume();
	}

	/**
	 * @see ISuspendResume#canSuspend()
	 */
	@Override
	public boolean canSuspend() {
		return getThread().canSuspend();
	}

	/**
	 * @see IStep#canStepInto()
	 */
	@Override
	public boolean canStepInto() {
		try {
			return exists() && isTopStackFrame() && !isObsolete()
					&& getThread().canStepInto();
		} catch (DebugException e) {
			logError(e);
			return false;
		}
	}

	/**
	 * @see IStep#canStepOver()
	 */
	@Override
	public boolean canStepOver() {
		return exists() && !isObsolete() && getThread().canStepOver();
	}

	/**
	 * @see IStep#canStepReturn()
	 */
	@Override
	public boolean canStepReturn() {
		try {
			if (!exists() || isObsolete() || !getThread().canStepReturn()) {
				return false;
			}
			List<IJavaStackFrame> frames = ((JDIThread) getThread())
					.computeStackFrames();
			if (frames != null && !frames.isEmpty()) {
				boolean bottomFrame = this
						.equals(frames.get(frames.size() - 1));
				boolean aboveObsoleteFrame = false;
				if (!bottomFrame) {
					int index = frames.indexOf(this);
					if (index < frames.size() - 1
							&& ((JDIStackFrame) frames.get(index + 1))
									.isObsolete()) {
						aboveObsoleteFrame = true;
					}
				}
				return !bottomFrame && !aboveObsoleteFrame;
			}
		} catch (DebugException e) {
			logError(e);
		}
		return false;
	}

	/**
	 * Returns the underlying method associated with this stack frame,
	 * retrieving the method is necessary.
	 */
	public Method getUnderlyingMethod() {
		synchronized (fThread) {
			return fLocation.method();
		}
	}

	/**
	 * @see IStackFrame#getVariables()
	 */
	@Override
	public IVariable[] getVariables() throws DebugException {
		List<IJavaVariable> list = getVariables0();
		return list.toArray(new IVariable[list.size()]);
	}

	protected List<IJavaVariable> getVariables0() throws DebugException {
		synchronized (fThread) {
			if (fVariables == null) {

				// throw exception if native method, so variable view will
				// update
				// with information message
				if (isNative()) {
					requestFailed(
							JDIDebugModelMessages.JDIStackFrame_Variable_information_unavailable_for_native_methods,
							null);
				}

				Method method = getUnderlyingMethod();
				fVariables = new ArrayList<>();
				// #isStatic() does not claim to throw any exceptions - so it is
				// not try/catch coded
				if (method.isStatic()) {
					// add statics
					List<Field> allFields = null;
					ReferenceType declaringType = method.declaringType();
					try {
						allFields = declaringType.allFields();
					} catch (RuntimeException e) {
						targetRequestFailed(
								MessageFormat.format(
										JDIDebugModelMessages.JDIStackFrame_exception_retrieving_fields,
										e.toString()), e);
						// execution will not reach this line, as
						// #targetRequestFailed will throw an exception
						return Collections.EMPTY_LIST;
					}
					if (allFields != null) {
						Iterator<Field> fields = allFields.iterator();
						while (fields.hasNext()) {
							Field field = fields.next();
							if (field.isStatic()) {
								fVariables.add(new JDIFieldVariable(
										(JDIDebugTarget) getDebugTarget(),
										field, declaringType));
							}
						}
						Collections.sort(fVariables,
								new Comparator<IJavaVariable>() {
									@Override
									public int compare(IJavaVariable a, IJavaVariable b) {
										JDIFieldVariable v1 = (JDIFieldVariable) a;
										JDIFieldVariable v2 = (JDIFieldVariable) b;
										try {
											return v1.getName()
													.compareToIgnoreCase(
															v2.getName());
										} catch (DebugException de) {
											logError(de);
											return -1;
										}
									}
								});
					}
				} else {
					// add "this"
					ObjectReference t = getUnderlyingThisObject();
					if (t != null) {
						fVariables.add(new JDIThisVariable(
								(JDIDebugTarget) getDebugTarget(), t));
					}
				}
				if (LambdaUtils.isLambdaFrame(this)) {
						List<IJavaStackFrame> frames = fThread.computeStackFrames();
						int previousIndex = frames.indexOf(this) + 1;
						if (previousIndex > 0 && previousIndex < frames.size()) {
							IJavaStackFrame previousFrame = frames.get(previousIndex);
							fVariables.add(new JDILambdaVariable((JDIDebugTarget) getDebugTarget(), previousFrame, ((JDIStackFrame) previousFrame).getUnderlyingThisObject()));
						}
					}
				addStepReturnValue(fVariables);
				// add locals
				Iterator<LocalVariable> variables = getUnderlyingVisibleVariables()
						.iterator();
				while (variables.hasNext()) {
					LocalVariable var = variables.next();
					fVariables.add(new JDILocalVariable(this, var));
				}
			} else if (fRefreshVariables) {
				updateVariables();
			}
			fRefreshVariables = false;
			return fVariables;
		}
	}

	/**
	 * If there is a return value from a "step return" that belongs to this frame, insert it as first element
	 *
	 * @param variables
	 */
	private void addStepReturnValue(List<IJavaVariable> variables) {
		if (fIsTop) {
			MethodResult methodResult = fThread.getMethodResult();
			if (methodResult != null) {
				if (methodResult.fResultType == ResultType.returned) {
					if (fDepth + 1 != methodResult.fTargetFrameCount) {
						// can happen e.g., because of checkPackageAccess/System.getSecurityManager()
						return;
					}
					String name = MessageFormat.format(JDIDebugModelMessages.JDIStackFrame_ReturnValue, methodResult.fMethod.name());
					variables.add(0, new JDIReturnValueVariable(name, JDIValue.createValue(getJavaDebugTarget(), methodResult.fValue), true));
				} else if (methodResult.fResultType == ResultType.returning) {
					String name = MessageFormat.format(JDIDebugModelMessages.JDIStackFrame_ReturningValue, methodResult.fMethod.name());
					variables.add(0, new JDIReturnValueVariable(name, JDIValue.createValue(getJavaDebugTarget(), methodResult.fValue), true));
				} else if (methodResult.fResultType == ResultType.threw) {
					if (fDepth + 1 > methodResult.fTargetFrameCount) {
						// don't know if this really can happen, but other jvm suprises were not expected either
						return;
					}
					String name = MessageFormat.format(JDIDebugModelMessages.JDIStackFrame_ExceptionThrown, methodResult.fMethod.name());
					variables.add(0, new JDIReturnValueVariable(name, JDIValue.createValue(getJavaDebugTarget(), methodResult.fValue), true));
				} else if (methodResult.fResultType == ResultType.throwing) {
					String name = MessageFormat.format(JDIDebugModelMessages.JDIStackFrame_ThrowingException, methodResult.fMethod.name());
					variables.add(0, new JDIReturnValueVariable(name, JDIValue.createValue(getJavaDebugTarget(), methodResult.fValue), true));
				} else if (methodResult.fResultType == ResultType.step_timeout) {
					String msg = JDIDebugModelMessages.JDIStackFrame_NotObservedBecauseOfTimeout;
					variables.add(0, new JDIReturnValueVariable(JDIDebugModelMessages.JDIStackFrame_NoMethodReturnValue, new JDIPlaceholderValue(getJavaDebugTarget(), msg), false));
				}
			} else if(JDIThread.showStepResultIsEnabled()) {
				variables.add(0, new JDIReturnValueVariable(JDIDebugModelMessages.JDIStackFrame_NoMethodReturnValue, new JDIPlaceholderValue(getJavaDebugTarget(), ""), false)); //$NON-NLS-1$
			}
		}
	}

	/**
	 * @see IStackFrame#getName()
	 */
	@Override
	public String getName() throws DebugException {
		return getMethodName();
	}

	/**
	 * @see IJavaStackFrame#getArgumentTypeNames()
	 */
	@Override
	public List<String> getArgumentTypeNames() throws DebugException {
		try {
			Method underlyingMethod = getUnderlyingMethod();
			String genericSignature = underlyingMethod.genericSignature();
			if (genericSignature == null) {
				// no generic signature
				return underlyingMethod.argumentTypeNames();
			}
			// generic signature
			String[] parameterTypes = Signature
					.getParameterTypes(genericSignature);
			List<String> argumentTypeNames = new ArrayList<>();
			for (String parameterType : parameterTypes) {
				argumentTypeNames.add(Signature.toString(parameterType)
						.replace('/', '.'));
			}
			return argumentTypeNames;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retrieving_argument_type_names,
							e.toString()), e);
			// execution will never reach this line, as
			// #targetRequestFailed will throw an exception
			return null;
		}
	}

	/**
	 * @see IStackFrame#getLineNumber()
	 */
	@Override
	public int getLineNumber() throws DebugException {
		synchronized (fThread) {
			try {
				return fLocation.lineNumber();
			} catch (RuntimeException e) {
				if (getThread().isSuspended()) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIStackFrame_exception_retrieving_line_number,
									e.toString()), e);
				}
			}
		}
		return -1;
	}

	/**
	 * @see IStep#isStepping()
	 */
	@Override
	public boolean isStepping() {
		return getThread().isStepping();
	}

	/**
	 * @see ISuspendResume#isSuspended()
	 */
	@Override
	public boolean isSuspended() {
		return getThread().isSuspended();
	}

	/**
	 * @see ISuspendResume#resume()
	 */
	@Override
	public void resume() throws DebugException {
		getThread().resume();
	}

	/**
	 * @see IStep#stepInto()
	 */
	@Override
	public void stepInto() throws DebugException {
		if (!canStepInto()) {
			return;
		}
		getThread().stepInto();
	}

	/**
	 * @see IStep#stepOver()
	 */
	@Override
	public void stepOver() throws DebugException {
		if (!canStepOver()) {
			return;
		}
		if (isTopStackFrame()) {
			getThread().stepOver();
		} else {
			((JDIThread) getThread()).stepToFrame(this);
		}
	}

	/**
	 * @see IStep#stepReturn()
	 */
	@Override
	public void stepReturn() throws DebugException {
		if (!canStepReturn()) {
			return;
		}
		if (isTopStackFrame()) {
			getThread().stepReturn();
		} else {
			List<IJavaStackFrame> frames = ((JDIThread) getThread())
					.computeStackFrames();
			int index = frames.indexOf(this);
			if (index >= 0 && index < frames.size() - 1) {
				IStackFrame nextFrame = frames.get(index + 1);
				((JDIThread) getThread()).stepToFrame(nextFrame);
			}
		}
	}

	/**
	 * @see ISuspendResume#suspend()
	 */
	@Override
	public void suspend() throws DebugException {
		getThread().suspend();
	}

	/**
	 * Incrementally updates this stack frames variables.
	 *
	 * @see JDIDebugElement#targetRequestFailed(String, RuntimeException)
	 */
	protected void updateVariables() throws DebugException {
		if (fVariables == null) {
			return;
		}

		// remove old return value first, so the "this" updating logic below works
		if (!fVariables.isEmpty() && fVariables.get(0) instanceof JDIReturnValueVariable) {
			fVariables.remove(0);
		}

		Method method = getUnderlyingMethod();
		int index = 0;
		if (!method.isStatic()) {
			// update "this"
			ObjectReference thisObject;
			thisObject = getUnderlyingThisObject();
			JDIThisVariable oldThisObject = null;
			if (!fVariables.isEmpty()
					&& fVariables.get(0) instanceof JDIThisVariable) {
				oldThisObject = (JDIThisVariable) fVariables.get(0);
			}
			if (thisObject == null && oldThisObject != null) {
				// removal of 'this'
				fVariables.remove(0);
				index = 0;
			} else {
				if (oldThisObject == null && thisObject != null) {
					// creation of 'this'
					oldThisObject = new JDIThisVariable(
							(JDIDebugTarget) getDebugTarget(), thisObject);
					fVariables.add(0, oldThisObject);
					index = 1;
				} else {
					if (oldThisObject != null) {
						// 'this' still exists, replace with new 'this' if a
						// different receiver
						if (!oldThisObject.retrieveValue().equals(thisObject)) {
							fVariables.remove(0);
							fVariables.add(0, new JDIThisVariable(
									(JDIDebugTarget) getDebugTarget(),
									thisObject));
						}
						index = 1;
					}
				}
			}
		}

		List<LocalVariable> locals = null;
		try {
			locals = getUnderlyingStackFrame().visibleVariables();
		} catch (AbsentInformationException e) {
			locals = Collections.EMPTY_LIST;
		} catch (NativeMethodException e) {
			locals = Collections.EMPTY_LIST;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retrieving_visible_variables,
							e.toString()), e);
			// execution will not reach this line, as
			// #targetRequestFailed will throw an exception
			return;
		}
		int localIndex = -1;
		while (index < fVariables.size()) {
			Object var = fVariables.get(index);
			if (var instanceof JDILocalVariable) {
				JDILocalVariable local = (JDILocalVariable) fVariables
						.get(index);
				localIndex = locals.indexOf(local.getLocal());
				if (localIndex >= 0) {
					// update variable with new underling JDI LocalVariable
					local.setLocal(locals.get(localIndex));
					locals.remove(localIndex);
					index++;
				} else {
					// remove variable
					fVariables.remove(index);
				}
			} else {
				// field variable of a static frame
				index++;
			}
		}

		// add any new locals
		Iterator<LocalVariable> newOnes = locals.iterator();
		while (newOnes.hasNext()) {
			JDILocalVariable local = new JDILocalVariable(this, newOnes.next());
			fVariables.add(local);
		}

		addStepReturnValue(fVariables);
	}

	/**
	 * @see org.eclipse.debug.core.model.IDropToFrame#canDropToFrame()
	 */
	@Override
	public boolean canDropToFrame() {
		return supportsDropToFrame();
	}

	/**
	 * @see IJavaStackFrame#supportsDropToFrame()
	 */
	@Override
	public boolean supportsDropToFrame() {
		JDIThread thread = (JDIThread) getThread();
		JDIDebugTarget target = (JDIDebugTarget) thread.getDebugTarget();
		try {
			if (!target.isAvailable() || !thread.isSuspended()
					|| thread.isTerminated() || thread.isInvokingMethod()) {
				return false;
			}
			boolean j9Support = false;
			boolean jdkSupport = target.canPopFrames();
			VirtualMachine vm = getVM();
			if (vm == null) {
				return false;
			}
			try {
				j9Support = (thread.getUnderlyingThread() instanceof org.eclipse.jdi.hcr.ThreadReference)
						&& ((org.eclipse.jdi.hcr.VirtualMachine) vm)
								.canDoReturn();
			} catch (UnsupportedOperationException uoe) {
				j9Support = false;
			}

			if (jdkSupport || j9Support) {
				// Also ensure that this frame and no frames above this
				// frame are native. Unable to pop native stack frames.
				List<IJavaStackFrame> frames = thread.computeStackFrames();
				if (jdkSupport) {
					// JDK 1.4 VMs are currently unable to pop the bottom
					// stack frame.
					if ((frames.size() > 0)
							&& frames.get(frames.size() - 1) == this) {
						return false;
					}
				}
				int index = 0;
				JDIStackFrame frame = null;
				while (index < frames.size()) {
					frame = (JDIStackFrame) frames.get(index);
					index++;
					if (frame.isNative()) {
						return false;
					}
					if (frame.equals(this)) {
						if (jdkSupport) {
							// JDK 1.4 VMs are currently unable to pop the
							// frame directly above a native frame
							if (index < frames.size()
									&& ((JDIStackFrame) frames.get(index))
											.isNative()) {
								return false;
							}
						}
						return true;
					}
				}
			}
			return false;
		} catch (DebugException e) {
			if (e.getStatus().getException() instanceof IncompatibleThreadStateException
					|| e.getStatus().getCode() == IJavaThread.ERR_THREAD_NOT_SUSPENDED) {
				// if the thread has since resumed, drop is not supported
				return false;
			}
			logError(e);
		} catch (UnsupportedOperationException e) {
			// drop to frame not supported - this is an expected
			// exception for VMs that do not support drop to frame
			return false;
		} catch (RuntimeException e) {
			internalError(e);
		}
		return false;
	}

	/**
	 * @see IJavaStackFrame#dropToFrame()
	 */
	@Override
	public void dropToFrame() throws DebugException {
		if (supportsDropToFrame()) {
			((JDIThread) getThread()).dropToFrame(this);
		} else {
			notSupported(JDIDebugModelMessages.JDIStackFrame_Drop_to_frame_not_supported);
		}
	}

	public void popFrame() throws DebugException {
		if (supportsDropToFrame()) {
			((JDIThread) getThread()).popFrame(this);
		} else {
			notSupported(JDIDebugModelMessages.JDIStackFrame_pop_frame_not_supported);
		}
	}

	/**
	 * @see IJavaStackFrame#findVariable(String)
	 */
	@Override
	public IJavaVariable findVariable(String varName) throws DebugException {
		if (isNative()) {
			return null;
		}
		IVariable[] variables = getVariables();
		List<IJavaVariable> possibleMatches = new ArrayList<>();
		IJavaVariable thisVariable = null;
		for (IVariable variable : variables) {
			IJavaVariable var = (IJavaVariable) variable;
			if (var.getName().equals(varName)) {
				possibleMatches.add(var);
			}
			if (var instanceof JDIThisVariable) {
				// save for later - check for instance and static variables
				thisVariable = var;
			}
		}
		for(IJavaVariable variable: possibleMatches){
			// Local Variable has more preference than Field Variable
			if(variable instanceof JDILocalVariable){
				return variable;
			}
		}
		if(possibleMatches.size() > 0) {
			return possibleMatches.get(0);
		}

		if (thisVariable != null) {
			IVariable[] thisChildren = thisVariable.getValue().getVariables();
			for (IVariable element : thisChildren) {
				IJavaVariable var = (IJavaVariable) element;
				if (var.getName().equals(varName)) {
					return var;
				}
			}
		}
		return null;
	}

	/**
	 * Retrieves visible variables in this stack frame handling any exceptions.
	 * Returns an empty list if there are no variables.
	 *
	 * @see JDIDebugElement#targetRequestFailed(String, RuntimeException)
	 */
	protected List<LocalVariable> getUnderlyingVisibleVariables() throws DebugException {
		synchronized (fThread) {
			List<LocalVariable> variables = Collections.EMPTY_LIST;
			try {
				variables = getUnderlyingStackFrame().visibleVariables();
			} catch (AbsentInformationException e) {
				setLocalsAvailable(false);
			} catch (NativeMethodException e) {
				setLocalsAvailable(false);
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIStackFrame_exception_retrieving_visible_variables_2,
								e.toString()), e);
			}
			return variables;
		}
	}

	/**
	 * Retrieves 'this' from the underlying stack frame. Returns
	 * <code>null</code> for static stack frames.
	 *
	 * @see JDIDebugElement#targetRequestFailed(String, RuntimeException)
	 */
	protected ObjectReference getUnderlyingThisObject() throws DebugException {
		synchronized (fThread) {
			if ((fStackFrame == null || fThisObject == null) && !isStatic()) {
				try {
					fThisObject = getUnderlyingStackFrame().thisObject();
				} catch (RuntimeException e) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIStackFrame_exception_retrieving_this,
									e.toString()), e);
					// execution will not reach this line, as
					// #targetRequestFailed will throw an exception
					return null;
				}
			}
			return fThisObject;
		}
	}

	/**
	 * @see IAdaptable#getAdapter(Class)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == IJavaStackFrame.class || adapter == IJavaModifiers.class) {
			return (T) this;
		}
		return super.getAdapter(adapter);
	}

	/**
	 * @see IJavaStackFrame#getSignature()
	 */
	@Override
	public String getSignature() throws DebugException {
		try {
			return getUnderlyingMethod().signature();
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retrieving_method_signature,
							e.toString()), e);
			// execution will not reach this line, as
			// #targetRequestFailed will throw an exception
			return null;
		}
	}

	/**
	 * @see IJavaStackFrame#getDeclaringTypeName()
	 */
	@Override
	public String getDeclaringTypeName() throws DebugException {
		synchronized (fThread) {
			try {
				if (isObsolete()) {
					return JDIDebugModelMessages.JDIStackFrame__unknown_declaring_type__1;
				}
				return JDIReferenceType.getGenericName(getUnderlyingMethod()
						.declaringType());
			} catch (RuntimeException e) {
				if (getThread().isSuspended()) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIStackFrame_exception_retrieving_declaring_type,
									e.toString()), e);
				}
				return JDIDebugModelMessages.JDIStackFrame__unknown_declaring_type__1;
			}
		}
	}

	/**
	 * @see IJavaStackFrame#getReceivingTypeName()
	 */
	@Override
	public String getReceivingTypeName() throws DebugException {
		if (fStackFrame == null || fReceivingTypeName == null) {
			try {
				if (isObsolete()) {
					fReceivingTypeName = JDIDebugModelMessages.JDIStackFrame__unknown_receiving_type__2;
				} else {
					ObjectReference thisObject = getUnderlyingThisObject();
					if (thisObject == null) {
						fReceivingTypeName = getDeclaringTypeName();
					} else {
						fReceivingTypeName = JDIReferenceType
								.getGenericName(thisObject.referenceType());
					}
				}
			} catch (RuntimeException e) {
				if (getThread().isSuspended()) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIStackFrame_exception_retrieving_receiving_type,
									e.toString()), e);
				}
				return JDIDebugModelMessages.JDIStackFrame__unknown_receiving_type__2;
			}
		}
		return fReceivingTypeName;
	}

	/**
	 * @see IJavaStackFrame#getMethodName()
	 */
	@Override
	public String getMethodName() throws DebugException {
		try {
			return getUnderlyingMethod().name();
		} catch (RuntimeException e) {
			if (getThread().isSuspended()) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIStackFrame_exception_retrieving_method_name,
								e.toString()), e);
			}
			return JDIDebugModelMessages.JDIStackFrame__unknown_method__1;
		}
	}

	/**
	 * @see IJavaStackFrame#isNative()
	 */
	@Override
	public boolean isNative() throws DebugException {
		return getUnderlyingMethod().isNative();
	}

	/**
	 * @see IJavaStackFrame#isConstructor()
	 */
	@Override
	public boolean isConstructor() throws DebugException {
		return getUnderlyingMethod().isConstructor();
	}

	/**
	 * @see IJavaStackFrame#isStaticInitializer()
	 */
	@Override
	public boolean isStaticInitializer() throws DebugException {
		return getUnderlyingMethod().isStaticInitializer();
	}

	/**
	 * @see IJavaModifiers#isFinal()
	 */
	@Override
	public boolean isFinal() throws DebugException {
		return getUnderlyingMethod().isFinal();
	}

	/**
	 * @see IJavaStackFrame#isSynchronized()
	 */
	@Override
	public boolean isSynchronized() throws DebugException {
		return getUnderlyingMethod().isSynchronized();
	}

	/**
	 * @see IJavaModifiers#isSynthetic()
	 */
	@Override
	public boolean isSynthetic() throws DebugException {
		return getUnderlyingMethod().isSynthetic();
	}

	/**
	 * @see IJavaModifiers#isPublic()
	 */
	@Override
	public boolean isPublic() throws DebugException {
		return getUnderlyingMethod().isPublic();
	}

	/**
	 * @see IJavaModifiers#isPrivate()
	 */
	@Override
	public boolean isPrivate() throws DebugException {
		return getUnderlyingMethod().isPrivate();
	}

	/**
	 * @see IJavaModifiers#isProtected()
	 */
	@Override
	public boolean isProtected() throws DebugException {
		return getUnderlyingMethod().isProtected();
	}

	/**
	 * @see IJavaModifiers#isPackagePrivate()
	 */
	@Override
	public boolean isPackagePrivate() throws DebugException {
		return getUnderlyingMethod().isPackagePrivate();
	}

	/**
	 * @see IJavaModifiers#isStatic()
	 */
	@Override
	public boolean isStatic() throws DebugException {
		return getUnderlyingMethod().isStatic();
	}

	/**
	 * @see IJavaStackFrame#getSourceName()
	 */
	@Override
	public String getSourceName() throws DebugException {
		synchronized (fThread) {
			return getSourceName(fLocation);
		}
	}

	/**
	 * Returns the source from the default stratum of the given location or
	 * <code>null</code> if not available (missing attribute).
	 */
	private String getSourceName(Location location) throws DebugException {
		try {
			return location.sourceName();
		} catch (AbsentInformationException e) {
			return null;
		} catch (NativeMethodException e) {
			return null;
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retrieving_source_name,
							e.toString()), e);
		}
		return null;
	}

	private boolean equals(Object o1, Object o2) {
		if (o1 == null) {
			return o2 == null;
		}
		return o1.equals(o2);
	}

	protected boolean isTopStackFrame() throws DebugException {
		IStackFrame tos = getThread().getTopStackFrame();
		return tos != null && tos.equals(this);
	}

	/**
	 * Sets this stack frame to be out of synch. Note that passing
	 * <code>true</code> to this method marks this stack frame as out of synch
	 * permanently (statically).
	 */
	public void setOutOfSynch(boolean outOfSynch) {
		fIsOutOfSynch = outOfSynch;
	}

	/**
	 * @see IJavaStackFrame#isOutOfSynch()
	 */
	@Override
	public boolean isOutOfSynch() throws DebugException {
		if (fIsOutOfSynch) {
			return true;
		}
		// if this frame's thread is not suspended, the out-of-synch info cannot
		// change until it suspends again
		if (getThread().isSuspended()) {
			JDIDebugTarget target = (JDIDebugTarget) getDebugTarget();
			if (target.hasHCROccurred()
					&& target.isOutOfSynch(getUnderlyingMethod()
							.declaringType().name())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @see IJavaStackFrame#isObsolete()
	 */
	@Override
	public boolean isObsolete() {
		if (!JDIDebugPlugin.isJdiVersionGreaterThanOrEqual(new int[] { 1, 4 })
				|| !((JDIDebugTarget) getDebugTarget()).hasHCROccurred()) {
			// If no hot code replace has occurred, this frame
			// cannot be obsolete.
			return false;
		}
		// if this frame's thread is not suspended, the obsolete status cannot
		// change until it suspends again
		synchronized (fThread) {
			if (getThread().isSuspended()) {
				return getUnderlyingMethod().isObsolete();
			}
			return false;
		}
	}

	protected boolean exists() {
		synchronized (fThread) {
			return fDepth != -1;
		}
	}

	/**
	 * @see ITerminate#canTerminate()
	 */
	@Override
	public boolean canTerminate() {
		return exists() && getThread().canTerminate()
				|| getDebugTarget().canTerminate();
	}

	/**
	 * @see ITerminate#isTerminated()
	 */
	@Override
	public boolean isTerminated() {
		return getThread().isTerminated();
	}

	/**
	 * @see ITerminate#terminate()
	 */
	@Override
	public void terminate() throws DebugException {
		if (getThread().canTerminate()) {
			getThread().terminate();
		} else {
			getDebugTarget().terminate();
		}
	}

	/**
	 * Returns this stack frame's underlying JDI frame.
	 *
	 * @exception DebugException
	 *                if this stack frame does not currently have an underlying
	 *                frame (is in an interim state where this frame's thread
	 *                has been resumed, and is not yet suspended).
	 */
	protected StackFrame getUnderlyingStackFrame() throws DebugException {
		synchronized (fThread) {
			if (fStackFrame == null) {
				if (fDepth == -1) {
					throw new DebugException(new Status(IStatus.ERROR,
							JDIDebugPlugin.getUniqueIdentifier(),
							IJavaStackFrame.ERR_INVALID_STACK_FRAME,
							JDIDebugModelMessages.JDIStackFrame_25, null));
				}
				if (fThread.isSuspended()) {
					// re-index stack frames - See Bug 47198
					fThread.computeStackFrames();
					if (fDepth == -1) {
						// If depth is -1, then this is an invalid frame
						throw new DebugException(new Status(IStatus.ERROR,
								JDIDebugPlugin.getUniqueIdentifier(),
								IJavaStackFrame.ERR_INVALID_STACK_FRAME,
								JDIDebugModelMessages.JDIStackFrame_25, null));
					}
				} else {
					throw new DebugException(new Status(IStatus.ERROR,
							JDIDebugPlugin.getUniqueIdentifier(),
							IJavaThread.ERR_THREAD_NOT_SUSPENDED,
							JDIDebugModelMessages.JDIStackFrame_25, null));
				}
			}
			return fStackFrame;
		}
	}

	/**
	 * Sets the underlying JDI StackFrame. Called by a thread when incrementally
	 * updating after a step has completed.
	 *
	 * @param frame
	 *            The underlying stack frame
	 */
	protected void setUnderlyingStackFrame(StackFrame frame) {
		synchronized (fThread) {
			fStackFrame = frame;
			if (frame == null) {
				fRefreshVariables = true;
			}
		}
	}

	protected void setThread(JDIThread thread) {
		fThread = thread;
	}

	protected void setVariables(List<IJavaVariable> variables) {
		fVariables = variables;
	}

	/**
	 * @see IJavaStackFrame#getLocalVariables()
	 */
	@Override
	public IJavaVariable[] getLocalVariables() throws DebugException {
		List<LocalVariable> list = getUnderlyingVisibleVariables();
		IJavaVariable[] locals = new IJavaVariable[list.size()];
		for (int i = 0; i < list.size(); i++) {
			locals[i] = new JDILocalVariable(this, list.get(i));
		}
		return locals;
	}

	/**
	 * @see IJavaStackFrame#getThis()
	 */
	@Override
	public IJavaObject getThis() throws DebugException {
		IJavaObject receiver = null;
		if (!isStatic() && !isNative()) {
			ObjectReference thisObject = getUnderlyingThisObject();
			if (thisObject != null) {
				receiver = (IJavaObject) JDIValue.createValue(
						(JDIDebugTarget) getDebugTarget(), thisObject);
			}
		}
		return receiver;
	}

	/**
	 * Java stack frames do not support registers
	 *
	 * @see IStackFrame#getRegisterGroups()
	 */
	@Override
	public IRegisterGroup[] getRegisterGroups() {
		return new IRegisterGroup[0];
	}

	/**
	 * @see IJavaStackFrame#getDeclaringType()
	 */
	@Override
	public IJavaClassType getDeclaringType() throws DebugException {
		Method method = getUnderlyingMethod();
		try {
			Type type = method.declaringType();
			if (type instanceof ClassType) {
				return (IJavaClassType) JDIType.createType(
						(JDIDebugTarget) getDebugTarget(), type);
			}
			targetRequestFailed(JDIDebugModelMessages.JDIStackFrame_0, null);
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retreiving_declaring_type,
							e.toString()), e);
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaStackFrame#getReferenceType()
	 */
	@Override
	public IJavaReferenceType getReferenceType() throws DebugException {
		Method method = getUnderlyingMethod();
		try {
			Type type = method.declaringType();
			return (IJavaReferenceType) JDIType.createType(
					(JDIDebugTarget) getDebugTarget(), type);
		} catch (RuntimeException e) {
			targetRequestFailed(
					MessageFormat.format(
							JDIDebugModelMessages.JDIStackFrame_exception_retreiving_declaring_type,
							e.toString()), e);
		}
		return null;
	}

	/**
	 * Expression level stepping not supported.
	 *
	 * @see IStackFrame#getCharEnd()
	 */
	@Override
	public int getCharEnd() {
		return -1;
	}

	/**
	 * Expression level stepping not supported.
	 *
	 * @see IStackFrame#getCharStart()
	 */
	@Override
	public int getCharStart() {
		return -1;
	}

	/**
	 * Clears the cached data of this stack frame. The underlying stack frame
	 * has changed in such a way that the cached data may not be valid.
	 */
	private void clearCachedData() {
		fThisObject = null;
		fReceivingTypeName = null;
	}

	/**
	 * @see IJavaStackFrame#wereLocalsAvailable()
	 */
	@Override
	public boolean wereLocalsAvailable() {
		return fLocalsAvailable;
	}

	/**
	 * Sets whether locals were available. If the setting is not the same as the
	 * current value, a change event is fired such that a UI client can update.
	 *
	 * @param available
	 *            whether local variable information is available for this stack
	 *            frame.
	 */
	private void setLocalsAvailable(boolean available) {
		if (available != fLocalsAvailable) {
			fLocalsAvailable = available;
			fireChangeEvent(DebugEvent.STATE);
		}
	}

	/**
	 * @see IStackFrame#hasRegisterGroups()
	 */
	@Override
	public boolean hasRegisterGroups() {
		return false;
	}

	/**
	 * @see IStackFrame#hasVariables()
	 */
	@Override
	public boolean hasVariables() throws DebugException {
		return getVariables0().size() > 0;
	}

	/**
	 * @see org.eclipse.debug.core.model.IFilteredStep#canStepWithFilters()
	 */
	@Override
	public boolean canStepWithFilters() {
		if (canStepInto()) {
			String[] filters = getJavaDebugTarget().getStepFilters();
			return filters != null && filters.length > 0;
		}
		return false;
	}

	/**
	 * @see org.eclipse.debug.core.model.IFilteredStep#stepWithFilters()
	 */
	@Override
	public void stepWithFilters() throws DebugException {
		((IJavaThread) getThread()).stepWithFilters();
	}

	/**
	 * @see org.eclipse.jdt.debug.core.IJavaStackFrame#getSourcePath(java.lang.String)
	 */
	@Override
	public String getSourcePath(String stratum) throws DebugException {
		synchronized (fThread) {
			try {
				return fLocation.sourcePath(stratum);
			} catch (AbsentInformationException e) {
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIStackFrame_exception_retrieving_source_path,
								e.toString()), e);
			}
		}
		return null;
	}

	/**
	 * @see org.eclipse.jdt.debug.core.IJavaStackFrame#getSourcePath()
	 */
	@Override
	public String getSourcePath() throws DebugException {
		synchronized (fThread) {
			try {
				return fLocation.sourcePath();
			} catch (AbsentInformationException e) {
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIStackFrame_exception_retrieving_source_path,
								e.toString()), e);
			}
		}
		return null;
	}

	/*
	 * @see
	 * org.eclipse.jdt.debug.core.IJavaStackFrame#getLineNumber(java.lang.String
	 * )
	 */
	@Override
	public int getLineNumber(String stratum) throws DebugException {
		synchronized (fThread) {
			try {
				return fLocation.lineNumber(stratum);
			} catch (RuntimeException e) {
				if (getThread().isSuspended()) {
					targetRequestFailed(
							MessageFormat.format(
									JDIDebugModelMessages.JDIStackFrame_exception_retrieving_line_number,
									e.toString()), e);
				}
			}
		}
		return -1;
	}

	/*
	 * @see
	 * org.eclipse.jdt.debug.core.IJavaStackFrame#getSourceName(java.lang.String
	 * )
	 */
	@Override
	public String getSourceName(String stratum) throws DebugException {
		synchronized (fThread) {
			try {
				return fLocation.sourceName(stratum);
			} catch (AbsentInformationException e) {
			} catch (NativeMethodException e) {
			} catch (RuntimeException e) {
				targetRequestFailed(
						MessageFormat.format(
								JDIDebugModelMessages.JDIStackFrame_exception_retrieving_source_name,
								e.toString()), e);
			}
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaStackFrame#isVarargs()
	 */
	@Override
	public boolean isVarArgs() throws DebugException {
		return getUnderlyingMethod().isVarArgs();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.debug.core.IJavaStackFrame#canForceReturn()
	 */
	@Override
	public boolean canForceReturn() {
		if (getJavaDebugTarget().supportsForceReturn() && isSuspended()) {
			try {
				if (!isNative()) {
					if (isTopStackFrame()) {
						return true;
					}
					List<IJavaStackFrame> frames = fThread.computeStackFrames();
					int index = frames.indexOf(this);
					if (index > 0) {
						JDIStackFrame prev = (JDIStackFrame) frames
								.get(index - 1);
						return prev.canDropToFrame();
					}
				}
			} catch (DebugException e) {
			}
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.eclipse.jdt.debug.core.IJavaStackFrame#forceReturn(org.eclipse.jdt
	 * .debug.core.IJavaValue)
	 */
	@Override
	public void forceReturn(IJavaValue value) throws DebugException {
		if (isTopStackFrame()) {
			fThread.forceReturn(value);
		} else {
			// first check assignment compatible
			Method method = getUnderlyingMethod();
			try {
				ValueImpl.checkValue(((JDIValue) value).getUnderlyingValue(),
						method.returnType(),
						(VirtualMachineImpl) method.virtualMachine());
			} catch (InvalidTypeException e) {
				targetRequestFailed(JDIDebugModelMessages.JDIStackFrame_26, e);
			} catch (ClassNotLoadedException e) {
				targetRequestFailed(JDIDebugModelMessages.JDIThread_48, e);
			}
			List<IJavaStackFrame> frames = fThread.computeStackFrames();
			int index = frames.indexOf(this);
			if (index > 0) {
				JDIStackFrame prev = (JDIStackFrame) frames.get(index - 1);
				fThread.popFrame(prev);
				fThread.forceReturn(value);
			}
		}
	}

	public void setIsTop(boolean isTop) {
		this.fIsTop = isTop;
	}
}
