/*******************************************************************************
 * Copyright (c) 2000, 2017 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.util.HashMap;
import java.util.Map;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.jdt.debug.core.IJavaLineBreakpoint;
import org.eclipse.jdt.debug.core.IJavaWatchpoint;
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 com.sun.jdi.Field;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.event.AccessWatchpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.ModificationWatchpointEvent;
import com.sun.jdi.request.AccessWatchpointRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.ModificationWatchpointRequest;
import com.sun.jdi.request.WatchpointRequest;

public class JavaWatchpoint extends JavaLineBreakpoint implements
		IJavaWatchpoint {

	public static final String JAVA_WATCHPOINT = "org.eclipse.jdt.debug.javaWatchpointMarker"; //$NON-NLS-1$
	/**
	 * Watchpoint attribute storing the access value (value
	 * <code>"org.eclipse.jdt.debug.core.access"</code>). This attribute is
	 * stored as a <code>boolean</code>, indicating whether a watchpoint is an
	 * access watchpoint.
	 */
	protected static final String ACCESS = "org.eclipse.jdt.debug.core.access"; //$NON-NLS-1$
	/**
	 * Watchpoint attribute storing the modification value (value
	 * <code>"org.eclipse.jdt.debug.core.modification"</code>). This attribute
	 * is stored as a <code>boolean</code>, indicating whether a watchpoint is a
	 * modification watchpoint.
	 */
	protected static final String MODIFICATION = "org.eclipse.jdt.debug.core.modification"; //$NON-NLS-1$
	/**
	 * Watchpoint attribute storing the auto_disabled value (value
	 * <code>"org.eclipse.jdt.debug.core.auto_disabled"</code>). This attribute
	 * is stored as a <code>boolean</code>, indicating whether a watchpoint has
	 * been auto-disabled (as opposed to being disabled explicitly by the user)
	 */
	protected static final String AUTO_DISABLED = "org.eclipse.jdt.debug.core.auto_disabled"; //$NON-NLS-1$

	/**
	 * Breakpoint attribute storing the name of the field on which a breakpoint
	 * is set. (value <code>"org.eclipse.jdt.debug.core.fieldName"</code>). This
	 * attribute is a <code>String</code>.
	 */
	protected static final String FIELD_NAME = "org.eclipse.jdt.debug.core.fieldName"; //$NON-NLS-1$
	/**
	 * Flag indicating that this breakpoint last suspended execution due to a
	 * field access
	 */
	protected static final Integer ACCESS_EVENT = Integer.valueOf(0);
	/**
	 * Flag indicating that this breakpoint last suspended execution due to a
	 * field modification
	 */
	protected static final Integer MODIFICATION_EVENT = Integer.valueOf(1);
	/**
	 * Maps each debug target that is suspended for this breakpoint to reason
	 * that this breakpoint suspended it. Reasons include:
	 * <ol>
	 * <li>Field access (value <code>ACCESS_EVENT</code>)</li>
	 * <li>Field modification (value <code>MODIFICATION_EVENT</code>)</li>
	 * </ol>
	 */
	private HashMap<JDIDebugTarget, Integer> fLastEventTypes = new HashMap<>(10);

	public JavaWatchpoint() {
	}

	/**
	 * @see JDIDebugModel#createWatchpoint(IResource, String, String, int, int,
	 *      int, int, boolean, Map)
	 */
	public JavaWatchpoint(final IResource resource, final String typeName,
			final String fieldName, final int lineNumber, final int charStart,
			final int charEnd, final int hitCount, final boolean add,
			final Map<String, Object> attributes) throws DebugException {
		IWorkspaceRunnable wr = monitor -> {
			// add attributes
			addLineBreakpointAttributes(attributes, getModelIdentifier(),
					true, lineNumber, charStart, charEnd);
			addTypeNameAndHitCount(attributes, typeName, hitCount);
			attributes.put(SUSPEND_POLICY, Integer.valueOf(getDefaultSuspendPolicy()));
			// configure the field handle
			addFieldName(attributes, fieldName);
			// configure the access and modification flags to defaults
			addDefaultAccessAndModification(attributes);

			setMarker(resource.createMarker(JAVA_WATCHPOINT, attributes));

			register(add);
		};
		run(getMarkerRule(resource), wr);
	}

	/**
	 * @see JavaBreakpoint#createRequest(JDIDebugTarget, ReferenceType)
	 *
	 *      Creates and installs an access and modification watchpoint request
	 *      in the given reference type, configuring the requests as appropriate
	 *      for this watchpoint. The requests are then enabled based on whether
	 *      this watchpoint is an access watchpoint, modification watchpoint, or
	 *      both. Finally, the requests are registered with the given target.
	 */
	@Override
	protected boolean createRequest(JDIDebugTarget target, ReferenceType type)
			throws CoreException {
		if (shouldSkipBreakpoint()) {
			return false;
		}
		Field field = null;

		field = type.fieldByName(getFieldName());
		if (field == null) {
			// error
			return false;
		}
		AccessWatchpointRequest accessRequest = null;
		ModificationWatchpointRequest modificationRequest = null;
		if (target.supportsAccessWatchpoints()) {
			accessRequest = createAccessWatchpoint(target, field);
			registerRequest(accessRequest, target);
		} else {
			notSupported(JDIDebugBreakpointMessages.JavaWatchpoint_no_access_watchpoints);
		}
		if (target.supportsModificationWatchpoints()) {
			modificationRequest = createModificationWatchpoint(target, field);
			if (modificationRequest == null) {
				return false;
			}
			registerRequest(modificationRequest, target);
			return true;
		}
		notSupported(JDIDebugBreakpointMessages.JavaWatchpoint_no_modification_watchpoints);
		return false;
	}

	/**
	 * @see JavaBreakpoint#setRequestThreadFilter(EventRequest)
	 */
	@Override
	protected void setRequestThreadFilter(EventRequest request,
			ThreadReference thread) {
		((WatchpointRequest) request).addThreadFilter(thread);
	}

	/**
	 * Either access or modification watchpoints are not supported. Throw an
	 * appropriate exception.
	 *
	 * @param message
	 *            the message that states that access or modification
	 *            watchpoints are not supported
	 */
	protected void notSupported(String message) throws DebugException {
		throw new DebugException(new Status(IStatus.ERROR,
				DebugPlugin.getUniqueIdentifier(),
				DebugException.NOT_SUPPORTED, message, null)); //
	}

	/**
	 * Create an access watchpoint for the given breakpoint and associated field
	 */
	protected AccessWatchpointRequest createAccessWatchpoint(
			JDIDebugTarget target, Field field) throws CoreException {
		return (AccessWatchpointRequest) createWatchpoint(target, field, true);
	}

	/**
	 * Create a modification watchpoint for the given breakpoint and associated
	 * field
	 */
	protected ModificationWatchpointRequest createModificationWatchpoint(
			JDIDebugTarget target, Field field) throws CoreException {
		return (ModificationWatchpointRequest) createWatchpoint(target, field,
				false);
	}

	/**
	 * Create a watchpoint for the given breakpoint and associated field.
	 *
	 * @param target
	 *            the target in which the request will be installed
	 * @param field
	 *            the field on which the request will be set
	 * @param access
	 *            <code>true</code> if an access watchpoint will be created.
	 *            <code>false</code> if a modification watchpoint will be
	 *            created.
	 *
	 * @return an WatchpointRequest (AccessWatchpointRequest if access is
	 *         <code>true</code>; ModificationWatchpointRequest if access is
	 *         <code>false</code>).
	 */
	protected WatchpointRequest createWatchpoint(JDIDebugTarget target,
			Field field, boolean access) throws CoreException {
		WatchpointRequest request = null;
		EventRequestManager manager = target.getEventRequestManager();
		if (manager == null) {
			target.requestFailed(
					JDIDebugBreakpointMessages.JavaWatchpoint_Unable_to_create_breakpoint_request___VM_disconnected__1,
					new VMDisconnectedException());
		}
		try {
			if (access) {
				request = manager.createAccessWatchpointRequest(field);
			} else {
				request = manager.createModificationWatchpointRequest(field);
			}
			configureRequest(request, target);
		} catch (VMDisconnectedException e) {
			if (!target.isAvailable()) {
				return null;
			}
			target.internalError(e);
			return null;
		} catch (RuntimeException e) {
			target.internalError(e);
			return null;
		}
		return request;
	}

	/**
	 * @see JavaBreakpoint#recreateRequest(EventRequest, JDIDebugTarget)
	 */
	protected EventRequest recreateRequest(EventRequest request,
			JDIDebugTarget target) throws CoreException {
		try {
			Field field = ((WatchpointRequest) request).field();
			if (request instanceof AccessWatchpointRequest) {
				request = createAccessWatchpoint(target, field);
			} else if (request instanceof ModificationWatchpointRequest) {
				request = createModificationWatchpoint(target, field);
			}
		} catch (VMDisconnectedException e) {
			if (!target.isAvailable()) {
				return request;
			}
			target.internalError(e);
			return request;
		} catch (RuntimeException e) {
			target.internalError(e);
		}
		return request;
	}

	/**
	 * @see IBreakpoint#setEnabled(boolean)
	 *
	 *      If the watchpoint is not watching access or modification, set the
	 *      default values. If this isn't done, the resulting state (enabled
	 *      with access and modification both disabled) is ambiguous.
	 */
	@Override
	public void setEnabled(boolean enabled) throws CoreException {
		if (enabled) {
			if (!(isAccess() || isModification())) {
				setDefaultAccessAndModification();
			}
		}
		super.setEnabled(enabled);
	}

	/**
	 * @see org.eclipse.debug.core.model.IWatchpoint#isAccess()
	 */
	@Override
	public boolean isAccess() throws CoreException {
		return ensureMarker().getAttribute(ACCESS, false);
	}

	/**
	 * Sets whether this breakpoint will suspend execution when its associated
	 * field is accessed. If true and this watchpoint is disabled, this
	 * watchpoint is automatically enabled. If both access and modification are
	 * false, this watchpoint is automatically disabled.
	 *
	 * @param access
	 *            whether to suspend on field access
	 * @exception CoreException
	 *                if unable to set the property on this breakpoint's
	 *                underlying marker
	 * @see org.eclipse.debug.core.model.IWatchpoint#setAccess(boolean)
	 */
	@Override
	public void setAccess(boolean access) throws CoreException {
		if (access == isAccess()) {
			return;
		}
		setAttribute(ACCESS, access);
		if (access && !isEnabled()) {
			setEnabled(true);
		} else if (!(access || isModification())) {
			setEnabled(false);
		}
		recreate();
	}

	/**
	 * @see org.eclipse.debug.core.model.IWatchpoint#isModification()
	 */
	@Override
	public boolean isModification() throws CoreException {
		return ensureMarker().getAttribute(MODIFICATION, false);
	}

	/**
	 * Sets whether this breakpoint will suspend execution when its associated
	 * field is modified. If true and this watchpoint is disabled, this
	 * watchpoint is automatically enabled. If both access and modification are
	 * false, this watchpoint is automatically disabled.
	 *
	 * @param modification
	 *            whether to suspend on field modification
	 * @exception CoreException
	 *                if unable to set the property on this breakpoint's
	 *                underlying marker
	 * @see org.eclipse.debug.core.model.IWatchpoint#setModification(boolean)
	 */
	@Override
	public void setModification(boolean modification) throws CoreException {
		if (modification == isModification()) {
			return;
		}
		setAttribute(MODIFICATION, modification);
		if (modification && !isEnabled()) {
			setEnabled(true);
		} else if (!(modification || isAccess())) {
			setEnabled(false);
		}
		recreate();
	}

	/**
	 * Sets the default access and modification attributes of the watchpoint.
	 * The default values are:
	 * <ul>
	 * <li>access = <code>false</code>
	 * <li>modification = <code>true</code>
	 * <ul>
	 */
	protected void setDefaultAccessAndModification() throws CoreException {
		boolean[] def = getDefaultAccessAndModificationValues();
		Object[] values = new Object[def.length];
		for (int i = 0; i < def.length; i++) {
			values[i] = Boolean.valueOf(def[i]);
		}
		String[] attributes = new String[] { ACCESS, MODIFICATION };
		setAttributes(attributes, values);
	}

	/**
	 * Returns the default access and modification suspend option for a new
	 * watchpoint based on the user preference settings The return array will
	 * only ever contain two values, where the possibilities are:
	 * <ul>
	 * <li> <code>{true, true}</code> - both access and modification are enabled</li>
	 * <li> <code>{true, false}</code> - access is enabled and modification is
	 * disabled</li>
	 * <li> <code>{false, true}</code> -access is disabled and modification is
	 * enabled</li>
	 * </ul>
	 * The default returned array is <code>{true, true}</code>
	 *
	 * @return an array of two boolean values representing the default access
	 *         and modification settings
	 *
	 * @since 3.3.1
	 */
	protected boolean[] getDefaultAccessAndModificationValues() {
		int value = Platform.getPreferencesService().getInt(
				JDIDebugPlugin.getUniqueIdentifier(),
				JDIDebugPlugin.PREF_DEFAULT_WATCHPOINT_SUSPEND_POLICY,
				0,
				null);
		switch (value) {
		case 0: {
			return new boolean[] { true, true };
		}
		case 1: {
			return new boolean[] { true, false };
		}
		case 2: {
			return new boolean[] { false, true };
		}
		default: {
			return new boolean[] { true, true };
		}
		}
	}

	/**
	 * Adds the default access and modification attributes of the watchpoint to
	 * the given map
	 * <ul>
	 * <li>access = true
	 * <li>modification = true
	 * <li>auto disabled = false
	 * <ul>
	 */
	protected void addDefaultAccessAndModification(Map<String, Object> attributes) {
		boolean[] values = getDefaultAccessAndModificationValues();
		attributes.put(ACCESS, (values[0] ? Boolean.TRUE : Boolean.FALSE));
		attributes
				.put(MODIFICATION, (values[1] ? Boolean.TRUE : Boolean.FALSE));
		attributes.put(AUTO_DISABLED, Boolean.FALSE);
	}

	/**
	 * Adds the field name to the given attribute map
	 */
	protected void addFieldName(Map<String, Object> attributes, String fieldName) {
		attributes.put(FIELD_NAME, fieldName);
	}

	/**
	 * @see IJavaWatchpoint#getFieldName()
	 */
	@Override
	public String getFieldName() throws CoreException {
		return ensureMarker().getAttribute(FIELD_NAME, null);
	}

	/**
	 * Store the type of the event, then handle it as specified in the
	 * superclass. This is useful for correctly generating the thread text when
	 * asked (assumes thread text is requested after the event is passed to this
	 * breakpoint.
	 *
	 * Also, @see JavaBreakpoint#handleEvent(Event, JDIDebugTarget)
	 */
	@Override
	public boolean handleEvent(Event event, JDIDebugTarget target,
			boolean suspendVote, EventSet eventSet) {
		if (event instanceof AccessWatchpointEvent) {
			fLastEventTypes.put(target, ACCESS_EVENT);
		} else if (event instanceof ModificationWatchpointEvent) {
			fLastEventTypes.put(target, MODIFICATION_EVENT);
		}
		return super.handleEvent(event, target, suspendVote, eventSet);
	}

	/**
	 * @see JavaBreakpoint#updateEnabledState(EventRequest, JDIDebugTarget)
	 */
	@Override
	protected void updateEnabledState(EventRequest request,
			JDIDebugTarget target) throws CoreException {
		boolean enabled = isEnabled();
		if (request instanceof AccessWatchpointRequest) {
			if (isAccess()) {
				if (enabled != request.isEnabled()) {
					internalUpdateEnabledState(request, enabled, target);
				}
			} else {
				if (request.isEnabled()) {
					internalUpdateEnabledState(request, false, target);
				}
			}
		}
		if (request instanceof ModificationWatchpointRequest) {
			if (isModification()) {
				if (enabled != request.isEnabled()) {
					internalUpdateEnabledState(request, enabled, target);
				}
			} else {
				if (request.isEnabled()) {
					internalUpdateEnabledState(request, false, target);
				}
			}
		}
	}

	/**
	 * @see IJavaWatchpoint#isAccessSuspend(IDebugTarget)
	 */
	@Override
	public boolean isAccessSuspend(IDebugTarget target) {
		Integer lastEventType = fLastEventTypes.get(target);
		if (lastEventType == null) {
			return false;
		}
		return lastEventType.equals(ACCESS_EVENT);
	}

	/**
	 * @see IJavaLineBreakpoint#supportsCondition()
	 */
	@Override
	public boolean supportsCondition() {
		return true;
	}

	/**
	 * @see org.eclipse.jdt.internal.debug.core.breakpoints.JavaBreakpoint#removeFromTarget(JDIDebugTarget)
	 */
	@Override
	public void removeFromTarget(JDIDebugTarget target) throws CoreException {
		fLastEventTypes.remove(target);
		super.removeFromTarget(target);
	}

	/**
	 * @see org.eclipse.jdt.internal.debug.core.breakpoints.JavaBreakpoint#addInstanceFilter(EventRequest,
	 *      ObjectReference)
	 */
	@Override
	protected void addInstanceFilter(EventRequest request,
			ObjectReference object) {
		if (request instanceof WatchpointRequest) {
			((WatchpointRequest) request).addInstanceFilter(object);
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.debug.core.model.IWatchpoint#supportsAccess()
	 */
	@Override
	public boolean supportsAccess() {
		return true;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.debug.core.model.IWatchpoint#supportsModification()
	 */
	@Override
	public boolean supportsModification() {
		return true;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.jdt.internal.debug.core.breakpoints.JavaBreakpoint#
	 * installableReferenceType(com.sun.jdi.ReferenceType,
	 * org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget)
	 */
	@Override
	protected boolean installableReferenceType(ReferenceType type,
			JDIDebugTarget target) throws CoreException {
		String installableType = getTypeName();
		String queriedType = type.name();
		if (installableType == null || queriedType == null) {
			return false;
		}
		if (installableType.equals(queriedType)) {
			return queryInstallListeners(target, type);
		}

		return false;
	}
}
