/**********************************************************************
 * Copyright (c) 2005, 2014 IBM 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:
 *     IBM - Initial API and implementation
 *     Bernd Hufmann - Updated for TMF
 **********************************************************************/

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

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

import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.Accessible;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleControlAdapter;
import org.eclipse.swt.accessibility.AccessibleControlEvent;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.AsyncMessage;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.AsyncMessageReturn;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.BaseMessage;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.ExecutionOccurrence;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Frame;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.GraphNode;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.ITimeRange;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Lifeline;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.Metrics;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.SDTimeEvent;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.core.SyncMessage;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.drawings.IColor;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.drawings.impl.ColorImpl;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.preferences.SDViewPref;
import org.eclipse.tracecompass.tmf.ui.views.uml2sd.util.TimeEventComparator;

/**
 * <p>
 * The time compression bar implementation.
 * </p>
 *
 * @version 1.0
 * @author sveyrier
 */
public class TimeCompressionBar extends ScrollView implements DisposeListener {

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------
    private static final int BASE_RED_VALUE = 255;
    private static final int BASE_GREEN_BLUE_VALUE = 225;
    private static final int COLOR_STEP = 25;
    private static final int NUMBER_STEPS = 10;

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

    /**
     * The listener list
     */
    private List<ITimeCompressionListener> fListenerList = null;
    /**
     * The current frame displayed.
     */
    private Frame fFrame = null;
    /**
     * List of time events.
     */
    private List<SDTimeEvent> fNodeList = null;
    /**
     * The minimum time delta.
     */
    private ITmfTimestamp fMinTime = TmfTimestamp.fromSeconds(0);
    /**
     * The maximum time delta.
     */
    private ITmfTimestamp fMaxTime = TmfTimestamp.fromSeconds(0);
    /**
     * The current zoom value.
     */
    private float fZoomValue = 1;
    /**
     * The tooltip to display.
     */
    private DrawableToolTip fTooltip = null;
    /**
     * Array of colors for displaying wight of time deltas.
     */
    private ColorImpl[] fColors;
    /**
     * The accessible object reference.
     */
    private Accessible fAccessible = null;
    /**
     * The focused widget reference.
     */
    private int fFocusedWidget = -1;
    /**
     * The current lifeline.
     */
    private Lifeline fLifeline = null;
    /**
     * The current start event value.
     */
    private int fLifelineStart = 0;
    /**
     * The current number of events.
     */
    private int fLifelineNumEvents = 0;
    /**
     * The Current color of range to display.
     */
    private IColor fLifelineColor = null;
    /**
     * The next graph node y coordinate.
     */
    private int fNextNodeY = 0;
    /**
     * The previous graph node y coordinate.
     */
    private int fPrevNodeY = 0;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------
    /**
     * Standard constructor
     *
     * @param parent
     *            The parent composite
     * @param s
     *            The style bits
     */
    public TimeCompressionBar(Composite parent, int s) {
        super(parent, s | SWT.NO_BACKGROUND, false);
        setVScrollBarMode(ScrollView.ALWAYS_OFF);
        setHScrollBarMode(ScrollView.ALWAYS_OFF);
        fListenerList = new ArrayList<>();
        fColors = new ColorImpl[NUMBER_STEPS];
        int greenBlue = BASE_GREEN_BLUE_VALUE;
        final int step = COLOR_STEP;
        for (int i = 0; i < fColors.length; i++) {
            fColors[i] = new ColorImpl(Display.getDefault(), BASE_RED_VALUE, greenBlue, greenBlue);
            greenBlue -= step;
        }
        super.addDisposeListener(this);

        fAccessible = getViewControl().getAccessible();

        fAccessible.addAccessibleListener(new AccessibleAdapter() {
            @Override
            public void getName(AccessibleEvent e) {
                // Case toolTip
                if (e.childID == 0) {
                    if (fTooltip != null) {
                        e.result = fTooltip.getAccessibleText();
                    }
                } else if (e.childID == 1) {
                    createFakeTooltip();
                    e.result = fTooltip.getAccessibleText();
                }
            }
        });

        fAccessible.addAccessibleControlListener(new AccessibleControlAdapter() {
            @Override
            public void getFocus(AccessibleControlEvent e) {
                if (fFocusedWidget == -1) {
                    e.childID = ACC.CHILDID_SELF;
                }
                else {
                    e.childID = fFocusedWidget;
                }
            }

            @Override
            public void getRole(AccessibleControlEvent e) {
                switch (e.childID) {
                case ACC.CHILDID_SELF:
                    e.detail = ACC.ROLE_CLIENT_AREA;
                    break;
                case 0:
                    e.detail = ACC.ROLE_TOOLTIP;
                    break;
                case 1:
                    e.detail = ACC.ROLE_LABEL;
                    break;
                default:
                    break;
                }
            }

            @Override
            public void getState(AccessibleControlEvent e) {
                e.detail = ACC.STATE_FOCUSABLE;
                if (e.childID == ACC.CHILDID_SELF) {
                    e.detail |= ACC.STATE_FOCUSED;
                } else {
                    e.detail |= ACC.STATE_SELECTABLE;
                    if (e.childID == fFocusedWidget) {
                        e.detail |= ACC.STATE_FOCUSED | ACC.STATE_SELECTED | ACC.STATE_CHECKED;
                    }
                }
            }
        });

        getViewControl().addTraverseListener(new LocalTraverseListener());

        addTraverseListener(new LocalTraverseListener());

        getViewControl().addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                redraw();
            }

            @Override
            public void focusLost(FocusEvent e) {
                redraw();
            }
        });
    }

    // ------------------------------------------------------------------------
    // Methods
    // ------------------------------------------------------------------------

    /**
     * Sets the focus widget
     *
     * @param newFocusShape
     *            widget reference to set
     */
    void setFocus(int newFocusShape) {
        fFocusedWidget = newFocusShape;
        if (fFocusedWidget == -1) {
            getViewControl().getAccessible().setFocus(ACC.CHILDID_SELF);
        } else {
            getViewControl().getAccessible().setFocus(fFocusedWidget);
        }
    }

    /**
     * Sets the current frame.
     *
     * @param theFrame
     *            The frame to set
     */
    public void setFrame(Frame theFrame) {
        fFrame = theFrame;
        fMinTime = fFrame.getMinTime();
        fMaxTime = fFrame.getMaxTime();
    }

    @Override
    protected void drawContents(GC gc, int clipx, int clipy, int clipw, int cliph) {
        if (fFrame == null) {
            return;
        }
        fNodeList = new ArrayList<>();
        int messageArraysStep = 1;

        if ((Metrics.getMessageFontHeigth() + Metrics.MESSAGES_NAME_SPACING * 2) * fZoomValue < Metrics.MESSAGE_SIGNIFICANT_VSPACING + 1) {
            messageArraysStep = Math.round(Metrics.MESSAGE_SIGNIFICANT_VSPACING + 1 / ((Metrics.getMessageFontHeigth() + Metrics.MESSAGES_NAME_SPACING * 2) * fZoomValue));
        }

        int firstVisible = fFrame.getFirstVisibleSyncMessage();
        if (firstVisible > 0) {
            firstVisible = firstVisible - 1;
        }
        for (int i = firstVisible; i < fFrame.syncMessageCount(); i = i + messageArraysStep) {
            SyncMessage m = fFrame.getSyncMessage(i);
            if (addMessageIfVisible(m)) {
                break;
            }
        }

        firstVisible = fFrame.getFirstVisibleSyncMessageReturn();
        if (firstVisible > 0) {
            firstVisible = firstVisible - 1;
        }
        for (int i = firstVisible; i < fFrame.syncMessageReturnCount(); i = i + messageArraysStep) {
            SyncMessage m = fFrame.getSyncMessageReturn(i);
            if (addMessageIfVisible(m)) {
                break;
            }
        }

        firstVisible = fFrame.getFirstVisibleAsyncMessage();
        if (firstVisible > 0) {
            firstVisible = firstVisible - 1;
        }
        for (int i = firstVisible; i < fFrame.asyncMessageCount(); i = i + messageArraysStep) {
            BaseMessage m = fFrame.getAsyncMessage(i);
            if (addMessageIfVisible(m)) {
                break;
            }
        }

        firstVisible = fFrame.getFirstVisibleAsyncMessageReturn();
        if (firstVisible > 0) {
            firstVisible = firstVisible - 1;
        }
        for (int i = firstVisible; i < fFrame.asyncMessageReturnCount(); i = i + messageArraysStep) {
            AsyncMessageReturn m = fFrame.getAsyncMessageReturn(i);
            if (addMessageIfVisible(m)) {
                break;
            }
        }

        List<SDTimeEvent> executionOccurrencesWithTime = fFrame.getExecutionOccurrencesWithTime();
        if (executionOccurrencesWithTime != null) {
            fNodeList.addAll(executionOccurrencesWithTime);
        }

        SDTimeEvent[] temp = fNodeList.toArray(new SDTimeEvent[fNodeList.size()]);
        Arrays.sort(temp, new TimeEventComparator());
        fNodeList = Arrays.asList(temp);

        Image dbuffer = new Image(getDisplay(), getClientArea().width, getClientArea().height);
        GC gcim = new GC(dbuffer);

        for (int i = 0; i < fNodeList.size() - 1; i++) {
            SDTimeEvent m1 = fNodeList.get(i);
            SDTimeEvent m2 = fNodeList.get(i + 1);

            if ((SDViewPref.getInstance().excludeExternalTime()) && ((m1.getGraphNode() instanceof BaseMessage) && (m2.getGraphNode() instanceof BaseMessage))) {
                BaseMessage mes1 = (BaseMessage) m1.getGraphNode();
                BaseMessage mes2 = (BaseMessage) m2.getGraphNode();
                if ((mes2.getStartLifeline() == null) || (mes1.getEndLifeline() == null)) {
                    continue;
                }
            }

            fMinTime = fFrame.getMinTime();
            fMaxTime = fFrame.getMaxTime();
            ITmfTimestamp minMaxdelta = fMaxTime.getDelta(fMinTime);
            double gr = (minMaxdelta.getValue()) / (double) NUMBER_STEPS;

            ITmfTimestamp delta = m2.getTime().getDelta(m1.getTime()).getDelta(fMinTime);
            long absDelta = Math.abs(delta.getValue());

            ColorImpl color;
            if (gr != 0) {
                int colIndex = Math.round((float) (absDelta / gr));
                if (colIndex < fColors.length && colIndex > 0) {
                    color = fColors[colIndex - 1];
                } else if (colIndex <= 0) {
                    color = fColors[0];
                } else {
                    color = fColors[fColors.length - 1];
                }
            } else {
                color = fColors[0];
            }

            if (color.getColor() instanceof Color) {
                gcim.setBackground((Color) color.getColor());
            }
            PairOfYs poy = adjustHeights(m1, m2);
            int y1 = poy.getY1();
            int y2 = poy.getY2();
            gcim.fillRectangle(contentsToViewX(0), contentsToViewY(Math.round(y1 * fZoomValue)), 10, Math.round((y2 - y1) * fZoomValue) + 1);
            if (messageArraysStep == 1) {
                Color backupColor = gcim.getForeground();
                gcim.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
                gcim.drawRectangle(contentsToViewX(0), contentsToViewY(Math.round(y1 * fZoomValue)), 9, Math.round((y2 - y1) * fZoomValue));
                gcim.setForeground(backupColor);
            }
        }
        if (getViewControl().isFocusControl() || isFocusControl()) {
            gcim.drawFocus(contentsToViewX(0), contentsToViewY(Math.round(fPrevNodeY * fZoomValue)), contentsToViewX(10), Math.round((fNextNodeY - fPrevNodeY) * fZoomValue));
        }
        try {
            gc.drawImage(dbuffer, 0, 0, getClientArea().width, getClientArea().height, 0, 0, getClientArea().width, getClientArea().height);
        } catch (Exception e) {
            Activator.getDefault().logError("Error drawing image", e); //$NON-NLS-1$
        }
        gcim.dispose();
        dbuffer.dispose();
        gc.dispose();
    }

    private boolean addMessageIfVisible(BaseMessage m) {
        if (m instanceof ITimeRange) {
            ITimeRange timeRange = (ITimeRange) m;
            if (timeRange.hasTimeInfo()) {
                SDTimeEvent t = new SDTimeEvent(timeRange.getStartTime(), m.getStartOccurrence(), timeRange);
                fNodeList.add(t);
                if (m instanceof AsyncMessage) {
                    t = new SDTimeEvent(timeRange.getEndTime(), m.getEndOccurrence(), timeRange);
                    fNodeList.add(t);
                }
                return isNotVisible(m);
            }
        }
        return false;
    }

    private boolean isNotVisible(BaseMessage m) {
        return m.getY() * fZoomValue > getContentsY() + getVisibleHeight();
    }

    private static int addHeightIfAsynchronousMessage(SDTimeEvent timeEvent, int yPos) {
        if (timeEvent.getGraphNode() instanceof AsyncMessage) {
            AsyncMessage as = (AsyncMessage) timeEvent.getGraphNode();
            if (as.getEndTime() == timeEvent.getTime()) {
                return yPos + as.getHeight();
            }
        }
        return yPos;
    }

    private static int addHeight(SDTimeEvent timeEvent, int yPos) {
        ExecutionOccurrence eo = (ExecutionOccurrence) timeEvent.getGraphNode();
        if (timeEvent.getEvent() == eo.getEndOccurrence()) {
            return yPos + eo.getHeight();
        }
        return yPos;
    }

    private static int addHeightIfExecutionOccurence(SDTimeEvent timeWidget, int yPos) {
        if (timeWidget.getGraphNode() instanceof ExecutionOccurrence) {
            ExecutionOccurrence eo2 = (ExecutionOccurrence) timeWidget.getGraphNode();
            if (timeWidget.getEvent() == eo2.getEndOccurrence()) {
                return yPos + eo2.getHeight();
            }
        }
        return yPos;
    }

    /**
     * Checks for focus of children.
     *
     * @param children
     *            Control to check
     * @return true if child is on focus else false
     */
    protected boolean checkFocusOnChilds(Control children) {
        if (children instanceof Composite) {
            Control[] child = ((Composite) children).getChildren();
            for (int i = 0; i < child.length; i++) {
                if (child[i].isFocusControl()) {
                    return true;
                }
                checkFocusOnChilds(child[i]);
            }
        }
        return false;
    }

    @Override
    public boolean isFocusControl() {
        Control[] child = getChildren();
        for (int i = 0; i < child.length; i++) {
            if (child[i].isFocusControl()) {
                return true;
            }
            checkFocusOnChilds(child[i]);
        }
        return false;
    }

    @Override
    protected void contentsMouseMoveEvent(MouseEvent event) {
        if (fTooltip != null) {
            fTooltip.hideToolTip();
        }
        super.contentsMouseMoveEvent(event);
        if (!isFocusControl() || getViewControl().isFocusControl()) {
            Control[] child = getParent().getChildren();
            for (int i = 0; i < child.length; i++) {
                if (child[i].isFocusControl()) {
                    break;
                }
            }
        }
        setFocus(-1);
    }

    @Override
    protected void contentsMouseHover(MouseEvent e) {
        if (fTooltip == null) {
            fTooltip = new DrawableToolTip(this);
        }
        if (fFrame != null) {
            setFocus(0);
            for (int i = 0; i < fNodeList.size() - 1; i++) {
                SDTimeEvent m1 = fNodeList.get(i);
                SDTimeEvent m2 = fNodeList.get(i + 1);

                if (skipIfLifelineIsNull(m1, m2)) {
                    continue;
                }
                PairOfYs poy = adjustHeights(m1, m2);

                int m1Y = Math.round(poy.getY1() * fZoomValue);
                int m2Y = Math.round(poy.getY2() * fZoomValue);
                if ((m1Y < e.y) && (m2Y >= e.y)) {
                    ITmfTimestamp delta = m2.getTime().getDelta(m1.getTime());
                    fTooltip.showToolTip(delta, fMinTime, fMaxTime);
                }
            }
        }
        setFocus(0);
    }

    @Override
    protected void contentsMouseExit(MouseEvent e) {
        if (fTooltip != null) {
            fTooltip.hideToolTip();
        }
    }

    @Override
    protected void contentsMouseUpEvent(MouseEvent event) {
        selectTimeDelta(event.y, 0);
        setFocus();
        super.contentsMouseUpEvent(event);
    }

    /**
     * Force the time compression bar to highlight the event occurrences between
     * the two given messages. The event occurrences are highlighted on the
     * first message's end lifeline
     *
     * @param mes1
     *            the first message
     * @param mes2
     *            the second message
     */
    public void highlightRegion(BaseMessage mes1, BaseMessage mes2) {
        BaseMessage localMes1 = mes1;
        BaseMessage localMes2 = mes2;

        if (fFrame == null) {
            return;
        }
        if (!(localMes1 instanceof ITimeRange)) {
            return;
        }
        if (!(localMes2 instanceof ITimeRange)) {
            return;
        }
        ITimeRange t1 = (ITimeRange) localMes1;
        ITimeRange t2 = (ITimeRange) localMes2;

        ITmfTimestamp time1 = t1.getStartTime();
        ITmfTimestamp time2 = t2.getStartTime();
        int event1 = localMes1.getEventOccurrence();
        int event2 = localMes2.getEventOccurrence();

        if (localMes1 instanceof AsyncMessage) {
            AsyncMessage as = (AsyncMessage) localMes1;
            time1 = as.getEndTime();
            event1 = as.getEndOccurrence();
        }
        if (localMes2 instanceof AsyncMessage) {
            AsyncMessage as = (AsyncMessage) localMes2;
            if (as.getEndOccurrence() > as.getStartOccurrence()) {
                time1 = as.getEndTime();
                event1 = as.getEndOccurrence();
            } else {
                time1 = as.getStartTime();
                event1 = as.getStartOccurrence();
            }
        }

        if (event1 > event2) {
            BaseMessage tempMes = localMes2;
            localMes2 = localMes1;
            localMes1 = tempMes;

            t1 = (ITimeRange) localMes1;
            t2 = (ITimeRange) localMes2;

            time1 = t1.getStartTime();
            time2 = t2.getStartTime();
            event1 = localMes1.getEventOccurrence();
            event2 = localMes2.getEventOccurrence();

            if (localMes1 instanceof AsyncMessage) {
                AsyncMessage as = (AsyncMessage) localMes1;
                time1 = as.getEndTime();
                event1 = as.getEndOccurrence();
            }
            if (localMes2 instanceof AsyncMessage) {
                AsyncMessage as = (AsyncMessage) localMes2;
                if (as.getEndOccurrence() > as.getStartOccurrence()) {
                    time1 = as.getEndTime();
                    event1 = as.getEndOccurrence();
                } else {
                    time1 = as.getStartTime();
                    event1 = as.getStartOccurrence();
                }
            }
        }

        ITmfTimestamp minMaxdelta = fMaxTime.getDelta(fMinTime);
        double gr = (minMaxdelta.getValue()) / (double) NUMBER_STEPS;

        ITmfTimestamp delta = time2.getDelta(time1).getDelta(fMinTime);
        long absDelta = Math.abs(delta.getValue());

        int colIndex = 0;
        if (gr != 0) {
            colIndex = Math.round((float) (absDelta / gr));
            if (colIndex >= fColors.length) {
                colIndex = fColors.length - 1;
            } else if (colIndex < 0) {
                colIndex = 0;
            }
        } else {
            colIndex = 0;
        }
        for (int j = 0; j < fListenerList.size(); j++) {
            ITimeCompressionListener list = fListenerList.get(j);
            if (localMes1.getEndLifeline() != null) {
                list.deltaSelected(localMes1.getEndLifeline(), event1, event2 - event1, fColors[colIndex]);
            } else if (localMes2.getStartLifeline() != null) {
                list.deltaSelected(localMes2.getStartLifeline(), event1, event2 - event1, fColors[colIndex]);
            } else {
                list.deltaSelected(localMes1.getStartLifeline(), event1, event2 - event1, fColors[colIndex]);
            }
        }
    }

    /**
     * Force the time compression bar to highlight the event occurrences between
     * the two given messages. The event occurrences are highlighted on the
     * first message's end lifeline
     *
     * @param mes1
     *            the first message
     * @param mes2
     *            the second message
     */
    public void highlightRegionSync(final BaseMessage mes1, final BaseMessage mes2) {
        getDisplay().syncExec(() -> highlightRegion(mes1, mes2));
    }

    @Override
    public void scrollBy(int x, int y) {
        // Do nothing
    }

    /**
     * Sets the zoom value.
     *
     * @param value
     *            The zoom value to set.
     */
    public void setZoom(float value) {
        fZoomValue = value;
        redraw();
    }

    /**
     * Adds a listener to the time compression listener list to be notified
     * about selected deltas.
     *
     * @param listener
     *            The listener to add
     */
    public void addTimeCompressionListener(ITimeCompressionListener listener) {
        if (!fListenerList.contains(listener)) {
            fListenerList.add(listener);
        }
    }

    /**
     * Removes a time compression listener.
     *
     * @param listener
     *            The listener to remove.
     */
    public void removeSelectionChangedListener(ITimeCompressionListener listener) {
        fListenerList.remove(listener);
    }

    @Override
    public void widgetDisposed(DisposeEvent e) {
        if (fTooltip != null) {
            fTooltip.dispose();
        }
        super.removeDisposeListener(this);
        for (int i = 0; i < fColors.length; i++) {
            fColors[i].dispose();
        }
    }

    @Override
    protected void keyPressedEvent(KeyEvent event) {
        if (fTooltip != null) {
            fTooltip.hideToolTip();
        }
        if (!isFocusControl() || getViewControl().isFocusControl()) {
            Control[] child = getParent().getChildren();
            for (int i = 0; i < child.length; i++) {
                if (child[i].isFocusControl()) {
                    break;
                }
            }
        }
        setFocus(-1);

        boolean top = false;
        if (fNextNodeY == 0) {
            top = true;
        }
        if ((fFrame != null) && (fNextNodeY == 0)) {
            for (int i = 0; i < fNodeList.size() - 1 && i < 1; i++) {
                SDTimeEvent m1 = fNodeList.get(i);
                SDTimeEvent m2 = fNodeList.get(i + 1);
                if (skipIfLifelineIsNull(m1, m2)) {
                    continue;
                }

                PairOfYs poy = adjustHeights(m1, m2);

                fPrevNodeY = Math.round(poy.getY1() * fZoomValue);
                fNextNodeY = Math.round(poy.getY2() * fZoomValue);
            }
        }

        if (fLifeline != null) {
            for (int j = 0; j < fListenerList.size(); j++) {
                ITimeCompressionListener list = fListenerList.get(j);
                list.deltaSelected(fLifeline, fLifelineStart, fLifelineNumEvents, fLifelineColor);
            }
        }

        if (event.keyCode == SWT.ARROW_DOWN) {
            if (!top) {
                selectTimeDelta(fNextNodeY + 1, 1);
            } else {
                selectTimeDelta(fPrevNodeY + 1, 1);
            }
            setFocus(1);
        } else if (event.keyCode == SWT.ARROW_UP) {
            selectTimeDelta(fPrevNodeY - 1, 2);
            setFocus(1);
        } else if (event.keyCode == SWT.ARROW_RIGHT) {
            selectTimeDelta(fPrevNodeY, 1);
            setFocus(1);
        }
        super.keyPressedEvent(event);
    }

    /**
     * Selects the time delta for given delta y coordinate and direction.
     *
     * @param dy
     *            The delta in y coordinate.
     * @param direction
     *            0 no direction, 1 = down, 2 = up
     */
    protected void selectTimeDelta(int dy, int direction) {
        SDTimeEvent lastM1 = null;
        SDTimeEvent lastM2 = null;
        int lastY1 = 0;
        int lastY2 = 0;
        boolean done = false;
        if (fFrame != null) {
            for (int i = 0; i < fNodeList.size() - 1; i++) {
                SDTimeEvent m1 = fNodeList.get(i);
                SDTimeEvent m2 = fNodeList.get(i + 1);
                if (skipIfLifelineIsNull(m1, m2)) {
                    continue;
                }

                PairOfYs poy = adjustHeights(m1, m2);

                int m1Y = Math.round(poy.getY1() * fZoomValue);
                int m2Y = Math.round(poy.getY2() * fZoomValue);

                if ((m1Y < dy) && (m2Y > dy) || (!done && m2Y > dy && direction == 1 && lastM1 != null) || (!done && m1Y > dy && direction == 2 && lastM1 != null)) {
                    if (m1Y > dy && direction == 2) {
                        m1 = lastM1;
                        m2 = lastM2;
                        m1Y = lastY1;
                        m2Y = lastY2;
                    }
                    done = true;
                    fPrevNodeY = m1Y;
                    fNextNodeY = m2Y;
                    ITmfTimestamp minMaxdelta = fMaxTime.getDelta(fMinTime);
                    double gr = (minMaxdelta.getValue()) / (double) NUMBER_STEPS;

                    ITmfTimestamp delta = m2.getTime().getDelta(m1.getTime()).getDelta(fMinTime);
                    long absDelta = Math.abs(delta.getValue());

                    int colIndex = 0;
                    if (gr != 0) {
                        colIndex = Math.round((float) (absDelta / gr));
                        if (colIndex >= fColors.length) {
                            colIndex = fColors.length - 1;
                        } else if (colIndex < 0) {
                            colIndex = 0;
                        }
                    } else {
                        colIndex = 0;
                    }
                    if (m1.getGraphNode() instanceof BaseMessage) {
                        BaseMessage mes1 = (BaseMessage) m1.getGraphNode();
                        if (mes1.getEndLifeline() != null) {
                            fLifeline = mes1.getEndLifeline();
                            fLifelineStart = m1.getEvent();
                            fLifelineNumEvents = m2.getEvent() - m1.getEvent();
                            fLifelineColor = fColors[colIndex];
                        } else if (m2.getGraphNode() instanceof BaseMessage && ((BaseMessage) m2.getGraphNode()).getStartLifeline() != null) {
                            fLifeline = ((BaseMessage) m2.getGraphNode()).getStartLifeline();
                            fLifelineStart = m1.getEvent();
                            fLifelineNumEvents = m2.getEvent() - m1.getEvent();
                            fLifelineColor = fColors[colIndex];
                        } else {
                            fLifeline = mes1.getStartLifeline();
                            fLifelineStart = m1.getEvent();
                            fLifelineNumEvents = m2.getEvent() - m1.getEvent();
                            fLifelineColor = fColors[colIndex];
                        }
                    } else if (m1.getGraphNode() instanceof ExecutionOccurrence) {
                        if (m2.getGraphNode() instanceof ExecutionOccurrence) {
                            ExecutionOccurrence eo = (ExecutionOccurrence) m2.getGraphNode();
                            fLifeline = eo.getLifeline();
                            fLifelineStart = m1.getEvent();
                            fLifelineNumEvents = m2.getEvent() - m1.getEvent();
                            fLifelineColor = fColors[colIndex];
                        } else {
                            ExecutionOccurrence eo = (ExecutionOccurrence) m1.getGraphNode();
                            fLifeline = eo.getLifeline();
                            fLifelineStart = m1.getEvent();
                            fLifelineNumEvents = m2.getEvent() - m1.getEvent();
                            fLifelineColor = fColors[colIndex];
                        }
                    }
                    for (int j = 0; j < fListenerList.size(); j++) {
                        ITimeCompressionListener list = fListenerList.get(j);
                        list.deltaSelected(fLifeline, fLifelineStart, fLifelineNumEvents, fLifelineColor);
                    }
                    break;
                }
                lastM1 = m1;
                lastM2 = m2;
                lastY1 = m1Y;
                lastY2 = m2Y;
            }
        }
    }

    /**
     * Creates a fake tool tip.
     */
    protected void createFakeTooltip() {
        if (fTooltip == null) {
            fTooltip = new DrawableToolTip(this);
        }

        if (fFrame != null) {
            setFocus(0);
            for (int i = 0; i < fNodeList.size() - 1; i++) {
                SDTimeEvent m1 = fNodeList.get(i);
                SDTimeEvent m2 = fNodeList.get(i + 1);
                if (skipIfLifelineIsNull(m1, m2)) {
                    continue;
                }
                PairOfYs poy = adjustHeights(m1, m2);
                int m1Y = Math.round(poy.getY1() * fZoomValue);
                int m2Y = Math.round(poy.getY2() * fZoomValue);
                if ((m1Y < fPrevNodeY + 1) && (m2Y >= fPrevNodeY + 1)) {
                    ITmfTimestamp delta = m2.getTime().getDelta(m1.getTime());
                    fTooltip.showToolTip(delta, fMinTime, fMaxTime);
                    fTooltip.hideToolTip();
                }
            }
        }
    }

    private static PairOfYs adjustHeights(SDTimeEvent m1, SDTimeEvent m2) {
        int y1 = ((GraphNode) m1.getGraphNode()).getY();
        int y2 = ((GraphNode) m2.getGraphNode()).getY();

        y1 = addHeightIfAsynchronousMessage(m1, y1);
        y2 = addHeightIfAsynchronousMessage(m2, y2);
        if (m1.getGraphNode() instanceof ExecutionOccurrence) {
            y1 = addHeight(m1, y1);
            y2 = addHeightIfExecutionOccurence(m2, y2);
        }
        return new PairOfYs(y1, y2);
    }

    private static class PairOfYs {
        private final int fY1, fY2;

        public PairOfYs(int y1, int y2) {
            fY1 = y1;
            fY2 = y2;
        }

        public int getY1() {
            return fY1;
        }

        public int getY2() {
            return fY2;
        }

    }

    private static boolean skipIfLifelineIsNull(SDTimeEvent m1, SDTimeEvent m2) {
        if ((SDViewPref.getInstance().excludeExternalTime()) && ((m1.getGraphNode() instanceof BaseMessage) && (m2.getGraphNode() instanceof BaseMessage))) {
            BaseMessage mes1 = (BaseMessage) m1.getGraphNode();
            BaseMessage mes2 = (BaseMessage) m2.getGraphNode();
            if ((mes2.getStartLifeline() == null) || (mes1.getEndLifeline() == null)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Traverse Listener implementation.
     */
    protected static class LocalTraverseListener implements TraverseListener {
        @Override
        public void keyTraversed(TraverseEvent e) {
            if ((e.detail == SWT.TRAVERSE_TAB_NEXT) || (e.detail == SWT.TRAVERSE_TAB_PREVIOUS)) {
                e.doit = true;
            }
        }
    }
}
