/*******************************************************************************
 * Copyright (c) 2011, 2014 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
 *   Bernd Hufmann - Add support for event collapsing
 ******************************************************************************/

package org.eclipse.tracecompass.tmf.ui.viewers.events;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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.tracecompass.internal.tmf.core.filter.TmfCollapseFilter;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.core.component.ITmfEventProvider;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventType;
import org.eclipse.tracecompass.tmf.core.filter.ITmfFilter;
import org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest;
import org.eclipse.tracecompass.tmf.core.request.TmfEventRequest;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

/**
 * The generic TMF Events table events cache
 *
 * This can help avoid re-reading the trace when the user scrolls a window,
 * for example.
 *
 * @author Patrick Tasse
 */
public class TmfEventsCache {

    /**
     * The generic TMF Events table cached event.
     *
     * @author Patrick Tasse
     */
    public static class CachedEvent implements ITmfEvent {
        /**
         * Event reference.
         *
         * When {@link TmfCollapseFilter} is active then it's event reference
         * of the first event of repeated events.
         */
        ITmfEvent event;
        /**
         * Events rank.
         *
         * When {@link TmfCollapseFilter} is active then it's event rank of the
         * first event of repeated events.
         */
        long rank;
        /**
         * Number times event is repeated. Updated by using {@link TmfCollapseFilter}
         */
        long repeatCount;

        /**
         * Constructor for new cached events.
         *
         * @param iTmfEvent
         *            The original trace event
         * @param rank
         *            The rank of this event in the trace
         */
        public CachedEvent (ITmfEvent iTmfEvent, long rank) {
            this.event = iTmfEvent;
            this.rank = rank;
        }

        @Override
        public <T> T getAdapter(Class<T> adapterType) {
            return event.getAdapter(adapterType);
        }

        @Override
        public ITmfTrace getTrace() {
            return event.getTrace();
        }

        @Override
        public long getRank() {
            return event.getRank();
        }

        /**
         * @since 1.0
         */
        @Override
        public String getName() {
            return event.getName();
        }

        @Override
        public ITmfTimestamp getTimestamp() {
            return event.getTimestamp();
        }

        @Override
        public ITmfEventType getType() {
            return event.getType();
        }

        @Override
        public ITmfEventField getContent() {
            return event.getContent();
        }
    }

    private final CachedEvent[] fCache;
    private final int fCacheSize;
    private int fCacheStartIndex = 0;
    private int fCacheEndIndex   = 0;

    private ITmfTrace fTrace;
    private final TmfEventsTable fTable;
    private ITmfFilter fFilter;
    private final List<Integer> fFilterIndex = new ArrayList<>(); // contains the event rank at each 'cache size' filtered events
    private boolean fCollapseFilterEnabled = false;

    /**
     * Constructor for the event cache
     *
     * @param cacheSize
     *            The size of the cache, in number of events
     * @param table
     *            The Events table this cache will cover
     */
    public TmfEventsCache(int cacheSize, TmfEventsTable table) {
        fCacheSize = cacheSize;
        fCache = new CachedEvent[cacheSize * 2]; // the cache holds two blocks of cache size
        fTable = table;
    }

    /**
     * Assign a new trace to this events cache. This clears the current
     * contents.
     *
     * @param trace
     *            The trace to assign.
     */
    public void setTrace(ITmfTrace trace) {
        fTrace = trace;
        clear();
    }

    /**
     * Clear the current contents of this cache.
     */
    public synchronized void clear() {
        if (job != null && job.getState() != Job.NONE) {
            job.cancel();
        }
        Arrays.fill(fCache, null);
        fCacheStartIndex = 0;
        fCacheEndIndex = 0;
        fFilterIndex.clear();
    }

    /**
     * Apply a filter on this event cache. This clears the current cache
     * contents.
     *
     * @param filter
     *            The ITmfFilter to apply.
     * @param collapseFilterEnabled
     *            true if the collapse filter is enabled
     * @since 2.0
     */
    public void applyFilter(ITmfFilter filter, boolean collapseFilterEnabled) {
        fFilter = filter;
        fCollapseFilterEnabled = collapseFilterEnabled;
        clear();
    }

    /**
     * Clear the current filter on this cache. This also clears the current
     * cache contents.
     */
    public void clearFilter() {
        fFilter = null;
        fCollapseFilterEnabled = false;
        clear();
    }

    /**
     * Get an event from the cache. If the cache does not contain the event,
     * a cache population request is triggered.
     *
     * @param index
     *            The index of this event in the cache
     * @return The cached event, or 'null' if the event is not in the cache
     */
    public synchronized CachedEvent getEvent(int index) {
        if ((index >= fCacheStartIndex) && (index < fCacheEndIndex)) {
            int i = index - fCacheStartIndex;
            return fCache[i];
        }
        populateCache(index);
        return null;
    }

    /**
     * Peek an event in the cache. Does not trigger cache population.
     *
     * @param index
     *            Index of the event to peek
     * @return The cached event, or 'null' if the event is not in the cache
     */
    public synchronized CachedEvent peekEvent(int index) {
        if ((index >= fCacheStartIndex) && (index < fCacheEndIndex)) {
            int i = index - fCacheStartIndex;
            return fCache[i];
        }
        return null;
    }

    /**
     * Add a trace event to the cache.
     *
     * @param event
     *            The original trace event to be cached
     * @param rank
     *            The rank of this event in the trace
     * @param index
     *            The index this event will occupy in the cache
     */
    public synchronized void storeEvent(ITmfEvent event, long rank, int index) {
        if (index == fCacheEndIndex) {
            int i = index - fCacheStartIndex;
            if (i < fCache.length) {
                fCache[i] = new CachedEvent(event, rank);
                fCacheEndIndex++;
            }
        }
        if ((fFilter != null) && ((index % fCacheSize) == 0)) {
            int i = index / fCacheSize;
            fFilterIndex.add(i, Integer.valueOf((int) rank));
        }
    }

    /**
     * Update event repeat count at index
     *
     * @param index
     *            The index this event occupies in the cache
     */
    public synchronized void updateCollapsedEvent(int index) {
        int i = index - fCacheStartIndex;
        if (i < fCache.length) {
            fCache[i].repeatCount++;
        }
    }

    /**
     * Get the cache index of an event from his rank in the trace. This will
     * take in consideration any filter that might be applied.
     *
     * @param rank
     *            The rank of the event in the trace
     * @return The position (index) this event should use once cached
     */
    public int getFilteredEventIndex(final long rank) {
        int current;
        int startRank;
        TmfEventRequest request;
        final ITmfFilter filter = fFilter;
        synchronized (this) {
            int start = 0;
            int end = fFilterIndex.size();

            if ((fCacheEndIndex - fCacheStartIndex) > 1) {
                if (rank < fCache[0].rank) {
                    end = (fCacheStartIndex / fCacheSize) + 1;
                } else if (rank > fCache[fCacheEndIndex - fCacheStartIndex - 1].rank) {
                    start = fCacheEndIndex / fCacheSize;
                } else {
                    for (int i = 0; i < (fCacheEndIndex - fCacheStartIndex); i++) {
                        if (fCache[i].rank >= rank) {
                            return fCacheStartIndex + i;
                        }
                    }
                    return fCacheEndIndex;
                }
            }

            current = (start + end) / 2;
            while (current != start) {
                if (rank < fFilterIndex.get(current)) {
                    end = current;
                    current = (start + end) / 2;
                } else {
                    start = current;
                    current = (start + end) / 2;
                }
            }
            startRank = fFilterIndex.isEmpty() ? 0 : fFilterIndex.get(current);
        }

        final int index = current * fCacheSize;

        class DataRequest extends TmfEventRequest {
            ITmfFilter requestFilter;
            TmfCollapseFilter requestCollapsedFilter;
            int requestRank;
            int requestIndex;

            DataRequest(Class<? extends ITmfEvent> dataType, ITmfFilter reqFilter, int start, int nbRequested) {
                super(dataType, TmfTimeRange.ETERNITY, start, nbRequested,
                        TmfEventRequest.ExecutionType.FOREGROUND);
                requestFilter = reqFilter;
                requestRank = start;
                requestIndex = index;
                requestCollapsedFilter = fCollapseFilterEnabled ? new TmfCollapseFilter() : null;
            }

            @Override
            public void handleData(ITmfEvent event) {
                super.handleData(event);
                if (isCancelled()) {
                    return;
                }
                if (requestRank >= rank) {
                    cancel();
                    return;
                }
                requestRank++;
                if (requestFilter.matches(event)) {
                    if (requestCollapsedFilter == null || requestCollapsedFilter.matches(event)) {
                        requestIndex++;
                    }
                }
            }

            public int getFilteredIndex() {
                return requestIndex;
            }
        }

        request = new DataRequest(ITmfEvent.class, filter, startRank, ITmfEventRequest.ALL_DATA);
        ((ITmfEventProvider) fTrace).sendRequest(request);
        try {
            request.waitForCompletion();
            return ((DataRequest) request).getFilteredIndex();
        } catch (InterruptedException e) {
            Activator.getDefault().logError("Filter request interrupted!", e); //$NON-NLS-1$
            Thread.currentThread().interrupt();
        }
        return 0;
    }

    // ------------------------------------------------------------------------
    // Event cache population
    // ------------------------------------------------------------------------

    // The event fetching job
    private Job job;
    private synchronized void populateCache(final int index) {

        /* Check if the current job will fetch the requested event:
         * 1. The job must exist
         * 2. It must be running (i.e. not completed)
         * 3. The requested index must be within the cache range
         *
         * If the job meets these conditions, we simply exit.
         * Otherwise, we create a new job but we might have to cancel
         * an existing job for an obsolete range.
         */
        if (job != null) {
            if (job.getState() != Job.NONE) {
                if ((index >= fCacheStartIndex) && (index < (fCacheStartIndex + fCache.length))) {
                    return;
                }
                // The new index is out of the requested range
                // Kill the job and start a new one
                job.cancel();
            }
        }

        // Populate the cache starting at the index that is one block less
        // of cache size than the requested index. The cache will hold two
        // consecutive blocks of cache size, centered on the requested index.
        fCacheStartIndex = Math.max(0, index - fCacheSize);
        fCacheEndIndex   = fCacheStartIndex;

        job = new Job("Fetching Events") { //$NON-NLS-1$
            private int startIndex = fCacheStartIndex;
            private int skipCount = 0;
            @Override
            protected IStatus run(final IProgressMonitor monitor) {

                int nbRequested;
                if (fFilter == null) {
                    nbRequested = fCache.length;
                } else {
                    nbRequested = ITmfEventRequest.ALL_DATA;
                    int i = startIndex / fCacheSize;
                    if (i < fFilterIndex.size()) {
                        skipCount = startIndex - (i * fCacheSize);
                        startIndex = fFilterIndex.get(i);
                    }
                }

                TmfEventRequest request = new TmfEventRequest(ITmfEvent.class,
                        TmfTimeRange.ETERNITY,
                        startIndex,
                        nbRequested,
                        TmfEventRequest.ExecutionType.FOREGROUND) {
                    private int count = 0;
                    private long rank = startIndex;
                    private TmfCollapseFilter collapseFilter = fCollapseFilterEnabled ? new TmfCollapseFilter() : null;
                    @Override
                    public void handleData(ITmfEvent event) {
                        // If the job is canceled, cancel the request so waitForCompletion() will unlock
                        if (monitor.isCanceled()) {
                            cancel();
                            return;
                        }
                        super.handleData(event);
                        if ((fFilter == null) || fFilter.matches(event)) {
                            if (collapseFilter == null || collapseFilter.matches(event)) {
                                if (skipCount-- <= 0) {
                                    synchronized (TmfEventsCache.this) {
                                        if (monitor.isCanceled()) {
                                            return;
                                        }
                                        fCache[count] = new CachedEvent(event, rank);
                                        count++;
                                        fCacheEndIndex++;
                                    }
                                    if (fFilter != null) {
                                        fTable.cacheUpdated(false);
                                    }
                                }
                            } else if ((count > 0) && (skipCount <= 0)) {
                                fCache[count - 1].repeatCount++;
                            }
                        }
                        if (count >= fCache.length) {
                            cancel();
                        } else if ((fFilter != null) && (count >= (fTable.getTable().getItemCount() - 3))) { // -1 for header row, -2 for top and bottom filter status rows
                            cancel();
                        }
                        rank++;
                    }
                };

                ((ITmfEventProvider) fTrace).sendRequest(request);
                try {
                    request.waitForCompletion();
                } catch (InterruptedException e) {
                    Activator.getDefault().logError("Wait for completion interrupted for populateCache ", e); //$NON-NLS-1$
                    Thread.currentThread().interrupt();
                }

                fTable.cacheUpdated(true);

                // Flag the UI thread that the cache is ready
                if (monitor.isCanceled()) {
                    return Status.CANCEL_STATUS;
                }
                return Status.OK_STATUS;
            }
        };
        //job.setSystem(true);
        job.setPriority(Job.SHORT);
        job.schedule();
    }

}
