/*******************************************************************************
 * Copyright (c) 2011, 2019 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:
 *   Francois Chouinard - Initial API and implementation
 *   Bernd Hufmann - Implementation of new interfaces/listeners and support for
 *                   time stamp in any order
 *   Francois Chouinard - Moved from LTTng to TMF
 *   Francois Chouinard - Added support for empty initial buckets
 *   Patrick Tasse - Support selection range
 *   Jean-Christian Kouamé, Simon Delisle - Added support to manage lost events
 *   Xavier Raynaud - Support multi-trace coloring
 *******************************************************************************/

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

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.core.runtime.ListenerList;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;

/**
 * Histogram-independent data model.
 *
 * It has the following characteristics:
 * <ul>
 * <li>The <i>basetime</i> is the timestamp of the first event
 * <li>There is a fixed number (<i>n</i>) of buckets of uniform duration (
 * <i>d</i>)
 * <li>The <i>timespan</i> of the model is thus: <i>n</i> * <i>d</i> time units
 * <li>Bucket <i>i</i> holds the number of events that occurred in time range: [
 * <i>basetime</i> + <i>i</i> * <i>d</i>, <i>basetime</i> + (<i>i</i> + 1) *
 * <i>d</i>)
 * </ul>
 * Initially, the bucket durations is set to 1ns. As the events are read, they
 * are tallied (using <i>countEvent()</i>) in the appropriate bucket (relative
 * to the <i>basetime</i>).
 * <p>
 * Eventually, an event will have a timestamp that exceeds the <i>timespan</i>
 * high end (determined by <i>n</i>, the number of buckets, and <i>d</i>, the
 * bucket duration). At this point, the histogram needs to be compacted. This is
 * done by simply merging adjacent buckets by pair, in effect doubling the
 * <i>timespan</i> (<i>timespan'</i> = <i>n</i> * <i>d'</i>, where <i>d'</i> = 2
 * <i>d</i>). This compaction happens as needed as the trace is read.
 * <p>
 * The model allows for timestamps in not increasing order. The timestamps can
 * be fed to the model in any order. If an event has a timestamp less than the
 * <i>basetime</i>, the buckets will be moved to the right to account for the
 * new smaller timestamp. The new <i>basetime</i> is a multiple of the bucket
 * duration smaller then the previous <i>basetime</i>. Note that the
 * <i>basetime</i> might no longer be the timestamp of an event. If necessary,
 * the buckets will be compacted before moving to the right. This might be
 * necessary to not lose any event counts at the end of the buckets array.
 * <p>
 * The mapping from the model to the UI is performed by the <i>scaleTo()</i>
 * method. By keeping the number of buckets <i>n</i> relatively large with
 * respect to to the number of pixels in the actual histogram, we should achieve
 * a nice result when visualizing the histogram.
 * <p>
 *
 * @version 2.0
 * @author Francois Chouinard
 */
public class HistogramDataModel implements IHistogramDataModel {

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

    /**
     * The default number of buckets
     */
    public static final int DEFAULT_NUMBER_OF_BUCKETS = 16 * 1000;

    /**
     * Number of events after which listeners will be notified.
     */
    public static final int REFRESH_FREQUENCY = DEFAULT_NUMBER_OF_BUCKETS;

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

    // Trace management
    private ITmfTrace fTrace = null;
    private final Map<ITmfTrace, Integer> fTraceMap = new LinkedHashMap<>();

    // Bucket management
    private final int fNbBuckets;
    private final HistogramBucket[] fBuckets;
    private final long[] fLostEventsBuckets;
    private long fBucketDuration;
    private long fNbEvents;
    private int fLastBucket;

    // Timestamps
    private long fFirstBucketTime; // could be negative when analyzing events
                                   // with descending order!!!
    private long fFirstEventTime;
    private long fEndTime;
    private long fSelectionBegin;
    private long fSelectionEnd;
    private long fTimeLimit;

    // Private listener lists
    private final ListenerList<IHistogramModelListener> fModelListeners;

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

    /**
     * Default constructor with default number of buckets.
     */
    public HistogramDataModel() {
        this(0, DEFAULT_NUMBER_OF_BUCKETS);
    }

    /**
     * Default constructor with default number of buckets.
     *
     * @param startTime
     *            The histogram start time
     */
    public HistogramDataModel(long startTime) {
        this(startTime, DEFAULT_NUMBER_OF_BUCKETS);
    }

    /**
     * Constructor with non-default number of buckets.
     *
     * @param nbBuckets
     *            A number of buckets.
     */
    public HistogramDataModel(int nbBuckets) {
        this(0, nbBuckets);
    }

    /**
     * Constructor with non-default number of buckets.
     *
     * @param startTime
     *            the histogram start time
     * @param nbBuckets
     *            A number of buckets.
     */
    public HistogramDataModel(long startTime, int nbBuckets) {
        fFirstBucketTime = fFirstEventTime = fEndTime = startTime;
        fNbBuckets = nbBuckets;
        fBuckets = new HistogramBucket[nbBuckets];
        fLostEventsBuckets = new long[nbBuckets];
        fModelListeners = new ListenerList<>();
        clear();
    }

    /**
     * Copy constructor.
     *
     * @param other
     *            A model to copy.
     */
    public HistogramDataModel(HistogramDataModel other) {
        fNbBuckets = other.fNbBuckets;
        fBuckets = new HistogramBucket[fNbBuckets];
        for (int i = 0; i < fNbBuckets; i++) {
            fBuckets[i] = new HistogramBucket(other.fBuckets[i]);
        }
        fLostEventsBuckets = Arrays.copyOf(other.fLostEventsBuckets, fNbBuckets);
        fBucketDuration = Math.max(other.fBucketDuration, 1);
        fNbEvents = other.fNbEvents;
        fLastBucket = other.fLastBucket;
        fFirstBucketTime = other.fFirstBucketTime;
        fFirstEventTime = other.fFirstEventTime;
        fEndTime = other.fEndTime;
        fSelectionBegin = other.fSelectionBegin;
        fSelectionEnd = other.fSelectionEnd;
        fTimeLimit = other.fTimeLimit;
        fModelListeners = new ListenerList<>();
        for (IHistogramModelListener listener : other.fModelListeners) {
            fModelListeners.add(listener);
        }
    }

    /**
     * Disposes the data model
     */
    public void dispose() {
        fTraceMap.clear();
        fTrace = null;
    }

    // ------------------------------------------------------------------------
    // Accessors
    // ------------------------------------------------------------------------

    /**
     * Returns the number of events in the data model.
     *
     * @return number of events.
     */
    public long getNbEvents() {
        return fNbEvents;
    }

    /**
     * Returns the number of buckets in the model.
     *
     * @return number of buckets.
     */
    public int getNbBuckets() {
        return fNbBuckets;
    }

    /**
     * Returns the current bucket duration.
     *
     * @return bucket duration
     */
    public long getBucketDuration() {
        return fBucketDuration;
    }

    /**
     * Returns the time value of the first bucket in the model.
     *
     * @return time of first bucket.
     */
    public long getFirstBucketTime() {
        return fFirstBucketTime;
    }

    /**
     * Returns the time of the first event in the model.
     *
     * @return time of first event.
     */
    public long getStartTime() {
        return fFirstEventTime;
    }

    /**
     * Sets the trace of this model.
     *
     * @param trace
     *            - a {@link ITmfTrace}
     */
    public void setTrace(ITmfTrace trace) {
        this.fTrace = trace;
        fTraceMap.clear();
        int i = 0;
        for (ITmfTrace tr : TmfTraceManager.getTraceSet(fTrace)) {
            fTraceMap.put(tr, i);
            i++;
        }
    }

    /**
     * Gets the trace of this model.
     *
     * @return a {@link ITmfTrace}
     */
    public ITmfTrace getTrace() {
        return this.fTrace;
    }

    /**
     * Gets the traces names of this model.
     *
     * @return an array of trace names
     */
    public String[] getTraceNames() {
        return TmfTraceManager.getTraceSet(fTrace).stream()
            .map(ITmfTrace::getName)
            .toArray(String[]::new);
    }

    /**
     * Gets the number of traces of this model.
     *
     * @return the number of traces of this model.
     */
    public int getNbTraces() {
        Collection<ITmfTrace> traces = TmfTraceManager.getTraceSet(fTrace);
        if (traces.isEmpty()) {
            return 1; //
        }
        return traces.size();
    }

    /**
     * Sets the model start time
     *
     * @param startTime
     *            the histogram range start time
     * @param endTime
     *            the histogram range end time
     */
    public void setTimeRange(long startTime, long endTime) {
        fFirstBucketTime = fFirstEventTime = fEndTime = fTimeLimit = startTime;
        fBucketDuration = 1;
        updateEndTime();
        while (endTime >= fTimeLimit && fTimeLimit < Long.MAX_VALUE) {
            mergeBuckets();
        }
    }

    /**
     * Set the end time. Setting this ensures that the corresponding bucket is
     * displayed regardless of the event counts.
     *
     * @param endTime
     *            the time of the last used bucket
     */
    public void setEndTime(long endTime) {
        fEndTime = endTime;
        fLastBucket = (int) ((endTime - fFirstBucketTime) / fBucketDuration);
    }

    /**
     * Returns the end time.
     *
     * @return the time of the last used bucket
     */
    public long getEndTime() {
        return fEndTime;
    }

    /**
     * Returns the begin time of the current selection in the model.
     *
     * @return the begin time of the current selection.
     */
    public long getSelectionBegin() {
        return fSelectionBegin;
    }

    /**
     * Returns the end time of the current selection in the model.
     *
     * @return the end time of the current selection.
     */
    public long getSelectionEnd() {
        return fSelectionEnd;
    }

    /**
     * Returns the time limit with is: start time + nbBuckets * bucketDuration
     *
     * @return the time limit.
     */
    public long getTimeLimit() {
        return fTimeLimit;
    }

    // ------------------------------------------------------------------------
    // Listener handling
    // ------------------------------------------------------------------------

    /**
     * Add a listener to the model to be informed about model changes.
     *
     * @param listener
     *            A listener to add.
     */
    public void addHistogramListener(IHistogramModelListener listener) {
        fModelListeners.add(listener);
    }

    /**
     * Remove a given model listener.
     *
     * @param listener
     *            A listener to remove.
     */
    public void removeHistogramListener(IHistogramModelListener listener) {
        fModelListeners.remove(listener);
    }

    // Notify listeners (always)
    private void fireModelUpdateNotification() {
        fireModelUpdateNotification(0);
    }

    // Notify listener on boundary
    private void fireModelUpdateNotification(long count) {
        if ((count % REFRESH_FREQUENCY) == 0) {
            Object[] listeners = fModelListeners.getListeners();
            for (Object listener2 : listeners) {
                IHistogramModelListener listener = (IHistogramModelListener) listener2;
                listener.modelUpdated();
            }
        }
    }

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

    @Override
    public void complete() {
        fireModelUpdateNotification();
    }

    /**
     * Clear the histogram model.
     *
     * @see org.eclipse.tracecompass.tmf.ui.views.distribution.model.IBaseDistributionModel#clear()
     */
    @Override
    public synchronized void clear() {
        Arrays.fill(fBuckets, null);
        Arrays.fill(fLostEventsBuckets, 0);
        fNbEvents = 0;
        fFirstBucketTime = 0;
        fFirstEventTime = 0;
        fEndTime = 0;
        fSelectionBegin = 0;
        fSelectionEnd = 0;
        fLastBucket = -1;
        fBucketDuration = 1;
        updateEndTime();
        fireModelUpdateNotification();
    }

    /**
     * Sets the current selection time range (no notification of listeners)
     *
     * @param beginTime
     *            The selection begin time.
     * @param endTime
     *            The selection end time.
     */
    public void setSelection(long beginTime, long endTime) {
        fSelectionBegin = beginTime;
        fSelectionEnd = endTime;
    }

    /**
     * Sets the current selection time range with notification of listeners
     *
     * @param beginTime
     *            The selection begin time.
     * @param endTime
     *            The selection end time.
     */
    public void setSelectionNotifyListeners(long beginTime, long endTime) {
        fSelectionBegin = beginTime;
        fSelectionEnd = endTime;
        fireModelUpdateNotification();
    }

    /**
     * Add event to the correct bucket, compacting the if needed.
     *
     * @param eventCount
     *            The current event Count (for notification purposes)
     * @param timestamp
     *            The timestamp of the event to count
     * @param trace
     *            The event trace
     */
    @Override
    public synchronized void countEvent(long eventCount, long timestamp, ITmfTrace trace) {

        // Validate
        if (timestamp < 0) {
            return;
        }

        // Set the start/end time if not already done
        if ((fFirstBucketTime == 0) && (fLastBucket == -1) && (fBuckets[0] == null) && (timestamp > 0)) {
            fFirstBucketTime = timestamp;
            fFirstEventTime = timestamp;
            updateEndTime();
        }

        if (timestamp < fFirstEventTime) {
            fFirstEventTime = timestamp;
        }

        if (fEndTime < timestamp) {
            fEndTime = timestamp;
        }

        if (timestamp >= fFirstBucketTime) {

            // Compact as needed
            while (timestamp >= fTimeLimit && fTimeLimit < Long.MAX_VALUE) {
                mergeBuckets();
            }

        } else {

            // get offset for adjustment
            long preMergeOffset = getOffset(timestamp);

            // Compact as needed
            while ((fLastBucket + preMergeOffset) >= fNbBuckets) {
                mergeBuckets();
                preMergeOffset = getOffset(timestamp);
            }

            // after merging the offset should be less than number of buckets
            int offset = (int) preMergeOffset;
            moveBuckets(offset);

            fLastBucket = fLastBucket + offset;

            fFirstBucketTime = fFirstBucketTime - (offset * fBucketDuration);
            updateEndTime();
        }

        // Increment the right bucket
        int index = (int) ((timestamp - fFirstBucketTime) / fBucketDuration);
        if (fBuckets[index] == null) {
            fBuckets[index] = new HistogramBucket(getNbTraces());
        }
        Integer traceIndex = fTraceMap.get(trace);
        if (traceIndex == null) {
            traceIndex = 0;
        }
        fBuckets[index].addEvent(traceIndex);
        fNbEvents++;
        if (fLastBucket < index) {
            fLastBucket = index;
        }

        fireModelUpdateNotification(eventCount);
    }

    /**
     * Add lost event to the correct bucket, compacting the if needed.
     *
     * @param timeRange
     *            time range of a lost event
     * @param nbLostEvents
     *            the number of lost events
     * @param fullRange
     *            Full range or time range for histogram request
     */
    public void countLostEvent(TmfTimeRange timeRange, long nbLostEvents, boolean fullRange) {

        long startTime = timeRange.getStartTime().getValue();
        long endTime = timeRange.getEndTime().getValue();

        // Validate
        if (startTime < 0 || endTime < 0) {
            return;
        }

        // Set the start/end time if not already done
        if ((fFirstBucketTime == 0) && (fLastBucket == -1) && (fBuckets[0] == null)) {
            fFirstBucketTime = startTime;
            fFirstEventTime = startTime;
            updateEndTime();
        }

        // Compact as needed
        if (fullRange) {
            fEndTime = Math.max(fEndTime, endTime);
            while (endTime >= fTimeLimit && fTimeLimit < Long.MAX_VALUE) {
                mergeBuckets();
            }
        }

        int indexStart = (int) ((startTime - fFirstBucketTime) / fBucketDuration);
        int indexEnd = (int) ((endTime - fFirstBucketTime) / fBucketDuration);
        int nbBucketRange = (indexEnd - indexStart) + 1;

        double lostEventsPerBucket = (double) nbLostEvents / nbBucketRange;

        // Increment the right bucket, bear in mind that ranges make it almost
        // certain that some lost events are out of range when not full range
        double remainder = 0.0;
        indexEnd = Math.min(indexEnd, fLostEventsBuckets.length - 1);
        for (int index = indexStart; index <= indexEnd; index++) {
            remainder += lostEventsPerBucket;
            long lostEvents = Math.round(remainder);
            fLostEventsBuckets[index] += lostEvents;
            remainder -= lostEvents;
        }

        fNbEvents++;
        if (fullRange) {
            fLastBucket = Math.max(fLastBucket, indexEnd);
        }

        fireModelUpdateNotification(nbLostEvents);
    }

    /**
     * Scale the model data to the width, height and bar width requested.
     *
     * @param width
     *            A width of the histogram canvas
     * @param height
     *            A height of the histogram canvas
     * @param barWidth
     *            A width (in pixel) of a histogram bar
     * @return the result array of size [width] and where the highest value
     *         doesn't exceed [height]
     *
     * @see org.eclipse.tracecompass.tmf.ui.views.histogram.IHistogramDataModel#scaleTo(int,
     *      int, int)
     */
    @Override
    public HistogramScaledData scaleTo(int width, int height, int barWidth) {
        // Basic validation
        if ((width <= 0) || (height <= 0) || (barWidth <= 0)) {
            throw new AssertionError("Invalid histogram dimensions (" + width + "x" + height + ", barWidth=" + barWidth + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        }
        if (fBucketDuration == 0) {
            throw new IllegalStateException("Bucket width is 0, that should be impossible"); //$NON-NLS-1$
        }

        // The result structure
        HistogramScaledData result = new HistogramScaledData(width, height, barWidth);

        // Scale horizontally
        result.fMaxValue = 0;

        int nbBars = width / barWidth;
        double bucketsPerBar = ((double) fLastBucket / nbBars);
        final long modelBucketStartTime = fFirstBucketTime;
        final long modelBucketEndTime = fEndTime;
        /*
         * If there is only one model bucket, use a duration of 1 to spread the
         * value over the scaled width, but store a scaled bucket duration of 0
         * to prevent the half-bucket offset in the bucket time calculations.
         */
        double bucketDuration = Math.max(modelBucketEndTime - modelBucketStartTime, 1) / (double) nbBars;
        result.fBucketDuration = fLastBucket == 0 ? 0 : bucketDuration;
        int scaledCount = 0;
        int scaledCountLostEvent = 0;
        int offset = (int) (0.5 / bucketDuration);
        for (int i = 0; i < result.fData.length; i++) {
            result.fData[i] = new HistogramBucket(getNbTraces());
        }
        for (int modelIndex = 0; modelIndex <= fLastBucket; modelIndex++) {
            double done = (double) modelIndex / (double) (fLastBucket);
            double doneNext = (double) (modelIndex + 1) / (double) (fLastBucket);
            final int scaledStart = Math.max((int) (done * nbBars) - offset, 0);
            final int scaledEnd = Math.min((int) (doneNext * nbBars) - offset, nbBars - 1);
            int scaledIndex = scaledStart;
            final HistogramBucket currentModelBucket = fBuckets[modelIndex];
            final long lostEvents = fLostEventsBuckets[modelIndex];
            if (currentModelBucket != null || lostEvents != 0) {
                do {
                    // Make sure last model bucket counted in last scaled index
                    scaledIndex = Math.min(scaledIndex, nbBars - 1);
                    if (result.fData[scaledIndex].getNbEvents() == 0) {
                        scaledCount = 0;
                        scaledCountLostEvent = 0;
                    }
                    if (currentModelBucket != null) {
                        result.fData[scaledIndex].add(currentModelBucket);
                        scaledCount += currentModelBucket.getNbEvents();
                        if (!currentModelBucket.isEmpty()) {
                            result.fLastBucket = scaledIndex;
                        }
                    }
                    result.fLostEventsData[scaledIndex] += lostEvents;
                    scaledCountLostEvent += lostEvents;
                    result.fMaxValue = Math.max(result.fMaxValue, scaledCount);
                    result.fMaxCombinedValue = Math.max(result.fMaxCombinedValue, scaledCount + scaledCountLostEvent);
                    scaledIndex++;
                } while (scaledIndex < scaledEnd);
            }
        }

        // Scale vertically
        if (result.fMaxValue > 0) {
            result.fScalingFactor = (double) height / result.fMaxValue;
        }
        if (result.fMaxCombinedValue > 0) {
            result.fScalingFactorCombined = (double) height / result.fMaxCombinedValue;
        }

        fBucketDuration = Math.max(fBucketDuration, 1);

        // Set selection begin and end index in the scaled histogram
        if (fSelectionBegin == fEndTime) {
            // make sure selection is visible at the end
            result.fSelectionBeginBucket = result.fWidth - 1;
        } else {
            result.fSelectionBeginBucket = (int) Math.round(((fSelectionBegin - fFirstBucketTime) / (double) fBucketDuration) / bucketsPerBar);
        }

        if (fSelectionEnd == fEndTime) {
            // make sure selection is visible at the end
            result.fSelectionEndBucket = result.fWidth - 1;
        } else {
            result.fSelectionEndBucket = (int) Math.round(((fSelectionEnd - fFirstBucketTime) / (double) fBucketDuration) / bucketsPerBar);
        }

        result.fFirstBucketTime = fFirstBucketTime;
        result.fFirstEventTime = fFirstEventTime;
        return result;
    }

    // ------------------------------------------------------------------------
    // Helper functions
    // ------------------------------------------------------------------------

    private void updateEndTime() {
        long newLimit = fFirstBucketTime + (fNbBuckets * fBucketDuration);
        // Make sure there is no overflow
        fTimeLimit = fFirstBucketTime <= newLimit ? newLimit : Long.MAX_VALUE;
    }

    private void mergeBuckets() {
        for (int i = 0; i < (fNbBuckets / 2); i++) {
            fBuckets[i] = new HistogramBucket(fBuckets[2 * i], fBuckets[(2 * i) + 1]);
            fLostEventsBuckets[i] = fLostEventsBuckets[2 * i] + fLostEventsBuckets[(2 * i) + 1];
        }
        Arrays.fill(fBuckets, fNbBuckets / 2, fNbBuckets, null);
        Arrays.fill(fLostEventsBuckets, fNbBuckets / 2, fNbBuckets, 0);
        fBucketDuration *= 2;
        updateEndTime();
        fLastBucket = (fNbBuckets / 2) - 1;
    }

    private void moveBuckets(int offset) {
        for (int i = fNbBuckets - 1; i >= offset; i--) {
            fBuckets[i] = new HistogramBucket(fBuckets[i - offset]);
            fLostEventsBuckets[i] = fLostEventsBuckets[i - offset];
        }

        for (int i = 0; i < offset; i++) {
            fBuckets[i] = null;
            fLostEventsBuckets[i] = 0;
        }
    }

    private long getOffset(long timestamp) {
        long offset = (fFirstBucketTime - timestamp) / fBucketDuration;
        if (((fFirstBucketTime - timestamp) % fBucketDuration) != 0) {
            offset++;
        }
        return offset;
    }
}
