/*******************************************************************************
 * Copyright (c) 2010, 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
 *******************************************************************************/

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

import java.util.ArrayList;
import java.util.Iterator;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.ui.views.colors.ColorSettingsManager;
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.TimeEvent;

import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;

/**
 * Event in the time chart view
 *
 * @version 1.0
 * @author Patrick Tasse
 * @since 4.0
 */
public class TimeChartEvent implements ITimeEvent {

    private final TimeChartAnalysisEntry fParentEntry;
    private long fTime;
    private long fDuration;
    private long fFirstRank;
    private long fLastRank;
    private final RankRangeList fRankRangeList;
    private long fNbEvents;
    private int fColorSettingPriority;
    private boolean fIsBookmark;
    private boolean fIsVisible;
    private boolean fIsSearchMatch;
    private TimeChartAnalysisEntry fItemizedEntry;
    private boolean fItemizing;

    /**
     * Standard constructor
     *
     * @param parentEntry
     *            The parent entry
     * @param event
     *            The event from which this time chart event originates
     * @param rank
     *            The rank of the event in the trace
     * @param decorationProvider
     *            The decoration provider to use
     */
    public TimeChartEvent(TimeChartAnalysisEntry parentEntry, ITmfEvent event,
            long rank, TimeChartDecorationProvider decorationProvider) {
        fParentEntry = parentEntry;
        fTime = event.getTimestamp().toNanos();
        fDuration = 0;
        fFirstRank = fLastRank = rank;
        fRankRangeList = new RankRangeList(rank);
        fNbEvents = 1;
        fColorSettingPriority = ColorSettingsManager.getColorSettingPriority(event);
        fIsBookmark = decorationProvider.isBookmark(rank);
        fIsVisible = decorationProvider.isVisible(event);
        fIsSearchMatch = decorationProvider.isSearchMatch(event);
    }

    @Override
    public ITimeGraphEntry getEntry() {
        return fParentEntry;
    }

    @Override
    public long getTime() {
        return fTime;
    }

    @Override
    public long getDuration() {
        return fDuration;
    }

    @Override
    public ITimeEvent splitBefore(long splitTime) {
        return (splitTime > fTime ?
                new TimeEvent(getEntry(), fTime, Math.min(fDuration, splitTime - fTime)) :
                null);
    }

    @Override
    public ITimeEvent splitAfter(long splitTime) {
        return (splitTime < fTime + fDuration ?
                new TimeEvent(getEntry(), Math.max(fTime, splitTime), fDuration - Math.max(0, splitTime - fTime)) :
                null);
    }

    /**
     * Retrieve the rank of the trace event which started this time event.
     *
     * @return The rank of the beginning
     */
    public long getFirstRank() {
        return fFirstRank;
    }

    /**
     * Retrieve the rank of the trace event which *finished* this time event.
     *
     * @return The rank of the end
     */
    public long getLastRank() {
        return fLastRank;
    }

    /**
     * Get the list of rank ranges corresponding to this time event.
     *
     * @return The rank range list
     */
    public RankRangeList getRankRangeList() {
        return fRankRangeList;
    }

    /**
     * Merge another time event with this one.
     *
     * @param event
     *            The other event
     */
    public void merge(TimeChartEvent event) {
        mergeDecorations(event);
        if (fTime == event.getTime() && fDuration == event.getDuration()) {
            return;
        }
        long endTime = Math.max(fTime + fDuration, event.getTime() + event.getDuration());
        fTime = Math.min(fTime, event.getTime());
        fDuration = endTime - fTime;
        fFirstRank = Math.min(fFirstRank, event.fFirstRank);
        fLastRank = Math.max(fLastRank, event.fLastRank);
        fNbEvents += event.fNbEvents;
        fItemizedEntry = null;
        synchronized (fRankRangeList) {
            fRankRangeList.merge(event.getRankRangeList());
        }
    }

    /**
     * Merge the decorations of another time event with the decorations of this
     * one.
     *
     * @param event
     *            The other event
     */
    public void mergeDecorations(TimeChartEvent event) {
        fColorSettingPriority = Math.min(fColorSettingPriority, event.getColorSettingPriority());
        fIsBookmark |= event.fIsBookmark;
        fIsVisible |= event.fIsVisible;
        fIsSearchMatch |= event.fIsSearchMatch;
    }

    /**
     * Get the number of time events that have been merged with this one (starts
     * counting at 1 if no merge happened).
     *
     * @return The current number of events in the bath
     */
    public long getNbEvents() {
        return fNbEvents;
    }

    /**
     * Retrieve the color setting priority.
     *
     * @return The priority
     */
    public int getColorSettingPriority() {
        return fColorSettingPriority;
    }

    /**
     * Set the color setting priority.
     *
     * @param priority
     *            The priority to set
     */
    public void setColorSettingPriority(int priority) {
        fColorSettingPriority = priority;
    }

    /**
     * Check if this time event is bookmarked
     *
     * @return Y/N
     */
    public boolean isBookmarked() {
        return fIsBookmark;
    }

    /**
     * Set this time event to be bookmarked or not.
     *
     * @param isBookmarked
     *            Should time time event become a bookmark, or not
     */
    public void setIsBookmarked(boolean isBookmarked) {
        fIsBookmark = isBookmarked;
    }

    /**
     * Check if this time is currently visible or not.
     *
     * @return If the event is visible
     */
    public boolean isVisible() {
        return fIsVisible;
    }

    /**
     * Set this time event to visible (or to non-visible).
     *
     * @param isVisible The new status
     */
    public void setIsVisible(boolean isVisible) {
        fIsVisible = isVisible;
    }

    /**
     * Check if the time event matches the current search.
     *
     * @return If it matches, Y/N
     */
    public boolean isSearchMatch() {
        return fIsSearchMatch;
    }

    /**
     * Mark this event as matching (or non-matching) the current search.
     *
     * @param isSearchMatch
     *            The new matching status
     */
    public void setIsSearchMatch(boolean isSearchMatch) {
        fIsSearchMatch = isSearchMatch;
    }

    /**
     * Set this event's itemized entry.
     *
     * @param timeAnalysisEntry
     *            The entry to set
     */
    public void setItemizedEntry(TimeChartAnalysisEntry timeAnalysisEntry) {
        fItemizedEntry = timeAnalysisEntry;
    }

    /**
     * Retrieve this event's itemized entry.
     *
     * @return The itemized entry that was previously set
     */
    public TimeChartAnalysisEntry getItemizedEntry() {
        return fItemizedEntry;
    }

    /**
     * @return Has this time event been set to itemizing?
     */
    public boolean isItemizing() {
        return fItemizing;
    }

    /**
     * Set this event's itemizing flag to true or false.
     *
     * @param itemizing
     *            The new value
     */
    public void setItemizing(boolean itemizing) {
        fItemizing = itemizing;
    }

    /**
     * Inner class to define a range in terms of ranks in the trace.
     *
     * @version 1.0
     * @author Patrick Tasse
     */
    public class RankRange {
        private long firstRank;
        private long lastRank;

        /**
         * Standard constructor
         *
         * @param firstRank
         *            The first (earliest) rank of the range
         * @param lastRank
         *            The last (latest) rank of the range
         */
        public RankRange(long firstRank, long lastRank) {
            this.firstRank = firstRank;
            this.lastRank = lastRank;
        }

        /**
         * Retrieve the start rank of this range.
         *
         * @return The first rank
         */
        public long getFirstRank() {
            return firstRank;
        }

        /**
         * Retrieve the end rank of this range
         *
         * @return The end rank
         */
        public long getLastRank() {
            return lastRank;
        }

        /**
         * Calculate the minimal distance between two RankRange's
         *
         * @param range
         *            The other range
         * @return The distance, in "number of events" between the two ranges
         */
        public long distanceFrom(RankRange range) {
            if (range.lastRank < fFirstRank) {
                return fFirstRank - range.lastRank;
            } else if (range.firstRank > fLastRank) {
                return range.firstRank - fLastRank;
            } else {
                return 0;
            }
        }

        @Override
        public String toString() {
            return "["+firstRank+","+lastRank+"]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        }
    }

    private class RankRangeList extends ArrayList<RankRange> {

        private static final long serialVersionUID = 6060485531208535986L;

        public RankRangeList(long rank) {
            super(1);
            add(new RankRange(rank, rank));
        }

        public void merge(RankRangeList rankRangeList) {
            long threshold = fParentEntry.getTrace().getCacheSize();
            for (RankRange newRange : rankRangeList) {
                boolean merged = false;
                for (RankRange oldRange : fRankRangeList) {
                    if (newRange.distanceFrom(oldRange) <= threshold) {
                        oldRange.firstRank = Math.min(oldRange.firstRank, newRange.firstRank);
                        oldRange.lastRank = Math.max(oldRange.lastRank, newRange.lastRank);
                        merged = true;
                        break;
                    }
                }
                if (!merged) {
                    add(newRange);
                }
            }
            Iterator<RankRange> iterator = fRankRangeList.iterator();
            RankRange previous = null;
            while (iterator.hasNext()) {
                RankRange range = iterator.next();
                if (previous != null && range.distanceFrom(previous) <= threshold) {
                    previous.firstRank = Math.min(previous.firstRank, range.firstRank);
                    previous.lastRank = Math.max(previous.lastRank, range.lastRank);
                    iterator.remove();
                }
                previous = range;
            }
        }
    }

    @Override
    public @NonNull Multimap<@NonNull String, @NonNull Object> getMetadata() {
        return ImmutableMultimap.of();
    }
}
