/*******************************************************************************
 * Copyright (c) 2015, 2018 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:
 *   Patrick Tasse - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.ui.views.timegraph;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.internal.provisional.tmf.ui.widgets.ViewFilterDialog;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.tmf.core.model.timegraph.IFilterProperty;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphPresentationProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ILinkEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.IMarkerEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry.Sampling;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Table;

/**
 * An abstract time graph view where each entry's time event list is populated
 * from a state system. The state system full state is queried in chronological
 * order before creating the time event lists as this is optimal for state
 * system queries.
 *
 * @since 1.1
 */
public abstract class AbstractStateSystemTimeGraphView extends AbstractTimeGraphView {

    private static final @NonNull Comparator<ITmfStateInterval> INTERVALS_COMPARATOR = Comparator.comparing(ITmfStateInterval::getStartTime).thenComparing(ITmfStateInterval::getEndTime);

    private static final @NonNull Comparator<ITimeEvent> TIME_EVENT_COMPARATOR = Comparator.comparing(ITimeEvent::getTime).thenComparing(ITimeEvent::getDuration);

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

    private static final long MAX_INTERVALS = 1000000;

    // ------------------------------------------------------------------------
    // Fields
    // ------------------------------------------------------------------------

    /** The state system to entry list hash map */
    private final Map<ITmfStateSystem, List<@NonNull TimeGraphEntry>> fSSEntryListMap = new HashMap<>();

    /** The trace to state system multi map */
    private final Multimap<ITmfTrace, ITmfStateSystem> fTraceSSMap = HashMultimap.create();

    private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(AbstractStateSystemTimeGraphView.class);

    private static final int DEFAULT_BUFFER_SIZE = 3;

    // ------------------------------------------------------------------------
    // Classes
    // ------------------------------------------------------------------------

    /**
     * Handler for state system queries
     */
    public interface IQueryHandler {
        /**
         * Handle a full or partial list of full states. This can be called many
         * times for the same query if the query result is split, in which case
         * the previous full state is null only the first time it is called, and
         * set to the last full state of the previous call from then on.
         *
         * @param fullStates
         *            the list of full states
         * @param prevFullState
         *            the previous full state, or null
         */
        void handle(@NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState);
    }

    private class ZoomThreadByTime extends ZoomThread {
        private static final int BG_SEARCH_RESOLUTION = 1;
        private final @NonNull Collection<@NonNull TimeGraphEntry> fVisibleEntries;
        private final @NonNull List<ITmfStateSystem> fZoomSSList;
        private boolean fClearZoomedLists;

        public ZoomThreadByTime(@NonNull Collection<@NonNull TimeGraphEntry> entries, @NonNull List<ITmfStateSystem> ssList, long startTime, long endTime, long resolution, boolean restart) {
            super(startTime, endTime, resolution);
            fZoomSSList = ssList;
            fClearZoomedLists = !restart;
            fVisibleEntries = entries;
        }

        @Override
        public void doRun() {
            final List<ILinkEvent> links = new ArrayList<>();
            final List<IMarkerEvent> markers = new ArrayList<>();
            if (fClearZoomedLists) {
                clearZoomedLists();
            }
            for (ITmfStateSystem ss : fZoomSSList) {
                List<TimeGraphEntry> entryList = null;
                synchronized (fSSEntryListMap) {
                    entryList = fSSEntryListMap.get(ss);
                }
                if (entryList != null) {
                    zoomByTime(ss, entryList, links, markers, getZoomStartTime(), getZoomEndTime(), getResolution(), getMonitor());
                }
            }
            if (!getMonitor().isCanceled()) {
                /* Refresh the trace-specific markers when zooming */
                markers.addAll(getTraceMarkerList(getZoomStartTime(), getZoomEndTime(), getResolution(), getMonitor()));
                applyResults(() -> {
                    getTimeGraphViewer().setLinks(links);
                    getTimeGraphViewer().setMarkerCategories(getMarkerCategories());
                    getTimeGraphViewer().setMarkers(markers);
                });
            } else {
                TraceCompassLogUtils.traceInstant(LOGGER, Level.FINE, "TimeGraphView:ZoomThreadCanceled"); //$NON-NLS-1$
            }
        }

        @Override
        public void cancel() {
            super.cancel();
            if (fClearZoomedLists) {
                clearZoomedLists();
            }
        }

        private void zoomByTime(final ITmfStateSystem ss, final List<TimeGraphEntry> entryList, final List<ILinkEvent> links, final List<IMarkerEvent> markers,
                long startTime, long endTime, long resolution, final @NonNull IProgressMonitor monitor) {
            final long start = Math.max(startTime, ss.getStartTime());
            final long end = Math.min(endTime, ss.getCurrentEndTime());
            final boolean fullRange = getZoomStartTime() <= getStartTime() && getZoomEndTime() >= getEndTime();
            if (end < start) {
                return;
            }

            ViewFilterDialog timeEventFilterDialog = getViewFilterDialog();
            boolean isFilterActive = timeEventFilterDialog != null && timeEventFilterDialog.isFilterActive();
            getTimeGraphViewer().setTimeEventFilterApplied(isFilterActive);

            boolean hasSavedFilter = timeEventFilterDialog != null && timeEventFilterDialog.hasActiveSavedFilters();
            getTimeGraphViewer().setSavedFilterStatus(hasSavedFilter);

            if (fullRange) {
                redraw();
            }
            @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull Object>>> predicates = generateRegexPredicate();
            Sampling sampling = new Sampling(getZoomStartTime(), getZoomEndTime(), predicates, getResolution());
            Iterable<@NonNull TimeGraphEntry> incorrectSample = Iterables.filter(fVisibleEntries, entry -> !sampling.equals(entry.getSampling()));
            /* Only keep entries that are a member or child of the ss entry list */
            Iterable<@NonNull TimeGraphEntry> entries = Iterables.filter(incorrectSample, entry -> isMember(entry, entryList));
            // set gaps to null when there is no active filter
            Map<TimeGraphEntry, List<ITimeEvent>> gaps = isFilterActive ? new HashMap<>() : null;
            doZoom(ss, links, markers, resolution, monitor, start, end, sampling, entries, predicates, gaps);

            if (isFilterActive && gaps != null && !gaps.isEmpty()) {
                doBgSearch(ss, BG_SEARCH_RESOLUTION, monitor, gaps, predicates);
            }
        }

        private void doZoom(final ITmfStateSystem ss, final List<ILinkEvent> links, final List<IMarkerEvent> markers, long resolution, final @NonNull IProgressMonitor monitor, final long start, final long end, Sampling sampling,
                Iterable<@NonNull TimeGraphEntry> entries, @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull Object>>> predicates, Map<TimeGraphEntry, List<ITimeEvent>> gaps) {
            queryFullStates(ss, start, end, resolution, monitor, (@NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState) -> {
                try (TraceCompassLogUtils.ScopeLog scope = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "ZoomThread:GettingStates");) { //$NON-NLS-1$

                    for (TimeGraphEntry entry : entries) {
                        if (!sampling.equals(entry.getSampling())) {
                            zoom(checkNotNull(entry), ss, fullStates, prevFullState, predicates, monitor, gaps);
                        }
                    }
                }
                /* Refresh the arrows when zooming */
                try (TraceCompassLogUtils.ScopeLog linksLogger1 = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "ZoomThread:GettingLinks")) { //$NON-NLS-1$
                    links.addAll(getLinkList(ss, fullStates, prevFullState, monitor));
                }
                /* Refresh the view-specific markers when zooming */
                try (TraceCompassLogUtils.ScopeLog linksLogger2 = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "ZoomThread:GettingMarkers")) { //$NON-NLS-1$
                    markers.addAll(getViewMarkerList(ss, fullStates, prevFullState, monitor));
                }
                refresh();
            });
            if (!monitor.isCanceled()) {
                applyResults(() -> entries.forEach(entry -> {
                    if (!sampling.equals(entry.getSampling())) {
                        if (!predicates.isEmpty()) {
                            /*
                             * Merge contiguous null events due to split query
                             */
                            List<ITimeEvent> eventList = Lists.newArrayList(entry.getTimeEventsIterator(getZoomStartTime(), getZoomEndTime(), getResolution()));
                            doFilterEventList(entry, eventList, predicates);
                            entry.setZoomedEventList(eventList);
                        }
                        entry.setSampling(sampling);
                    }
                }));
            }
            refresh();
        }

        private void zoom(@NonNull TimeGraphEntry entry, ITmfStateSystem ss, @NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState,
                @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull Object>>> predicates, @NonNull IProgressMonitor monitor, Map<TimeGraphEntry, List<ITimeEvent>> gaps) {
            List<ITimeEvent> eventList = getEventList(entry, ss, fullStates, prevFullState, monitor);

            if (gaps != null) {
                // gaps != null means that there is active filter
                getEnventListGaps(entry, eventList, gaps);
            }

            if (eventList != null && !monitor.isCanceled()) {
                doFilterEventList(entry, eventList, predicates);
                applyResults(() -> {
                    for (ITimeEvent event : eventList) {
                            entry.addZoomedEvent(event);
                    }
                });
            }
        }

        private void doBgSearch(ITmfStateSystem ss, int resolution, @NonNull IProgressMonitor monitor, Map<TimeGraphEntry, List<ITimeEvent>> gaps,
                @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull Object>>> predicates) {
            try (TraceCompassLogUtils.ScopeLog poc = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINE, "TimegraphBgSearch")) { //$NON-NLS-1$

                ViewFilterDialog timeEventFilterDialog = getViewFilterDialog();
                boolean hasActiveSavedFilters = timeEventFilterDialog.hasActiveSavedFilters();

                //Regroup the gaps by overlapping gaps
                Table<ITimeGraphEntry, @NonNull Pair<Long, Long>, List<ITimeEvent>> gapsTable = regroupOverlappingGaps(gaps);

                //Get the quarks per entry once
                Map<ITimeGraphEntry, Collection<Integer>> quarksPerEntry = new HashMap<>();
                for (ITimeGraphEntry entry : gapsTable.rowKeySet()) {
                    Collection<Integer> quarks = getQuarksForEntry(entry, ss);
                    quarksPerEntry.put(entry, quarks);
                }

                for (@NonNull Entry<Pair<Long, Long>, Map<ITimeGraphEntry, List<ITimeEvent>>> tableEntry : gapsTable.columnMap().entrySet()) {
                    Map<ITimeGraphEntry, List<ITimeEvent>> row = tableEntry.getValue();
                    List<Integer> quarks = new ArrayList<>();

                    //Get the need quarks for this slot
                    for (ITimeGraphEntry entry : row.keySet()) {
                        Collection<Integer> entryQuarks = quarksPerEntry.get(entry);
                        if (entryQuarks != null) {
                            entryQuarks.forEach(quark -> quarks.add(quark));
                        }
                    }

                    queryRangeStates(ss, tableEntry.getKey().getLeft(), tableEntry.getKey().getRight(), quarks, resolution, monitor, (@NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState) -> {
                        for (Entry<ITimeGraphEntry, List<ITimeEvent>> entryGap : row.entrySet()) {
                            List<ITimeEvent> gapEvents = entryGap.getValue();
                            TimeGraphEntry entry = (TimeGraphEntry) entryGap.getKey();
                            List<ITimeEvent> eventList = getEventList(Objects.requireNonNull(entry), ss, fullStates, prevFullState, monitor);

                            if (eventList != null && !eventList.isEmpty() && !monitor.isCanceled()) {
                                doFilterEventList(entry, eventList, predicates);
                                for (ITimeEvent event : eventList) {
                                    int pos = Collections.binarySearch(gapEvents, event, TimeGraphEntry.WITHIN_COMPARATOR);
                                    // If the event is within a gap
                                    if (pos >= 0) {
                                        ITimeEvent gap = gapEvents.get(pos);
                                        if (!monitor.isCanceled()) {
                                            // if any underlying event is not dimmed,
                                            // then set the related gap event dimmed
                                            // property to false
                                            boolean dimmed = gap.isPropertyActive(IFilterProperty.DIMMED);
                                            if (dimmed && !event.isPropertyActive(IFilterProperty.DIMMED)) {
                                                gap.setProperty(IFilterProperty.DIMMED, false);
                                            }

                                            // if any underlying event is not excluded,
                                            // then set the related gap event exclude
                                            // status property to false and add the gap
                                            // back to the zoom event list
                                            if (hasActiveSavedFilters && !event.isPropertyActive(IFilterProperty.EXCLUDE)) {
                                                gap.setProperty(IFilterProperty.EXCLUDE, false);
                                                applyResults(() -> {
                                                    entry.updateZoomedEvent(gap);
                                                });
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    });
                    refresh();
                }
            }
        }

        private void clearZoomedLists() {
            for (ITmfStateSystem ss : fZoomSSList) {
                List<TimeGraphEntry> entryList = null;
                synchronized (fSSEntryListMap) {
                    entryList = fSSEntryListMap.get(ss);
                }
                if (entryList != null) {
                    for (TimeGraphEntry entry : entryList) {
                        clearZoomedList(entry);
                    }
                }
            }
            fClearZoomedLists = false;
        }

        private void clearZoomedList(TimeGraphEntry entry) {
            entry.setZoomedEventList(null);
            entry.setSampling(null);
            for (TimeGraphEntry child : entry.getChildren()) {
                clearZoomedList(child);
            }
        }

        private boolean isMember(TimeGraphEntry entry, List<TimeGraphEntry> ssEntryList) {
            TimeGraphEntry ssEntry = entry;
            while (ssEntry != null) {
                if (ssEntryList.contains(ssEntry)) {
                    return true;
                }
                ssEntry = ssEntry.getParent();
            }
            return false;
        }
    }

    private static @NonNull Table<ITimeGraphEntry, @NonNull Pair<Long, Long>, List<ITimeEvent>> regroupOverlappingGaps(Map<? extends ITimeGraphEntry, List<ITimeEvent>> gapsMap) {

        Table<ITimeGraphEntry, @NonNull Pair<Long, Long>, List<ITimeEvent>> gapsTable = HashBasedTable.create();
        PriorityQueue<ITimeEvent> queue = new PriorityQueue<>(TIME_EVENT_COMPARATOR);
        gapsMap.values().forEach(events -> queue.addAll(events));
        Pair<Long, Long> previous = null;
        while(!queue.isEmpty()) {
            ITimeEvent event = Objects.requireNonNull(queue.poll());

            ITimeGraphEntry entry = event.getEntry();

            Pair<Long, Long> result = previous != null && event.getTime() < previous.getRight() && event.getTime() + event.getDuration() > previous.getLeft() ? previous : null;
            if (result != null) {
                Pair<Long, Long> merged = mergeInterval(result, event);
                Map<ITimeGraphEntry, List<ITimeEvent>> column = new HashMap<>(gapsTable.column(result));
                if (!result.equals(merged)) {
                    gapsTable.columnKeySet().remove(result);
                    result = merged;
                    for (Entry<ITimeGraphEntry, List<ITimeEvent>> row : column.entrySet()) {
                        gapsTable.put(row.getKey(), result, row.getValue());
                    }
                }
            } else {
                result = Objects.requireNonNull(Pair.of(event.getTime(), event.getTime() + event.getDuration()));
            }

            previous = result;
            List<ITimeEvent> events = gapsTable.get(entry, result);
            if (events == null) {
                events = new ArrayList<>();
                gapsTable.put(entry, result, events);
            }
            events.add(event);

        }

        return gapsTable;
    }

    private static @NonNull Pair<Long, Long> mergeInterval(Pair<Long, Long> source, ITimeEvent event) {
        long left = Math.min(source.getLeft(), event.getTime());
        long right = Math.max(source.getRight(), event.getTime() + event.getDuration());
        return Objects.requireNonNull(Pair.of(left, right));
    }

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------

    /**
     * Constructs a time graph view that contains either a time graph viewer or
     * a time graph combo.
     *
     * By default, the view uses a time graph viewer. To use a time graph combo,
     * the subclass constructor must call {@link #setTreeColumns(String[])} and
     * {@link #setTreeLabelProvider(TreeLabelProvider)}.
     *
     * @param id
     *            The id of the view
     * @param pres
     *            The presentation provider
     */
    public AbstractStateSystemTimeGraphView(String id, TimeGraphPresentationProvider pres) {
        super(id, pres);
    }

    // ------------------------------------------------------------------------
    // Internal
    // ------------------------------------------------------------------------

    /**
     *
     * Gather the events list's gaps and add them to the map of gaps by entry.
     * The default implementation do nothing and views that need to do
     * background search need to provide their own implementation.
     *
     * @param entry
     *            The entry to find gaps
     * @param eventList
     *            The event list
     * @param gaps
     *            The map of gaps by entry
     * @since 4.3
     */
    protected void getEnventListGaps(@NonNull TimeGraphEntry entry, List<ITimeEvent> eventList, Map<TimeGraphEntry, List<ITimeEvent>> gaps) {
        // Do nothing here. Must be implemented by subclasses that need this behavior
    }

    /**
     * Get the list of attributes in given state system needed to build this
     * specific entry
     *
     * @param entry
     *            The entry to build
     * @param ss
     *            The related state system
     * @return The list of attributes needed to build the entry
     * @since 4.3
     */
    protected @NonNull Collection<@NonNull Integer> getQuarksForEntry(@NonNull ITimeGraphEntry entry, ITmfStateSystem ss) {
        return IntStream.range(0, ss.getNbAttributes()).boxed().collect(Collectors.toList());
    }

    /**
     * Gets the entry list for a state system. These entries, and their
     * recursive children, will have their event lists computed using the
     * provided state system.
     *
     * @param ss
     *            the state system
     *
     * @return the entry list
     */
    protected @Nullable List<@NonNull TimeGraphEntry> getEntryList(ITmfStateSystem ss) {
        synchronized (fSSEntryListMap) {
            return fSSEntryListMap.get(ss);
        }
    }

    /**
     * Sets the entry list for a state system. These entries, and their
     * recursive children, will have their event lists computed using the
     * provided state system. The root of each entry (or the entry itself if it
     * is a root) will be added to the parent trace's entry list.
     *
     * @param trace
     *            the parent trace
     * @param ss
     *            the state system
     * @param list
     *            the list of time graph entries
     */
    protected void putEntryList(ITmfTrace trace, ITmfStateSystem ss, List<@NonNull TimeGraphEntry> list) {
        super.addToEntryList(trace, getRootEntries(list));
        synchronized (fSSEntryListMap) {
            fSSEntryListMap.put(ss, new CopyOnWriteArrayList<>(list));
            fTraceSSMap.put(trace, ss);
        }
    }

    /**
     * Adds a list of entries to the entry list for a state system. These
     * entries, and their recursive children, will have their event lists
     * computed using the provided state system. The root of each entry (or the
     * entry itself if it is a root) will be added to the parent trace's entry
     * list.
     *
     * @param trace
     *            the parent trace
     * @param ss
     *            the state system
     * @param list
     *            the list of time graph entries to add
     */
    protected void addToEntryList(ITmfTrace trace, ITmfStateSystem ss, List<@NonNull TimeGraphEntry> list) {
        super.addToEntryList(trace, getRootEntries(list));
        synchronized (fSSEntryListMap) {
            List<@NonNull TimeGraphEntry> entryList = fSSEntryListMap.get(ss);
            if (entryList == null) {
                fSSEntryListMap.put(ss, new CopyOnWriteArrayList<>(list));
            } else {
                entryList.addAll(list);
            }
            fTraceSSMap.put(trace, ss);
        }
    }

    /**
     * Removes a list of entries from the entry list for a state system. These
     * entries, and their recursive children, will no longer have their event
     * lists computed using the provided state system. Each entry that is itself
     * a root will be removed from the parent trace's entry list.
     *
     * @param trace
     *            the parent trace
     * @param ss
     *            the state system
     * @param list
     *            the list of time graph entries to remove
     */
    protected void removeFromEntryList(ITmfTrace trace, ITmfStateSystem ss, List<TimeGraphEntry> list) {
        super.removeFromEntryList(trace, list);
        synchronized (fSSEntryListMap) {
            List<TimeGraphEntry> entryList = fSSEntryListMap.get(ss);
            if (entryList != null) {
                entryList.removeAll(list);
                if (entryList.isEmpty()) {
                    fTraceSSMap.remove(trace, ss);
                }
            }
        }
    }

    private static List<@NonNull TimeGraphEntry> getRootEntries(List<@NonNull TimeGraphEntry> list) {
        Set<@NonNull TimeGraphEntry> roots = new LinkedHashSet<>();
        for (@NonNull TimeGraphEntry entry : list) {
            TimeGraphEntry root = entry;
            while (root.getParent() != null) {
                root = root.getParent();
            }
            roots.add(root);
        }
        return new ArrayList<>(roots);
    }

    @Override
    protected @Nullable ZoomThread createZoomThread(long startTime, long endTime, long resolution, boolean restart) {
        List<ITmfStateSystem> ssList = null;
        synchronized (fSSEntryListMap) {
            ssList = new ArrayList<>(fTraceSSMap.get(getTrace()));
        }
        if (ssList.isEmpty()) {
            return null;
        }
        return new ZoomThreadByTime(getVisibleItems(DEFAULT_BUFFER_SIZE), ssList, startTime, endTime, resolution, restart);
    }

    /**
     * Query the state system full state for the given time range.
     *
     * @param ss
     *            The state system
     * @param start
     *            The start time
     * @param end
     *            The end time
     * @param resolution
     *            The resolution
     * @param monitor
     *            The progress monitor
     * @param handler
     *            The query handler
     */
    protected void queryFullStates(ITmfStateSystem ss, long start, long end, long resolution,
            @NonNull IProgressMonitor monitor, @NonNull IQueryHandler handler) {
        if (end < start) {
            /* We have an empty trace, the state system will be empty: nothing to do here. */
            return;
        }
        List<List<ITmfStateInterval>> fullStates = new ArrayList<>();
        List<ITmfStateInterval> prevFullState = null;
        try {
            long time = start;
            while (true) {
                if (monitor.isCanceled()) {
                    break;
                }
                List<ITmfStateInterval> fullState = ss.queryFullState(time);
                fullStates.add(fullState);
                if (fullStates.size() * fullState.size() > MAX_INTERVALS) {
                    handler.handle(fullStates, prevFullState);
                    prevFullState = fullStates.get(fullStates.size() - 1);
                    fullStates.clear();
                }
                if (time >= end) {
                    break;
                }
                time = Math.min(end, time + resolution);
            }
            if (!fullStates.isEmpty()) {
                handler.handle(fullStates, prevFullState);
            }
        } catch (StateSystemDisposedException e) {
            /* Ignored */
        }
    }

    /**
     * Query the state system for a list of specific state and for the given
     * time range.
     *
     * @param ss
     *            The state system
     * @param start
     *            The start time
     * @param end
     *            The end time
     * @param quarks
     *            The list of quarks/state to query
     * @param resolution
     *            The resolution
     * @param monitor
     *            The progress monitor
     * @param handler
     *            The query handler
     *
     * @since 4.3
     */
    protected void queryRangeStates(ITmfStateSystem ss, long start, long end, @NonNull Collection<@NonNull Integer> quarks, long resolution,
            @NonNull IProgressMonitor monitor, @NonNull IQueryHandler handler) {
        if (end < start) {
            /*
             * We have an empty trace, the state system will be empty: nothing
             * to do here.
             */
            return;
        }

        try {
            Iterable<@NonNull ITmfStateInterval> intervals = ss.query2D(quarks, start, end);
            Map<Integer, Integer> ptr = new HashMap<>();
            Map<Integer, List<ITmfStateInterval>> itvs = new HashMap<>();
            intervals.forEach(itv -> {
                int key = itv.getAttribute();
                List<ITmfStateInterval> set = itvs.get(key);
                if (set == null) {
                    set = new ArrayList<>();
                    itvs.put(key, set);
                    ptr.put(key, 0);
                }
                set.add(itv);
            });

            // Sort the intervals
            for (Entry<Integer, List<ITmfStateInterval>> entry : itvs.entrySet()) {
                entry.getValue().sort(INTERVALS_COMPARATOR);
            }

            List<List<ITmfStateInterval>> fullStates = new ArrayList<>();
            List<ITmfStateInterval> prevFullState = null;

            long time = start;
            while (true) {
                if (monitor.isCanceled()) {
                    break;
                }
                int nbAttributes = ss.getNbAttributes();
                List<ITmfStateInterval> fullState = getFullStateForTime(time, itvs, ptr, nbAttributes);
                fullStates.add(fullState);
                if (fullStates.size() * fullState.size() > MAX_INTERVALS) {
                    handler.handle(fullStates, prevFullState);
                    prevFullState = fullStates.get(fullStates.size() - 1);
                    fullStates.clear();
                }
                if (time >= end) {
                    break;
                }
                time = Math.min(end, time + resolution);
            }
            if (!fullStates.isEmpty()) {
                handler.handle(fullStates, prevFullState);
            }
        } catch (StateSystemDisposedException e1) {
            // Do nothing
        }
    }

    private static List<ITmfStateInterval> getFullStateForTime(long time, @NonNull Map<@NonNull Integer, @NonNull List<@NonNull ITmfStateInterval>> itvs, @NonNull Map<@NonNull Integer, @NonNull Integer> ptr, int nbAttributes) {
        List<ITmfStateInterval> fullState = new ArrayList<>();
        for (int key = 0; key < nbAttributes; key++) {
            fullState.add(null);
        }

        for (Entry<Integer, List<ITmfStateInterval>> entry : itvs.entrySet()) {
            List<ITmfStateInterval> intervals = entry.getValue();

            int key = Objects.requireNonNull(entry.getKey());
            int index = Objects.requireNonNull(ptr.get(key));

            ITmfStateInterval interval = Objects.requireNonNull(intervals.get(index));
            while(!isBounded(time, interval) && ++index < intervals.size()) {
                interval = Objects.requireNonNull(intervals.get(index));
                ptr.put(key, index);
            }

            if (index < intervals.size()) {
                fullState.set(key, interval);
            }
        }
        return fullState;
    }

    private static boolean isBounded(long time, ITmfStateInterval interval) {
        return interval.getStartTime() <= time && interval.getEndTime() >= time;
    }

    /**
     * Gets the list of events for an entry for a given list of full states.
     * <p>
     * Called from the ZoomThread for every entry to update the zoomed event
     * list. Can be an empty implementation if the view does not support zoomed
     * event lists. Can also be used to compute the full event list.
     *
     * @param tgentry
     *            The time graph entry
     * @param ss
     *            The state system
     * @param fullStates
     *            A list of full states
     * @param prevFullState
     *            The previous full state, or null
     * @param monitor
     *            A progress monitor
     * @return The list of time graph events
     */
    protected abstract @Nullable List<ITimeEvent> getEventList(@NonNull TimeGraphEntry tgentry, ITmfStateSystem ss,
            @NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState, @NonNull IProgressMonitor monitor);

    /**
     * Gets the list of links (displayed as arrows) for a given list of full
     * states. The default implementation returns an empty list.
     *
     * @param ss
     *            The state system
     * @param fullStates
     *            A list of full states
     * @param prevFullState
     *            The previous full state, or null
     * @param monitor
     *            A progress monitor
     * @return The list of link events
     * @since 2.0
     */
    protected @NonNull List<ILinkEvent> getLinkList(ITmfStateSystem ss,
            @NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState, @NonNull IProgressMonitor monitor) {
        return new ArrayList<>();
    }

    /**
     * Gets the list of markers for a given list of full states. The default
     * implementation returns an empty list.
     *
     * @param ss
     *            The state system
     * @param fullStates
     *            A list of full states
     * @param prevFullState
     *            The previous full state, or null
     * @param monitor
     *            A progress monitor
     * @return The list of marker events
     * @since 2.0
     */
    protected @NonNull List<IMarkerEvent> getViewMarkerList(ITmfStateSystem ss,
            @NonNull List<List<ITmfStateInterval>> fullStates, @Nullable List<ITmfStateInterval> prevFullState, @NonNull IProgressMonitor monitor) {
        return new ArrayList<>();
    }

    @Override
    protected void resetView(ITmfTrace viewTrace) {
        // Don't remove super call
        super.resetView(viewTrace);
        synchronized (fSSEntryListMap) {
            for (ITmfStateSystem ss : fTraceSSMap.removeAll(viewTrace)) {
                fSSEntryListMap.remove(ss);
            }
        }
    }
}
