/*******************************************************************************
 * Copyright (c) 2009, 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.core.breakpoints;

import java.text.MessageFormat;

import org.eclipse.core.runtime.CoreException;
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.IDebugTarget;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.Message;
import org.eclipse.jdt.debug.core.IJavaBreakpoint;
import org.eclipse.jdt.debug.core.IJavaBreakpointListener;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaLineBreakpoint;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaPrimitiveValue;
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.eval.IAstEvaluationEngine;
import org.eclipse.jdt.debug.eval.ICompiledExpression;
import org.eclipse.jdt.debug.eval.IEvaluationListener;
import org.eclipse.jdt.debug.eval.IEvaluationResult;
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.JDINullValue;
import org.eclipse.jdt.internal.debug.core.model.JDIThread;
import org.eclipse.jdt.internal.debug.core.model.JDIValue;

import com.sun.jdi.VMDisconnectedException;

/**
 * Breakpoint listener to handle breakpoint conditions.
 *
 * @since 3.5
 */
public class ConditionalBreakpointHandler implements IJavaBreakpointListener {

	/**
	 * Whether the condition had compile or runtime errors
	 */
	private boolean fHasErrors = false;

	/**
	 * Listens for evaluation completion for condition evaluation. If an
	 * evaluation evaluates <code>true</code> or has an error, this breakpoint
	 * will suspend the thread in which the breakpoint was hit. If the
	 * evaluation returns <code>false</code>, the thread is resumed.
	 */
	class EvaluationListener implements IEvaluationListener {

		/**
		 * Lock for synchronizing evaluation
		 */
		private Object fLock = new Object();

		/**
		 * The breakpoint that was hit
		 */
		private JavaLineBreakpoint fBreakpoint;

		/**
		 * Result of the vote
		 */
		private int fVote;

		EvaluationListener(JavaLineBreakpoint breakpoint) {
			fBreakpoint = breakpoint;
		}

		@Override
		public void evaluationComplete(IEvaluationResult result) {
			fVote = determineVote(result);
			synchronized (fLock) {
				fLock.notifyAll();
			}
		}

		/**
		 * Processes the result to determine whether to suspend or resume.
		 *
		 * @param result
		 *            evaluation result
		 * @return vote
		 */
		private int determineVote(IEvaluationResult result) {
			if (result.isTerminated()) {
				// indicates the user terminated the evaluation
				return SUSPEND;
			}
			JDIThread thread = (JDIThread) result.getThread();
			if (result.hasErrors()) {
				DebugException exception = result.getException();
				if (exception == null) {
					return DONT_SUSPEND;
				}
				Throwable wrappedException = exception.getStatus()
						.getException();
				if (wrappedException instanceof VMDisconnectedException) {
					// VM terminated/disconnected during evaluation
					return DONT_SUSPEND;
				}
				fireConditionHasRuntimeErrors(fBreakpoint, exception);
				return SUSPEND;
			}
			try {
				IValue value = result.getValue();
				if (fBreakpoint.isConditionSuspendOnTrue()) {
					if (value instanceof IJavaPrimitiveValue) {
						// Suspend when the condition evaluates true
						IJavaPrimitiveValue javaValue = (IJavaPrimitiveValue) value;
						if (javaValue.getJavaType().getName()
								.equals("boolean")) { //$NON-NLS-1$
							if (javaValue.getBooleanValue()) {
								return SUSPEND;
							}
							return DONT_SUSPEND;
						}
					}
					if ((value instanceof JDIValue) && !(value instanceof JDINullValue)) {
						JDIValue jdiValue = (JDIValue)value;
						// Suspend if return is Boolean(true) else don't suspend (no error dialog)
						if (jdiValue.getJavaType().getName().equals("java.lang.Boolean")) {//$NON-NLS-1$
							IJavaPrimitiveValue javaValue = (IJavaPrimitiveValue) ((IJavaObject) jdiValue).getField("value", false).getValue(); //$NON-NLS-1$
							if (javaValue.getBooleanValue()) {
								return SUSPEND;
							}
							return DONT_SUSPEND;
						}
						return DONT_SUSPEND;
					}
					IStatus status = new Status(
							IStatus.ERROR,
							JDIDebugPlugin.getUniqueIdentifier(),
							MessageFormat.format(JDIDebugBreakpointMessages.ConditionalBreakpointHandler_1, value.getReferenceTypeName()));
					// result was not JDIValue
					fireConditionHasRuntimeErrors(fBreakpoint, new DebugException(status));
					return SUSPEND;
				}
				IDebugTarget debugTarget = thread.getDebugTarget();
				IValue lastValue = fBreakpoint
						.setCurrentConditionValue(debugTarget, value);
				if (!value.equals(lastValue)) {
					return SUSPEND;
				}
				return DONT_SUSPEND;
			} catch (DebugException e) {
				// Suspend when an error occurs
				JDIDebugPlugin.log(e);
				return SUSPEND;
			}
		}

		/**
		 * Result of the conditional expression evaluation - to resume or not
		 * resume, that is the question.
		 *
		 * @return vote result
		 */
		int getVote() {
			return fVote;
		}

		/**
		 * Returns the lock object to synchronize this evaluation.
		 *
		 * @return lock object
		 */
		Object getLock() {
			return fLock;
		}
	}

	@Override
	public void addingBreakpoint(IJavaDebugTarget target,
			IJavaBreakpoint breakpoint) {
	}

	@Override
	public void breakpointHasCompilationErrors(IJavaLineBreakpoint breakpoint,
			Message[] errors) {
	}

	@Override
	public void breakpointHasRuntimeException(IJavaLineBreakpoint breakpoint,
			DebugException exception) {
	}

	@Override
	public int breakpointHit(IJavaThread thread, IJavaBreakpoint breakpoint) {
		if (breakpoint instanceof IJavaLineBreakpoint) {
			JavaLineBreakpoint lineBreakpoint = (JavaLineBreakpoint) breakpoint;
			try {
				final String condition = lineBreakpoint.getCondition();
				if (condition == null) {
					return SUSPEND;
				}
				EvaluationListener listener = new EvaluationListener(
						lineBreakpoint);
				IJavaStackFrame frame = (IJavaStackFrame) thread
						.getTopStackFrame();
				IJavaProject project = lineBreakpoint.getJavaProject(frame);
				if (project == null) {
					fireConditionHasErrors(
							lineBreakpoint,
							new Message[] { new Message(
									JDIDebugBreakpointMessages.JavaLineBreakpoint_Unable_to_compile_conditional_breakpoint___missing_Java_project_context__1,
									-1) });
					return SUSPEND;
				}
				IJavaDebugTarget target = (IJavaDebugTarget) thread
						.getDebugTarget();
				IAstEvaluationEngine engine = getEvaluationEngine(target,
						project);
				if (engine == null) {
					// If no engine is available, suspend
					return SUSPEND;
				}
				ICompiledExpression expression = lineBreakpoint
						.getExpression(thread);
				if (expression == null) {
					expression = engine.getCompiledExpression(condition, frame);
					lineBreakpoint.setExpression(thread, expression);
				}
				if (expression.hasErrors()) {
					fireConditionHasErrors(lineBreakpoint,
							getMessages(expression));
					return SUSPEND;
				}
				Object lock = listener.getLock();
				synchronized (lock) {
					engine.evaluateExpression(expression, frame, listener,
							DebugEvent.EVALUATION_IMPLICIT, false);
					// TODO: timeout?
					try {
						lock.wait();
					} catch (InterruptedException e) {
						fireConditionHasRuntimeErrors(
								lineBreakpoint,
								new DebugException(
										new Status(
												IStatus.ERROR,
												JDIDebugPlugin
														.getUniqueIdentifier(),
												JDIDebugBreakpointMessages.ConditionalBreakpointHandler_0,
												e)));
						return SUSPEND;
					}
				}
				return listener.getVote();
			} catch (CoreException e) {
				DebugException de = null;
				if (e instanceof DebugException) {
					de = (DebugException) e;
				} else {
					de = new DebugException(e.getStatus());
				}
				fireConditionHasRuntimeErrors(lineBreakpoint, de);
			}
		}
		return SUSPEND;
	}

	@Override
	public void breakpointInstalled(IJavaDebugTarget target,
			IJavaBreakpoint breakpoint) {
	}

	@Override
	public void breakpointRemoved(IJavaDebugTarget target,
			IJavaBreakpoint breakpoint) {
	}

	@Override
	public int installingBreakpoint(IJavaDebugTarget target,
			IJavaBreakpoint breakpoint, IJavaType type) {
		return 0;
	}

	/**
	 * Returns an evaluation engine for evaluating this breakpoint's condition
	 * in the given target and project context.
	 * @param vm the VM to get an evaluation engine for
	 * @param project the project context
	 * @return a new {@link IAstEvaluationEngine}
	 */
	private IAstEvaluationEngine getEvaluationEngine(IJavaDebugTarget vm, IJavaProject project) {
		return ((JDIDebugTarget) vm).getEvaluationEngine(project);
	}

	private void fireConditionHasRuntimeErrors(IJavaLineBreakpoint breakpoint, DebugException exception) {
		fHasErrors = true;
		JDIDebugPlugin.getDefault().fireBreakpointHasRuntimeException(breakpoint, exception);
	}

	/**
	 * Notifies listeners that a conditional breakpoint expression has been
	 * compiled that contains errors
	 * @param breakpoint the breakpoint that has errors in its condition
	 * @param messages the error messages
	 */
	private void fireConditionHasErrors(IJavaLineBreakpoint breakpoint, Message[] messages) {
		fHasErrors = true;
		JDIDebugPlugin.getDefault().fireBreakpointHasCompilationErrors(breakpoint, messages);
	}

	/**
	 * Convert an array of <code>String</code> to an array of
	 * <code>Message</code>.
	 * @param expression the expression to get messages from
	 * @return the array of {@link Message}s from the expression
	 */
	private Message[] getMessages(ICompiledExpression expression) {
		String[] errorMessages = expression.getErrorMessages();
		Message[] messages = new Message[errorMessages.length];
		for (int i = 0; i < messages.length; i++) {
			messages[i] = new Message(errorMessages[i], -1);
		}
		return messages;
	}

	/**
	 * Returns whether errors were encountered when evaluating the condition
	 * (compilation or runtime).
	 *
	 * @return whether errors were encountered when evaluating the condition
	 */
	public boolean hasErrors() {
		return fHasErrors;
	}
}
