/*******************************************************************************
 * Copyright (c) 2006, 2015 Wind River 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:
 *     Wind River Systems - initial API and implementation
 *     Ericsson	AB		  - Modified for handling of multiple threads
 *******************************************************************************/
package org.eclipse.cdt.examples.dsf.pda.service;

import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.Immutable;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.command.IEventListener;
import org.eclipse.cdt.dsf.service.AbstractDsfService;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.examples.dsf.pda.PDAPlugin;
import org.eclipse.cdt.examples.dsf.pda.service.commands.AbstractPDACommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDACommandResult;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAResumeCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAStepCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAStepReturnCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDASuspendCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAVMResumeCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAVMSuspendCommand;
import org.osgi.framework.BundleContext;

/**
 * Service for monitoring and controlling execution state of the DPA
 * program.
 * <p>
 * This service depends on the {@link PDACommandControl} service.
 * It must be initialized before this service is initialized.
 * </p>
 */
public class PDARunControl extends AbstractDsfService implements IRunControl, IEventListener {
	// Implementation note about tracking execution state:
	// This class implements event handlers for the events that are generated by
	// this service itself.  When the event is dispatched, these handlers will
	// be called first, before any of the clients.  These handlers update the
	// service's internal state information to make them consistent with the
	// events being issued.  Doing this in the handlers as opposed to when
	// the events are generated, guarantees that the state of the service will
	// always be consistent with the events.
	// The purpose of this pattern is to allow clients that listen to service
	// events and track service state, to be perfectly in sync with the service
	// state.

	static final private IExecutionDMContext[] EMPTY_TRIGGERING_CONTEXTS_ARRAY = new IExecutionDMContext[0];

	@Immutable
	private static class ThreadResumedEvent extends AbstractDMEvent<IExecutionDMContext> implements IResumedDMEvent {
		private final StateChangeReason fReason;

		ThreadResumedEvent(PDAThreadDMContext ctx, StateChangeReason reason) {
			super(ctx);
			fReason = reason;
		}

		@Override
		public StateChangeReason getReason() {
			return fReason;
		}

		@Override
		public String toString() {
			return "THREAD RESUMED: " + getDMContext() + " (" + fReason + ")";
		}
	}

	@Immutable
	private static class VMResumedEvent extends AbstractDMEvent<IExecutionDMContext>
			implements IContainerResumedDMEvent {
		private final StateChangeReason fReason;

		VMResumedEvent(PDAVirtualMachineDMContext ctx, StateChangeReason reason) {
			super(ctx);
			fReason = reason;
		}

		@Override
		public StateChangeReason getReason() {
			return fReason;
		}

		@Override
		public IExecutionDMContext[] getTriggeringContexts() {
			return EMPTY_TRIGGERING_CONTEXTS_ARRAY;
		}

		@Override
		public String toString() {
			return "VM RESUMED: (" + fReason + ")";
		}
	}

	@Immutable
	private static class ThreadSuspendedEvent extends AbstractDMEvent<IExecutionDMContext>
			implements ISuspendedDMEvent {
		private final StateChangeReason fReason;

		ThreadSuspendedEvent(PDAThreadDMContext ctx, StateChangeReason reason) {
			super(ctx);
			fReason = reason;
		}

		@Override
		public StateChangeReason getReason() {
			return fReason;
		}

		@Override
		public String toString() {
			return "THREAD SUSPENDED: " + getDMContext() + " (" + fReason + ")";
		}
	}

	@Immutable
	private static class VMSuspendedEvent extends AbstractDMEvent<IExecutionDMContext>
			implements IContainerSuspendedDMEvent {
		private final StateChangeReason fReason;

		final private IExecutionDMContext[] fTriggeringThreads;

		VMSuspendedEvent(PDAVirtualMachineDMContext ctx, PDAThreadDMContext threadCtx, StateChangeReason reason) {
			super(ctx);
			fReason = reason;
			if (threadCtx != null) {
				fTriggeringThreads = new IExecutionDMContext[] { threadCtx };
			} else {
				fTriggeringThreads = EMPTY_TRIGGERING_CONTEXTS_ARRAY;
			}
		}

		@Override
		public StateChangeReason getReason() {
			return fReason;
		}

		@Override
		public IExecutionDMContext[] getTriggeringContexts() {
			return fTriggeringThreads;
		}

		@Override
		public String toString() {
			return "THREAD SUSPENDED: " + getDMContext() + " (" + fReason + ", trigger = "
					+ Arrays.asList(fTriggeringThreads) + ")";
		}
	}

	@Immutable
	private static class ExecutionDMData implements IExecutionDMData {
		private final StateChangeReason fReason;

		ExecutionDMData(StateChangeReason reason) {
			fReason = reason;
		}

		@Override
		public StateChangeReason getStateChangeReason() {
			return fReason;
		}
	}

	private static class ThreadStartedEvent extends AbstractDMEvent<IExecutionDMContext> implements IStartedDMEvent {
		ThreadStartedEvent(PDAThreadDMContext threadCtx) {
			super(threadCtx);
		}
	}

	private static class ThreadExitedEvent extends AbstractDMEvent<IExecutionDMContext> implements IExitedDMEvent {
		ThreadExitedEvent(PDAThreadDMContext threadCtx) {
			super(threadCtx);
		}
	}

	// Services
	private PDACommandControl fCommandControl;

	// Reference to the virtual machine data model context
	private PDAVirtualMachineDMContext fDMContext;

	// VM state flags
	private boolean fVMSuspended = true;
	private boolean fVMResumePending = false;
	private boolean fVMSuspendPending = false;
	private boolean fVMStepping = false;
	private StateChangeReason fVMStateChangeReason = StateChangeReason.UNKNOWN;

	// Threads' state data
	private static class ThreadInfo {
		final PDAThreadDMContext fContext;
		boolean fSuspended = false;
		boolean fResumePending = false;
		boolean fSuspendPending = false;
		boolean fStepping = false;
		StateChangeReason fStateChangeReason = StateChangeReason.UNKNOWN;

		ThreadInfo(PDAThreadDMContext context) {
			fContext = context;
		}

		@Override
		public String toString() {
			return fContext.toString() + " (" + (fSuspended ? "SUSPENDED, " : "SUSPENDED, ") + fStateChangeReason
					+ (fResumePending ? ", RESUME_PENDING, " : "") + (fSuspendPending ? ", SUSPEND_PENDING, " : "")
					+ (fStepping ? ", SUSPEND_PENDING, " : "") + ")";

		}
	}

	private Map<Integer, ThreadInfo> fThreads = new LinkedHashMap<>();

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

	@Override
	protected BundleContext getBundleContext() {
		return PDAPlugin.getBundleContext();
	}

	@Override
	public void initialize(final RequestMonitor rm) {
		super.initialize(new RequestMonitor(getExecutor(), rm) {
			@Override
			protected void handleSuccess() {
				doInitialize(rm);
			}
		});
	}

	private void doInitialize(final RequestMonitor rm) {
		// Cache a reference to the command control and the virtual machine context
		fCommandControl = getServicesTracker().getService(PDACommandControl.class);
		fDMContext = fCommandControl.getContext();

		// Create the main thread context.
		fThreads.put(1, new ThreadInfo(new PDAThreadDMContext(getSession().getId(), fDMContext, 1)));

		// Add the run control service as a listener to PDA events, to catch
		// suspended/resumed/started/exited events from the command control.
		fCommandControl.addEventListener(this);

		// Add the run control service as a listener to service events as well,
		// in order to process our own suspended/resumed/started/exited events.
		getSession().addServiceEventListener(this, null);

		// Register the service with OSGi
		register(new String[] { IRunControl.class.getName(), PDARunControl.class.getName() },
				new Hashtable<String, String>());

		rm.done();
	}

	@Override
	public void shutdown(final RequestMonitor rm) {
		fCommandControl.removeEventListener(this);
		getSession().removeServiceEventListener(this);
		super.shutdown(rm);
	}

	@Override
	public void eventReceived(Object output) {
		if (!(output instanceof String))
			return;
		String event = (String) output;

		int nameEnd = event.indexOf(' ');
		nameEnd = nameEnd == -1 ? event.length() : nameEnd;
		String eventName = event.substring(0, nameEnd);

		PDAThreadDMContext thread = null;
		StateChangeReason reason = StateChangeReason.UNKNOWN;
		if (event.length() > nameEnd + 1) {
			if (Character.isDigit(event.charAt(nameEnd + 1))) {
				int threadIdEnd = event.indexOf(' ', nameEnd + 1);
				threadIdEnd = threadIdEnd == -1 ? event.length() : threadIdEnd;
				try {
					int threadId = Integer.parseInt(event.substring(nameEnd + 1, threadIdEnd));
					if (fThreads.containsKey(threadId)) {
						thread = fThreads.get(threadId).fContext;
					} else {
						// In case where a suspended event follows directly a
						// started event, a thread may not be in the list of
						// known threads yet.  In this case create the
						// thread context based on the ID.
						thread = new PDAThreadDMContext(getSession().getId(), fDMContext, threadId);
					}
				} catch (NumberFormatException e) {
				}
				if (threadIdEnd + 1 < event.length()) {
					reason = parseStateChangeReason(event.substring(threadIdEnd + 1));
				}
			} else {
				reason = parseStateChangeReason(event.substring(nameEnd + 1));
			}
		}

		// Handle PDA debugger suspended/resumed events and issue the
		// corresponding Data Model events.  Do not update the state
		// information until we start dispatching the service events.
		IDMEvent<?> dmEvent = null;
		if (eventName.equals("suspended") && thread != null) {
			dmEvent = new ThreadSuspendedEvent(thread, reason);
		} else if (eventName.equals("resumed") && thread != null) {
			dmEvent = new ThreadResumedEvent(thread, reason);
		} else if (event.startsWith("vmsuspended")) {
			dmEvent = new VMSuspendedEvent(fDMContext, thread, reason);
		} else if (event.startsWith("vmresumed")) {
			dmEvent = new VMResumedEvent(fDMContext, reason);
		} else if (event.startsWith("started") && thread != null) {
			dmEvent = new ThreadStartedEvent(thread);
		} else if (event.startsWith("exited") && thread != null) {
			dmEvent = new ThreadExitedEvent(thread);
		}

		if (dmEvent != null) {
			getSession().dispatchEvent(dmEvent, getProperties());
		}
	}

	private StateChangeReason parseStateChangeReason(String reasonString) {
		if (reasonString.startsWith("breakpoint") || reasonString.startsWith("watch")) {
			return StateChangeReason.BREAKPOINT;
		} else if (reasonString.equals("step") || reasonString.equals("drop")) {
			return StateChangeReason.STEP;
		} else if (reasonString.equals("client")) {
			return StateChangeReason.USER_REQUEST;
		} else if (reasonString.startsWith("event")) {
			return StateChangeReason.SIGNAL;
		} else {
			return StateChangeReason.UNKNOWN;
		}

	}

	@DsfServiceEventHandler
	public void eventDispatched(ThreadResumedEvent e) {
		ThreadInfo info = fThreads.get(((PDAThreadDMContext) e.getDMContext()).getID());
		if (info != null) {
			info.fSuspended = false;
			info.fResumePending = false;
			info.fStateChangeReason = e.getReason();
			info.fStepping = e.getReason().equals(StateChangeReason.STEP);
		}
	}

	@DsfServiceEventHandler
	public void eventDispatched(VMResumedEvent e) {
		fVMSuspended = false;
		fVMResumePending = false;
		fVMStateChangeReason = e.getReason();
		fVMStepping = e.getReason().equals(StateChangeReason.STEP);
		for (ThreadInfo info : fThreads.values()) {
			info.fSuspended = false;
			info.fStateChangeReason = StateChangeReason.CONTAINER;
			info.fStepping = false;
		}
	}

	@DsfServiceEventHandler
	public void eventDispatched(ThreadSuspendedEvent e) {
		ThreadInfo info = fThreads.get(((PDAThreadDMContext) e.getDMContext()).getID());
		if (info != null) {
			info.fSuspended = true;
			info.fSuspendPending = false;
			info.fStateChangeReason = e.getReason();
			info.fStepping = e.getReason().equals(StateChangeReason.STEP);
		}
	}

	@DsfServiceEventHandler
	public void eventDispatched(VMSuspendedEvent e) {
		fVMStateChangeReason = e.getReason();
		fVMSuspendPending = false;
		fVMSuspended = true;
		fVMStepping = false;
		List<IExecutionDMContext> triggeringContexts = Arrays.asList(e.getTriggeringContexts());
		for (ThreadInfo info : fThreads.values()) {
			info.fSuspended = true;
			info.fStateChangeReason = triggeringContexts.contains(info.fContext) ? StateChangeReason.STEP
					: StateChangeReason.CONTAINER;
			info.fStepping = false;
		}
	}

	@DsfServiceEventHandler
	public void eventDispatched(ThreadStartedEvent e) {
		PDAThreadDMContext threadCtx = (PDAThreadDMContext) e.getDMContext();
		fThreads.put(threadCtx.getID(), new ThreadInfo(threadCtx));
	}

	@DsfServiceEventHandler
	public void eventDispatched(ThreadExitedEvent e) {
		PDAThreadDMContext threadCtx = (PDAThreadDMContext) e.getDMContext();
		fThreads.remove(threadCtx.getID());
	}

	@Override
	public void canResume(IExecutionDMContext context, DataRequestMonitor<Boolean> rm) {
		rm.setData(doCanResume(context));
		rm.done();
	}

	private boolean doCanResume(IExecutionDMContext context) {
		if (context instanceof PDAThreadDMContext) {
			PDAThreadDMContext threadContext = (PDAThreadDMContext) context;
			// Threads can be resumed only if the VM is not suspended.
			if (!fVMSuspended) {
				ThreadInfo state = fThreads.get(threadContext.getID());
				if (state != null) {
					return state.fSuspended && !state.fResumePending;
				}
			}
		} else {
			return fVMSuspended && !fVMResumePending;
		}
		return false;
	}

	private boolean doCanStep(IExecutionDMContext context, StepType stepType) {
		if (stepType == StepType.STEP_OVER || stepType == StepType.STEP_RETURN) {
			if (context instanceof PDAThreadDMContext) {
				PDAThreadDMContext threadContext = (PDAThreadDMContext) context;
				// Only threads can be stepped.  But they can be stepped
				// while the VM is suspended or when just the thread is suspended.
				ThreadInfo state = fThreads.get(threadContext.getID());
				if (state != null) {
					return state.fSuspended && !state.fResumePending;
				}
			}
		}
		return false;
	}

	@Override
	public void canSuspend(IExecutionDMContext context, DataRequestMonitor<Boolean> rm) {
		rm.setData(doCanSuspend(context));
		rm.done();
	}

	private boolean doCanSuspend(IExecutionDMContext context) {
		if (context instanceof PDAThreadDMContext) {
			PDAThreadDMContext threadContext = (PDAThreadDMContext) context;
			// Threads can be resumed only if the VM is not suspended.
			if (!fVMSuspended) {
				ThreadInfo state = fThreads.get(threadContext.getID());
				if (state != null) {
					return !state.fSuspended && !state.fSuspendPending;
				}
			}
		} else {
			return !fVMSuspended && !fVMSuspendPending;
		}
		return false;
	}

	@Override
	public boolean isSuspended(IExecutionDMContext context) {
		if (context instanceof PDAThreadDMContext) {
			PDAThreadDMContext threadContext = (PDAThreadDMContext) context;
			// Threads can be resumed only if the VM is not suspended.
			if (!fVMSuspended) {
				ThreadInfo state = fThreads.get(threadContext.getID());
				if (state != null) {
					return state.fSuspended;
				}
			}
		}
		return fVMSuspended;
	}

	@Override
	public boolean isStepping(IExecutionDMContext context) {
		if (!isSuspended(context)) {
			if (context instanceof PDAThreadDMContext) {
				PDAThreadDMContext threadContext = (PDAThreadDMContext) context;
				// Threads can be resumed only if the VM is not suspended.
				if (!fVMStepping) {
					ThreadInfo state = fThreads.get(threadContext.getID());
					if (state != null) {
						return state.fStepping;
					}
				}
			}
			return fVMStepping;
		}
		return false;
	}

	@Override
	public void resume(IExecutionDMContext context, final RequestMonitor rm) {
		assert context != null;

		if (doCanResume(context)) {
			if (context instanceof PDAThreadDMContext) {
				final PDAThreadDMContext threadCtx = (PDAThreadDMContext) context;
				fThreads.get(threadCtx.getID()).fResumePending = true;
				fCommandControl.queueCommand(new PDAResumeCommand(threadCtx),
						new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
							@Override
							protected void handleFailure() {
								ThreadInfo threadState = fThreads.get(threadCtx.getID());
								if (threadState != null) {
									threadState.fResumePending = false;
								}
								super.handleFailure();
							}
						});
			} else {
				fVMResumePending = true;
				fCommandControl.queueCommand(new PDAVMResumeCommand(fDMContext),
						new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
							@Override
							protected void handleFailure() {
								fVMResumePending = false;
								super.handleFailure();
							}
						});
			}
		} else {
			PDAPlugin.failRequest(rm, INVALID_STATE, "Given context: " + context + ", is already running.");
		}
	}

	@Override
	public void suspend(IExecutionDMContext context, final RequestMonitor rm) {
		assert context != null;

		if (doCanSuspend(context)) {
			if (context instanceof PDAThreadDMContext) {
				final PDAThreadDMContext threadCtx = (PDAThreadDMContext) context;
				fThreads.get(threadCtx.getID()).fSuspendPending = true;
				fCommandControl.queueCommand(new PDASuspendCommand(threadCtx),
						new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
							@Override
							protected void handleFailure() {
								ThreadInfo threadState = fThreads.get(threadCtx.getID());
								if (threadState != null) {
									threadState.fSuspendPending = false;
								}
								super.handleFailure();
							}
						});
			} else {
				fVMSuspendPending = true;
				fCommandControl.queueCommand(new PDAVMSuspendCommand(fDMContext),
						new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
							@Override
							protected void handleFailure() {
								fVMSuspendPending = false;
								super.handleFailure();
							}
						});
			}
		} else {
			PDAPlugin.failRequest(rm, IDsfStatusConstants.INVALID_STATE,
					"Given context: " + context + ", is already suspended.");
		}
	}

	@Override
	public void canStep(IExecutionDMContext context, StepType stepType, DataRequestMonitor<Boolean> rm) {
		rm.setData(doCanStep(context, stepType));
		rm.done();
	}

	@Override
	public void step(IExecutionDMContext context, StepType stepType, final RequestMonitor rm) {
		assert context != null;

		if (doCanStep(context, stepType)) {
			final PDAThreadDMContext threadCtx = (PDAThreadDMContext) context;
			final boolean vmWasSuspneded = fVMSuspended;

			if (vmWasSuspneded) {
				fVMResumePending = true;
			} else {
				fThreads.get(threadCtx.getID()).fResumePending = true;
			}

			AbstractPDACommand<PDACommandResult> stepCommand = stepType == StepType.STEP_RETURN
					? new PDAStepReturnCommand(threadCtx)
					: new PDAStepCommand(threadCtx);

			fCommandControl.queueCommand(stepCommand, new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
				@Override
				protected void handleFailure() {
					// If the step command failed, we no longer
					// expect to receive a resumed event.
					if (vmWasSuspneded) {
						fVMResumePending = false;
					} else {
						ThreadInfo threadState = fThreads.get(threadCtx.getID());
						if (threadState != null) {
							threadState.fResumePending = false;
						}
					}
				}
			});

		} else {
			PDAPlugin.failRequest(rm, INVALID_STATE, "Cannot resume context");
			return;
		}
	}

	@Override
	public void getExecutionContexts(final IContainerDMContext containerDmc,
			final DataRequestMonitor<IExecutionDMContext[]> rm) {
		IExecutionDMContext[] threads = new IExecutionDMContext[fThreads.size()];
		int i = 0;
		for (ThreadInfo info : fThreads.values()) {
			threads[i++] = info.fContext;
		}
		rm.setData(threads);
		rm.done();
	}

	@Override
	public void getExecutionData(IExecutionDMContext dmc, DataRequestMonitor<IExecutionDMData> rm) {
		if (dmc instanceof PDAThreadDMContext) {
			ThreadInfo info = fThreads.get(((PDAThreadDMContext) dmc).getID());
			if (info == null) {
				PDAPlugin.failRequest(rm, INVALID_HANDLE, "Unknown DMC type");
				return;
			}
			rm.setData(new ExecutionDMData(info.fStateChangeReason));
		} else {
			rm.setData(new ExecutionDMData(fVMStateChangeReason));
		}
		rm.done();
	}
}
