/*******************************************************************************
 * Copyright (c) 2011, 2020 Ericsson
 *
 * All rights reserved. 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:
 *   Marc Dumais - Initial implementation
 *   Francois Chouinard - Misc improvements, DSF signal handling, dynamic experiment
 *   Patrick Tasse - Updated for TMF 2.0
 *   Bernd Hufmann - Fixed deadlock during shutdown
 *******************************************************************************/

package org.eclipse.tracecompass.internal.gdbtrace.core.trace;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

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.Query;
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.debug.service.command.ICommandControlService;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlDMContext;
import org.eclipse.cdt.dsf.gdb.IGdbDebugPreferenceConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch;
import org.eclipse.cdt.dsf.gdb.service.GDBTraceControl_7_2.TraceRecordSelectedChangedEvent;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceRecordDMContext;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceRecordDMData;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceRecordSelectedChangedDMEvent;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceStatusDMData;
import org.eclipse.cdt.dsf.gdb.service.IGDBTraceControl.ITraceTargetDMContext;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIProcesses;
import org.eclipse.cdt.dsf.mi.service.MIBreakpointDMData;
import org.eclipse.cdt.dsf.mi.service.MIBreakpoints;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakListInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakpoint;
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.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchesListener2;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ITreeModelViewer;
import org.eclipse.debug.ui.AbstractDebugView;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.debug.ui.contexts.DebugContextEvent;
import org.eclipse.debug.ui.contexts.IDebugContextListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.internal.gdbtrace.core.GdbTraceCorePlugin;
import org.eclipse.tracecompass.internal.gdbtrace.core.event.GdbTraceEvent;
import org.eclipse.tracecompass.internal.gdbtrace.core.event.GdbTraceEventContent;
import org.eclipse.tracecompass.tmf.core.event.TmfEventField;
import org.eclipse.tracecompass.tmf.core.event.TmfEventType;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
import org.eclipse.tracecompass.tmf.ui.editors.ITmfTraceEditor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;

/**
 * Adaptor to access GDB Tracepoint frames, previously collected and saved in a
 * file by GDB. One instance of this maps to a single DSF-GDB session.
 * <p>
 * This class offers the functions of starting a post-mortem GDB session with a
 * tracepoint data file, navigate the data frames and return the data contained
 * in a given tracepoint frame.
 * <p>
 * Note: GDB 7.2 or later is required to handle tracepoints
 *
 * @author Marc Dumais
 * @author Francois Chouinard
 */
@SuppressWarnings("restriction")
public class DsfGdbAdaptor {

    private static final Object SESSION_LOCK = new Object();
    private static final String INTERRUPTION_EXCEPTION = "Interruption exception"; //$NON-NLS-1$
    private static final String GDB_EXCEPTION = "GDB exception"; //$NON-NLS-1$
    private static final String REQUEST_REJECTED_EXCEPTION = "Request rejected exception"; //$NON-NLS-1$
    private static final String TIMEOUT = "Timeout"; //$NON-NLS-1$

    private GdbTrace fGdbTrace;

    private int fNumberOfFrames = 0;
    private boolean fIsTimeoutEnabled;
    private int fTimeout;

    private ILaunch fLaunch;
    private boolean isTerminating;
    private DsfSession fDsfSession = null;
    private String fSessionId;

    private String tracedExecutable = ""; //$NON-NLS-1$

    private String gdb72Executable = ""; //$NON-NLS-1$
    private String fTraceFilePath = ""; //$NON-NLS-1$
    private String fTraceFile = ""; //$NON-NLS-1$
    private String sourceLocator = ""; //$NON-NLS-1$

    // To save tracepoints detailed info. The key is the rank of the
    // breakpoint (tracepoint is a kind of breakpoint)
    private Map<Integer, MIBreakpointDMData> fTpInfo = new HashMap<>();

    private TmfEventType tmfEventType = new TmfEventType("GDB Tracepoint", TmfEventField.makeRoot(new String[] { "Content" })); //$NON-NLS-1$ //$NON-NLS-2$

    {
        new DsfGdbPlatformEventListener();
    }

    /**
     * <b><u>DsfGdbPlatformEventListener</u></b>
     * <p>
     * Listens to platform and DSF-GDB events that announce important events
     * about the launchers or a change in debug context that we might need to
     * react-to.
     * <p>
     *
     * @author Francois Chouinard
     */
    private class DsfGdbPlatformEventListener implements
            ILaunchesListener2, IDebugContextListener {

        /**
         *
         */
        public DsfGdbPlatformEventListener() {
            Display.getDefault().syncExec(() -> {
                DebugPlugin.getDefault().getLaunchManager().addLaunchListener(DsfGdbPlatformEventListener.this);
                IWorkbench wb = PlatformUI.getWorkbench();
                IWorkbenchWindow win = wb.getActiveWorkbenchWindow();
                DebugUITools.getDebugContextManager().getContextService(win).addDebugContextListener(DsfGdbPlatformEventListener.this);
            });
        }

        @Override
        public synchronized void launchesRemoved(ILaunch[] launches) {
            // Do nothing
        }

        @Override
        public synchronized void launchesAdded(ILaunch[] launches) {
            // Do nothing
        }

        @Override
        public synchronized void launchesChanged(ILaunch[] launches) {
            // Do nothing
        }

        @Override
        public synchronized void launchesTerminated(ILaunch[] launches) {
            for (ILaunch launch : launches) {
                String sessionId = ((GdbLaunch) launch).getSession().getId();
                closeGdbTraceEditor(sessionId);
            }
        }

        private String fCurrentSessionId = ""; //$NON-NLS-1$
        @Override
        public void debugContextChanged(DebugContextEvent event) {
            ISelection selection = event.getContext();
            if (selection instanceof IStructuredSelection) {
                List<?> eventContextList = ((IStructuredSelection) selection).toList();
                for (Object eventContext : eventContextList) {
                    if (eventContext instanceof IAdaptable) {
                        Object contextObject = ((IAdaptable) eventContext).getAdapter(IDMContext.class);
                        IDMContext context = (IDMContext) contextObject;
                        if (context != null) {
                            String sessionId;
                            synchronized (SESSION_LOCK) {
                                sessionId = context.getSessionId();
                                if (sessionId.equals(fCurrentSessionId)) {
                                    return;
                                }
                                fCurrentSessionId = sessionId;
                            }
                            // Get the current trace record
                            final DsfExecutor executor = DsfSession.getSession(sessionId).getExecutor();
                            final DsfServicesTracker tracker = new DsfServicesTracker(GdbTraceCorePlugin.getBundleContext(), sessionId);
                            Query<ITraceRecordDMContext> getCurrentRecordQuery = new Query<ITraceRecordDMContext>() {
                                @Override
                                public void execute(final DataRequestMonitor<ITraceRecordDMContext> queryRm) {
                                    final IGDBTraceControl traceControl = tracker.getService(IGDBTraceControl.class);
                                    final ICommandControlService commandControl = tracker.getService(ICommandControlService.class);
                                    if (traceControl != null && commandControl != null) {
                                        ITraceTargetDMContext traceContext = (ITraceTargetDMContext) commandControl.getContext();
                                        traceControl.getCurrentTraceRecordContext(traceContext, queryRm);
                                    } else {
                                        queryRm.done();
                                    }
                                }
                            };
                            try {
                                executor.execute(getCurrentRecordQuery);
                                ITraceRecordDMContext record;
                                if (DsfGdbAdaptor.this.fIsTimeoutEnabled) {
                                    record = getCurrentRecordQuery.get(fTimeout, TimeUnit.MILLISECONDS);
                                } else {
                                    record = getCurrentRecordQuery.get();
                                }
                                // If we get a trace record, it means that this can be used
                                if (record != null && record.getRecordId() != null) {
                                    int recordId = Integer.parseInt(record.getRecordId());
                                    selectGdbTraceEditor(sessionId, recordId);
                                    break;
                                }
                            } catch (InterruptedException e) {
                                GdbTraceCorePlugin.logError(INTERRUPTION_EXCEPTION, e);
                                Thread.currentThread().interrupt();
                            } catch (ExecutionException e) {
                                GdbTraceCorePlugin.logError(GDB_EXCEPTION, e);
                            } catch (RejectedExecutionException e) {
                                GdbTraceCorePlugin.logError(REQUEST_REJECTED_EXCEPTION, e);
                            } catch (TimeoutException e) {
                                GdbTraceCorePlugin.logError(TIMEOUT, e);
                            } finally {
                                tracker.dispose();
                            }
                            // else not DSF-GDB or GDB < 7.2
                        }
                    }
                    // else not DSF
                }
            }
        }
    } //  class DsfGdbPlatformEventListener

    /**
     * Constructor for DsfGdbAdaptor.  This is used when we want to launch a
     * DSF-GDB session and use it as source in our tracing perspective.
     * i.e. when launching from the Project Explorer
     *
     * @param trace  the GDB trace
     * @param gdbExec  GDB executable.  Must be version 7.2 or later.
     * @param traceFile  previously generated GDB tracepoint file
     * @param tracedExecutable  executable that was used to generate the tracefile
     *  workspace, where the traced executable was taken from.
     */
    public DsfGdbAdaptor(GdbTrace trace, String gdbExec, String traceFile, String tracedExecutable) {
        this.fGdbTrace = trace;
        this.gdb72Executable = gdbExec;
        this.fTraceFilePath = traceFile;
        this.fTraceFile = traceFile.substring(traceFile.lastIndexOf(IPath.SEPARATOR) + 1);
        this.tracedExecutable = tracedExecutable;

        try {
            launchDGBPostMortemTrace();
        } catch (CoreException e) {
            GdbTraceCorePlugin.logError(e.getMessage(), e);
        }
    }

    /**
     * Builds a launcher and launches a Post-mortem GDB session, based on a
     * previously-gathered GDB Tracepoint file.  The information used to
     * create the launcher is provided to the constructor of this class,
     * at instantiation time.
     * <p>
     * Note: Requires GDB 7.2 or later
     */
    private void launchDGBPostMortemTrace() throws CoreException {
        fIsTimeoutEnabled = Platform.getPreferencesService().getBoolean(GdbPlugin.PLUGIN_ID, IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT, false, null);
        if (fIsTimeoutEnabled) {
            fTimeout = Platform.getPreferencesService().getInt(GdbPlugin.PLUGIN_ID, IGdbDebugPreferenceConstants.PREF_COMMAND_TIMEOUT_VALUE, IGdbDebugPreferenceConstants.COMMAND_TIMEOUT_VALUE_DEFAULT, null);
        }

        ILaunchConfigurationType configType = DebugPlugin
                .getDefault()
                .getLaunchManager()
                .getLaunchConfigurationType("org.eclipse.cdt.launch.postmortemLaunchType"); //$NON-NLS-1$
        ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, fTraceFile);

        wc.setAttribute("org.eclipse.cdt.dsf.gdb.DEBUG_NAME", gdb72Executable); //$NON-NLS-1$
        wc.setAttribute("org.eclipse.cdt.dsf.gdb.POST_MORTEM_TYPE", "TRACE_FILE"); //$NON-NLS-1$ //$NON-NLS-2$
        wc.setAttribute("org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR", 0); //$NON-NLS-1$
        wc.setAttribute("org.eclipse.cdt.launch.COREFILE_PATH", fTraceFilePath); //$NON-NLS-1$
        wc.setAttribute("org.eclipse.cdt.launch.DEBUGGER_START_MODE", "core"); //$NON-NLS-1$ //$NON-NLS-2$
        wc.setAttribute("org.eclipse.cdt.launch.PROGRAM_NAME", tracedExecutable); //$NON-NLS-1$
        // So that the GDB launch is synchronous
        wc.setAttribute("org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND", false); //$NON-NLS-1$

        if (!sourceLocator.isEmpty()) {
            wc.setAttribute("org.eclipse.debug.core.source_locator_memento", sourceLocator); //$NON-NLS-1$
        }

        // Launch GDB session
        fLaunch = wc.doSave().launch("debug", null); //$NON-NLS-1$
        isTerminating = false;

        if (fLaunch instanceof GdbLaunch) {
            fSessionId = ((GdbLaunch) fLaunch).getSession().getId();
        }

        fDsfSession = ((GdbLaunch) fLaunch).getSession();
        fDsfSession.addServiceEventListener(this, null);

        // Find the number of frames contained in the tracepoint file
        fNumberOfFrames = findNumFrames();
    }

    /**
     * This method terminates the current DSF-GDB session
     */
    public void dispose() {
        if (fLaunch != null && fLaunch.canTerminate() && !isTerminating) {
            isTerminating = true;
            try {
                fLaunch.terminate();
            } catch (DebugException e) {
                GdbTraceCorePlugin.logError(e.getMessage(), e);
            }
            fLaunch = null;
        }
    }

    /**
     * This method will try (once per call) to get the number of GDB tracepoint
     * frames for the current session, from DSF-GDB, until it succeeds at
     * getting an amount different than zero.
     *
     * @return The number of frames in current session or zero if unsuccessful
     */
    public int getNumberOfFrames() {
        if (fNumberOfFrames == 0) {
            fNumberOfFrames = findNumFrames();
        }
        return fNumberOfFrames;
    }

    /**
     * Wrapper around the selecting of a frame and the reading of its
     * information. this is a work-around for the potential problem of
     * concurrent access to these functions by more than one thread, where two
     * clients might interfere with each other.
     * <p>
     * Note: We also try to get the tracepoint info here, if it's not already
     * filled-in.
     *
     * @param rank
     *            a long corresponding to the number of the frame to be selected
     *            and read
     * @return A GdbTraceEvent object, or null in case of failure.
     */
    public synchronized GdbTraceEvent selectAndReadFrame(final long rank) {
        // lazy init of tracepoints info
        if (fTpInfo.isEmpty()) {
            getTracepointInfo();
        }
        if (selectDataFrame(rank, false)) {
            GdbTraceEvent event = getTraceFrameData(rank);
            long ts = event.getTimestamp().getValue();
            if (ts == rank) {
                return event;
            }
        }
        return null;
    }

    /**
     * This class implements a best-effort look-up of the detailed tracepoint
     * information (source code filename, line number, etc...).
     */
    private void getTracepointInfo() {

        // Get the latest executor/service tracker
        final DsfExecutor executor = DsfSession.getSession(fSessionId).getExecutor();
        final DsfServicesTracker tracker = new DsfServicesTracker(GdbTraceCorePlugin.getBundleContext(), fSessionId);

        Query<Object> selectRecordQuery = new Query<Object>() {
            @Override
            public void execute(final DataRequestMonitor<Object> drm) {

                // A breakpoint is no longer GDB-global but tied to a specific process
                // So we need to find our process and the ask for its breakpoints
                IMIProcesses procService = tracker.getService(IMIProcesses.class);
                final ICommandControlService cmdControl = tracker.getService(ICommandControlService.class);
                if (procService == null || cmdControl == null) {
                    drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                    drm.done();
                    return;
                }

                ITraceTargetDMContext context = (ITraceTargetDMContext) cmdControl.getContext();
                ICommandControlDMContext cmdControlDMC = DMContexts.getAncestorOfType(context, ICommandControlDMContext.class);

                procService.getProcessesBeingDebugged(
                        cmdControlDMC,
                        new DataRequestMonitor<IDMContext[]>(executor, drm) {
                            @Override
                            protected void handleSuccess() {
                                if (getData() == null || getData().length < 1) {
                                    drm.done();
                                    return;
                                }

                                // Choose the first process for now, until gdb can tell
                                // us which process the trace record is associated with.
                                IContainerDMContext containerDMC = (IContainerDMContext)(getData()[0]);
                                IBreakpointsTargetDMContext bpTargetDMC = DMContexts.getAncestorOfType(containerDMC , IBreakpointsTargetDMContext.class);

                                IMICommandControl commandService = tracker.getService(IMICommandControl.class);
                                if (commandService == null) {
                                    drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                                    drm.done();
                                    return;
                                }

                                CommandFactory cmdFactory = commandService.getCommandFactory();
                                MIBreakpoints bpService = tracker.getService(MIBreakpoints.class);
                                if (cmdFactory == null || bpService == null) {
                                    drm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                                    drm.done();
                                    return;
                                }

                                // Execute the command
                                cmdControl.queueCommand(cmdFactory.createMIBreakList(bpTargetDMC),
                                        new DataRequestMonitor<MIBreakListInfo>(executor, drm) {
                                    @Override
                                    protected void handleSuccess() {
                                        MIBreakpoint[] breakpoints = getData().getMIBreakpoints();
                                        for (int i = 0; i < breakpoints.length; i++) {
                                            MIBreakpointDMData breakpoint = bpService.createMIBreakpointDMData(breakpoints[i]);
                                            String type = breakpoint.getBreakpointType();
                                            // Only save info if the current breakpoint is of type tracepoint
                                            if (type.compareTo(MIBreakpoints.TRACEPOINT) == 0 ) {
                                                fTpInfo.put(Integer.valueOf(breakpoint.getReference()), breakpoint);
                                            }
                                        }
                                        drm.done();
                                    }
                                });
                            }
                        });
            }
        };
        try {
            executor.execute(selectRecordQuery);
            if (fIsTimeoutEnabled) {
                selectRecordQuery.get(fTimeout, TimeUnit.MILLISECONDS); // blocks until time out
            } else {
                selectRecordQuery.get(); // blocks
            }
        } catch (InterruptedException e) {
            GdbTraceCorePlugin.logError(INTERRUPTION_EXCEPTION, e);
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            GdbTraceCorePlugin.logError(GDB_EXCEPTION, e);
        } catch (RejectedExecutionException e) {
            GdbTraceCorePlugin.logError(REQUEST_REJECTED_EXCEPTION, e);
        } catch (TimeoutException e) {
            GdbTraceCorePlugin.logError(TIMEOUT, e);
        } finally {
            tracker.dispose();
        }
    }

    /**
     * Returns the number of frames contained in currently loaded tracepoint GDB
     * session.
     * <p>
     * Note: A postmortem GDB session must be started before calling
     *         this method
     *
     * @return the number of frames contained in currently loaded tracepoint GDB
     *         session or zero in case of error
     */
    private synchronized int findNumFrames() {
        int frameNum = 0;

        if (DsfSession.getSession(fSessionId) == null) {
            return 0;
        }

        final DsfExecutor executor = DsfSession.getSession(fSessionId)
                .getExecutor();
        final DsfServicesTracker tracker = new DsfServicesTracker(
                GdbTraceCorePlugin.getBundleContext(), fSessionId);

        Query<ITraceStatusDMData> selectRecordQuery = new Query<ITraceStatusDMData>() {
            @Override
            public void execute(
                    final DataRequestMonitor<ITraceStatusDMData> queryRm) {
                final IGDBTraceControl traceControl = tracker
                        .getService(IGDBTraceControl.class);

                final ICommandControlService commandControl = tracker.getService(ICommandControlService.class);
                if (commandControl == null) {
                    queryRm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                    queryRm.done();
                    return;
                }
                final ITraceTargetDMContext dmc = (ITraceTargetDMContext) commandControl
                        .getContext();

                if (traceControl != null) {
                    traceControl.getTraceStatus(dmc, queryRm);
                } else {
                    queryRm.done();
                }
            }
        };
        try {
            executor.execute(selectRecordQuery);
            ITraceStatusDMData data;
            if (fIsTimeoutEnabled) {
                data = selectRecordQuery.get(fTimeout, TimeUnit.MILLISECONDS); // blocks until time out
            } else {
                data = selectRecordQuery.get(); // blocks
            }

            frameNum = data.getNumberOfCollectedFrame();
        } catch (InterruptedException e) {
            GdbTraceCorePlugin.logError(INTERRUPTION_EXCEPTION, e);
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            GdbTraceCorePlugin.logError(GDB_EXCEPTION, e);
        } catch (RejectedExecutionException e) {
            GdbTraceCorePlugin.logError(REQUEST_REJECTED_EXCEPTION, e);
        } catch (TimeoutException e) {
            GdbTraceCorePlugin.logError(TIMEOUT, e);
        } finally {
            tracker.dispose();
        }
        return frameNum;
    }

    /**
     * This method uses the DSF-GDB interface to select a given frame number in
     * the current GDB tracepoint session.
     *
     * @param rank
     *            the rank of the tracepoint frame to select.
     * @param update
     *            true if visualization should be updated
     * @return boolean true if select worked.
     */
    public boolean selectDataFrame(final long rank, final boolean update) {
        boolean status = true;

        final DsfSession dsfSession = DsfSession.getSession(fSessionId);
        if (dsfSession == null) {
            return false;
        }

        if (update) {
            /*
             * Clear the selection to ensure that the new selection is not
             * prevented from overriding the current selection by the DSF
             * selection policy. This could be removed when DSF provides
             * an API to force the trace record selection in the Debug view.
             */
            Display.getDefault().syncExec(() -> {
                for (IWorkbenchWindow wbWindow : PlatformUI.getWorkbench().getWorkbenchWindows()) {
                    for (IWorkbenchPage wbPage : wbWindow.getPages()) {
                        IViewPart vp = wbPage.findView(IDebugUIConstants.ID_DEBUG_VIEW);
                        if (vp instanceof AbstractDebugView) {
                            Viewer viewer = ((AbstractDebugView) vp).getViewer();
                            if (viewer instanceof ITreeModelViewer) {
                                ((ITreeModelViewer) viewer).setSelection(StructuredSelection.EMPTY, false, true);
                            }
                        }
                    }
                }
            });
        }

        final DsfExecutor executor = dsfSession.getExecutor();
        final DsfServicesTracker tracker = new DsfServicesTracker(GdbTraceCorePlugin.getBundleContext(), fSessionId);

        Query<Object> selectRecordQuery = new Query<Object>() {
            @Override
            public void execute(final DataRequestMonitor<Object> queryRm) {
                final IGDBTraceControl traceControl = tracker.getService(IGDBTraceControl.class);

                final ICommandControlService commandControl = tracker.getService(ICommandControlService.class);
                if (commandControl == null) {
                    queryRm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                    queryRm.done();
                    return;
                }

                final ITraceTargetDMContext dmc = (ITraceTargetDMContext) commandControl.getContext();

                if (traceControl != null) {
                    ITraceRecordDMContext newCtx = traceControl.createTraceRecordContext(dmc, Integer.toString((int) rank));
                    if (update) {
                        dsfSession.dispatchEvent(new TraceRecordSelectedChangedEvent(newCtx), new Hashtable<String, String>());
                    }
                    traceControl.selectTraceRecord(newCtx, queryRm);
                } else {
                    queryRm.done();
                }
            }
        };
        try {
            executor.execute(selectRecordQuery);
            if (fIsTimeoutEnabled) {
                selectRecordQuery.get(fTimeout, TimeUnit.MILLISECONDS); // blocks until time out
            } else {
                selectRecordQuery.get(); // blocks
            }
        } catch (InterruptedException e) {
            status = false;
            GdbTraceCorePlugin.logError(INTERRUPTION_EXCEPTION, e);
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            status = false;
            GdbTraceCorePlugin.logError(GDB_EXCEPTION, e);
        } catch (RejectedExecutionException e) {
            status = false;
            GdbTraceCorePlugin.logError(REQUEST_REJECTED_EXCEPTION, e);
        } catch (TimeoutException e) {
            status = false;
            GdbTraceCorePlugin.logError(TIMEOUT, e);
        } finally {
            tracker.dispose();
        }
        return status;
    }

    /**
     * This method uses DSF-GDB to read the currently selected GDB tracepoint
     * data frame.   An object of type GdbTraceEvent is build based on the
     * information contained in the data frame and returned to the caller.
     * <p>
     * NOTE : A frame must be selected before calling this method!
     *
     * @param rank  for internal purposes - does <b>not</b> control which
     * frame will be read!
     * @return parsed tp frame, in the form of a GdbTraceEvent
     */
    private GdbTraceEvent getTraceFrameData(final long rank) {

        if (DsfSession.getSession(fSessionId) == null) {
            return null;
        }

        final DsfExecutor executor = DsfSession.getSession(fSessionId).getExecutor();
        final DsfServicesTracker tracker = new DsfServicesTracker(GdbTraceCorePlugin.getBundleContext(), fSessionId);

        Query<ITraceRecordDMData> getFrameDataQuery = new Query<ITraceRecordDMData>() {
            @Override
            public void execute(final DataRequestMonitor<ITraceRecordDMData> rm) {
                final IGDBTraceControl traceControl = tracker.getService(IGDBTraceControl.class);

                final ICommandControlService commandControl = tracker.getService(ICommandControlService.class);
                if (commandControl == null) {
                    rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Could not find necessary services", null)); //$NON-NLS-1$
                    rm.done();
                    return;
                }
                final ITraceTargetDMContext dmc = (ITraceTargetDMContext) commandControl.getContext();

                if (traceControl != null) {
                    traceControl.getCurrentTraceRecordContext(dmc,
                            new DataRequestMonitor<ITraceRecordDMContext>(executor, rm) {
                        @Override
                        protected void handleSuccess() {
                            traceControl.getTraceRecordData(getData(), rm);
                        }
                    });
                } else {
                    rm.done();
                }
            }
        };
        try {
            // Execute the above query
            executor.execute(getFrameDataQuery);
            ITraceRecordDMData data;
            if (fIsTimeoutEnabled) {
                data = getFrameDataQuery.get(fTimeout, TimeUnit.MILLISECONDS); // blocking call until time out
            } else {
                data = getFrameDataQuery.get();
            }

            if (data == null) {
                return null;
            }

            String ts = data.getTimestamp();
            if (ts == null) {
                ts = "0"; //$NON-NLS-1$
            }

            // get corresponding TP data
            String tmfEventRef;
            MIBreakpointDMData bp = fTpInfo.get(Integer.valueOf(data.getTracepointNumber()));
            if (bp != null) {
                tmfEventRef = bp.getFileName() + ":" + bp.getLineNumber() + " :: " + bp.getFunctionName(); //$NON-NLS-1$ //$NON-NLS-2$
            } else {
                tmfEventRef = tracedExecutable;
            }

            GdbTraceEventContent evContent = new GdbTraceEventContent(
                    data.getContent(),
                    Integer.parseInt(data.getTracepointNumber()),
                    Integer.parseInt(data.getRecordId()));

            GdbTraceEvent ev = new GdbTraceEvent(fGdbTrace,
                    TmfTimestamp.fromSeconds(Integer.parseInt(data.getRecordId())),
                    "Tracepoint: " + data.getTracepointNumber() + ", Frame: " + data.getRecordId(),  //$NON-NLS-1$ //$NON-NLS-2$
                    tmfEventType,
                    evContent,
                    tmfEventRef);

            return ev;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return createExceptionEvent(rank, INTERRUPTION_EXCEPTION);
        } catch (java.util.concurrent.ExecutionException e) {
            return createExceptionEvent(rank, GDB_EXCEPTION);
        } catch (RejectedExecutionException e) {
            return createExceptionEvent(rank, REQUEST_REJECTED_EXCEPTION);
        } catch (TimeoutException e) {
            return createExceptionEvent(rank, TIMEOUT);
        }

        finally {
            tracker.dispose();
        }
    }

    /**
     * This is a helper method for getTraceFrameData, to create for it a
     * "best effort" GdbTraceEvent when a problem occurs during the reading.
     *
     * @param rank
     *            long containing the number of the frame where the problem
     *            occurred
     * @param message
     *            String containing a brief explanation of problem.
     * @return a GdbTraceEvent object, filled as best as possible
     */
    private GdbTraceEvent createExceptionEvent(final long rank, final String message) {
        // get corresponding TP data
        String tmfEventRef;
        String tmfEventSrc;
        MIBreakpointDMData bp = fTpInfo.get((int) rank);
        if (bp != null) {
            tmfEventRef = bp.getFileName() + ":" + bp.getLineNumber() + " :: " + bp.getFunctionName(); //$NON-NLS-1$ //$NON-NLS-2$
            tmfEventSrc = bp.getFileName() + " :: " + bp.getFunctionName() + ", line: " + bp.getLineNumber(); //$NON-NLS-1$ //$NON-NLS-2$
        } else {
            tmfEventRef = tracedExecutable;
            tmfEventSrc = "Tracepoint: n/a"; //$NON-NLS-1$
        }

        GdbTraceEventContent evContent = new GdbTraceEventContent("ERROR: " + message, 0, 0); //$NON-NLS-1$

        GdbTraceEvent ev = new GdbTraceEvent(fGdbTrace,
                TmfTimestamp.fromSeconds(rank),
                tmfEventSrc,
                tmfEventType,
                evContent,
                tmfEventRef);

        return ev;
    }

    /**
     * @return DSF-GDB session id of the current session.
     */
    public String getSessionId() {
        return fSessionId;
    }

    /**
     * Handler method that catches the DSF "record selected changed" event.
     * It in turn creates a TMF "time sync" signal.
     * @param event TraceRecordSelectedChangedEvent:  The DSF event.
     */
    @DsfServiceEventHandler
    public void handleDSFRecordSelectedEvents(final ITraceRecordSelectedChangedDMEvent event) {
        if (event instanceof TraceRecordSelectedChangedEvent) {
            TraceRecordSelectedChangedEvent traceEvent = (TraceRecordSelectedChangedEvent) event;
            ITraceRecordDMContext context = traceEvent.getDMContext();
            final String reference = context.getRecordId();
            if (reference != null) {
                int recordId = Integer.parseInt(reference);
                selectGdbTraceEditor(context.getSessionId(), recordId);
            }
        }
    }

    private static void closeGdbTraceEditor(final String sessionId) {
        Display.getDefault().asyncExec(() -> {
            for (IWorkbenchWindow wbWindow : PlatformUI.getWorkbench().getWorkbenchWindows()) {
                for (IWorkbenchPage wbPage : wbWindow.getPages()) {
                    for (IEditorReference editorReference : wbPage.getEditorReferences()) {
                        IEditorPart editor = editorReference.getEditor(false);
                        if (editor instanceof ITmfTraceEditor) {
                            ITmfTrace trace = ((ITmfTraceEditor) editor).getTrace();
                            if (trace instanceof GdbTrace) {
                                if (((GdbTrace) trace).getDsfSessionId().equals(sessionId)) {
                                    wbPage.closeEditor(editor, false);
                                }
                            }
                        }
                    }
                }
            }
        });
    }

    private static void selectGdbTraceEditor(final String sessionId, final int recordId) {
        Display.getDefault().asyncExec(() -> {
            for (IWorkbenchWindow wbWindow : PlatformUI.getWorkbench().getWorkbenchWindows()) {
                for (IWorkbenchPage wbPage : wbWindow.getPages()) {
                    for (IEditorReference editorReference : wbPage.getEditorReferences()) {
                        IEditorPart editor = editorReference.getEditor(false);
                        if (editor instanceof ITmfTraceEditor) {
                            ITmfTrace trace = ((ITmfTraceEditor) editor).getTrace();
                            if (trace instanceof GdbTrace) {
                                if (((GdbTrace) trace).getDsfSessionId().equals(sessionId)) {
                                    wbPage.bringToTop(editor);
                                    if (recordId != -1) {
                                        gotoRank(editor, recordId);
                                    }
                                    return;
                                }
                            } else if (trace instanceof TmfExperiment) {
                                TmfExperiment experiment = (TmfExperiment) trace;
                                List<ITmfTrace> expTraces = experiment.getTraces();
                                int nbTraces = expTraces.size();
                                for (int i = 0; i < nbTraces; i++) {
                                    GdbTrace gdbTrace = (GdbTrace) expTraces.get(i);
                                    if (gdbTrace.getDsfSessionId().equals(sessionId)) {
                                        wbPage.bringToTop(editor);
                                        if (recordId != -1) {
                                            int rank = recordId * nbTraces + i;
                                            gotoRank(editor, rank);
                                        }
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        });
    }

    private static void gotoRank(IEditorPart editor, int rank) {
        IEditorInput editorInput = editor.getEditorInput();
        if (editorInput instanceof IFileEditorInput) {
            IFile file = ((IFileEditorInput) editorInput).getFile();
            try {
                final IMarker marker = file.createMarker(IMarker.MARKER);
                marker.setAttribute(IMarker.LOCATION, (Integer) rank);
                IDE.gotoMarker(editor, marker);
                marker.delete();
            } catch (CoreException e) {
                GdbTraceCorePlugin.logError(e.getMessage(), e);
            }
        }
    }
}
