/*****************************************************************
 * Copyright (c) 2010, 2015 Texas Instruments 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:
 *     Patrick Chuong (Texas Instruments) - Pin and Clone Supports (331781)
 *     Patrick Chuong (Texas Instruments) - Add support for icon overlay in the debug view (Bug 334566)
 *****************************************************************/
package org.eclipse.cdt.dsf.gdb.internal.ui;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

import org.eclipse.cdt.debug.ui.IPinProvider;
import org.eclipse.cdt.debug.ui.PinElementHandle;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateInDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IProcesses;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMData;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IStartedDMEvent;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlDMContext;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlShutdownDMEvent;
import org.eclipse.cdt.dsf.debug.ui.viewmodel.launch.StateChangedEvent;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IAdaptable;
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.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IWorkbenchPart;

/**
 * GDB pin provider implementation.
 */
public class GdbPinProvider implements IPinProvider {
	private static class GdbPinElementColorDescriptor implements IPinElementColorDescriptor {
		int fColor = GREEN;

		GdbPinElementColorDescriptor(int color) {
			fColor = color;
		}

		@Override
		public int getOverlayColor() {
			return fColor;
		}

		@Override
		public ImageDescriptor getToolbarIconDescriptor() {
			return null;
		}
	}

	/**
	 * A set of pinned element handles and their callback.
	 */
	private static Map<IPinElementHandle, IPinModelListener> gsPinnedHandles = Collections
			.synchronizedMap(new HashMap<IPinElementHandle, IPinModelListener>());

	/**
	 * Dsf session.
	 */
	private final DsfSession fSession;

	/**
	 * Constructor.
	 *
	 * @param session
	 */
	public GdbPinProvider(DsfSession session) {
		fSession = session;

		session.getExecutor().execute(() -> fSession.addServiceEventListener(GdbPinProvider.this, null));
	}

	/**
	 * Dispose all resources.
	 */
	public void dispose() {
		try {
			fSession.getExecutor().execute(() -> fSession.removeServiceEventListener(GdbPinProvider.this));
		} catch (RejectedExecutionException e) {
			// Session already gone.
		}
	}

	/**
	 * Returns the pinned element handles.
	 *
	 * @return the element handles.
	 */
	public static Set<IPinElementHandle> getPinnedHandles() {
		return gsPinnedHandles.keySet();
	}

	private static IMIExecutionDMContext getExecutionDmc(IDMContext dmc) {
		return DMContexts.getAncestorOfType(dmc, IMIExecutionDMContext.class);
	}

	private static IProcessDMContext getProcessDmc(IDMContext dmc) {
		return DMContexts.getAncestorOfType(dmc, IProcessDMContext.class);
	}

	private IThreadDMData getData(final IThreadDMContext threadDmc) {
		if (threadDmc == null || !fSession.isActive())
			return null;

		IThreadDMData data = null;
		final DsfServicesTracker tracker = new DsfServicesTracker(GdbUIPlugin.getBundleContext(), fSession.getId());
		try {
			Query<IThreadDMData> query = new Query<IThreadDMData>() {
				@Override
				protected void execute(final DataRequestMonitor<IThreadDMData> rm) {
					final IProcesses processes = tracker.getService(IProcesses.class);
					if (processes != null) {
						processes.getExecutionData(threadDmc, rm);
					} else {
						rm.setData(null);
						rm.done();
					}
				}
			};

			ImmediateInDsfExecutor immediateExecutor = new ImmediateInDsfExecutor(fSession.getExecutor());
			immediateExecutor.execute(query);
			data = query.get(2, TimeUnit.SECONDS); // timeout in 2 seconds, in case the call to execute got stuck
		} catch (Exception e) {
			GdbUIPlugin.log(e);
		} finally {
			if (tracker != null)
				tracker.dispose();
		}
		return data;
	}

	private String getLabel(IThreadDMData data) {
		String label = ""; //$NON-NLS-1$
		if (data != null) {
			String name = data.getName();
			String id = data.getId();
			if (name != null && !name.isEmpty())
				label = name;
			else if (id != null && !id.isEmpty())
				label = id;
		}
		return label;
	}

	private String getCombinedLabels(IThreadDMContext processDmc, IMIExecutionDMContext execDmc) {
		// get the process label
		IThreadDMData processData = getData(processDmc);
		String label = getLabel(processData);

		// get the execution (thread) context label
		if (execDmc != null) {
			String threadId = execDmc.getThreadId();
			label += !label.isEmpty() ? ": " : ""; //$NON-NLS-1$//$NON-NLS-2$
			label += "Thread [" + threadId + "]"; //$NON-NLS-1$//$NON-NLS-2$
		}
		return label;
	}

	@Override
	public boolean isPinnable(IWorkbenchPart part, Object debugContext) {
		if (debugContext instanceof IAdaptable) {
			return ((IAdaptable) debugContext).getAdapter(IDMContext.class) != null;
		}
		return false;
	}

	@Override
	public IPinElementHandle pin(IWorkbenchPart part, Object debugContext, IPinModelListener listener) {
		Object pinContext = debugContext;
		String label = ""; //$NON-NLS-1$
		String sessionId = ""; //$NON-NLS-1$

		IDMContext dmc = null;
		if (debugContext instanceof IAdaptable) {
			dmc = ((IAdaptable) debugContext).getAdapter(IDMContext.class);

			if (dmc != null) {
				sessionId = dmc.getSessionId() + "."; //$NON-NLS-1$
				IMIExecutionDMContext execDmc = getExecutionDmc(dmc);
				IProcessDMContext processDmc = getProcessDmc(dmc);

				label = getCombinedLabels(processDmc, execDmc);

				// set the pin context to a thread if it exist
				if (execDmc != null) {
					dmc = execDmc;
					pinContext = execDmc;

					// otherwise, set it to the DM context
				} else {
					pinContext = dmc;
				}
			}
		}

		IPinElementColorDescriptor colorDesc = new GdbPinElementColorDescriptor(
				GdbPinColorTracker.INSTANCE.addRef(sessionId + label));
		PinElementHandle handle = new PinElementHandle(pinContext, label, colorDesc);
		gsPinnedHandles.put(handle, listener);
		dispatchChangedEvent(dmc);

		return handle;
	}

	@Override
	public void unpin(IWorkbenchPart part, IPinElementHandle handle) {
		// remove the handle from the cache
		gsPinnedHandles.remove(handle);

		// dispatch the event to update the handle DM context
		Object debugContext = handle.getDebugContext();
		if (debugContext instanceof IAdaptable) {
			IDMContext dmc = ((IAdaptable) debugContext).getAdapter(IDMContext.class);
			GdbPinColorTracker.INSTANCE.removeRef(dmc.getSessionId() + "." + handle.getLabel()); //$NON-NLS-1$
			dispatchChangedEvent(dmc);

		}
	}

	@Override
	public boolean isPinnedTo(Object debugContext, IPinElementHandle handle) {
		Object handleDebugContext = handle.getDebugContext();

		if (debugContext instanceof IAdaptable && handleDebugContext instanceof IAdaptable) {
			IDMContext dmc = ((IAdaptable) debugContext).getAdapter(IDMContext.class);
			IDMContext hDmc = ((IAdaptable) handleDebugContext).getAdapter(IDMContext.class);

			if (dmc != null && hDmc != null) {
				if (dmc.getSessionId().equals(hDmc.getSessionId())) {
					IMIExecutionDMContext execDmc = getExecutionDmc(dmc);
					IProcessDMContext processDmc = getProcessDmc(dmc);

					String label = getCombinedLabels(processDmc, execDmc);
					return label.equals(handle.getLabel());
				}
			}
		}
		return false;
	}

	/**
	 * Dispatch the change event for the given DM context.
	 *
	 * @param dmc the DM context
	 */
	private void dispatchChangedEvent(IDMContext dmc) {
		if (dmc == null)
			return;

		try {
			DsfSession session = DsfSession.getSession(dmc.getSessionId());
			if (session != null && session.isActive())
				session.dispatchEvent(new StateChangedEvent(dmc), null);
		} catch (RejectedExecutionException e) {
			// Session already gone.
		}
	}

	/**
	 * Handle start event and re-attach the DM context to the pinned handles. The DM context
	 * is used for dispatching event to update the element label.
	 */
	@DsfServiceEventHandler
	public void handleEvent(final IStartedDMEvent event) {
		final IDMContext eventDmc = event.getDMContext();
		final IMIExecutionDMContext eventExecDmc = getExecutionDmc(eventDmc);
		final IProcessDMContext eventProcessDmc = getProcessDmc(eventDmc);

		if (eventProcessDmc != null) {
			for (final IPinElementHandle h : getPinnedHandles()) {
				new Job("Updating pin handler debug context") { //$NON-NLS-1$
					{
						setPriority(INTERACTIVE);
					}

					@Override
					protected IStatus run(IProgressMonitor monitor) {
						// only attach to the same pin handle if the session is not active
						PinElementHandle handle = ((PinElementHandle) h);
						Object handleDebugContext = handle.getDebugContext();
						if (handleDebugContext instanceof IAdaptable) {
							IDMContext handleDmc = ((IAdaptable) handleDebugContext).getAdapter(IDMContext.class);
							if (handleDmc != null) {
								DsfSession session = DsfSession.getSession(handleDmc.getSessionId());
								if (session == null || !session.isActive()) {
									String handleLabel = handle.getLabel();
									String label = getCombinedLabels(eventProcessDmc, eventExecDmc);

									if (label.equals(handleLabel)) {
										IDMContext newDmc = eventExecDmc != null ? eventExecDmc : eventDmc;
										handle.setDebugContext(newDmc);
										dispatchChangedEvent(newDmc);
									}
								}
							}
						}
						return Status.OK_STATUS;
					}
				}.schedule();
			}
		}
	}

	@DsfServiceEventHandler
	public void handleEvent(final ICommandControlShutdownDMEvent event) {
		handleInvalidModelContext(event);
	}

	@DsfServiceEventHandler
	public void handleEvent(final IExitedDMEvent event) {
		handleInvalidModelContext(event);
	}

	@DsfServiceEventHandler
	public void handleEvent(final IResumedDMEvent event) {
		handleInvalidModelContext(event);
	}

	private void handleInvalidModelContext(IDMEvent<?> event) {
		Set<Entry<IPinElementHandle, IPinModelListener>> entries = gsPinnedHandles.entrySet();
		for (final Entry<IPinElementHandle, IPinModelListener> e : entries) {
			final IPinModelListener listener = e.getValue();
			if (listener != null) {
				IPinElementHandle handle = e.getKey();

				Object handleObject = handle.getDebugContext();
				if (handleObject instanceof IDMContext) {
					IDMContext handleDmc = (IDMContext) handleObject;
					IDMContext eventDmc = event.getDMContext();

					// First check if we have a thread.  We must use IMIExecutionDMContext and not
					// IExecutionDMContext because IExecutionDMContext also represents a process
					IMIExecutionDMContext execEventDmc = DMContexts.getAncestorOfType(eventDmc,
							IMIExecutionDMContext.class);
					IMIExecutionDMContext execHandleDmc = DMContexts.getAncestorOfType(handleDmc,
							IMIExecutionDMContext.class);

					// Make sure both dmcs are not null to know if we should compare thread dmcs or container dmcs
					if (execEventDmc != null && execHandleDmc != null) {
						// It is a thread event, but is it the same as the pin handle?
						if (execEventDmc.equals(execHandleDmc)) {
							fireModleChangeEvent(listener, null);
						}
						continue;
					}

					// If we weren't dealing with a thread for either the event or the handle,
					// let's check for IMIContainerDMContext
					IMIContainerDMContext procEventDmc = DMContexts.getAncestorOfType(eventDmc,
							IMIContainerDMContext.class);
					IMIContainerDMContext procHandleDmc = DMContexts.getAncestorOfType(handleDmc,
							IMIContainerDMContext.class);
					if (procEventDmc != null && procHandleDmc != null) {
						if (procEventDmc.equals(procHandleDmc)) {
							fireModleChangeEvent(listener, null);
						}
						continue;
					}

					// If we got a shutdown event
					if (eventDmc instanceof ICommandControlDMContext) {
						fireModleChangeEvent(listener, null);
						continue;
					}
				}
			}
		}
	}

	private void fireModleChangeEvent(final IPinModelListener listener, final ISelection selection) {
		new Job("Model Changed") { //$NON-NLS-1$
			{
				setSystem(true);
			}

			@Override
			protected IStatus run(IProgressMonitor arg0) {
				listener.modelChanged(selection);
				return Status.OK_STATUS;
			}
		}.schedule();
	}
}
