/**********************************************************************
 * Copyright (c) 2011, 2015 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:
 *   Bernd Hufmann - Initial API and implementation
 **********************************************************************/

package org.eclipse.tracecompass.tmf.ui.views.uml2sd.loader;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;

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.viewers.ISelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.core.component.TmfComponent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest;
import org.eclipse.tracecompass.tmf.core.request.TmfEventRequest;
import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceClosedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceSelectedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfWindowRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.core.uml2sd.ITmfSyncSequenceDiagramEvent;
import org.eclipse.tracecompass.tmf.core.uml2sd.TmfSyncSequenceDiagramEvent;
import org.eclipse.tracecompass.tmf.ui.editors.ITmfTraceEditor;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.SDView;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Frame;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.GraphNode;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Lifeline;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.dialogs.Criteria;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.dialogs.FilterCriteria;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.dialogs.FilterListDialog;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.handlers.provider.ISDAdvancedPagingProvider;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.handlers.provider.ISDFilterProvider;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.handlers.provider.ISDFindProvider;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.handlers.provider.ISDGraphNodeSupporter;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.load.IUml2SDLoader;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.progress.IProgressConstants;

/**
 * <p>
 * This class is a reference implementation of the
 * <code>org.eclipse.tracecompass.tmf.ui.Uml2SDLoader</code> extension point. It
 * provides a Sequence Diagram loader for a user space trace with specific trace
 * content for sending and receiving signals between components. I also includes
 * a default implementation for the <code>ITmfEvent</code> parsing.
 * </p>
 *
 * The class <code>TmfUml2SDSyncLoader</code> analyzes events from type
 * <code>ITmfEvent</code> and creates events type
 * <code>ITmfSyncSequenceDiagramEvent</code> if the <code>ITmfEvent</code>
 * contains all relevant information. The analysis checks that the event type
 * strings contains either string SEND or RECEIVE. If event type matches these
 * key words, the analyzer will look for strings sender, receiver and signal in
 * the event fields of type <code>ITmfEventField</code>. If all the data is
 * found a sequence diagram event from can be created. Note that Sync Messages
 * are assumed, which means start and end time are the same. <br>
 * <br>
 * The parsing of the <code>ITmfEvent</code> is done in the method
 * <code>getSequnceDiagramEvent()</code> of class
 * <code>TmfUml2SDSyncLoader</code>. By extending the class
 * <code>TmfUml2SDSyncLoader</code> and overwriting
 * <code>getSequnceDiagramEvent()</code> a customized parsing algorithm can be
 * implemented.<br>
 * <br>
 * Note that combined traces of multiple components, that contain the trace
 * information about the same interactions are not supported in the class
 * <code>TmfUml2SDSyncLoader</code>.
 *
 * @version 1.0
 * @author Bernd Hufmann
 */
public class TmfUml2SDSyncLoader extends TmfComponent implements IUml2SDLoader, ISDFindProvider, ISDFilterProvider, ISDAdvancedPagingProvider, ISelectionListener {

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------

    /**
     * Default title name.
     */
    protected static final String TITLE = Messages.TmfUml2SDSyncLoader_ViewName;

    /**
     * Maximum number of messages per page.
     */
    protected static final int MAX_NUM_OF_MSG = 10000;

    private static final int INDEXING_THREAD_SLEEP_VALUE = 100;

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    // Experiment attributes
    /**
     * The TMF trace reference.
     */
    protected ITmfTrace fTrace = null;
    /**
     * The current indexing event request.
     */
    protected ITmfEventRequest fIndexRequest = null;
    /**
     * The current request to fill a page.
     */
    protected ITmfEventRequest fPageRequest = null;
    /**
     * Flag whether the time range signal was sent by this loader class or not
     */
    protected volatile boolean fIsSignalSent = false;

    // The view and event attributes
    /**
     * The sequence diagram view reference.
     */
    protected SDView fView = null;
    /**
     * The current sequence diagram frame reference.
     */
    protected Frame fFrame = null;
    /**
     * The list of sequence diagram events of current page.
     */
    protected List<ITmfSyncSequenceDiagramEvent> fEvents = new ArrayList<>();

    // Checkpoint and page attributes
    /**
     * The checkpoints of the whole sequence diagram trace (i.e. start time
     * stamp of each page)
     */
    protected List<TmfTimeRange> fCheckPoints = new ArrayList<>(MAX_NUM_OF_MSG);
    /**
     * The current page displayed.
     */
    protected volatile int fCurrentPage = 0;
    /**
     * The current time selected.
     */
    protected ITmfTimestamp fCurrentTime = null;
    /**
     * Flag to specify that selection of message is done by selection or by
     * signal.
     */
    protected volatile boolean fIsSelect = false;

    // Search attributes
    /**
     * The job for searching across pages.
     */
    protected SearchJob fFindJob = null;
    /**
     * List of found nodes within a page.
     */
    protected List<GraphNode> fFindResults = new ArrayList<>();
    /**
     * The current find criteria reference
     */
    protected Criteria fFindCriteria = null;
    /**
     * The current find index within the list of found nodes
     * (<code>fFindeResults</code> within a page.
     */
    protected volatile int fCurrentFindIndex = 0;

    // Filter attributes
    /**
     * The list of active filters.
     */
    protected List<FilterCriteria> fFilterCriteria = null;

    // Thread synchronization
    /**
     * The synchronization lock.
     */
    protected ReentrantLock fLock = new ReentrantLock();

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------
    /**
     * Default constructor
     */
    public TmfUml2SDSyncLoader() {
        super(TITLE);
    }

    /**
     * Constructor
     *
     * @param name
     *            Name of loader
     */
    public TmfUml2SDSyncLoader(String name) {
        super(name);
    }

    // ------------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------------

    /**
     * Returns the current time if available else null.
     *
     * @return the current time if available else null
     */
    public ITmfTimestamp getCurrentTime() {
        fLock.lock();
        try {
            if (fCurrentTime != null) {
                return fCurrentTime;
            }
            return null;
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Waits for the page request to be completed
     */
    public void waitForCompletion() {
        fLock.lock();
        ITmfEventRequest request = fPageRequest;
        fLock.unlock();
        if (request != null) {
            try {
                request.waitForCompletion();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Handler for the trace opened signal.
     *
     * @param signal
     *            The trace opened signal
     */
    @TmfSignalHandler
    public void traceOpened(TmfTraceOpenedSignal signal) {
        fTrace = signal.getTrace();
        loadTrace();
    }

    /**
     * Signal handler for the trace selected signal.
     *
     * Spawns a request to index the trace (checkpoints creation) as well as it
     * fills the first page.
     *
     * @param signal
     *            The trace selected signal
     */
    @TmfSignalHandler
    public void traceSelected(TmfTraceSelectedSignal signal) {
        // Update the trace reference
        ITmfTrace trace = signal.getTrace();
        if (!trace.equals(fTrace)) {
            fTrace = trace;
        }
        loadTrace();
    }

    /**
     * Method for loading the current selected trace into the view. Sub-class
     * need to override this method to add the view specific implementation.
     */
    protected void loadTrace() {
        ITmfEventRequest indexRequest = null;
        fLock.lock();

        try {
            final Job job = new IndexingJob("Indexing " + getName() + "..."); //$NON-NLS-1$ //$NON-NLS-2$
            job.setUser(false);
            job.schedule();

            indexRequest = fIndexRequest;

            cancelOngoingRequests();

            TmfTimeRange window = TmfTimeRange.ETERNITY;

            fIndexRequest = new TmfEventRequest(ITmfEvent.class, window, 0,
                    ITmfEventRequest.ALL_DATA, ITmfEventRequest.ExecutionType.BACKGROUND) {

                private ITmfTimestamp fFirstTime = null;
                private ITmfTimestamp fLastTime = null;
                private int fNbSeqEvents = 0;
                private final List<ITmfSyncSequenceDiagramEvent> fSdEvents = new ArrayList<>(MAX_NUM_OF_MSG);

                @Override
                public void handleData(ITmfEvent event) {
                    super.handleData(event);

                    ITmfSyncSequenceDiagramEvent sdEvent = getSequenceDiagramEvent(event);
                    ITmfTimestamp firstTime = fFirstTime;
                    ITmfTimestamp lastTime = fLastTime;

                    if (sdEvent != null) {
                        ++fNbSeqEvents;

                        if (firstTime == null) {
                            firstTime = event.getTimestamp();
                            fFirstTime = firstTime;
                        }

                        lastTime = event.getTimestamp();
                        fLastTime = lastTime;

                        if ((fNbSeqEvents % MAX_NUM_OF_MSG) == 0) {
                            fLock.lock();
                            try {
                                fCheckPoints.add(new TmfTimeRange(firstTime, lastTime));
                                if (fView != null) {
                                    fView.updateCoolBar();
                                }
                            } finally {
                                fLock.unlock();
                            }
                            fFirstTime = null;

                        }

                        if (fNbSeqEvents > MAX_NUM_OF_MSG) {
                            // page is full
                            return;
                        }

                        fSdEvents.add(sdEvent);

                        if (fNbSeqEvents == MAX_NUM_OF_MSG) {
                            fillCurrentPage(fSdEvents);
                        }
                    }
                }

                @Override
                public void handleSuccess() {
                    final ITmfTimestamp firstTime = fFirstTime;
                    final ITmfTimestamp lastTime = fLastTime;
                    if ((firstTime != null) && (lastTime != null)) {

                        fLock.lock();
                        try {
                            fCheckPoints.add(new TmfTimeRange(firstTime, lastTime));
                            if (fView != null) {
                                fView.updateCoolBar();
                            }
                        } finally {
                            fLock.unlock();
                        }
                    }

                    if (fNbSeqEvents <= MAX_NUM_OF_MSG) {
                        fillCurrentPage(fSdEvents);
                    }

                    super.handleSuccess();
                }

                @Override
                public void handleCompleted() {
                    if (fEvents.isEmpty()) {
                        fFrame = new Frame();
                        // make sure that view is not null when setting frame
                        SDView sdView;
                        fLock.lock();
                        try {
                            sdView = fView;
                        } finally {
                            fLock.unlock();
                        }
                        if (sdView != null) {
                            sdView.setFrameSync(fFrame);
                        }
                    }
                    super.handleCompleted();
                    job.cancel();
                }
            };

        } finally {
            fLock.unlock();
        }
        if (indexRequest != null && !indexRequest.isCompleted()) {
            indexRequest.cancel();
        }
        resetLoader();
        fTrace.sendRequest(fIndexRequest);
    }

    /**
     * Signal handler for the trace closed signal.
     *
     * @param signal
     *            The trace closed signal
     */
    @TmfSignalHandler
    public void traceClosed(TmfTraceClosedSignal signal) {
        if (signal.getTrace() != fTrace) {
            return;
        }
        ITmfEventRequest indexRequest = null;
        fLock.lock();
        try {
            indexRequest = fIndexRequest;
            fIndexRequest = null;

            cancelOngoingRequests();

            if (fFilterCriteria != null) {
                fFilterCriteria.clear();
            }

            FilterListDialog.deactivateSavedGlobalFilters();
        } finally {
            fTrace = null;
            fLock.unlock();
        }
        if (indexRequest != null && !indexRequest.isCompleted()) {
            indexRequest.cancel();
        }

        resetLoader();
    }

    /**
     * Moves to the page that contains the time provided by the signal. The
     * messages will be selected if the provided time is the time of a message.
     *
     * @param signal
     *            The selection range signal
     * @since 1.0
     */
    @TmfSignalHandler
    public void selectionRangeUpdated(TmfSelectionRangeUpdatedSignal signal) {
        fLock.lock();
        try {
            if ((signal.getSource() != this) && (fFrame != null) && (!fCheckPoints.isEmpty())) {
                fCurrentTime = signal.getBeginTime();
                fIsSelect = true;
                moveToMessage();
            }
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Moves to the page that contains the current time provided by signal. No
     * message will be selected however the focus will be set to the message if
     * the provided time is the time of a message.
     *
     * @param signal
     *            The window range signal
     * @since 1.0
     */
    @TmfSignalHandler
    public void windowRangeUpdated(TmfWindowRangeUpdatedSignal signal) {
        fLock.lock();
        try {
            if ((signal.getSource() != this) && (fFrame != null) && !fIsSignalSent && (!fCheckPoints.isEmpty())) {
                TmfTimeRange newTimeRange = signal.getCurrentRange();

                fIsSelect = false;
                fCurrentTime = newTimeRange.getStartTime();

                moveToMessage();
            }
        } finally {
            fLock.unlock();
        }

    }

    @Override
    public void setViewer(SDView viewer) {

        fLock.lock();
        try {
            fView = viewer;
            PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService().addPostSelectionListener(this);
            fView.setSDFindProvider(this);
            fView.setSDPagingProvider(this);
            fView.setSDFilterProvider(this);

            resetLoader();
            IEditorPart editor = fView.getSite().getPage().getActiveEditor();
            if (editor instanceof ITmfTraceEditor) {
                ITmfTrace trace = ((ITmfTraceEditor) editor).getTrace();
                if (trace != null) {
                    traceSelected(new TmfTraceSelectedSignal(this, trace));
                }
            }
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public String getTitleString() {
        return getName();
    }

    @Override
    public void dispose() {
        super.dispose();
        ITmfEventRequest indexRequest = null;
        fLock.lock();
        try {
            IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
            // During Eclipse shutdown the active workbench window is null
            if (window != null) {
                window.getSelectionService().removePostSelectionListener(this);
            }

            indexRequest = fIndexRequest;
            fIndexRequest = null;
            cancelOngoingRequests();

            fView.setSDFindProvider(null);
            fView.setSDPagingProvider(null);
            fView.setSDFilterProvider(null);
            fView = null;
        } finally {
            fLock.unlock();
        }
        if (indexRequest != null && !indexRequest.isCompleted()) {
            indexRequest.cancel();
        }
    }

    @Override
    public boolean isNodeSupported(int nodeType) {
        switch (nodeType) {
        case ISDGraphNodeSupporter.LIFELINE:
        case ISDGraphNodeSupporter.SYNCMESSAGE:
            return true;

        default:
            break;
        }
        return false;
    }

    @Override
    public String getNodeName(int nodeType, String loaderClassName) {
        switch (nodeType) {
        case ISDGraphNodeSupporter.LIFELINE:
            return Messages.TmfUml2SDSyncLoader_CategoryLifeline;
        case ISDGraphNodeSupporter.SYNCMESSAGE:
            return Messages.TmfUml2SDSyncLoader_CategoryMessage;
        default:
            break;
        }
        return ""; //$NON-NLS-1$
    }

    @Override
    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
        ISelection sel = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService().getSelection();
        if ((sel != null) && (sel instanceof StructuredSelection)) {
            StructuredSelection stSel = (StructuredSelection) sel;
            if (stSel.getFirstElement() instanceof TmfSyncMessage) {
                TmfSyncMessage syncMsg = ((TmfSyncMessage) stSel.getFirstElement());
                ITmfTimestamp startTime = syncMsg.getStartTime();
                if (startTime == null) {
                    startTime = TmfTimestamp.BIG_BANG;
                }
                broadcast(new TmfSelectionRangeUpdatedSignal(this, startTime, startTime, fTrace));
            }
        }
    }

    @Override
    public boolean find(Criteria toSearch) {
        fLock.lock();
        try {
            if (fFrame == null) {
                return false;
            }

            if ((fFindResults == null) || (fFindCriteria == null) || !fFindCriteria.compareTo(toSearch)) {
                fFindResults = new CopyOnWriteArrayList<>();
                fFindCriteria = toSearch;
                if (fFindCriteria.isLifeLineSelected()) {
                    for (int i = 0; i < fFrame.lifeLinesCount(); i++) {
                        if (fFindCriteria.matches(fFrame.getLifeline(i).getName())) {
                            fFindResults.add(fFrame.getLifeline(i));
                        }
                    }
                }

                ArrayList<GraphNode> msgs = new ArrayList<>();
                if (fFindCriteria.isSyncMessageSelected()) {
                    for (int i = 0; i < fFrame.syncMessageCount(); i++) {
                        if (fFindCriteria.matches(fFrame.getSyncMessage(i).getName())) {
                            msgs.add(fFrame.getSyncMessage(i));
                        }
                    }
                }

                if (!msgs.isEmpty()) {
                    fFindResults.addAll(msgs);
                }

                List<GraphNode> selection = fView.getSDWidget().getSelection();
                if ((selection != null) && (selection.size() == 1)) {
                    fCurrentFindIndex = fFindResults.indexOf(selection.get(0)) + 1;
                } else {
                    fCurrentFindIndex = 0;
                }
            } else {
                // ++ is not atomic, but we are in a lock
                fCurrentFindIndex = fCurrentFindIndex + 1;
            }

            if (fFindResults.size() > fCurrentFindIndex) {
                GraphNode current = fFindResults.get(fCurrentFindIndex);
                fView.getSDWidget().moveTo(current);
                return true;
            }
            fFindResults = null;
            fCurrentFindIndex = 0;
            return findInNextPages(fFindCriteria); // search in other page
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void cancel() {
        cancelOngoingRequests();
    }

    @Override
    public boolean filter(List<FilterCriteria> filters) {
        fLock.lock();
        try {
            cancelOngoingRequests();

            if (filters == null) {
                fFilterCriteria = new ArrayList<>();
            } else {
                List<FilterCriteria> list = filters;
                fFilterCriteria = new ArrayList<>(list);
            }

            fillCurrentPage(fEvents);

        } finally {
            fLock.unlock();
        }
        return true;
    }

    @Override
    public boolean hasNextPage() {
        fLock.lock();
        try {
            int size = fCheckPoints.size();
            if (size > 0) {
                return fCurrentPage < (size - 1);
            }
        } finally {
            fLock.unlock();
        }
        return false;
    }

    @Override
    public boolean hasPrevPage() {
        fLock.lock();
        try {
            return fCurrentPage > 0;
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void nextPage() {
        fLock.lock();
        try {
            // Safety check
            if (fCurrentPage >= (fCheckPoints.size() - 1)) {
                return;
            }

            cancelOngoingRequests();
            fCurrentTime = null;
            // ++ is not atomic but we are in a lock
            fCurrentPage = fCurrentPage + 1;
            moveToPage();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void prevPage() {
        fLock.lock();
        try {
            // Safety check
            if (fCurrentPage <= 0) {
                return;
            }

            cancelOngoingRequests();
            fCurrentTime = null;
            // -- is not atomic but we are in a lock
            fCurrentPage = fCurrentPage - 1;
            moveToPage();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void firstPage() {
        fLock.lock();
        try {

            cancelOngoingRequests();
            fCurrentTime = null;
            fCurrentPage = 0;
            moveToPage();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void lastPage() {
        fLock.lock();
        try {
            cancelOngoingRequests();
            fCurrentTime = null;
            fCurrentPage = fCheckPoints.size() - 1;
            moveToPage();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public int currentPage() {
        fLock.lock();
        try {
            return fCurrentPage;
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public int pagesCount() {
        fLock.lock();
        try {
            return fCheckPoints.size();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void pageNumberChanged(int pagenNumber) {
        int localPageNumber = pagenNumber;

        fLock.lock();
        try {
            cancelOngoingRequests();

            if (localPageNumber < 0) {
                localPageNumber = 0;
            }
            int size = fCheckPoints.size();
            if (localPageNumber > (size - 1)) {
                localPageNumber = size - 1;
            }
            fCurrentPage = localPageNumber;
            moveToPage();
        } finally {
            fLock.unlock();
        }
    }

    @Override
    public void broadcast(TmfSignal signal) {
        fIsSignalSent = true;
        super.broadcast(signal);
        fIsSignalSent = false;
    }

    /**
     * Cancels any ongoing find operation
     */
    protected void cancelOngoingRequests() {
        fLock.lock();
        ITmfEventRequest pageRequest = null;
        try {
            // Cancel the search thread
            if (fFindJob != null) {
                fFindJob.cancel();
            }

            fFindResults = null;
            fFindCriteria = null;
            fCurrentFindIndex = 0;

            pageRequest = fPageRequest;
            fPageRequest = null;
        } finally {
            fLock.unlock();
        }
        if (pageRequest != null && !pageRequest.isCompleted()) {
            pageRequest.cancel();
        }
    }

    /**
     * Resets loader attributes
     */
    protected void resetLoader() {
        fLock.lock();
        try {
            fCurrentTime = null;
            fEvents.clear();
            fCheckPoints.clear();
            fCurrentPage = 0;
            fCurrentFindIndex = 0;
            fFindCriteria = null;
            fFindResults = null;
            fView.setFrameSync(new Frame());
            fFrame = null;
        } finally {
            fLock.unlock();
        }

    }

    /**
     * Fills current page with sequence diagram content.
     *
     * @param events
     *            sequence diagram events
     */
    protected void fillCurrentPage(List<ITmfSyncSequenceDiagramEvent> events) {

        fLock.lock();
        try {
            fEvents = new ArrayList<>(events);
            if (fView != null && !events.isEmpty()) {
                fView.toggleWaitCursorAsync(true);
            }
        } finally {
            fLock.unlock();
        }

        final Frame frame = new Frame();

        if (!events.isEmpty()) {
            Map<String, Lifeline> nodeToLifelineMap = new HashMap<>();

            frame.setName(Messages.TmfUml2SDSyncLoader_FrameName);

            for (int i = 0; i < events.size(); i++) {

                ITmfSyncSequenceDiagramEvent sdEvent = events.get(i);

                if ((nodeToLifelineMap.get(sdEvent.getSender()) == null) && (!filterLifeLine(sdEvent.getSender()))) {
                    Lifeline lifeline = new Lifeline();
                    lifeline.setName(sdEvent.getSender());
                    nodeToLifelineMap.put(sdEvent.getSender(), lifeline);
                    frame.addLifeLine(lifeline);
                }

                if ((nodeToLifelineMap.get(sdEvent.getReceiver()) == null) && (!filterLifeLine(sdEvent.getReceiver()))) {
                    Lifeline lifeline = new Lifeline();
                    lifeline.setName(sdEvent.getReceiver());
                    nodeToLifelineMap.put(sdEvent.getReceiver(), lifeline);
                    frame.addLifeLine(lifeline);
                }
            }

            int eventOccurence = 1;

            for (int i = 0; i < events.size(); i++) {
                ITmfSyncSequenceDiagramEvent sdEvent = events.get(i);

                // Check message filter
                if (filterMessage(sdEvent)) {
                    continue;
                }

                // Set the message sender and receiver
                Lifeline startLifeline = nodeToLifelineMap.get(sdEvent.getSender());
                Lifeline endLifeline = nodeToLifelineMap.get(sdEvent.getReceiver());

                // Check if any of the lifelines were filtered
                if ((startLifeline == null) || (endLifeline == null)) {
                    continue;
                }

                int tmp = Math.max(startLifeline.getEventOccurrence(), endLifeline.getEventOccurrence());
                eventOccurence = Math.max(eventOccurence, tmp);

                startLifeline.setCurrentEventOccurrence(eventOccurence);
                endLifeline.setCurrentEventOccurrence(eventOccurence);

                TmfSyncMessage message = new TmfSyncMessage(sdEvent, eventOccurence++);

                message.setStartLifeline(startLifeline);
                message.setEndLifeline(endLifeline);

                message.setTime(sdEvent.getStartTime());

                // add the message to the frame
                frame.addMessage(message);

            }
            fLock.lock();
            try {
                if (!fView.getSDWidget().isDisposed()) {
                    fView.getSDWidget().getDisplay().asyncExec(() -> {

                        fLock.lock();
                        try {
                            // check if view was disposed in the meanwhile
                            if ((fView != null) && (!fView.getSDWidget().isDisposed())) {
                                fFrame = frame;
                                fView.setFrame(fFrame);

                                if (fCurrentTime != null) {
                                    moveToMessageInPage();
                                }

                                if (fFindCriteria != null) {
                                    find(fFindCriteria);
                                }

                                fView.toggleWaitCursorAsync(false);
                            }
                        } finally {
                            fLock.unlock();
                        }

                    });
                }
            } finally {
                fLock.unlock();
            }
        }
    }

    /**
     * Moves to a certain message defined by timestamp (across pages)
     */
    protected void moveToMessage() {
        int page = 0;

        fLock.lock();
        try {
            page = getPage(fCurrentTime);

            if (page == fCurrentPage) {
                moveToMessageInPage();
                return;
            }
            fCurrentPage = page;
            moveToPage(false);
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Moves to a certain message defined by timestamp in current page
     */
    protected void moveToMessageInPage() {
        fLock.lock();
        try {
            if (!fView.getSDWidget().isDisposed()) {
                // Check for GUI thread
                if (Display.getCurrent() != null) {
                    // Already in GUI thread - execute directly
                    TmfSyncMessage prevMessage = null;
                    TmfSyncMessage syncMessage = null;
                    boolean isExactTime = false;
                    for (int i = 0; i < fFrame.syncMessageCount(); i++) {
                        if (fFrame.getSyncMessage(i) instanceof TmfSyncMessage) {
                            syncMessage = (TmfSyncMessage) fFrame.getSyncMessage(i);
                            if (syncMessage.getStartTime().compareTo(fCurrentTime) == 0) {
                                isExactTime = true;
                                break;
                            } else if ((syncMessage.getStartTime().compareTo(fCurrentTime) > 0) && (prevMessage != null)) {
                                syncMessage = prevMessage;
                                break;
                            }
                            prevMessage = syncMessage;
                        }
                    }
                    if (fIsSelect && isExactTime) {
                        fView.getSDWidget().moveTo(syncMessage);
                    } else {
                        fView.getSDWidget().ensureVisible(syncMessage);
                        fView.getSDWidget().clearSelection();
                        fView.getSDWidget().redraw();
                    }
                } else {
                    // Not in GUI thread - queue action in GUI thread.
                    fView.getSDWidget().getDisplay().asyncExec(this::moveToMessageInPage);
                }
            }
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Moves to a certain message defined by timestamp (across pages)
     */
    protected void moveToPage() {
        moveToPage(true);
    }

    /**
     * Moves to a certain page.
     *
     * @param notifyAll
     *            true to broadcast time range signal to other signal handlers
     *            else false
     */
    protected void moveToPage(boolean notifyAll) {

        TmfTimeRange window = null;

        fLock.lock();
        try {
            // Safety check
            if (fCurrentPage > fCheckPoints.size()) {
                return;
            }
            window = fCheckPoints.get(fCurrentPage);
        } finally {
            fLock.unlock();
        }

        if (window == null) {
            window = TmfTimeRange.ETERNITY;
        }

        fPageRequest = new TmfEventRequest(ITmfEvent.class, window, 0,
                ITmfEventRequest.ALL_DATA, ITmfEventRequest.ExecutionType.FOREGROUND) {
            private final List<ITmfSyncSequenceDiagramEvent> fSdEvent = new ArrayList<>();

            @Override
            public void handleData(ITmfEvent event) {
                super.handleData(event);

                ITmfSyncSequenceDiagramEvent sdEvent = getSequenceDiagramEvent(event);

                if (sdEvent != null) {
                    fSdEvent.add(sdEvent);
                }
            }

            @Override
            public void handleSuccess() {
                fillCurrentPage(fSdEvent);
                super.handleSuccess();
            }

        };

        fTrace.sendRequest(fPageRequest);

        if (notifyAll) {
            TmfTimeRange timeRange = getSignalTimeRange(window.getStartTime());
            broadcast(new TmfWindowRangeUpdatedSignal(this, timeRange, fTrace));
        }
    }

    /**
     * Gets page that contains timestamp
     *
     * @param time
     *            The timestamp
     * @return page that contains the time
     */
    protected int getPage(ITmfTimestamp time) {
        int page;
        int size;
        fLock.lock();
        try {
            size = fCheckPoints.size();
            for (page = 0; page < size; page++) {
                TmfTimeRange timeRange = fCheckPoints.get(page);
                if (timeRange.getEndTime().compareTo(time) >= 0) {
                    break;
                }
            }
            if (page >= size) {
                page = size - 1;
            }
            return page;
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Background search in trace for expression in criteria.
     *
     * @param findCriteria
     *            The find criteria
     * @return true if background request was started else false
     */
    protected boolean findInNextPages(Criteria findCriteria) {
        fLock.lock();
        try {
            if (fFindJob != null) {
                return true;
            }

            int nextPage = fCurrentPage + 1;

            if ((nextPage) >= fCheckPoints.size()) {
                // we are at the end
                return false;
            }

            TmfTimeRange window = new TmfTimeRange(fCheckPoints.get(nextPage).getStartTime(), fCheckPoints.get(fCheckPoints.size() - 1).getEndTime());
            fFindJob = new SearchJob(findCriteria, window);
            fFindJob.schedule();
            fView.toggleWaitCursorAsync(true);
        } finally {
            fLock.unlock();
        }
        return true;
    }

    /**
     * Gets time range for time range signal.
     *
     * @param startTime
     *            The start time of time range.
     * @return the time range
     */
    protected TmfTimeRange getSignalTimeRange(ITmfTimestamp startTime) {
        fLock.lock();
        try {
            TmfTimeRange currentRange = TmfTraceManager.getInstance().getCurrentTraceContext().getWindowRange();
            long offset = fTrace == null ? 0 : currentRange.getEndTime().getDelta(currentRange.getStartTime()).toNanos();
            ITmfTimestamp initialEndOfWindow = TmfTimestamp.create(startTime.getValue() + offset, startTime.getScale());
            return new TmfTimeRange(startTime, initialEndOfWindow);
        } finally {
            fLock.unlock();
        }
    }

    /**
     * Checks if filter criteria matches the message name in given SD event.
     *
     * @param sdEvent
     *            The SD event to check
     * @return true if match else false.
     */
    protected boolean filterMessage(ITmfSyncSequenceDiagramEvent sdEvent) {
        fLock.lock();
        try {
            if (fFilterCriteria != null) {
                for (FilterCriteria criteria : fFilterCriteria) {
                    if (criteria.isActive() && criteria.getCriteria().isSyncMessageSelected() && criteria.getCriteria().matches(sdEvent.getName())) {
                        return true;
                    }
                }
            }
        } finally {
            fLock.unlock();
        }
        return false;
    }

    /**
     * Checks if filter criteria matches a lifeline name (sender or receiver) in
     * given SD event.
     *
     * @param lifeline
     *            the message receiver
     * @return true if match else false.
     */
    protected boolean filterLifeLine(String lifeline) {
        fLock.lock();
        try {
            if (fFilterCriteria != null) {
                for (FilterCriteria criteria : fFilterCriteria) {
                    if (criteria.isActive() && criteria.getCriteria().isLifeLineSelected() && criteria.getCriteria().matches(lifeline)) {
                        return true;
                    }
                }
            }
        } finally {
            fLock.unlock();
        }
        return false;
    }

    /**
     * Job to search in trace for given time range.
     */
    protected class SearchJob extends Job {

        /**
         * The search event request.
         */
        protected final SearchEventRequest fSearchRequest;

        /**
         * Constructor
         *
         * @param findCriteria
         *            The search criteria
         * @param window
         *            Time range to search in
         */
        public SearchJob(Criteria findCriteria, TmfTimeRange window) {
            super(Messages.TmfUml2SDSyncLoader_SearchJobDescrition);
            fSearchRequest = new SearchEventRequest(window, ITmfEventRequest.ALL_DATA,
                    ITmfEventRequest.ExecutionType.FOREGROUND, findCriteria);
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            fSearchRequest.setMonitor(monitor);

            fTrace.sendRequest(fSearchRequest);

            try {
                fSearchRequest.waitForCompletion();
            } catch (InterruptedException e) {
                Activator.getDefault().logError("Search request interrupted!", e); //$NON-NLS-1$
                Thread.currentThread().interrupt();
            }

            IStatus status = Status.OK_STATUS;
            if (fSearchRequest.isFound() && (fSearchRequest.getFoundTime() != null)) {
                fCurrentTime = fSearchRequest.getFoundTime();

                /*
                 * Avoid double-selection. Selection will be done when calling
                 * find(criteria) after moving to relevant page
                 */
                fIsSelect = false;
                if (!fView.getSDWidget().isDisposed()) {
                    fView.getSDWidget().getDisplay().asyncExec(() -> moveToMessage());
                }
            } else {
                if (monitor.isCanceled()) {
                    status = Status.CANCEL_STATUS;
                } else {
                    // String was not found
                    status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, Messages.TmfUml2SDSyncLoader_SearchNotFound);
                }
                setProperty(IProgressConstants.KEEP_PROPERTY, Boolean.TRUE);
            }
            monitor.done();

            fLock.lock();
            try {
                fView.toggleWaitCursorAsync(false);
                fFindJob = null;
            } finally {
                fLock.unlock();
            }

            return status;
        }

        @Override
        protected void canceling() {
            fSearchRequest.cancel();
            fLock.lock();
            try {
                fFindJob = null;
            } finally {
                fLock.unlock();
            }
        }
    }

    /**
     * TMF event request for searching within trace.
     */
    protected class SearchEventRequest extends TmfEventRequest {

        /**
         * The find criteria.
         */
        private final Criteria fCriteria;
        /**
         * A progress monitor
         */
        private IProgressMonitor fMonitor;
        /**
         * Flag to indicate that node was found according the criteria .
         */
        private boolean fIsFound = false;
        /**
         * Time stamp of found item.
         */
        private ITmfTimestamp fFoundTime = null;

        /**
         * Constructor
         *
         * @param range
         *            see
         *            {@link TmfEventRequest#TmfEventRequest(Class, TmfTimeRange, long, int, org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType)
         *            TmfEventRequest}
         * @param nbRequested
         *            see
         *            {@link TmfEventRequest#TmfEventRequest(Class, TmfTimeRange, long, int, org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType)
         *            TmfEventRequest}
         * @param priority
         *            {@link org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType#FOREGROUND}
         *            or
         *            {@link org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType#BACKGROUND}
         * @param criteria
         *            The search criteria
         */
        public SearchEventRequest(TmfTimeRange range, int nbRequested, ExecutionType priority, Criteria criteria) {
            this(range, nbRequested, priority, criteria, null);
        }

        /**
         * Constructor
         *
         * @param range
         *            see
         *            {@link TmfEventRequest#TmfEventRequest(Class, TmfTimeRange, long, int, org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType)
         *            TmfEventRequest}
         * @param nbRequested
         *            see
         *            {@link TmfEventRequest#TmfEventRequest(Class, TmfTimeRange, long, int, org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType)
         *            TmfEventRequest}
         * @param priority
         *            {@link org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType#FOREGROUND}
         *            or
         *            {@link org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType#BACKGROUND}
         * @param criteria
         *            The search criteria
         * @param monitor
         *            progress monitor
         */
        public SearchEventRequest(TmfTimeRange range, int nbRequested, ExecutionType priority, Criteria criteria, IProgressMonitor monitor) {
            super(ITmfEvent.class, range, 0, nbRequested, priority);
            fCriteria = new Criteria(criteria);
            fMonitor = monitor;
        }

        @Override
        public void handleData(ITmfEvent event) {
            super.handleData(event);

            if ((fMonitor != null) && fMonitor.isCanceled()) {
                super.cancel();
                return;
            }

            ITmfSyncSequenceDiagramEvent sdEvent = getSequenceDiagramEvent(event);

            if (sdEvent != null) {

                if (fCriteria.isLifeLineSelected()) {
                    if (fCriteria.matches(sdEvent.getSender())) {
                        fFoundTime = event.getTimestamp();
                        fIsFound = true;
                        super.cancel();
                    }

                    if (fCriteria.matches(sdEvent.getReceiver())) {
                        fFoundTime = event.getTimestamp();
                        fIsFound = true;
                        super.cancel();
                    }
                }

                if (fCriteria.isSyncMessageSelected() && fCriteria.matches(sdEvent.getName())) {
                    fFoundTime = event.getTimestamp();
                    fIsFound = true;
                    super.cancel();
                }
            }
        }

        /**
         * Set progress monitor.
         *
         * @param monitor
         *            The monitor to assign
         */
        public void setMonitor(IProgressMonitor monitor) {
            fMonitor = monitor;
        }

        /**
         * Check if find criteria was met.
         *
         * @return true if find criteria was met.
         */
        public boolean isFound() {
            return fIsFound;
        }

        /**
         * Returns timestamp of found time.
         *
         * @return timestamp of found time.
         */
        public ITmfTimestamp getFoundTime() {
            return fFoundTime;
        }
    }

    /**
     * Job class to provide progress monitor feedback.
     *
     * @author Bernd Hufmann
     */
    protected static class IndexingJob extends Job {

        /**
         * @param name
         *            The job name
         */
        public IndexingJob(String name) {
            super(name);
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            while (!monitor.isCanceled()) {
                try {
                    Thread.sleep(INDEXING_THREAD_SLEEP_VALUE);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return Status.OK_STATUS;
                }
            }
            monitor.done();
            return Status.OK_STATUS;
        }
    }

    /**
     * Returns sequence diagram event if details in given event are available
     * else null.
     *
     * @param tmfEvent
     *            Event to parse for sequence diagram event details
     * @return sequence diagram event if details are available else null
     */
    protected ITmfSyncSequenceDiagramEvent getSequenceDiagramEvent(ITmfEvent tmfEvent) {
        // type = .*RECEIVE.* or .*SEND.*
        // content = sender:<sender name>:receiver:<receiver
        // name>,signal:<signal name>
        String eventName = tmfEvent.getName();
        if (eventName.contains(Messages.TmfUml2SDSyncLoader_EventTypeSend) || eventName.contains(Messages.TmfUml2SDSyncLoader_EventTypeReceive)) {
            Object sender = tmfEvent.getContent().getFieldValue(Object.class, NonNullUtils.checkNotNull(Messages.TmfUml2SDSyncLoader_FieldSender));
            Object receiver = tmfEvent.getContent().getFieldValue(Object.class, NonNullUtils.checkNotNull(Messages.TmfUml2SDSyncLoader_FieldReceiver));
            ITmfEventField content = tmfEvent.getContent();
            Object name = content.getFieldValue(Object.class, NonNullUtils.checkNotNull(Messages.TmfUml2SDSyncLoader_FieldSignal));
            if ((sender != null) && (receiver != null) && (name != null)) {
                ITmfSyncSequenceDiagramEvent sdEvent = new TmfSyncSequenceDiagramEvent(tmfEvent,
                        sender.toString(),
                        receiver.toString(),
                        name.toString());

                return sdEvent;
            }
        }
        return null;
    }
}
