/*******************************************************************************
 * Copyright (c) 2009, 2015 QNX Software Systems 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:
 *     QNX Software Systems - Initial API and implementation
 *     Hewlett-Packard Development Company - fix for bug 109733
 *     Wind River Systems   - Modified for new DSF Reference Implementation
 *     Marc Khouzam (Ericsson) - Display exit code in process console (Bug 402054)
 *******************************************************************************/

package org.eclipse.cdt.dsf.mi.service.command;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.concurrent.ThreadSafeAndProhibitedFromDsfExecutor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.debug.service.IProcesses;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IStartedDMEvent;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlShutdownDMEvent;
import org.eclipse.cdt.dsf.debug.service.command.ICommandListener;
import org.eclipse.cdt.dsf.debug.service.command.ICommandResult;
import org.eclipse.cdt.dsf.debug.service.command.ICommandToken;
import org.eclipse.cdt.dsf.debug.service.command.IEventListener;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIProcessDMContext;
import org.eclipse.cdt.dsf.mi.service.MIProcesses;
import org.eclipse.cdt.dsf.mi.service.command.commands.CLICommand;
import org.eclipse.cdt.dsf.mi.service.command.events.MIThreadGroupExitedEvent;
import org.eclipse.cdt.dsf.mi.service.command.output.MIGDBShowExitCodeInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOOBRecord;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MITargetStreamOutput;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.utils.pty.PTY;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * This Process implementation tracks one of the inferiors that is being debugged
 * by GDB.  The process object, although not displayed in the Debug view, is used to
 * channel the STDIO of the inferior process to the console view.
 *
 * @see org.eclipse.debug.core.model.IProcess
 */
public class MIInferiorProcess extends Process implements IEventListener, ICommandListener {

	// Indicates that the inferior has been started
	// It implies the ContainerDMContext is fully-formed
	// with the pid of the process (as a parent dmc)
	private boolean fStarted;

	// Indicates that the inferior has been terminated
	private boolean fTerminated;

	private OutputStream fOutputStream;
	private InputStream fInputStream;

	private PipedOutputStream fInputStreamPiped;

	private PipedInputStream fErrorStream;
	private PipedOutputStream fErrorStreamPiped;

	private final DsfSession fSession;

	private final IMICommandControl fCommandControl;
	private CommandFactory fCommandFactory;

	private IContainerDMContext fContainerDMContext;

	@ConfinedToDsfExecutor("fSession#getExecutor")
	private boolean fDisposed = false;

	/**
	 * Counter for tracking console commands sent by services.
	 *
	 * The CLI 'monitor' command produces target output which should
	 * not be written to the target console, since it is in response to a CLI
	 * command.  In fact, CLI commands should never have their output sent
	 * to the target console.
	 *
	 * This counter is incremented any time a CLI command is seen.  It is
	 * decremented whenever a CLI command is finished.  When counter
	 * value is 0, the inferior process writes the target output.
	 */
	private int fSuppressTargetOutputCounter = 0;

	@ThreadSafe
	Integer fExitCode = null;

	/**
	 * @returns whether the inferior has been started, which means
	 *          we can obtain its process id.
	 * @since 4.7
	 */
	protected boolean isStarted() {
		return fStarted;
	}

	/** @since 4.7 */
	protected IContainerDMContext getContainer() {
		return fContainerDMContext;
	}

	/** @since 4.7 */
	protected synchronized boolean isTerminated() {
		return fTerminated;
	}

	/**
	 * Creates an inferior process object which uses the given output stream
	 * to write the user standard input into.
	 *
	 * @param container The process that this inferior represents
	 * @param gdbOutputStream The output stream to use to write user IO into.
	 * @since 4.0
	 */
	@ConfinedToDsfExecutor("fSession#getExecutor")
	public MIInferiorProcess(IContainerDMContext container, OutputStream gdbOutputStream) {
		this(container, gdbOutputStream, null);
	}

	/**
	 * Creates an inferior process object which uses the given terminal
	 * to write the user standard input into.
	 *
	 * @param container The process that this inferior represents
	 * @param p The terminal to use to write user IO into.
	 * @since 4.0
	 */
	@ConfinedToDsfExecutor("fSession#getExecutor")
	public MIInferiorProcess(IContainerDMContext container, PTY p) {
		this(container, null, p);
	}

	/** @since 4.7 */
	@ConfinedToDsfExecutor("fSession#getExecutor")
	protected MIInferiorProcess(IContainerDMContext container, final OutputStream gdbOutputStream, PTY pty) {
		fSession = DsfSession.getSession(container.getSessionId());
		fSession.addServiceEventListener(this, null);

		fContainerDMContext = container;
		IMIProcessDMContext processDmc = DMContexts.getAncestorOfType(fContainerDMContext, IMIProcessDMContext.class);
		if (processDmc != null && processDmc.getProcId() != MIProcesses.UNKNOWN_PROCESS_ID) {
			// If we already know the pid, it implies the process is already started.
			// It also means we won't get the IStartedDMEvent for the process.
			// This happens when the inferior is restarted, in which case, this class
			// is created after the process is running and the IStartedDMEvent was sent.
			fStarted = true;
		}

		DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fSession.getId());
		fCommandControl = tracker.getService(IMICommandControl.class);
		tracker.dispose();

		fCommandFactory = fCommandControl.getCommandFactory();

		fCommandControl.addEventListener(this);
		fCommandControl.addCommandListener(this);

		if (pty != null) {
			fOutputStream = pty.getOutputStream();
			fInputStream = pty.getInputStream();
			fInputStreamPiped = null;
		} else {
			fOutputStream = new OutputStream() {
				@Override
				public void write(int b) throws IOException {
					gdbOutputStream.write(b);
				}
			};

			fInputStreamPiped = new PipedOutputStream();
			PipedInputStream inputStream = null;
			try {
				// Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154
				inputStream = new LargePipedInputStream(fInputStreamPiped);
			} catch (IOException e) {
			}
			fInputStream = inputStream;

		}

		// Note: We do not have any err stream from gdb/mi so this gdb
		// err channel instead.
		fErrorStreamPiped = new PipedOutputStream();
		PipedInputStream errorStream = null;
		try {
			// Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154
			errorStream = new LargePipedInputStream(fErrorStreamPiped);
		} catch (IOException e) {
		}
		fErrorStream = errorStream;
	}

	@ConfinedToDsfExecutor("fSession#getExecutor")
	public void dispose() {
		fSession.removeServiceEventListener(this);

		fCommandControl.removeEventListener(this);
		fCommandControl.removeCommandListener(this);

		closeIO();

		setTerminated();

		fDisposed = true;
	}

	@ConfinedToDsfExecutor("fSession#getExecutor")
	protected boolean isDisposed() {
		return fDisposed;
	}

	@Override
	public OutputStream getOutputStream() {
		return fOutputStream;
	}

	@Override
	public InputStream getInputStream() {
		return fInputStream;
	}

	@Override
	public InputStream getErrorStream() {
		return fErrorStream;
	}

	@ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor")
	public synchronized void waitForSync() throws InterruptedException {
		assert !fSession.getExecutor().isInExecutorThread();

		while (!fTerminated) {
			wait(100);
		}
	}

	/**
	 * @see java.lang.Process#waitFor()
	 */
	@ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor")
	@Override
	public int waitFor() throws InterruptedException {
		assert !fSession.getExecutor().isInExecutorThread();

		waitForSync();
		return exitValue();
	}

	@ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor")
	@Override
	public int exitValue() {
		assert !fSession.getExecutor().isInExecutorThread();

		synchronized (this) {
			if (fExitCode != null) {
				return fExitCode;
			}
		}

		// Fetch the exit code using $_exitcode of GDB when doing single
		// process debugging (GDB <= 7.1)
		// Note that for GDB 7.2, there is no proper solution for the exit code
		// because although we support multi-process, $_exitcode was still the
		// only way to get the exit code, and that variable does not work properly
		// with multi-process (it is re-used by the different processes).
		// We use it still for GDB 7.2, since the single-process case is the most common.
		try {
			Query<Integer> exitCodeQuery = new Query<Integer>() {
				@Override
				protected void execute(final DataRequestMonitor<Integer> rm) {
					// Guard against session disposed.
					if (!DsfSession.isSessionActive(fSession.getId())) {
						rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE,
								"Debug session already shut down.", null)); //$NON-NLS-1$
						rm.done();
						return;
					}

					if (isDisposed()) {
						rm.setData(0);
						rm.done();
					} else if (!fTerminated) {
						// This will cause ExecutionException to be thrown with a CoreException,
						// which will in turn contain the IllegalThreadStateException.
						rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE,
								"Inferior is still running.", new IllegalThreadStateException())); //$NON-NLS-1$
						rm.done();
					} else {
						// The exitCode from GDB does not seem to be handled for multi-process
						// so there is no point is specifying the container
						fCommandControl.queueCommand(
								fCommandFactory.createMIGDBShowExitCode(fCommandControl.getContext()),
								new DataRequestMonitor<MIGDBShowExitCodeInfo>(fSession.getExecutor(), rm) {
									@Override
									protected void handleSuccess() {
										rm.setData(getData().getCode());
										rm.done();
									}
								});

					}
				}
			};
			fSession.getExecutor().execute(exitCodeQuery);

			int exitCode = exitCodeQuery.get();
			synchronized (this) {
				fExitCode = exitCode;
			}
			return exitCode;
		} catch (RejectedExecutionException e) {
		} catch (InterruptedException e) {
		} catch (CancellationException e) {
		} catch (ExecutionException e) {
			if (e.getCause() instanceof CoreException
					&& ((CoreException) e.getCause()).getStatus().getException() instanceof RuntimeException) {
				throw (RuntimeException) ((CoreException) e.getCause()).getStatus().getException();
			}
		}
		return 0;
	}

	/**
	 * @see java.lang.Process#destroy()
	 */
	@Override
	public void destroy() {
		try {
			fSession.getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					doDestroy();
				}
			});
		} catch (RejectedExecutionException e) {
			// Session disposed.
		}
		closeIO();
	}

	private void closeIO() {
		try {
			if (fOutputStream != null)
				fOutputStream.close();
			// Make sure things get GCed
			fOutputStream = null;
		} catch (IOException e) {
		}
		try {
			if (fInputStream != null)
				fInputStream.close();
			// Make sure things get GCed
			fInputStream = null;
		} catch (IOException e) {
		}
		try {
			if (fInputStreamPiped != null)
				fInputStreamPiped.close();
			// Make sure things get GCed
			fInputStreamPiped = null;
		} catch (IOException e) {
		}
		try {
			if (fErrorStream != null)
				fErrorStream.close();
			// Make sure things get GCed
			fErrorStream = null;
		} catch (IOException e) {
		}
		try {
			if (fErrorStreamPiped != null)
				fErrorStreamPiped.close();
			// Make sure things get GCed
			fErrorStreamPiped = null;
		} catch (IOException e) {
		}
	}

	@ConfinedToDsfExecutor("fSession#getExecutor")
	private void doDestroy() {
		if (isDisposed() || !fSession.isActive() || fTerminated)
			return;

		DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fSession.getId());
		IProcesses procService = tracker.getService(IProcesses.class);
		tracker.dispose();
		if (procService != null) {
			IProcessDMContext procDmc = DMContexts.getAncestorOfType(fContainerDMContext, IProcessDMContext.class);
			procService.terminate(procDmc, new ImmediateRequestMonitor());
		} else {
			setTerminated();
		}
	}

	@ConfinedToDsfExecutor("fSession#getExecutor")
	private synchronized void setTerminated() {
		if (fTerminated)
			return;
		fTerminated = true;
		closeIO();
		notifyAll();
	}

	public OutputStream getPipedOutputStream() {
		return fInputStreamPiped;
	}

	public OutputStream getPipedErrorStream() {
		return fErrorStreamPiped;
	}

	@Override
	public void eventReceived(Object output) {
		for (MIOOBRecord oobr : ((MIOutput) output).getMIOOBRecords()) {
			if (oobr instanceof MITargetStreamOutput) {
				if (fSuppressTargetOutputCounter > 0)
					return;
				MITargetStreamOutput tgtOut = (MITargetStreamOutput) oobr;
				if (fInputStreamPiped != null && tgtOut.getString() != null) {
					try {
						fInputStreamPiped.write(tgtOut.getString().getBytes());
						fInputStreamPiped.flush();
					} catch (IOException e) {
					}
				}
			}
		}
	}

	@Override
	public void commandQueued(ICommandToken token) {
		// No action
	}

	@Override
	public void commandSent(ICommandToken token) {
		if (token.getCommand() instanceof CLICommand<?>) {
			fSuppressTargetOutputCounter++;
		}
	}

	@Override
	public void commandRemoved(ICommandToken token) {
		// No action
	}

	@Override
	public void commandDone(ICommandToken token, ICommandResult result) {
		if (token.getCommand() instanceof CLICommand<?>) {
			fSuppressTargetOutputCounter--;
		}
	}

	/**
	 * @since 4.0
	 */
	@DsfServiceEventHandler
	public void eventDispatched(IExitedDMEvent e) {
		if (e.getDMContext() instanceof IMIContainerDMContext) {
			// For multi-process, make sure the exited event
			// is actually for this inferior.
			if (e.getDMContext().equals(fContainerDMContext)) {
				// With recent changes, we notice a restart by the exited/started
				// events, and only then create the new inferior; this means the new
				// inferior will no longer receive the exited event for the old inferior.
				// But it does not hurt to leave the below if check just in case.
				assert fStarted : "Exited event should only be received for a started inferior"; //$NON-NLS-1$
				if (fStarted) {
					// Only mark this process as terminated if it was already
					// started.  This was to protect ourselves in the case of
					// a restart, where we used to create the new inferior before
					// killing the old one.  In that case we would get the exited
					// event of the old inferior, which we had to ignore.
					//
					setTerminated();
				}
			}
		}
	}

	/** @since 4.2 */
	@DsfServiceEventHandler
	public void eventDispatched(MIThreadGroupExitedEvent e) {
	}

	/**
	 * @since 4.0
	 */
	@DsfServiceEventHandler
	public void eventDispatched(IStartedDMEvent e) {
		if (e.getDMContext() instanceof IMIContainerDMContext) {
			// Mark the inferior started if the event is for this inferior.
			// We may get other started events in the case of a restart
			if (!fStarted) {
				// For multi-process, make sure the started event
				// is actually for this inferior.
				// We must compare the groupId and not the full context
				// because the container that we currently hold is incomplete
				// because the pid was not determined yet.
				String inferiorGroup = ((IMIContainerDMContext) fContainerDMContext).getGroupId();

				if (inferiorGroup == null || inferiorGroup.length() == 0) {
					// Single process case, so we know we have started
					fStarted = true;
					// Store the fully-formed container
					fContainerDMContext = (IMIContainerDMContext) e.getDMContext();
				} else {
					String startedGroup = ((IMIContainerDMContext) e.getDMContext()).getGroupId();
					if (inferiorGroup.equals(startedGroup)) {
						fStarted = true;
						// Store the fully-formed container
						fContainerDMContext = (IMIContainerDMContext) e.getDMContext();
					}
				}
			}
		}
	}

	/**
	 * @since 4.0
	 */
	@DsfServiceEventHandler
	public void eventDispatched(ICommandControlShutdownDMEvent e) {
		dispose();
	}
}
