/*******************************************************************************
 * Copyright (c) 2009, 2016 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
 *     Wind River Systems   - Modified for new DSF Reference Implementation
 *     Ericsson 		  	- Modified for additional features in DSF Reference implementation
 *******************************************************************************/

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.RejectedExecutionException;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.command.ICommand;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService;
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.command.commands.CLICommand;
import org.eclipse.cdt.dsf.mi.service.command.commands.MICommand;
import org.eclipse.cdt.dsf.mi.service.command.commands.MIInterpreterExecConsole;
import org.eclipse.cdt.dsf.mi.service.command.commands.RawCommand;
import org.eclipse.cdt.dsf.mi.service.command.output.MIConsoleStreamOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MILogStreamOutput;
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.service.DsfSession;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * This Process implementation tracks the GDB process.  This
 * process object is displayed in the Debug view and is used to
 * accept CLI commands and to write their output to the console.
 *
 * Starting with GDB 7.12, as long as a PTY is available,
 * this process is no longer used.  Instead, the real GDB process,
 * along with its console will be used directly.  A second PTY
 * will be used to communicate using MI.
 *
 * @see org.eclipse.debug.core.model.IProcess
 */
@ThreadSafe
public abstract class AbstractCLIProcess extends Process implements IEventListener, ICommandListener {
	public static final String PRIMARY_PROMPT = "(gdb)"; //$NON-NLS-1$
	public static final String SECONDARY_PROMPT = ">"; //$NON-NLS-1$

	// This is the command that will end a secondary prompt
	private static final String SECONDARY_PROMPT_END_COMMAND = "end"; //$NON-NLS-1$

	private final DsfSession fSession;
	private final ICommandControlService fCommandControl;
	private OutputStream fOutputStream;

	// Client process console stream.
	private PipedInputStream fMIInConsolePipe;
	private PipedOutputStream fMIOutConsolePipe;
	private PipedInputStream fMIInLogPipe;
	private PipedOutputStream fMIOutLogPipe;

	private boolean fDisposed = false;

	/**
	 * Counter for tracking console commands sent by services.
	 *
	 * Services may issue console commands when the available MI commands are
	 * not sufficient.  However, these commands may produce console and log
	 * output which should not be written to the user CLI terminal.
	 *
	 * This counter is incremented any time a console command is seen which was
	 * not generated by this class.  It is decremented whenever a service CLI
	 * command is finished.  When counter value is 0, the CLI process writes
	 * the console output.
	 */
	private int fSuppressConsoleOutputCounter = 0;

	// Primary prompt == "(gdb)"
	// Secondary Prompt == ">"
	// Secondary_prompt_missing means that the backend should be sending the secondary
	// prompt but it isn't.  So we do it ourselves.
	private enum PromptType {
		IN_PRIMARY_PROMPT, IN_SECONDARY_PROMPT, IN_SECONDARY_PROMPT_MISSING
	}

	private PromptType fPrompt = PromptType.IN_PRIMARY_PROMPT;

	/**
	 * @since 1.1
	 */
	@ConfinedToDsfExecutor("fSession#getExecutor")
	public AbstractCLIProcess(ICommandControlService commandControl) throws IOException {
		fSession = commandControl.getSession();
		fCommandControl = commandControl;

		if (handleIO()) {
			fOutputStream = new CLIOutputStream();

			commandControl.addEventListener(this);
			commandControl.addCommandListener(this);

			PipedInputStream miInConsolePipe = null;
			PipedOutputStream miOutConsolePipe = null;
			PipedInputStream miInLogPipe = null;
			PipedOutputStream miOutLogPipe = null;

			try {
				// Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154
				miOutConsolePipe = new PipedOutputStream();
				miInConsolePipe = new LargePipedInputStream(miOutConsolePipe);
				miOutLogPipe = new PipedOutputStream();
				miInLogPipe = new LargePipedInputStream(miOutLogPipe);
			} catch (IOException e) {
				ILog log = GdbPlugin.getDefault().getLog();
				if (log != null) {
					log.log(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Error when creating log pipes", e)); //$NON-NLS-1$
				}
			}
			fMIOutConsolePipe = miOutConsolePipe;
			fMIInConsolePipe = miInConsolePipe;
			fMIOutLogPipe = miOutLogPipe;
			fMIInLogPipe = miInLogPipe;
		}
	}

	/**
	 * Returns if this class should handle the IO streams of the CLI.
	 * @since 5.2
	 */
	protected boolean handleIO() {
		return true;
	}

	protected DsfSession getSession() {
		return fSession;
	}

	/**
	 * @since 1.1
	 */
	protected ICommandControlService getCommandControlService() {
		return fCommandControl;
	}

	protected boolean isDisposed() {
		return fDisposed;
	}

	@ConfinedToDsfExecutor("fSession#getExecutor")
	public void dispose() {
		if (fDisposed)
			return;

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

		closeIO();
		fDisposed = true;

		// We have memory leaks that prevent this class from being
		// GCed.  The problem becomes bad because we are holding
		// two LargePipedInputStream and eventually, the JUnit tests
		// run out of memory.  To address this particular problem,
		// before the actual causes of the leaks are fixed, lets
		// make sure we release all our four streams which all have
		// a reference to a LargePipedInputStream
		// Bug 323071
		fMIInConsolePipe = null;
		fMIInLogPipe = null;
		fMIOutConsolePipe = null;
		fMIOutLogPipe = null;
	}

	private void closeIO() {
		if (fMIOutConsolePipe != null) {
			try {
				fMIOutConsolePipe.close();
			} catch (IOException e) {
			}
		}
		if (fMIInConsolePipe != null) {
			try {
				fMIInConsolePipe.close();
			} catch (IOException e) {
			}
		}
		if (fMIOutLogPipe != null) {
			try {
				fMIOutLogPipe.close();
			} catch (IOException e) {
			}
		}
		if (fMIInLogPipe != null) {
			try {
				fMIInLogPipe.close();
			} catch (IOException e) {
			}
		}

	}

	/**
	 * @see java.lang.Process#getErrorStream()
	 */
	@Override
	public InputStream getErrorStream() {
		return fMIInLogPipe;
	}

	/**
	 * @see java.lang.Process#getInputStream()
	 */
	@Override
	public InputStream getInputStream() {
		return fMIInConsolePipe;
	}

	/**
	 * @see java.lang.Process#getOutputStream()
	 */
	@Override
	public OutputStream getOutputStream() {
		return fOutputStream;
	}

	@Override
	public void eventReceived(Object output) {
		if (fSuppressConsoleOutputCounter > 0)
			return;
		for (MIOOBRecord oobr : ((MIOutput) output).getMIOOBRecords()) {
			if (oobr instanceof MIConsoleStreamOutput) {
				MIConsoleStreamOutput out = (MIConsoleStreamOutput) oobr;
				String str = out.getString();

				if (str.trim().equals(SECONDARY_PROMPT)) {
					// Make sure to skip any secondary prompt that we
					// have already printed ourselves.  This would happen
					// when a new version of the backend starts sending
					// the secondary prompt for a command that it didn't
					// use to.  In this case, we still send it ourselves.
					if (inMissingSecondaryPrompt()) {
						return;
					}
					// Add a space for readability
					str = SECONDARY_PROMPT + ' ';
				}

				setPrompt(str);
				try {
					if (fMIOutConsolePipe != null) {
						fMIOutConsolePipe.write(str.getBytes());
						fMIOutConsolePipe.flush();
					}
				} catch (IOException e) {
				}
			} else if (oobr instanceof MILogStreamOutput) {
				MILogStreamOutput out = (MILogStreamOutput) oobr;
				String str = out.getString();
				if (str != null) {
					try {
						if (fMIOutLogPipe != null) {
							fMIOutLogPipe.write(str.getBytes());
							fMIOutLogPipe.flush();
						}
					} catch (IOException e) {
					}
				}
			}
		}
	}

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

	@Override
	public void commandSent(ICommandToken token) {
		// Bug 285170
		// Don't reset the fPrompt here, in case we are
		// dealing with the missing secondary prompt.

		ICommand<?> command = token.getCommand();
		// Check if the command is a CLI command and if it did not originate from this class.
		if ((command instanceof CLICommand<?> || command instanceof MIInterpreterExecConsole<?>)
				&& !(command instanceof ProcessCLICommand || command instanceof ProcessMIInterpreterExecConsole)) {
			fSuppressConsoleOutputCounter++;
		}

		// Bug 285170
		// Deal with missing secondary prompt, if needed.
		// The only two types we care about are ProcessMIInterpreterExecConsole
		// and RawCommand, both of which are MICommands
		if (command instanceof MICommand<?>) {
			checkMissingSecondaryPrompt((MICommand<?>) command);
		}
	}

	private void checkMissingSecondaryPrompt(MICommand<?> command) {
		// If the command send is one of ours, check if it is one that is missing a secondary prompt
		if (command instanceof ProcessMIInterpreterExecConsole) {
			String[] operations = ((ProcessMIInterpreterExecConsole) command).getParameters();
			if (operations != null && operations.length > 0) {
				// Get the command name.
				String operation = operations[0];
				int indx = operation.indexOf(' ');
				if (indx != -1) {
					operation = operation.substring(0, indx).trim();
				} else {
					operation = operation.trim();
				}

				if (isMissingSecondaryPromptCommand(operation)) {
					// For such commands, the backend does not send the secondary prompt
					// so we set it manually.  We'll remain in this state until we get
					// a commandDone() call.
					// This logic will still work when a new version of the backend
					// fixes this lack of secondary prompt.
					fPrompt = PromptType.IN_SECONDARY_PROMPT_MISSING;
				}
			}
		}

		// Even if the previous check didn't kick in, we may already be in the missing
		// secondary prompt case.  If so, we'll print the prompt ourselves.
		// Just make sure that this command is not ending the secondary prompt.
		if (fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING) {
			String operation = command.getOperation();
			if (operation != null) {
				int indx = operation.indexOf(' ');
				if (indx != -1) {
					operation = operation.substring(0, indx).trim();
				} else {
					operation = operation.trim();
				}

				if (!operation.equals(SECONDARY_PROMPT_END_COMMAND)) {
					// Add a space for readability
					String str = SECONDARY_PROMPT + ' ';
					try {
						if (fMIOutConsolePipe != null) {
							fMIOutConsolePipe.write(str.getBytes());
							fMIOutConsolePipe.flush();
						}
					} catch (IOException e) {
					}
				}
			}
		}
	}

	/**
	 * Check to see if the user typed a command that we know the backend
	 * does not send the secondary prompt for, but should.
	 * If so, we'll need to pretend we are receiving the secondary prompt.
	 *
	 * @since 3.0
	 */
	protected boolean isMissingSecondaryPromptCommand(String operation) {
		return false;
	}

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

	@Override
	public void commandDone(ICommandToken token, ICommandResult result) {
		// Whenever we get a command that is completed, we know we must be in the primary prompt
		fPrompt = PromptType.IN_PRIMARY_PROMPT;

		ICommand<?> command = token.getCommand();
		if ((command instanceof CLICommand<?> || command instanceof MIInterpreterExecConsole<?>)
				&& !(command instanceof ProcessCLICommand || command instanceof ProcessMIInterpreterExecConsole)) {
			fSuppressConsoleOutputCounter--;
		}
	}

	void setPrompt(String line) {
		fPrompt = PromptType.IN_PRIMARY_PROMPT;
		// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=109733
		if (line == null)
			return;
		line = line.trim();
		if (line.equals(SECONDARY_PROMPT)) {
			fPrompt = PromptType.IN_SECONDARY_PROMPT;
		}
	}

	public boolean inPrimaryPrompt() {
		return fPrompt == PromptType.IN_PRIMARY_PROMPT;
	}

	public boolean inSecondaryPrompt() {
		return fPrompt == PromptType.IN_SECONDARY_PROMPT || fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING;
	}

	/**
	 * @since 3.0
	 */
	public boolean inMissingSecondaryPrompt() {
		return fPrompt == PromptType.IN_SECONDARY_PROMPT_MISSING;
	}

	private boolean isMIOperation(String operation) {
		// The definition of an MI command states that it starts with
		//  [ token ] "-"
		// where 'token' is optional and a sequence of digits.
		// However, we don't accept a token from the user, because
		// we will be adding our own token when actually sending the command.
		if (operation.startsWith("-")) { //$NON-NLS-1$
			return true;
		}
		return false;
	}

	/**
	 * Return the context under which CLI commands are sent to the backend.
	 * <p>
	 * Returning a context narrower than ICommandControlDMContext will cause
	 * the communication with the backend to select the appropriate thread or
	 * frame.
	 *
	 * @return the CLI context
	 * @since 5.3
	 */
	protected IDMContext getCliCommandContext() {
		return getCommandControlService().getContext();
	}

	private class CLIOutputStream extends OutputStream {
		private final StringBuilder buf = new StringBuilder();

		@Override
		public void write(int b) throws IOException {
			buf.append((char) b);
			if (b == '\n') {
				// Throw away the newline.
				final String bufString = buf.toString().trim();
				buf.setLength(0);
				try {
					fSession.getExecutor().execute(new DsfRunnable() {
						@Override
						public void run() {
							try {
								post(bufString);
							} catch (IOException e) {
								// Pipe closed.
							}
						}
					});
				} catch (RejectedExecutionException e) {
					// Session disposed.
				}
			}
		}

		// Encapsulate the string sent to gdb in a fake
		// command and post it to the TxThread.
		public void post(String str) throws IOException {
			if (isDisposed())
				return;
			ICommand<MIInfo> cmd = null;
			// 1-
			// if We have the secondary prompt it means
			// that GDB is waiting for more feedback, use a RawCommand
			// 2-
			// Do not use the interpreter-exec for stepping operation
			// the UI will fall out of step.
			// Also, do not use "interpreter-exec console" for MI commands.
			// 3-
			// Normal Command Line Interface.
			boolean secondary = inSecondaryPrompt();
			IDMContext context = getCliCommandContext();
			if (secondary) {
				cmd = new RawCommand(context, str);
			} else if (!isMIOperation(str) && !CLIEventProcessor.isSteppingOperation(str)) {
				cmd = new ProcessMIInterpreterExecConsole(context, str);
			} else {
				cmd = new ProcessCLICommand(context, str);
			}
			final ICommand<MIInfo> finalCmd = cmd;
			fSession.getExecutor().execute(new DsfRunnable() {
				@Override
				public void run() {
					if (isDisposed())
						return;
					// Do not wait around for the answer.
					getCommandControlService().queueCommand(finalCmd, null);
				}
			});
		}
	}

	private class ProcessCLICommand extends CLICommand<MIInfo> {
		public ProcessCLICommand(IDMContext ctx, String oper) {
			super(ctx, oper);
		}
	}

	private class ProcessMIInterpreterExecConsole extends MIInterpreterExecConsole<MIInfo> {
		public ProcessMIInterpreterExecConsole(IDMContext ctx, String cmd) {
			super(ctx, cmd);
		}
	}
}
