/*******************************************************************************
 * Copyright (c) 2010, 2020 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.widgets.rawviewer;

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

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.trace.ITmfContext;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.themes.IThemeManager;

/**
 * TmfRawEventViewer allows for the display of the raw data for an arbitrarily
 * large number of TMF events.
 *
 * It is essentially a Composite of a StyledText area and a Slider, where the number
 * of visible lines in the StyledText control is set to fill the viewer display area.
 * An underlying data model is used to store a cache of event raw text line data.
 * The slider is ratio-based.
 *
 * @version 1.0
 * @author Patrick Tasse
 */
public class TmfRawEventViewer extends Composite implements ControlListener, SelectionListener, MouseListener,
        KeyListener, CaretListener, MouseMoveListener, MouseTrackListener, MouseWheelListener, IPropertyChangeListener {

    private static final Color COLOR_BACKGROUND_ODD = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE);
    private static final Color COLOR_BACKGROUND_EVEN = new Color(Display.getDefault(), 242, 242, 242);
    private static final String FONT_DEFINITION_ID = "org.eclipse.tracecompass.tmf.ui.font.eventraw"; //$NON-NLS-1$
    private static final String HIGHLIGHT_COLOR_DEFINITION_ID = "org.eclipse.tracecompass.tmf.ui.color.eventraw.highlight"; //$NON-NLS-1$
    private static final String SELECTION_COLOR_DEFINITION_ID = "org.eclipse.tracecompass.tmf.ui.color.eventraw.selection"; //$NON-NLS-1$
    private static final int MAX_LINE_DATA_SIZE = 1000;
    private static final int SLIDER_MAX = 1000000;
    private static final String EMPTY_STRING = ""; //$NON-NLS-1$
    private static final String LF = "\n"; //$NON-NLS-1$
    private static final @NonNull String CR_LF = "\r?\n"; //$NON-NLS-1$

    private ITmfTrace fTrace;
    private ITmfContext fBottomContext;

    private ScrolledComposite fScrolledComposite;
    private Composite fTextArea;
    private StyledText fStyledText;
    private Font fFixedFont;
    private Color fHighlightColor;
    private Color fSelectionColor;
    private Slider fSlider;
    private SliderThrottler fSliderThrottler;

    private final List<LineData> fLines = new ArrayList<>();
    private boolean fActualRanks = false;
    private int fTopLineIndex;
    private int fLastTopLineIndex;
    private final CaretPosition[] fStoredCaretPosition = new CaretPosition[]
                { new CaretPosition(0, 0), new CaretPosition(0,0)};
    private int fNumVisibleLines;
    private ITmfLocation fSelectedLocation = null;
    private long fHighlightedRank = Long.MIN_VALUE;
    private int fCursorYCoordinate = -1;
    private int fHoldSelection = 0;

    // ------------------------------------------------------------------------
    // Classes
    // ------------------------------------------------------------------------

    private static class LineData {
        long rank;
        ITmfLocation location;
        String string;
        public LineData(long rank, ITmfLocation location, String string) {
            this.rank = rank;
            this.location = location;
            if (string.length() == 0) {
                /* workaround for setLineBackground has no effect on empty line */
                this.string = " "; //$NON-NLS-1$
            } else {
                this.string = string;
            }
        }
        @Override
        public String toString() {
            return rank + " [" + location + "]: " + string; //$NON-NLS-1$ //$NON-NLS-2$
        }
    }

    private static class CaretPosition {
        int time;
        int caretOffset;
        public CaretPosition(int time, int caretOffset) {
            this.time = time;
            this.caretOffset = caretOffset;
        }
    }

    private class SliderThrottler extends Thread {
        private static final long DELAY = 400L;
        private static final long POLLING_INTERVAL = 10L;

        @Override
        public void run() {
            final long startTime = System.currentTimeMillis();
            while ((System.currentTimeMillis() - startTime) < DELAY) {
                try {
                    Thread.sleep(POLLING_INTERVAL);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            Display.getDefault().asyncExec(() -> {
                if (fSliderThrottler != SliderThrottler.this) {
                    return;
                }
                fSliderThrottler = null;
                if (SliderThrottler.this.isInterrupted() || fSlider.isDisposed()) {
                    return;
                }
                Event event = new Event();
                event.widget = TmfRawEventViewer.this;
                event.detail = SWT.NONE;
                widgetSelected(new SelectionEvent(event));
            });
        }
    }

    // ------------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------------

    /**
     * Constructor
     * @param parent The parent composite
     * @param style The style bits
     */
    public TmfRawEventViewer(Composite parent, int style) {
        super(parent, style & (~SWT.H_SCROLL) & (~SWT.V_SCROLL));

        // Set the layout
        GridLayout gridLayout = new GridLayout();
        gridLayout.numColumns = 2;
        gridLayout.horizontalSpacing = 0;
        gridLayout.verticalSpacing = 0;
        gridLayout.marginWidth = 0;
        gridLayout.marginHeight = 0;
        setLayout(gridLayout);

        // Create the controls
        createTextArea(style & SWT.H_SCROLL);
        createSlider(style & SWT.V_SCROLL);

        // Prevent the slider from being traversed
        setTabList(new Control[] { fScrolledComposite });

        addDisposeListener((e) -> {
            if (fBottomContext != null) {
                fBottomContext.dispose();
            }
            PlatformUI.getWorkbench().getThemeManager().removePropertyChangeListener(TmfRawEventViewer.this);
        });
    }

    // ------------------------------------------------------------------------
    // Font and color handling
    // ------------------------------------------------------------------------

    /**
     * Initialize the fonts.
     * @since 1.0
     */
    protected void initializeFonts() {
        FontRegistry fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
        fFixedFont = fontRegistry.get(FONT_DEFINITION_ID);
        fStyledText.setFont(fFixedFont);
    }

    /**
     * Initialize the colors.
     * @since 1.1
     */
    protected void initializeColors() {
        ColorRegistry colorRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry();
        fHighlightColor = colorRegistry.get(HIGHLIGHT_COLOR_DEFINITION_ID);
        fSelectionColor = colorRegistry.get(SELECTION_COLOR_DEFINITION_ID);
    }

    /**
     * @since 1.0
     */
    @Override
    public void propertyChange(PropertyChangeEvent event) {
        if ((IThemeManager.CHANGE_CURRENT_THEME.equals(event.getProperty())) ||
                (FONT_DEFINITION_ID.equals(event.getProperty()))) {
            initializeFonts();
            refreshTextArea();
        }
        if ((IThemeManager.CHANGE_CURRENT_THEME.equals(event.getProperty())) ||
                (HIGHLIGHT_COLOR_DEFINITION_ID.equals(event.getProperty())) ||
                (SELECTION_COLOR_DEFINITION_ID.equals(event.getProperty()))) {
            initializeColors();
            refreshTextArea();
        }
    }

    // ------------------------------------------------------------------------
    // Text area handling
    // ------------------------------------------------------------------------

    /**
     * Create the text area and add listeners
     */
    private void createTextArea(int style) {
        fScrolledComposite = new ScrolledComposite(this, style);
        fScrolledComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        fTextArea = new Composite(fScrolledComposite, SWT.NONE);
        fTextArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        fScrolledComposite.setContent(fTextArea);
        fScrolledComposite.setExpandHorizontal(true);
        fScrolledComposite.setExpandVertical(true);
        fScrolledComposite.setAlwaysShowScrollBars(true);
        fScrolledComposite.setMinSize(fTextArea.computeSize(SWT.DEFAULT, SWT.DEFAULT));
        fScrolledComposite.addControlListener(this);

        GridLayout textAreaGridLayout = new GridLayout();
        textAreaGridLayout.marginHeight = 0;
        textAreaGridLayout.marginWidth = 0;
        fTextArea.setLayout(textAreaGridLayout);

        fStyledText = new StyledText(fTextArea, SWT.READ_ONLY);
        fStyledText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

        initializeFonts();
        initializeColors();
        PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(this);

        fStyledText.addCaretListener(this);
        fStyledText.addMouseMoveListener(this);
        fStyledText.addMouseTrackListener(this);
        fStyledText.addMouseWheelListener(this);
        /* disable mouse scroll of horizontal scroll bar */
        fStyledText.addListener(SWT.MouseWheel, event -> event.doit = false);
        fStyledText.addKeyListener(this);

        fTextArea.setBackground(fStyledText.getBackground());
        fTextArea.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                fTextArea.setFocus();
            }
        });
    }

    // ------------------------------------------------------------------------
    // Slider handling
    // ------------------------------------------------------------------------

    private void createSlider(int style) {
        fSlider = new Slider(this, SWT.VERTICAL);
        fSlider.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
        fSlider.setValues(0, 0, SLIDER_MAX, SLIDER_MAX, 1, 1);
        fSlider.addSelectionListener(this);
        fSlider.addMouseListener(this);
        if ((style & SWT.V_SCROLL) == 0) {
            fSlider.setVisible(false);
        }
    }

    // ------------------------------------------------------------------------
    // Controls interactions
    // ------------------------------------------------------------------------

    @Override
    public boolean setFocus() {
        boolean isVisible = isVisible();
        if (isVisible) {
            fTextArea.setFocus();
        }
        return isVisible;
    }

    @Override
    public void setMenu(Menu menu) {
        fStyledText.setMenu(menu);
    }

    /**
     * Sets the trace and updates the content
     * @param trace The trace to set
     */
    public void setTrace(ITmfTrace trace) {
        fTrace = trace;
        fTopLineIndex = 0;
        fLines.clear();
        refreshEventCount();
    }

    /**
     * Refreshes the event count, updates the slider thumb and loads display
     */
    public void refreshEventCount() {
        if (fTrace != null) {
            if (fTrace.getNbEvents() > 0) {
                fSlider.setThumb((int) Math.max(SLIDER_MAX / fTrace.getNbEvents(), 1));
            } else {
                fSlider.setThumb(SLIDER_MAX);
            }

            if (!isVisible()) {
                return;
            }

            if (fLines.isEmpty()) {
                setTopRank(0);
            } else if (fLines.size() < fNumVisibleLines) {
                if (fBottomContext != null) {
                    fBottomContext.dispose();
                    fBottomContext = null;
                }
                loadLineData();
                fillTextArea();
                fSlider.setSelection((int) (SLIDER_MAX * fTrace.getLocationRatio(fLines.get(fTopLineIndex).location)));
            }
        } else {
            if (fBottomContext != null) {
                fBottomContext.dispose();
                fBottomContext = null;
            }
            fillTextArea();
            fSlider.setThumb(SLIDER_MAX);
            fSlider.setSelection(0);
        }
    }

    /**
     * Selects the event of given rank and makes it visible.
     * @param rank The rank of event
     */
    public void selectAndReveal(long rank) {
        if (fTrace == null || !isVisible()) {
            return;
        }
        if (fActualRanks && fTopLineIndex < fLines.size() && rank >= fLines.get(fTopLineIndex).rank) {
            int lastVisibleIndex = Math.min(fTopLineIndex + fNumVisibleLines, fLines.size()) - 1;
            if (rank <= fLines.get(lastVisibleIndex).rank) {
                for (int i = fTopLineIndex; i < fLines.size(); i++) {
                    if (fLines.get(i).rank == rank) {
                        fSelectedLocation = fLines.get(i).location;
                        break;
                    }
                }
                refreshLineBackgrounds();
                return;
            }
        }
        setTopRank(rank);
        if (!fLines.isEmpty() && fHoldSelection == 0) {
            fSelectedLocation = fLines.get(0).location;
            refreshLineBackgrounds();
        }
    }

    /**
     * Add a selection listener
     * @param listener A listener to add
     */
    public void addSelectionListener(Listener listener) {
        checkWidget();
        if (listener == null) {
            SWT.error (SWT.ERROR_NULL_ARGUMENT);
        }
        addListener (SWT.Selection, listener);
    }

    /**
     * Remove selection listener
     * @param listener A listener to remove
     */
    public void removeSelectionListener(Listener listener) {
        checkWidget();
        if (listener == null) {
            SWT.error (SWT.ERROR_NULL_ARGUMENT);
        }
        removeListener(SWT.Selection, listener);
    }

    private void sendSelectionEvent(LineData lineData) {
        Event event = new Event();
        if (fActualRanks) {
            event.data = Long.valueOf(lineData.rank);
        } else {
            event.data = lineData.location;
        }
        notifyListeners(SWT.Selection, event);
    }

    private void setTopRank(long rank) {
        if (fBottomContext != null) {
            fBottomContext.dispose();
        }
        fBottomContext = fTrace.seekEvent(rank);
        if (fBottomContext == null) {
            return;
        }
        fLines.clear();
        fActualRanks = true;
        fTopLineIndex = 0;
        loadLineData();
        refreshTextArea();
        if (fLines.isEmpty()) {
            fSlider.setSelection(0);
        } else {
            fSlider.setSelection((int) (SLIDER_MAX * fTrace.getLocationRatio(fLines.get(fTopLineIndex).location)));
        }
    }

    private void setTopPosition(double ratio) {
        if (fBottomContext != null) {
            fBottomContext.dispose();
        }
        fBottomContext = fTrace.seekEvent(ratio);
        if (fBottomContext == null) {
            return;
        }
        fBottomContext.setRank(0);
        fLines.clear();
        fActualRanks = false;
        fTopLineIndex = 0;
        loadLineData();
        refreshTextArea();
    }

    private void loadLineData() {
        if (fTopLineIndex < 0) {
            if (!fLines.isEmpty() && fTrace.getLocationRatio(fLines.get(0).location) > 0) {
                double lastRatio = fTrace.getLocationRatio(fLines.get(fLines.size() - 1).location);
                double firstRatio = fTrace.getLocationRatio(fLines.get(0).location);
                double delta;
                boolean singleEvent = false;
                if (firstRatio != lastRatio) {
                    // approximate ratio of at least 20 items
                    delta = Math.max(20, fNumVisibleLines) * (lastRatio - firstRatio) / (fLines.size() - 1);
                } else {
                    delta = Math.pow(10, -15);
                    singleEvent = true;
                }
                while (fTopLineIndex < 0) {
                    ITmfLocation endLocation = fLines.get(0).location;
                    firstRatio = Math.max(0, firstRatio - delta);
                    ITmfContext context = fTrace.seekEvent(firstRatio);
                    ITmfLocation location;
                    int index = 0;
                    long rank = 0;
                    while (!context.getLocation().equals(endLocation)) {
                        location = context.getLocation();
                        ITmfEvent event = fTrace.getNext(context);
                        if (event == null) {
                            break;
                        }
                        if (event.getContent() != null && event.getContent().getValue() != null) {
                            String[] lines = event.getContent().getValue().toString().split(CR_LF);
                            for (int i = 0; i < lines.length; i++) {
                                String line = lines[i];
                                LineData lineData = new LineData(rank, location, line);
                                fLines.add(index++, lineData);
                                fTopLineIndex++;
                                fLastTopLineIndex++;
                            }
                        } else {
                            LineData lineData = new LineData(rank, location, EMPTY_STRING);
                            fLines.add(index++, lineData);
                            fTopLineIndex++;
                            fLastTopLineIndex++;
                        }
                        rank++;
                    }
                    context.dispose();
                    long rankOffset = fLines.get(index).rank - rank;
                    for (int i = 0; i < index; i++) {
                        fLines.get(i).rank += rankOffset;
                    }
                    if (firstRatio == 0) {
                        break;
                    }
                    if (singleEvent) {
                        delta = Math.min(delta * 10, 0.1);
                    }
                }
            }
            if (fTopLineIndex < 0) {
                fTopLineIndex = 0;
            }
        }

        while (fLines.size() - fTopLineIndex < fNumVisibleLines) {
            if (fBottomContext == null) {
                if (fLines.isEmpty()) {
                    fBottomContext = fTrace.seekEvent(0);
                } else {
                    fBottomContext = fTrace.seekEvent(fLines.get(fLines.size() - 1).location);
                    fTrace.getNext(fBottomContext);
                }
                if (fBottomContext == null) {
                    break;
                }
            }
            long rank = fBottomContext.getRank();
            ITmfLocation location = fBottomContext.getLocation() != null ? fBottomContext.getLocation() : null;
            ITmfEvent event = fTrace.getNext(fBottomContext);
            if (event == null) {
                break;
            }
            if (event.getContent() != null && event.getContent().getValue() != null) {
                for (String line : event.getContent().getValue().toString().split(CR_LF)) {
                    int crPos;
                    if ((crPos = line.indexOf('\r')) != -1) {
                        line = line.substring(0, crPos);
                    }
                    LineData lineData = new LineData(rank, location, line);
                    fLines.add(lineData);
                }
            } else {
                LineData lineData = new LineData(rank, location, EMPTY_STRING);
                fLines.add(lineData);
            }
        }
        fTopLineIndex = Math.max(0, Math.min(fTopLineIndex, fLines.size() - 1));

        if (fLines.size() > MAX_LINE_DATA_SIZE) {
            if (fTopLineIndex < MAX_LINE_DATA_SIZE / 2) {
                long rank = fLines.get(MAX_LINE_DATA_SIZE - 1).rank;
                for (int i = MAX_LINE_DATA_SIZE; i < fLines.size(); i++) {
                    if (fLines.get(i).rank > rank) {
                        fLines.subList(i, fLines.size()).clear();
                        if (fBottomContext != null) {
                            fBottomContext.dispose();
                            fBottomContext = null;
                        }
                        break;
                    }
                }
            } else {
                long rank = fLines.get(fLines.size() - MAX_LINE_DATA_SIZE).rank;
                for (int i = fLines.size() - MAX_LINE_DATA_SIZE - 1; i >= 0; i--) {
                    if (fLines.get(i).rank < rank) {
                        fLines.subList(0, i + 1).clear();
                        fTopLineIndex -= (i + 1);
                        fLastTopLineIndex -= (i + 1);
                        break;
                    }
                }
            }
        }
    }

    private void refreshTextArea() {
        fStyledText.setText(EMPTY_STRING);
        for (int i = 0; i < fLines.size() - fTopLineIndex && i < fNumVisibleLines; i++) {
            if (i > 0)
             {
                fStyledText.append(LF);
            }
            LineData lineData = fLines.get(fTopLineIndex + i);
            fStyledText.append(lineData.string);
            setLineBackground(i, lineData);
        }
        fTextArea.layout();
        fScrolledComposite.setMinSize(fTextArea.computeSize(SWT.DEFAULT, SWT.DEFAULT));
        fLastTopLineIndex = fTopLineIndex;
    }

    private void fillTextArea() {
        int nextLine = fStyledText.getCharCount() == 0 ? 0 : fStyledText.getLineCount();
        for (int i = nextLine; i < fLines.size() - fTopLineIndex && i < fNumVisibleLines; i++) {
            if (i > 0)
             {
                fStyledText.append(LF);
            }
            LineData lineData = fLines.get(fTopLineIndex + i);
            fStyledText.append(lineData.string);
            setLineBackground(i, lineData);
        }
        int endLine = Math.min(fNumVisibleLines, fLines.size());
        if (endLine < fStyledText.getLineCount()) {
            int endOffset = fStyledText.getOffsetAtLine(endLine) - 1;
            if (endOffset > fStyledText.getCharCount()) {
                fHoldSelection++;
                fStyledText.replaceTextRange(endOffset, fStyledText.getCharCount() - endOffset, EMPTY_STRING);
                fHoldSelection--;
            }
        }
        fTextArea.layout();
        fScrolledComposite.setMinSize(fTextArea.computeSize(SWT.DEFAULT, SWT.DEFAULT));
    }

    private void updateTextArea() {
        if (fTopLineIndex < fLastTopLineIndex) {
            StringBuilder insertedText = new StringBuilder();
            for (int i = fTopLineIndex; i < fLastTopLineIndex; i++) {
                insertedText.append(fLines.get(i).string).append(LF);
            }
            fStyledText.replaceTextRange(0, 0, insertedText.toString());
            for (int i = 0; i < fLastTopLineIndex - fTopLineIndex; i++) {
                LineData lineData = fLines.get(fTopLineIndex + i);
                setLineBackground(i, lineData);
            }
            fLastTopLineIndex = fTopLineIndex;
        } else if (fTopLineIndex > fLastTopLineIndex) {
            int length = 0;
            for (int i = 0; i < fTopLineIndex - fLastTopLineIndex && i < fNumVisibleLines; i++) {
                length += fLines.get(i + fLastTopLineIndex).string.length();
                if (i < fStyledText.getLineCount()) {
                    length += 1;
                }
            }
            fStyledText.replaceTextRange(0, length, EMPTY_STRING);
            fLastTopLineIndex = fTopLineIndex;
            fillTextArea();
        }
        int endLine = Math.min(fNumVisibleLines, fLines.size());
        if (endLine < fStyledText.getLineCount()) {
            int endOffset = fStyledText.getOffsetAtLine(endLine) - 1;
            if (endOffset > fStyledText.getCharCount()) {
                fStyledText.replaceTextRange(endOffset, fStyledText.getCharCount() - endOffset, EMPTY_STRING);
            }
        }
        fTextArea.layout();
        fScrolledComposite.setMinSize(fTextArea.computeSize(SWT.DEFAULT, SWT.DEFAULT));
    }

    private void refreshLineBackgrounds() {
        for (int i = 0; (i < fStyledText.getLineCount()) && (i < fNumVisibleLines) && (i < fLines.size() - fTopLineIndex); i++) {
            LineData lineData = fLines.get(fTopLineIndex + i);
            setLineBackground(i, lineData);
        }
    }

    private void setLineBackground(int index, LineData lineData) {
        if (lineData.location.equals(fSelectedLocation)) {
            fStyledText.setLineBackground(index, 1, fSelectionColor);
        } else if (lineData.rank == fHighlightedRank) {
            fStyledText.setLineBackground(index, 1, fHighlightColor);
        } else if (lineData.rank % 2 == 0) {
            fStyledText.setLineBackground(index, 1, COLOR_BACKGROUND_EVEN);
        } else {
            fStyledText.setLineBackground(index, 1, COLOR_BACKGROUND_ODD);
        }
    }

    private void storeCaretPosition(int time, int caretOffset) {
        if (fStoredCaretPosition[0].time == time) {
            fStoredCaretPosition[0].caretOffset = caretOffset;
        } else {
            fStoredCaretPosition[1] = fStoredCaretPosition[0];
            fStoredCaretPosition[0] = new CaretPosition(time, caretOffset);
        }
    }

    private int getPreviousCaretOffset(int time) {
        if (fStoredCaretPosition[0].time == time) {
            return fStoredCaretPosition[1].caretOffset;
        }
        return fStoredCaretPosition[0].caretOffset;
    }

    private void updateHighlightedRank() {
        if (fCursorYCoordinate < 0 || fCursorYCoordinate > fStyledText.getSize().y) {
            if (fHighlightedRank != Long.MIN_VALUE) {
                fHighlightedRank = Long.MIN_VALUE;
                refreshLineBackgrounds();
            }
            return;
        }
        int offset = fStyledText.getOffsetAtPoint(new Point(0, fCursorYCoordinate));
        int line = fStyledText.getLineAtOffset(offset);
        if (line < fLines.size() - fTopLineIndex) {
            LineData lineData = fLines.get(fTopLineIndex + line);
            if (fHighlightedRank != lineData.rank) {
                fHighlightedRank = lineData.rank;
                refreshLineBackgrounds();
            }
        } else {
            if (fHighlightedRank != Long.MIN_VALUE) {
                fHighlightedRank = Long.MIN_VALUE;
                refreshLineBackgrounds();
            }
        }
    }

    // ------------------------------------------------------------------------
    // ControlListener (ScrolledComposite)
    // ------------------------------------------------------------------------

    @Override
    public void controlResized(ControlEvent event) {
        int areaHeight = fScrolledComposite.getSize().y;
        if (fScrolledComposite.getHorizontalBar() != null) {
            areaHeight -= fScrolledComposite.getHorizontalBar().getSize().y;
        }
        int lineHeight = fStyledText.getLineHeight();
        fNumVisibleLines = Math.max((areaHeight + lineHeight - 1) / lineHeight, 1);

        if (fBottomContext != null) {
            loadLineData();
            fillTextArea();
        }
    }

    @Override
    public void controlMoved(ControlEvent e) {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // SelectionListener (Slider)
    // ------------------------------------------------------------------------

    @Override
    public void widgetSelected(SelectionEvent e) {
        fTextArea.setFocus();
        if (fLines.isEmpty()) {
            return;
        }
        fHoldSelection++;
        switch (e.detail) {
            case SWT.DRAG:
            case SWT.NONE: {
                if (e.widget == fSlider) {
                    /*
                     * While the slider thumb is being dragged, only perform the
                     * refresh periodically. The event detail during the drag is
                     * SWT.DRAG on Windows and SWT.NONE on Linux.
                     */
                    if (fSliderThrottler == null) {
                        fSliderThrottler = new SliderThrottler();
                        fSliderThrottler.start();
                    }
                    fHoldSelection = 0;
                    return;
                }
                /*
                 * The selection event was sent by the viewer, refresh now.
                 */
                if (fSlider.getSelection() == 0 || fSlider.getThumb() == SLIDER_MAX) {
                    fLines.clear();
                    setTopPosition(0.0);
                    break;
                }
                double ratio = (double) fSlider.getSelection() / (SLIDER_MAX - fSlider.getThumb());
                double delta = Math.pow(10, -15);
                fLines.clear();
                while (fLines.isEmpty()) {
                    setTopPosition(ratio);
                    if (ratio == 0.0) {
                        break;
                    }
                    delta = Math.min(delta * 10, 0.1);
                    ratio = Math.max(ratio - delta, 0.0);
                }
                break;
            }
            case SWT.ARROW_DOWN: {
                if (fTopLineIndex >= fLines.size()) {
                    break;
                }
                fTopLineIndex++;
                loadLineData();
                updateTextArea();
                break;
            }
            case SWT.PAGE_DOWN: {
                fTopLineIndex += Math.max(fNumVisibleLines - 1, 1);
                loadLineData();
                updateTextArea();
                break;
            }
            case SWT.ARROW_UP: {
                if (fLines.isEmpty()) {
                    break;
                }
                fTopLineIndex--;
                loadLineData();
                updateTextArea();
                break;
            }
            case SWT.PAGE_UP: {
                fTopLineIndex -= Math.max(fNumVisibleLines - 1, 1);
                loadLineData();
                updateTextArea();
                break;
            }
            case SWT.HOME: {
                setTopPosition(0.0);
                break;
            }
            case SWT.END: {
                double ratio = 1.0;
                double delta = Math.pow(10, -15);
                fLines.clear();
                while (fLines.isEmpty()) {
                    setTopPosition(ratio);
                    if (ratio == 0.0) {
                        break;
                    }
                    delta = Math.min(delta * 10, 0.1);
                    ratio = Math.max(ratio - delta, 0.0);
                }
                break;
            }
            default:
                break;
        }
        if (e.detail != SWT.NONE) {
            fSlider.setSelection((int) (SLIDER_MAX * fTrace.getLocationRatio(fLines.get(fTopLineIndex).location)));
        }

        fHoldSelection = 0;
    }

    @Override
    public void widgetDefaultSelected(SelectionEvent e) {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // MouseListener (Slider)
    // ------------------------------------------------------------------------

    /**
     * @since 1.1
     */
    @Override
    public void mouseDown(MouseEvent e) {
        // Do nothing
    }

    /**
     * @since 1.1
     */
    @Override
    public void mouseUp(MouseEvent e) {
        if (e.button != 1) {
            return;
        }
        /*
         * When the mouse button is released, perform the refresh immediately
         * and interrupt and discard the slider throttler.
         */
        if (fSliderThrottler != null) {
            fSliderThrottler.interrupt();
            fSliderThrottler = null;
        }
        Event event = new Event();
        event.widget = this;
        event.detail = SWT.NONE;
        widgetSelected(new SelectionEvent(event));
    }

    /**
     * @since 1.1
     */
    @Override
    public void mouseDoubleClick(MouseEvent e) {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // KeyListener (StyledText)
    // ------------------------------------------------------------------------

    @Override
    public void keyPressed(KeyEvent e) {
        if (fLines.isEmpty()) {
            return;
        }
        int caretOffset = fStyledText.getCaretOffset();
        int previousCaretOffset = getPreviousCaretOffset(e.time);
        int previousLineAtCaretPosition = fStyledText.getLineAtOffset(previousCaretOffset);
        int previousColumnAtCaretPosition = getPreviousCaretOffset(e.time) - fStyledText.getOffsetAtLine(previousLineAtCaretPosition);
        switch (e.keyCode) {
            case SWT.ARROW_DOWN: {
                if (previousLineAtCaretPosition < (fNumVisibleLines - 2)) {
                    break;
                }
                fHoldSelection++;
                fTopLineIndex++;
                loadLineData();
                updateTextArea();
                fHoldSelection--;
                LineData lineData = fLines.get(fTopLineIndex + fStyledText.getLineAtOffset(fStyledText.getCaretOffset()));
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                break;
            }
            case SWT.PAGE_DOWN: {
                if (previousLineAtCaretPosition >= (fNumVisibleLines - 1)) {
                    fHoldSelection++;
                    if (fLines.get(fTopLineIndex + previousLineAtCaretPosition).rank % 2 == 0) {
                        fStyledText.setLineBackground(previousLineAtCaretPosition, 1, COLOR_BACKGROUND_EVEN);
                    } else {
                        fStyledText.setLineBackground(previousLineAtCaretPosition, 1, COLOR_BACKGROUND_ODD);
                    }
                    fSelectedLocation = null;
                    fTopLineIndex += Math.max(fNumVisibleLines - 1, 1);
                    loadLineData();
                    updateTextArea();
                    fHoldSelection--;
                }
                int line = Math.min(fNumVisibleLines - 1, fStyledText.getLineCount() - 1);
                int offset = fStyledText.getOffsetAtLine(line);
                fStyledText.setSelection(offset + Math.min(previousColumnAtCaretPosition, fLines.get(fTopLineIndex + line).string.length()));
                break;
            }
            case SWT.ARROW_RIGHT: {
                if (previousCaretOffset < fStyledText.getCharCount() || previousLineAtCaretPosition < (fNumVisibleLines - 2)) {
                    break;
                }
                fHoldSelection++;
                fTopLineIndex++;
                loadLineData();
                updateTextArea();
                fHoldSelection--;
                fStyledText.setSelection(fStyledText.getCaretOffset() + 1);
                break;
            }
            case SWT.ARROW_UP: {
                if (previousLineAtCaretPosition > 0) {
                    break;
                }
                if (fLines.isEmpty()) {
                    break;
                }
                fHoldSelection++;
                fTopLineIndex--;
                loadLineData();
                updateTextArea();
                fHoldSelection--;
                LineData lineData = fLines.get(fTopLineIndex);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                fStyledText.setSelection(caretOffset);
                break;
            }
            case SWT.PAGE_UP: {
                if (previousLineAtCaretPosition > 0) {
                    break;
                }
                fHoldSelection++;
                fTopLineIndex -= Math.max(fNumVisibleLines - 1, 1);
                loadLineData();
                updateTextArea();
                fHoldSelection--;
                LineData lineData = fLines.get(fTopLineIndex);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                fStyledText.setSelection(caretOffset);
                break;
            }
            case SWT.ARROW_LEFT: {
                if (previousCaretOffset > 0) {
                    break;
                }
                if (fLines.isEmpty()) {
                    break;
                }
                long topRank = fLines.get(fTopLineIndex).rank;
                fHoldSelection++;
                fTopLineIndex--;
                loadLineData();
                updateTextArea();
                fHoldSelection--;
                LineData lineData = fLines.get(fTopLineIndex);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                if (topRank != fLines.get(fTopLineIndex).rank) {
                    fStyledText.setSelection(fLines.get(fTopLineIndex).string.length());
                }
                break;
            }
            case SWT.HOME: {
                if ((e.stateMask & SWT.CTRL) == 0) {
                    break;
                }
                setTopPosition(0.0);
                LineData lineData = fLines.get(fTopLineIndex);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                break;
            }
            case SWT.END: {
                if ((e.stateMask & SWT.CTRL) == 0) {
                    break;
                }
                double ratio = 1.0;
                double delta = Math.pow(10, -15);
                fLines.clear();
                while (fLines.isEmpty()) {
                    setTopPosition(ratio);
                    if (ratio == 0.0) {
                        break;
                    }
                    delta = Math.min(delta * 10, 0.1);
                    ratio = Math.max(ratio - delta, 0.0);
                }
                LineData lineData = fLines.get(fTopLineIndex);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
                break;
            }
            default:
                break;
        }
        updateHighlightedRank();
        fSlider.setSelection((int) (SLIDER_MAX * fTrace.getLocationRatio(fLines.get(fTopLineIndex).location)));
    }

    @Override
    public void keyReleased(KeyEvent e) {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // CaretListener (StyledText)
    // ------------------------------------------------------------------------

    @Override
    public void caretMoved(CaretEvent event) {
        if (fHoldSelection == 0) {
            int line = fStyledText.getLineAtOffset(event.caretOffset);
            if (fTopLineIndex + line < fLines.size()) {
                LineData lineData = fLines.get(fTopLineIndex + line);
                if (!lineData.location.equals(fSelectedLocation)) {
                    fSelectedLocation = lineData.location;
                    refreshLineBackgrounds();
                    sendSelectionEvent(lineData);
                }
            }
        }
        storeCaretPosition(event.time, event.caretOffset);
        if (fHoldSelection == 0) {
            Point caret = fStyledText.getLocationAtOffset(fStyledText.getCaretOffset());
            Point origin = fScrolledComposite.getOrigin();
            if (origin.x > caret.x) {
                origin.x = caret.x;
            } else if (caret.x - origin.x > fScrolledComposite.getSize().x) {
                origin.x = caret.x - fScrolledComposite.getSize().x + 1;
            }
            fScrolledComposite.setOrigin(origin);
        }
    }

    // ------------------------------------------------------------------------
    // MouseMoveListener (StyledText)
    // ------------------------------------------------------------------------

    @Override
    public void mouseMove(MouseEvent e) {
        fCursorYCoordinate = e.y;
        if (e.y < 0 || e.y > fStyledText.getSize().y) {
            if (fHighlightedRank != Long.MIN_VALUE) {
                fHighlightedRank = Long.MIN_VALUE;
                refreshLineBackgrounds();
            }
            return;
        }
        int offset = fStyledText.getOffsetAtPoint(new Point(0, e.y));
        int line = fStyledText.getLineAtOffset(offset);
        if (line < fLines.size() - fTopLineIndex) {
            LineData lineData = fLines.get(fTopLineIndex + line);
            if (fHighlightedRank != lineData.rank) {
                fHighlightedRank = lineData.rank;
                refreshLineBackgrounds();
            }
        } else {
            if (fHighlightedRank != Long.MIN_VALUE) {
                fHighlightedRank = Long.MIN_VALUE;
                refreshLineBackgrounds();
            }
        }
    }

    // ------------------------------------------------------------------------
    // MouseTrackListener (StyledText)
    // ------------------------------------------------------------------------

    @Override
    public void mouseExit(MouseEvent e) {
        fCursorYCoordinate = -1;
        if (fHighlightedRank != Long.MIN_VALUE) {
            fHighlightedRank = Long.MIN_VALUE;
            refreshLineBackgrounds();
        }
    }

    @Override
    public void mouseEnter(MouseEvent e) {
        fCursorYCoordinate = e.y;
    }

    @Override
    public void mouseHover(MouseEvent e) {
        // Do nothing
    }

    // ------------------------------------------------------------------------
    // MouseWheelListener (StyledText)
    // ------------------------------------------------------------------------

    @Override
    public void mouseScrolled(MouseEvent e) {
        if (fLines.isEmpty() || e.count == 0) {
            return;
        }
        fHoldSelection++;
        fTopLineIndex -= e.count;
        loadLineData();
        updateTextArea();
        fHoldSelection = 0;
        updateHighlightedRank();
        fSlider.setSelection((int) (SLIDER_MAX * fTrace.getLocationRatio(fLines.get(fTopLineIndex).location)));
    }

}
