/*****************************************************************************
 * Copyright (c) 2007, 2019 Intel Corporation, 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:
 *   Intel Corporation - Initial API and implementation
 *   Vitaly A. Provodin, Intel - Initial API and implementation
 *   Alvaro Sanchez-Leon - Updated for TMF
 *   Patrick Tasse - Refactoring
 *****************************************************************************/

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

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Control;
import org.eclipse.tracecompass.internal.tmf.ui.Messages;
import org.eclipse.tracecompass.tmf.core.TmfStrings;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.ui.viewers.TmfAbstractToolTipHandler;
import org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils;
import org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils.Resolution;
import org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils.TimeFormat;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.ITimeGraphPresentationProvider;
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.NullTimeEvent;

/**
 * Handler for the tool tips in the generic time graph view.
 *
 * @version 1.0
 * @author Alvaro Sanchez-Leon
 * @author Patrick Tasse
 */
public class TimeGraphTooltipHandler extends TmfAbstractToolTipHandler {

    private static final String ROW_CATEGORY = "Row"; //$NON-NLS-1$
    private static final String MARKER_CATEGORY_PREFIX = "Marker "; //$NON-NLS-1$
    private static final String LINK_CATEGORY = "Link"; //$NON-NLS-1$
    private static final String STATE_CATEGORY = "State"; //$NON-NLS-1$
    private static final String MARKER_OFFSET = " "; //$NON-NLS-1$

    private static final String MIN_STRING = "< 0.01%"; //$NON-NLS-1$

    private static final double MIN_RATIO = 0.0001;

    private static final String MAX_STRING = "> 1000%"; //$NON-NLS-1$

    private static final int MAX_RATIO = 10;

    private static final int HOVER_MAX_DIST = 10;

    private ITimeDataProvider fTimeDataProvider;
    private ITimeGraphPresentationProvider fTimeGraphProvider = null;

    /**
     * Standard constructor
     *
     * @param graphProv
     *            The presentation provider
     * @param timeProv
     *            The time provider
     */
    public TimeGraphTooltipHandler(ITimeGraphPresentationProvider graphProv,
            ITimeDataProvider timeProv) {

        fTimeGraphProvider = graphProv;
        fTimeDataProvider = timeProv;
    }

    /**
     * Set the time data provider
     *
     * @param timeDataProvider
     *            The time data provider
     */
    public void setTimeProvider(ITimeDataProvider timeDataProvider) {
        fTimeDataProvider = timeDataProvider;
    }

    @Override
    protected void fill(Control control, MouseEvent event, Point pt) {
        TimeGraphControl timeGraphControl = (TimeGraphControl) control;
        if ((event.stateMask & SWT.MODIFIER_MASK) != SWT.SHIFT) {
            ILinkEvent linkEvent = timeGraphControl.getArrow(pt);
            if (linkEvent != null) {
                fillValues(linkEvent);
            }
        }
        if (getTipComposite().getChildren().length == 0) {
            ITimeGraphEntry entry = timeGraphControl.getEntry(pt);
            if (entry == null) {
                return;
            }
            fillValues(pt, timeGraphControl, entry);
            Iterable<IMarkerEvent> markers = getMarkers(timeGraphControl, entry, pt);
            int i = 0;
            for (IMarkerEvent marker : markers) {
                fillValues(MARKER_CATEGORY_PREFIX + i, marker);
                i++;
            }
        }
    }

    /**
     * Get a list of markers intersecting a point
     * @param timeGraphControl
     *
     * @param entry
     *            the entry to observe, can be null
     * @param pt
     *            the point to look at
     * @return the return value
     */
    private static Iterable<IMarkerEvent> getMarkers(TimeGraphControl timeGraphControl, ITimeGraphEntry entry, Point pt) {
        List<IMarkerEvent> markers = timeGraphControl.getMarkers();
        List<IMarkerEvent> retVal = new ArrayList<>();
        if (markers != null) {
            // Include markers up to 5 pixels left and right of cursor
            int fuzz = 5;
            long fuzzStart = timeGraphControl.getTimeAtX(pt.x - fuzz);
            long fuzzEnd = timeGraphControl.getTimeAtX(pt.x + fuzz);
            for (IMarkerEvent marker : markers) {
                if (Objects.equals(marker.getEntry(), entry)) {
                    long duration = marker.getDuration();
                    long startTime = marker.getTime();
                    long endTime = startTime + duration;
                    if ((startTime <= fuzzEnd) && (endTime >= fuzzStart)) {
                        retVal.add(marker);
                    }
                }
            }
        }
        return retVal;
    }

    private void fillValues(String itemName, IMarkerEvent marker) {
        Map<String, String> toolTips = fTimeGraphProvider.getEventHoverToolTipInfo(marker);
        String category = marker.getCategory();
        String label = marker.getLabel();
        addItem(itemName, category == null ? Messages.TimeGraphTooltipHandler_DefaultMarkerName : category, label == null ? "" : label); //$NON-NLS-1$
        long timestamp = marker.getTime();
        long duration = marker.getDuration();
        if (duration == 0) {
            addItem(ToolTipString.fromString(itemName), ToolTipString.fromString(MARKER_OFFSET + Messages.TimeGraphTooltipHandler_Timestamp), ToolTipString.fromTimestamp(TmfTimestamp.fromNanos(timestamp).toString(), timestamp));
        } else {
            addItem(ToolTipString.fromString(itemName), ToolTipString.fromString(MARKER_OFFSET + TmfStrings.startTime()), ToolTipString.fromTimestamp(TmfTimestamp.fromNanos(timestamp).toString(), timestamp));
            addItem(ToolTipString.fromString(itemName), ToolTipString.fromString(MARKER_OFFSET + TmfStrings.endTime()), ToolTipString.fromTimestamp(TmfTimestamp.fromNanos(timestamp + duration).toString(), timestamp + duration));
        }
        if (toolTips != null) {
            for (Entry<String, String> tooltip : toolTips.entrySet()) {
                addItem(itemName, MARKER_OFFSET + tooltip.getKey(), tooltip.getValue()); // $NON-NLS-1$
            }
        }
    }

    private void fillValues(Point pt, TimeGraphControl timeGraphControl, ITimeGraphEntry entry) {
        if (entry == null) {
            return;
        }
        if (entry.hasTimeEvents()) {
            long currPixelTime = timeGraphControl.getTimeAtX(pt.x);
            long nextPixelTime = timeGraphControl.getTimeAtX(pt.x + 1);
            if (nextPixelTime == currPixelTime) {
                nextPixelTime++;
            }
            ITimeEvent currEvent = Utils.findEvent(entry, currPixelTime, 0);
            ITimeEvent nextEvent = Utils.findEvent(entry, currPixelTime, 1);

            /*
             * if there is no current event at the start of the current pixel
             * range, or if the current event starts before the current pixel
             * range, use the next event as long as it starts within the current
             * pixel range
             */
            if ((currEvent == null || currEvent.getTime() < currPixelTime) &&
                    (nextEvent != null && nextEvent.getTime() < nextPixelTime)) {
                currEvent = nextEvent;
                currPixelTime = nextEvent.getTime();
            }

            /*
             * if there is still no current event, use the closest between the
             * next and previous event, as long as they are within a distance
             * threshold
             */
            if (currEvent == null || currEvent instanceof NullTimeEvent) {
                int nextDelta = Integer.MAX_VALUE;
                int prevDelta = Integer.MAX_VALUE;
                long nextTime = 0;
                long prevTime = 0;
                if (nextEvent != null && !(nextEvent instanceof NullTimeEvent)) {
                    nextTime = nextEvent.getTime();
                    nextDelta = Math.abs(timeGraphControl.getXForTime(nextTime) - pt.x);
                }
                ITimeEvent prevEvent = Utils.findEvent(entry, currPixelTime, -1);
                if (prevEvent != null && !(prevEvent instanceof NullTimeEvent)) {
                    prevTime = prevEvent.getTime() + prevEvent.getDuration() - 1;
                    prevDelta = Math.abs(pt.x - timeGraphControl.getXForTime(prevTime));
                }
                if (nextDelta < HOVER_MAX_DIST && nextDelta <= prevDelta) {
                    currEvent = nextEvent;
                    currPixelTime = nextTime;
                } else if (prevDelta < HOVER_MAX_DIST) {
                    currEvent = prevEvent;
                    currPixelTime = prevTime;
                }
            }

            // state name
            String stateTypeName = fTimeGraphProvider.getStateTypeName(entry);
            String entryName = entry.getName();
            if (stateTypeName == null) {
                stateTypeName = fTimeGraphProvider.getStateTypeName();
            }

            if (!entryName.isEmpty()) {
                addItem(ROW_CATEGORY, stateTypeName, entry.getName());
            }

            if (currEvent == null || currEvent instanceof NullTimeEvent) {
                return;
            }

            // state
            String state = fTimeGraphProvider.getEventName(currEvent);
            if (state != null) {
                addItem(STATE_CATEGORY, Messages.TmfTimeTipHandler_TRACE_STATE, state);
            }

            // This block receives a list of <String, String> values to
            // be added to the tip table
            Map<String, String> eventAddOns = fTimeGraphProvider.getEventHoverToolTipInfo(currEvent, currPixelTime);
            if (eventAddOns != null) {
                for (Entry<String, String> eventAddOn : eventAddOns.entrySet()) {
                    addItem(STATE_CATEGORY, eventAddOn.getKey(), eventAddOn.getValue());
                }
            }
            if (fTimeGraphProvider.displayTimesInTooltip()) {
                long eventStartTime = -1;
                long eventDuration = -1;
                long eventEndTime = -1;

                eventStartTime = currEvent.getTime();
                eventDuration = currEvent.getDuration();
                if (eventDuration < 0 && nextEvent != null) {
                    eventEndTime = nextEvent.getTime();
                    eventDuration = eventEndTime - eventStartTime;
                } else {
                    eventEndTime = eventStartTime + eventDuration;
                }

                Resolution res = Resolution.NANOSEC;
                TimeFormat tf = fTimeDataProvider.getTimeFormat().convert();
                String startTime = "?"; //$NON-NLS-1$
                String duration = "?"; //$NON-NLS-1$
                String endTime = "?"; //$NON-NLS-1$
                if (fTimeDataProvider instanceof ITimeDataProviderConverter) {
                    ITimeDataProviderConverter tdp = (ITimeDataProviderConverter) fTimeDataProvider;
                    if (eventStartTime > -1) {
                        eventStartTime = tdp.convertTime(eventStartTime);
                        startTime = FormatTimeUtils.formatTime(eventStartTime, tf, res);
                    }
                    if (eventEndTime > -1) {
                        eventEndTime = tdp.convertTime(eventEndTime);
                        endTime = FormatTimeUtils.formatTime(eventEndTime, tf, res);
                    }
                    if (eventDuration > -1) {
                        duration = FormatTimeUtils.formatDelta(eventEndTime - eventStartTime, tf, res);
                    }
                } else {
                    if (eventStartTime > -1) {
                        startTime = FormatTimeUtils.formatTime(eventStartTime, tf, res);
                    }
                    if (eventEndTime > -1) {
                        endTime = FormatTimeUtils.formatTime(eventEndTime, tf, res);
                    }
                    if (eventDuration > -1) {
                        duration = FormatTimeUtils.formatDelta(eventDuration, tf, res);
                    }
                }
                if (tf == TimeFormat.CALENDAR) {
                    addItem(STATE_CATEGORY, Messages.TmfTimeTipHandler_TRACE_DATE,
                            eventStartTime > -1 ? FormatTimeUtils.formatDate(eventStartTime) : "?"); //$NON-NLS-1$
                }
                if (eventDuration > 0) {
                    addItem(ToolTipString.fromString(STATE_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_TRACE_START_TIME), ToolTipString.fromTimestamp(startTime, eventStartTime));
                    addItem(ToolTipString.fromString(STATE_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_TRACE_STOP_TIME), ToolTipString.fromTimestamp(endTime, eventEndTime));
                } else {
                    addItem(ToolTipString.fromString(STATE_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_TRACE_EVENT_TIME), ToolTipString.fromTimestamp(startTime, eventStartTime));
                }

                if (eventDuration > 0) {
                    addItem(STATE_CATEGORY, Messages.TmfTimeTipHandler_DURATION, duration);
                    long begin = fTimeDataProvider.getSelectionBegin();
                    long end = fTimeDataProvider.getSelectionEnd();
                    final long delta = Math.abs(end - begin);
                    final double durationRatio = (double) eventDuration / (double) delta;
                    if (delta > 0) {
                        String percentage;
                        if (durationRatio > MAX_RATIO) {
                            percentage = MAX_STRING;
                        } else if (durationRatio < MIN_RATIO) {
                            percentage = MIN_STRING;
                        } else {
                            percentage = String.format("%,.2f%%", durationRatio * 100.0); //$NON-NLS-1$
                        }

                        addItem(STATE_CATEGORY, Messages.TmfTimeTipHandler_PERCENT_OF_SELECTION, percentage);
                    }
                }
            }
        }
    }

    private void fillValues(ILinkEvent linkEvent) {
        addItem(LINK_CATEGORY, Messages.TmfTimeTipHandler_LINK_SOURCE, linkEvent.getEntry().getName());
        addItem(LINK_CATEGORY, Messages.TmfTimeTipHandler_LINK_TARGET, linkEvent.getDestinationEntry().getName());

        // This block receives a list of <String, String> values to be
        // added to the tip table
        Map<String, String> eventAddOns = fTimeGraphProvider.getEventHoverToolTipInfo(linkEvent);
        if (eventAddOns != null) {
            for (Entry<String, String> eventAddOn : eventAddOns.entrySet()) {
                addItem(LINK_CATEGORY, eventAddOn.getKey(), eventAddOn.getValue());
            }
        }
        if (fTimeGraphProvider.displayTimesInTooltip()) {
            long sourceTime = linkEvent.getTime();
            long duration = linkEvent.getDuration();
            long targetTime = sourceTime + duration;
            if (fTimeDataProvider instanceof ITimeDataProviderConverter) {
                ITimeDataProviderConverter tdp = (ITimeDataProviderConverter) fTimeDataProvider;
                sourceTime = tdp.convertTime(sourceTime);
                targetTime = tdp.convertTime(targetTime);
                duration = targetTime - sourceTime;
            }
            Resolution res = Resolution.NANOSEC;
            TimeFormat tf = fTimeDataProvider.getTimeFormat().convert();
            if (tf == TimeFormat.CALENDAR) {
                addItem(LINK_CATEGORY, Messages.TmfTimeTipHandler_TRACE_DATE, FormatTimeUtils.formatDate(sourceTime));
            }
            if (duration > 0) {
                addItem(ToolTipString.fromString(LINK_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_LINK_SOURCE_TIME), ToolTipString.fromTimestamp(FormatTimeUtils.formatTime(sourceTime, tf, res), sourceTime));
                addItem(ToolTipString.fromString(LINK_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_LINK_TARGET_TIME), ToolTipString.fromTimestamp(FormatTimeUtils.formatTime(targetTime, tf, res), targetTime));
                addItem(LINK_CATEGORY, Messages.TmfTimeTipHandler_DURATION, FormatTimeUtils.formatDelta(duration, tf, res));
            } else {
                addItem(ToolTipString.fromString(LINK_CATEGORY), ToolTipString.fromString(Messages.TmfTimeTipHandler_LINK_TIME), ToolTipString.fromTimestamp(FormatTimeUtils.formatTime(sourceTime, tf, res), sourceTime));
            }
        }
    }
}
