/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.cdt.dsf.gdb.service;

import java.util.Map;
import java.util.Properties;

import org.eclipse.cdt.debug.core.CDebugUtils;
import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.ReflectionSequence;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IBreakpoints.IBreakpointsTargetDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.gdb.launching.LaunchMessages;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceTargetDMContext;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
import org.eclipse.cdt.dsf.mi.service.MIBreakpointsManager;
import org.eclipse.cdt.dsf.mi.service.MIProcesses;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IStatusHandler;

/** 
 * This sequence is used to start debugging a new process.
 *
 * @since 4.0
*/
public class DebugNewProcessSequence extends ReflectionSequence {

	private final static String INVALID = "invalid";   //$NON-NLS-1$

	private IGDBControl fCommandControl;
	private CommandFactory fCommandFactory;
	private IGDBBackend fBackend;
	private IGDBProcesses fProcService;
	private DsfServicesTracker fTracker;

	private IDMContext fContext;
	private String fBinaryName;
	private Map<String, Object> fAttributes;
	private IMIContainerDMContext fContainerCtx;
	
	// Store the dataRM so that we can fill it with the container context that we will be creating
	private DataRequestMonitor<IDMContext> fDataRequestMonitor;

	
	protected IMIContainerDMContext getContainerContext() {
		return fContainerCtx;
	}

	protected void setContainerContext(IMIContainerDMContext ctx) {
		fContainerCtx = ctx;
	}
	
	public DebugNewProcessSequence(DsfExecutor executor, boolean isInitial, IDMContext dmc, String file, Map<String, Object> attributes, DataRequestMonitor<IDMContext> rm) {
		super(executor, rm);
		fContext = dmc;
		fBinaryName = file;
		fAttributes = attributes;
		fDataRequestMonitor = rm;
	}

	@Override
	protected String[] getExecutionOrder(String group) {
		if (GROUP_TOP_LEVEL.equals(group)) {
			return new String[] {
					"stepInitializeBaseSequence",  //$NON-NLS-1$
					"stepSetEnvironmentVariables",   //$NON-NLS-1$
					"stepSetExecutable",   //$NON-NLS-1$
					"stepSetArguments",   //$NON-NLS-1$
					
					// For remote non-attach only
					"stepRemoteConnection",   //$NON-NLS-1$
					// For post-mortem launch only
					"stepSpecifyCoreFile",   //$NON-NLS-1$
					
					"stepStartTrackingBreakpoints", //$NON-NLS-1$
					"stepStartExecution",   //$NON-NLS-1$
					"stepCleanupBaseSequence",   //$NON-NLS-1$
			};
		}
		return null;
	}

	/** 
	 * Initialize the members of the DebugNewProcessSequence class.
	 * This step is mandatory for the rest of the sequence to complete.
	 */
	@Execute
	public void stepInitializeBaseSequence(RequestMonitor rm) {
		fTracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fContext.getSessionId());
		fBackend = fTracker.getService(IGDBBackend.class);
		fCommandControl = fTracker.getService(IGDBControl.class);
        fCommandFactory = fTracker.getService(IMICommandControl.class).getCommandFactory();		
		fProcService = fTracker.getService(IGDBProcesses.class);

        if (fBackend == null || fCommandControl == null || fCommandFactory == null || fProcService == null) {
			rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Cannot obtain service", null)); //$NON-NLS-1$
			rm.done();
			return;
		}

        // When we are starting to debug a new process, the container is the default process used by GDB.
        // We don't have a pid yet, so we can simply create the container with the UNIQUE_GROUP_ID
        setContainerContext(fProcService.createContainerContextFromGroupId(fCommandControl.getContext(), MIProcesses.UNIQUE_GROUP_ID));
        
        rm.done();
	}
	
	/** 
	 * Rollback method for {@link #stepInitializeBaseSequence()}
	 * @since 4.0 
	 */
	@RollBack("stepInitializeBaseSequence")
	public void rollBackInitializeBaseSequence(RequestMonitor rm) {
		if (fTracker != null) fTracker.dispose();
		fTracker = null;
		rm.done();
	}
	
	/**
	 * Specify environment variables if needed
	 */
	@Execute
	public void stepSetEnvironmentVariables(RequestMonitor rm) {
		boolean clear = false;
		Properties properties = new Properties();
		try {
			// here we need to pass the proper container context
			clear = fBackend.getClearEnvironment();
			properties = fBackend.getEnvironmentVariables();
		} catch (CoreException e) {
			rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED, "Cannot get environment information", e)); //$NON-NLS-1$
			rm.done();
			return;
		}

		if (clear == true || properties.size() > 0) {
			// here we need to pass the proper container context
			fCommandControl.setEnvironment(properties, clear, rm);
		} else {
			rm.done();
		}
	}

	/**
	 * Specify the executable file to be debugged and read the symbol table.
	 */
	@Execute
	public void stepSetExecutable(RequestMonitor rm) {
		boolean noFileCommand = CDebugUtils.getAttribute(
				fAttributes, 
				IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_USE_SOLIB_SYMBOLS_FOR_APP,
				IGDBLaunchConfigurationConstants.DEBUGGER_USE_SOLIB_SYMBOLS_FOR_APP_DEFAULT);

		if (!noFileCommand && fBinaryName != null && fBinaryName.length() > 0) {
			fCommandControl.queueCommand(
					fCommandFactory.createMIFileExecAndSymbols(getContainerContext(), fBinaryName), 
					new DataRequestMonitor<MIInfo>(ImmediateExecutor.getInstance(), rm));
		} else {
			rm.done();
		}
	}
	
	/**
	 * Specify the arguments to the program that will be run.
	 */
	@Execute
	public void stepSetArguments(RequestMonitor rm) {
		try {
			String args = fBackend.getProgramArguments();

			if (args != null) {
				String[] argArray = args.replaceAll("\n", " ").split(" ");  //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				fCommandControl.queueCommand(
						fCommandFactory.createMIGDBSetArgs(getContainerContext(), argArray), 
						new DataRequestMonitor<MIInfo>(ImmediateExecutor.getInstance(), rm));
			} else {
				rm.done();
			}
		} catch (CoreException e) {
			rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED, "Cannot get inferior arguments", e)); //$NON-NLS-1$
			rm.done();
		}    		
	}

	/** 
	 * If we are dealing with a remote debugging session, connect to the target.
	 * @since 4.0
	 */
	@Execute
	public void stepRemoteConnection(RequestMonitor rm) {
		// If we are dealing with a non-attach remote session, it is now time to connect
		// to the remote side.  Note that this is the 'target remote' case
		// and not the 'target extended-remote' case (remote attach session)
		// This step is actually global for GDB.  However, we have to do it after
		// we have specified the executable, so we have to do it here.
		// It is safe to do it here because a 'target remote' does not support
		// multi-process so this step will not be executed more than once.
		if (fBackend.getSessionType() == SessionType.REMOTE && !fBackend.getIsAttachSession()) {
			boolean isTcpConnection = CDebugUtils.getAttribute(
					fAttributes,
					IGDBLaunchConfigurationConstants.ATTR_REMOTE_TCP,
					false);

			if (isTcpConnection) {
				String remoteTcpHost = CDebugUtils.getAttribute(
						fAttributes,
						IGDBLaunchConfigurationConstants.ATTR_HOST, INVALID);
				String remoteTcpPort = CDebugUtils.getAttribute(
						fAttributes,
						IGDBLaunchConfigurationConstants.ATTR_PORT, INVALID);

				fCommandControl.queueCommand(
						fCommandFactory.createMITargetSelect(fCommandControl.getContext(), 
								remoteTcpHost, remoteTcpPort, false), 
								new DataRequestMonitor<MIInfo>(ImmediateExecutor.getInstance(), rm));
			} else {
				String serialDevice = CDebugUtils.getAttribute(
						fAttributes,
						IGDBLaunchConfigurationConstants.ATTR_DEV, INVALID);
				fCommandControl.queueCommand(
						fCommandFactory.createMITargetSelect(fCommandControl.getContext(), 
								serialDevice, false), 
								new DataRequestMonitor<MIInfo>(ImmediateExecutor.getInstance(), rm));
			}
		} else {
			rm.done();
		}
	}

	/** @since 4.0 */
	protected static class PromptForCoreJob extends Job {
		protected DataRequestMonitor<String> fRequestMonitor;

		public PromptForCoreJob(String name, DataRequestMonitor<String> rm) {
			super(name);
			fRequestMonitor = rm;
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			final IStatus promptStatus = new Status(IStatus.INFO, "org.eclipse.debug.ui", 200/*STATUS_HANDLER_PROMPT*/, "", null); //$NON-NLS-1$//$NON-NLS-2$
			final IStatus filePrompt = new Status(IStatus.INFO, "org.eclipse.cdt.dsf.gdb.ui", 1001, "", null); //$NON-NLS-1$//$NON-NLS-2$
			// consult a status handler
			final IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus);

			final Status NO_CORE_STATUS = new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1,
					LaunchMessages.getString("LocalCDILaunchDelegate.6"), //$NON-NLS-1$
					null);

			if (prompter == null) {
				fRequestMonitor.setStatus(NO_CORE_STATUS);
				fRequestMonitor.done();
				return Status.OK_STATUS;
			} 				

			try {
				Object result = prompter.handleStatus(filePrompt, null);
				 if (result == null) {
						fRequestMonitor.cancel();
				} else if (result instanceof String) {
					fRequestMonitor.setData((String)result);
				} else {
					fRequestMonitor.setStatus(NO_CORE_STATUS);
				}
			} catch (CoreException e) {
				fRequestMonitor.setStatus(NO_CORE_STATUS);
			}
			fRequestMonitor.done();

			return Status.OK_STATUS;
		}
	};
	
	/** 
	 * If we are dealing with a postmortem session, connect to the core/trace file.
	 * @since 4.0
	 */
	@Execute
	public void stepSpecifyCoreFile(final RequestMonitor rm) {
		// If we are dealing with a postmortem session, it is now time to connect
		// to the core/trace file.  We have to do this step after
		// we have specified the executable, so we have to do it here.
		// It is safe to do it here because a postmortem session does not support
		// multi-process so this step will not be executed more than once.
		// Bug 338730
		if (fBackend.getSessionType() == SessionType.CORE) {
			String coreFile = CDebugUtils.getAttribute(
					fAttributes,
					ICDTLaunchConfigurationConstants.ATTR_COREFILE_PATH, ""); //$NON-NLS-1$
			final String coreType = CDebugUtils.getAttribute(
					fAttributes,
					IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_POST_MORTEM_TYPE,
					IGDBLaunchConfigurationConstants.DEBUGGER_POST_MORTEM_TYPE_DEFAULT);
			
				if (coreFile.length() == 0) {
					new PromptForCoreJob(
							"Prompt for post mortem file",  //$NON-NLS-1$
							new DataRequestMonitor<String>(getExecutor(), rm) {
								@Override
								protected void handleCancel() {
									rm.cancel();
									rm.done();
								}
								@Override
								protected void handleSuccess() {
									String newCoreFile = getData();
									if (newCoreFile == null || newCoreFile.length()== 0) {
										rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Cannot get post mortem file path", null)); //$NON-NLS-1$
										rm.done();
									} else {
										if (coreType.equals(IGDBLaunchConfigurationConstants.DEBUGGER_POST_MORTEM_CORE_FILE)) {
											fCommandControl.queueCommand(
													fCommandFactory.createMITargetSelectCore(fCommandControl.getContext(), newCoreFile), 
													new DataRequestMonitor<MIInfo>(getExecutor(), rm));
										} else if (coreType.equals(IGDBLaunchConfigurationConstants.DEBUGGER_POST_MORTEM_TRACE_FILE)) {
											IGDBTraceControl traceControl = fTracker.getService(IGDBTraceControl.class);
											if (traceControl != null) {
												ITraceTargetDMContext targetDmc = DMContexts.getAncestorOfType(fCommandControl.getContext(), ITraceTargetDMContext.class);
												traceControl.loadTraceData(targetDmc, newCoreFile, rm);
											} else {
												rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Tracing not supported", null));
												rm.done();                                  
											}
										} else {
											rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Invalid post-mortem type", null));
											rm.done();
										}
									}
								}
							}).schedule();
				} else {
					if (coreType.equals(IGDBLaunchConfigurationConstants.DEBUGGER_POST_MORTEM_CORE_FILE)) {
						fCommandControl.queueCommand(
								fCommandFactory.createMITargetSelectCore(fCommandControl.getContext(), coreFile),
								new DataRequestMonitor<MIInfo>(getExecutor(), rm));
					} else if (coreType.equals(IGDBLaunchConfigurationConstants.DEBUGGER_POST_MORTEM_TRACE_FILE)) {
						IGDBTraceControl traceControl = fTracker.getService(IGDBTraceControl.class);
						if (traceControl != null) {
							ITraceTargetDMContext targetDmc = DMContexts.getAncestorOfType(fCommandControl.getContext(), ITraceTargetDMContext.class);
							traceControl.loadTraceData(targetDmc, coreFile, rm);
						} else {
							rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Tracing not supported", null));
							rm.done();
						}
					} else {
						rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Invalid post-mortem type", null));
						rm.done();
					}
				}
		} else {
			rm.done();
		}
	}
	
	/**
	 * Start tracking the breakpoints.  Note that for remote debugging
	 * we should first connect to the target.
	 */
	@Execute
	public void stepStartTrackingBreakpoints(RequestMonitor rm) {
		if (fBackend.getSessionType() != SessionType.CORE) {
			MIBreakpointsManager bpmService = fTracker.getService(MIBreakpointsManager.class);
			IBreakpointsTargetDMContext bpTargetDmc = DMContexts.getAncestorOfType(getContainerContext(), IBreakpointsTargetDMContext.class);
			bpmService.startTrackingBreakpoints(bpTargetDmc, rm);
		} else {
			rm.done();
		}
	}

	/**
	 * Start executing the program.
	 */
	@Execute
	public void stepStartExecution(final RequestMonitor rm) {
		if (fBackend.getSessionType() != SessionType.CORE) {
			// Overwrite the program name to use the binary name that was specified.
			// This is important for multi-process
			// Bug 342351
			fAttributes.put(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, fBinaryName);
			
			fProcService.start(getContainerContext(), fAttributes, new DataRequestMonitor<IContainerDMContext>(ImmediateExecutor.getInstance(), rm) {
				@Override
				protected void handleSuccess() {
					assert getData() instanceof IMIContainerDMContext;

					// Set the container that we created
					setContainerContext(DMContexts.getAncestorOfType(getData(), IMIContainerDMContext.class));
					fDataRequestMonitor.setData(getContainerContext());

					// Don't call fDataRequestMonitor.done(), the sequence will
					// automatically do that when it completes;
					rm.done();
				}
			});
		} else {
			fDataRequestMonitor.setData(getContainerContext());
			rm.done();
		}
	}
	
	/**
	 * Cleanup now that the sequence has been run.
	 * @since 4.0
	 */
	@Execute
	public void stepCleanupBaseSequence(RequestMonitor rm) {
		fTracker.dispose();
		fTracker = null;
		rm.done();
	}
}