/*******************************************************************************
 * Copyright (c) 2000, 2020 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.ast.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
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.eval.ast.instructions.Instruction;
import org.eclipse.jdt.internal.debug.eval.ast.instructions.InstructionSequence;

import com.sun.jdi.VMDisconnectedException;

public class Interpreter {
	private Instruction[] fInstructions;
	private int fInstructionCounter;
	private IRuntimeContext fContext;
	private Stack<Object> fStack;
	private IJavaValue fLastValue;

	/**
	 * The list of internal variables
	 */
	private Map<String, IVariable> fInternalVariables;

	/**
	 * List of objects for which GC has been disabled
	 */
	private List<IJavaObject> fPermStorage = null;

	private boolean fStopped = false;

	public Interpreter(InstructionSequence instructions, IRuntimeContext context) {
		fInstructions = instructions.getInstructions();
		fContext = context;
		fInternalVariables = new HashMap<>();
	}

	public void execute(boolean disableGcOnResult) throws CoreException {
		try {
			reset();
			while (fInstructionCounter < fInstructions.length && !fStopped) {
				Instruction instruction = fInstructions[fInstructionCounter++];
				instruction.setInterpreter(this);
				instruction.execute();
				instruction.setInterpreter(null);
			}
		} catch (CoreException e) {
			throw e;
		} catch (Exception e) {
			throw new CoreException(new Status(IStatus.ERROR,
					JDIDebugModel.getPluginIdentifier(), e.getMessage(), e));
		} finally {
			releaseObjects(disableGcOnResult);
		}
	}

	public void stop() {
		fStopped = true;
	}

	private void reset() {
		fStack = new Stack<>();
		fInstructionCounter = 0;
	}

	/**
	 * Jumps to a given address
	 */
	public void jump(int offset) {
		fInstructionCounter += offset;
	}

	/**
	 * Pushes an object onto the stack. Disables garbage collection for any
	 * interim object pushed onto the stack. Objects are released after the
	 * evaluation completes.
	 */
	public void push(Object object) {
		fStack.push(object);
		if (object instanceof IJavaObject) {
			disableCollection((IJavaObject) object);
		}
	}

	/**
	 * Avoid garbage collecting interim results.
	 *
	 * @param value
	 *            object to disable garbage collection for
	 */
	private void disableCollection(IJavaObject value) {
		if (fPermStorage == null) {
			fPermStorage = new ArrayList<>(5);
		}
		try {
			value.disableCollection();
			fPermStorage.add(value);
		} catch (CoreException e) {
			// don't worry about GC if the VM has terminated
			if ((e.getStatus().getException() instanceof VMDisconnectedException)) {
				return;
			}
			JDIDebugPlugin.log(e);
		}
	}

	/**
	 * Re-enable garbage collection if interim results.
	 */
	private void releaseObjects(boolean disableGcOnResult) {
		if (fPermStorage != null) {
			IJavaValue result = getResult();
			Iterator<IJavaObject> iterator = fPermStorage.iterator();
			while (iterator.hasNext()) {
				IJavaObject object = iterator.next();
				try {
					if (!disableGcOnResult || object != result) {
						object.enableCollection();
					}
				} catch (CoreException e) {
					// don't worry about GC if the VM has terminated
					if ((e.getStatus().getException() instanceof VMDisconnectedException)) {
						break;
					}
					JDIDebugPlugin.log(e);
				}
			}
			fPermStorage = null;
		}
	}

	/**
	 * Peeks at the top object of the stack
	 */
	public Object peek() {
		return fStack.peek();
	}

	/**
	 * Pops an object off of the stack
	 */
	public Object pop() {
		return fStack.pop();
	}

	/**
	 * Answers the context for the interpreter
	 */
	public IRuntimeContext getContext() {
		return fContext;
	}

	public IJavaValue getResult() {
		if (fStack == null || fStack.isEmpty()) {
			if (fLastValue == null) {
				return getContext().getVM().voidValue();
			}
			return fLastValue;
		}
		Object top = fStack.peek();
		if (top instanceof IJavaVariable) {
			try {
				return (IJavaValue) ((IJavaVariable) top).getValue();
			} catch (CoreException exception) {
				return getContext().getVM().newValue(
						exception.getStatus().getMessage());
			}
		}
		if (top instanceof IJavaValue) {
			return (IJavaValue) top;
		}
		// XXX: exception
		return null;
	}

	public void setLastValue(IJavaValue value) {
		fLastValue = value;
	}

	/**
	 * Create a new variable in the interpreter with the given name and the
	 * given type.
	 *
	 * @param name
	 *            the name of the variable to create.
	 * @param type
	 *            the type of the variable to create.
	 * @return the created variable.
	 */
	public IVariable createInternalVariable(String name, IJavaType referencType) {
		IVariable var = new InterpreterVariable(name, referencType,
				fContext.getVM());
		fInternalVariables.put(name, var);
		return var;
	}

	/**
	 * Return the variable with the given name. This method only looks in the
	 * list of internal variable (i.e. created by
	 * Interpreter#createInternalVariable(String, IJavaType))
	 *
	 * @param name
	 *            the name of the variable to retrieve.
	 * @return the corresponding variable, or <code>null</code> if there is
	 *         none.
	 */
	public IVariable getInternalVariable(String name) {
		return fInternalVariables.get(name);
	}
}
