/*******************************************************************************
 * Copyright (c) 2011 Ericsson and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Ericsson - Initial API and implementation 
 *     Marc Khouzam (Ericsson) - Support for fast tracepoints (Bug 346320)
 *     Marc Khouzam (Ericsson) - Fetch groupIds when getting breakpoints (Bug 360735)
 *******************************************************************************/

package org.eclipse.cdt.dsf.gdb.service;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IBreakpoints;
import org.eclipse.cdt.dsf.debug.service.IBreakpointsExtension;
import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.MIBreakpointDMData;
import org.eclipse.cdt.dsf.mi.service.MIBreakpoints;
import org.eclipse.cdt.dsf.mi.service.command.output.CLIInfoBreakInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakInsertInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakListInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakpoint;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.ILaunch;

/**
 * Breakpoint service for GDB 7.2.
 * It support MI for tracepoints.
 * It also support for fast vs normal tracepoints.
 *
 * @since 4.1
 */
public class GDBBreakpoints_7_2 extends GDBBreakpoints_7_0
{
	private IMICommandControl fConnection;
	
	private enum TracepointMode { FAST_THEN_NORMAL, FAST_ONLY, NORMAL_ONLY };
	   
	private TracepointMode fTracepointMode = TracepointMode.NORMAL_ONLY;

	public GDBBreakpoints_7_2(DsfSession session) {
		super(session);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.dsf.service.AbstractDsfService#initialize(org.eclipse.cdt.dsf.concurrent.RequestMonitor)
	 */
	@Override
	public void initialize(final RequestMonitor rm) {
		super.initialize(new ImmediateRequestMonitor(rm) {
			@Override
			protected void handleSuccess() {
				doInitialize(rm);
			}
		});
	}

	private void doInitialize(final RequestMonitor rm) {
    	// Get the services references
		fConnection = getServicesTracker().getService(IMICommandControl.class);

		setTracepointMode();
		
		// Register this service
		register(new String[] { IBreakpoints.class.getName(),
		                        IBreakpointsExtension.class.getName(),
								MIBreakpoints.class.getName(),
								GDBBreakpoints_7_0.class.getName(),
								GDBBreakpoints_7_2.class.getName() },
				new Hashtable<String, String>());

		rm.done();
	}

	@Override
	public void shutdown(RequestMonitor requestMonitor) {
        unregister();
		super.shutdown(requestMonitor);
	}
	
	/** 
	 * {@inheritDoc}
	 * 
	 * Starting with GDB 7.2, also provides information about which process each breakpoint applies to.
	 */
	@Override
	public void getBreakpoints(final IBreakpointsTargetDMContext context, final DataRequestMonitor<IBreakpointDMContext[]> drm)
	{
		if (bpThreadGroupInfoAvailable()) {
			// With GDB 7.6, we obtain the thread-groups to which a breakpoint applies
			// directly in the -break-list command, so we don't need to do any special processing.
			super.getBreakpoints(context, drm);
			return;
		}
		
		// Validate the context
		if (context == null) {
       		drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, UNKNOWN_EXECUTION_CONTEXT, null));
       		drm.done();
			return;
		}

		// Select the breakpoints context map
		// If it doesn't exist then no breakpoint was ever inserted for this breakpoint space.
		// In that case, return an empty list.
		final Map<Integer, MIBreakpointDMData> breakpointContext = getBreakpointMap(context);
		if (breakpointContext == null) {
       		drm.setData(new IBreakpointDMContext[0]);
       		drm.done();
			return;
		}

		// Execute the command
		fConnection.queueCommand(fConnection.getCommandFactory().createMIBreakList(context),
			new DataRequestMonitor<MIBreakListInfo>(getExecutor(), drm) {
				@Override
				protected void handleSuccess() {
					final MIBreakpoint[] breakpoints = getData().getMIBreakpoints();
					
					// Also fetch the information about which breakpoint belongs to which
					// process.  We currently can only obtain this information from the CLI
					// command.  This information is needed for breakpoint filtering.
					// Bug 360735
					fConnection.queueCommand(fConnection.getCommandFactory().createCLIInfoBreak(context),
							new ImmediateDataRequestMonitor<CLIInfoBreakInfo>(drm) {
						@Override
						protected void handleSuccess() {
							Map<Integer, String[]> groupIdMap = getData().getBreakpointToGroupMap();

							// Refresh the breakpoints map and format the result
							breakpointContext.clear();
							IBreakpointDMContext[] result = new IBreakpointDMContext[breakpoints.length];
							for (int i = 0; i < breakpoints.length; i++) {
								MIBreakpointDMData breakpointData = new MIBreakpointDMData(breakpoints[i]);
								
								// Now fill in the thread-group information into the breakpoint data
								// It is ok to get null.  For example, pending breakpoints are not
								// associated to a thread-group; also, when debugging a single process,
								// the thread-group list is empty.
								int reference = breakpointData.getReference();
								String[] groupIds = groupIdMap.get(reference);
								breakpointData.setGroupIds(groupIds);
								
								result[i] = new MIBreakpointDMContext(GDBBreakpoints_7_2.this, new IDMContext[] { context }, reference);
								breakpointContext.put(reference, breakpointData);
							}
							drm.setData(result);
							drm.done();
						}
					});
				}
			});
	}
	
	private void setTracepointMode() {
		ILaunch launch = (ILaunch)getSession().getModelAdapter(ILaunch.class);
		String tpMode = IGDBLaunchConfigurationConstants.DEBUGGER_TRACEPOINT_MODE_DEFAULT;
		try {
			tpMode = launch.getLaunchConfiguration().getAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_TRACEPOINT_MODE,
					IGDBLaunchConfigurationConstants.DEBUGGER_TRACEPOINT_MODE_DEFAULT);
		} catch (CoreException e) {
		}

		if (tpMode.equals(IGDBLaunchConfigurationConstants.DEBUGGER_TRACEPOINT_FAST_ONLY)) {
			fTracepointMode = TracepointMode.FAST_ONLY;
		} else if (tpMode.equals(IGDBLaunchConfigurationConstants.DEBUGGER_TRACEPOINT_NORMAL_ONLY)) {
			fTracepointMode = TracepointMode.NORMAL_ONLY;
		} else if (tpMode.equals(IGDBLaunchConfigurationConstants.DEBUGGER_TRACEPOINT_FAST_THEN_NORMAL)) {
			fTracepointMode = TracepointMode.FAST_THEN_NORMAL;
		} else {
			assert false : "Invalid tracepoint mode: " + tpMode; //$NON-NLS-1$
			fTracepointMode = TracepointMode.NORMAL_ONLY;         
		}
	}

	protected void sendTracepointCommand(final IBreakpointsTargetDMContext context, final Map<String, Object> attributes, boolean isFastTracepoint, final DataRequestMonitor<IBreakpointDMContext> drm) {
		// Select the context breakpoints map
		final Map<Integer, MIBreakpointDMData> contextBreakpoints = getBreakpointMap(context);
		if (contextBreakpoints == null) {
			drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, UNKNOWN_BREAKPOINT_CONTEXT, null));
			drm.done();
			return;
		}

		// Extract the relevant parameters (providing default values to avoid potential NPEs)
		final String location = formatLocation(attributes);
		if (location.equals(NULL_STRING)) {
			drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, UNKNOWN_BREAKPOINT_CONTEXT, null));
			drm.done();
			return;
		}

		final Boolean enabled        = (Boolean) getProperty(attributes, MIBreakpoints.IS_ENABLED,        true);
		final String  condition      = (String)  getProperty(attributes, MIBreakpoints.CONDITION,         NULL_STRING);

		fConnection.queueCommand(
				fConnection.getCommandFactory().createMIBreakInsert(context, false, isFastTracepoint, condition, 0, location, 0, !enabled, true),
				new DataRequestMonitor<MIBreakInsertInfo>(getExecutor(), drm) {
					@Override
					protected void handleSuccess() {
						// With MI, an invalid location won't generate an error
						if (getData().getMIBreakpoints().length == 0) {
							drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, BREAKPOINT_INSERTION_FAILURE, null));
							drm.done();
							return;
						}

						// Create a breakpoint object and store it in the map
						final MIBreakpointDMData newBreakpoint = new MIBreakpointDMData(getData().getMIBreakpoints()[0]);
						int reference = newBreakpoint.getNumber();
						if (reference == -1) {
							drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, BREAKPOINT_INSERTION_FAILURE, null));
							drm.done();
							return;
						}
						contextBreakpoints.put(reference, newBreakpoint);

						// Format the return value
						MIBreakpointDMContext dmc = new MIBreakpointDMContext(GDBBreakpoints_7_2.this, new IDMContext[] { context }, reference);
						drm.setData(dmc);

						// Flag the event
						getSession().dispatchEvent(new BreakpointAddedEvent(dmc), getProperties());

						// Tracepoints are created with no passcount (passcount are not 
						// the same thing as ignore-count, which is not supported by
						// tracepoints).  We have to set the passcount manually now.
						// Same for commands.
						Map<String,Object> delta = new HashMap<String,Object>();
						delta.put(MIBreakpoints.PASS_COUNT, getProperty(attributes, MIBreakpoints.PASS_COUNT, 0));
						delta.put(MIBreakpoints.COMMANDS, getProperty(attributes, MIBreakpoints.COMMANDS, "")); //$NON-NLS-1$
						modifyBreakpoint(dmc, delta, drm, false);
					}

					@Override
					protected void handleError() {
						drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, BREAKPOINT_INSERTION_FAILURE, getStatus().getException()));
						drm.done();
					}
				});
	}
	/**
	 * Add a tracepoint using MI.  We have three settings:
	 *   1- set only a fast tracepoint but if it fails, set a normal tracepoint
	 *   2- only set a fast tracepoint even if it fails
	 *   3- only set a normal tracepoint even if a fast tracepoint could have been used
	 */
	@Override
	protected void addTracepoint(final IBreakpointsTargetDMContext context, final Map<String, Object> attributes, final DataRequestMonitor<IBreakpointDMContext> drm) {
		// Unless we should only set normal tracepoints, we try to set a fast tracepoint.
		boolean isFastTracepoint = fTracepointMode != TracepointMode.NORMAL_ONLY;

		sendTracepointCommand(context, attributes, isFastTracepoint, new ImmediateDataRequestMonitor<IBreakpointDMContext>(drm) {
			@Override
			protected void handleSuccess() {
				// Tracepoint was set successfully.
				drm.setData(getData());
				drm.done();
			}
			@Override
			protected void handleError() {
				// Tracepoint failed to be set.
				if (fTracepointMode == TracepointMode.FAST_THEN_NORMAL) {
					// In this case, we failed to set a fast tracepoint, but we should try to set a normal one.
					sendTracepointCommand(context, attributes, false, drm);
				} else {
					// We either failed to set a fast tracepoint and we should not try to set a normal one,
					// or we failed to set a normal one.  Either way, we are done.
					drm.setStatus(getStatus());
					drm.done();
				}
			}
		});
	}
	
	/**
	 * Does the MI command -break-list provide information
	 * about which thread-group a breakpoint applies to?
	 * The use of this method allows us to avoid duplicating code.
	 * See Bug 402217
	 * 
	 * @return true if the information is available (GDB >= 7.6),
	 *         false otherwise.
	 * @since 4.2
	 */
	protected boolean bpThreadGroupInfoAvailable() {
		return false;
	}
}
