/*******************************************************************************
 * Copyright (c) 2010, 2018 Ericsson and others.
 *
 * 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:
 *   Matthew Khouzam - Initial API and implementation
 *   Francois Chouinard - Refactoring, slider support, bug fixing
 *   Patrick Tasse - Improvements and bug fixing
 *   Xavier Raynaud - Improvements
 ******************************************************************************/

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

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
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.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
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.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.ui.PlatformUI;

/**
 * <b><u>TmfVirtualTable</u></b>
 * <p>
 * TmfVirtualTable allows for the tabular display of arbitrarily large data sets
 * (well, up to Integer.MAX_VALUE or ~2G rows).
 *
 * It is essentially a Composite of Table and Slider, where the number of rows
 * in the table is set to fill the table display area. The slider is rank-based.
 *
 * It differs from Table with the VIRTUAL style flag where an empty entry is
 * created for each virtual row. This does not scale well for very large data sets.
 *
 * Styles:
 * H_SCROLL, V_SCROLL, SINGLE, MULTI, CHECK, FULL_SELECTION, HIDE_SELECTION, NO_SCROLL
 * @author Matthew Khouzam, Francois Chouinard, Patrick Tasse, Xavier Raynaud
 * @version $Revision: 1.0
 */
public class TmfVirtualTable extends Composite {

    // The table
    private Table   fTable;
    private int     fTableRows         = 0;      // Number of table rows
    private int     fFullyVisibleRows  = 0;      // Number of fully visible table rows
    private int     fFrozenRowCount    = 0;      // Number of frozen table rows at top of table

    private int     fTableTopEventRank = 0;      // Global rank of the first entry displayed
    private int     fSelectedEventRank = -1;     // Global rank of the selected event
    private int     fSelectedBeginRank = -1;     // Global rank of the selected begin event
    private boolean fPendingSelection  = false;  // Pending selection update

    private int     fTableItemCount    = 0;

    // The slider
    private Slider fSlider;
    private SliderThrottler fSliderThrottler;

    private int fLinuxItemHeight = 0;            // Calculated item height for Linux workaround
    private TooltipProvider tooltipProvider = null;
    private IDoubleClickListener doubleClickListener = null;

    private boolean fResetTopIndex = false;      // Flag to trigger reset of top index
    private ControlAdapter fResizeListener;      // Resize listener to update visible rows

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

    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() || fTable.isDisposed()) {
                    return;
                }
                refreshTable();
            });
        }
    }

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

    /**
     * Standard constructor
     *
     * @param parent
     *            The parent composite object
     * @param style
     *            The style to use
     */
    public TmfVirtualTable(Composite parent, int style) {
        super(parent, style & (~SWT.H_SCROLL) & (~SWT.V_SCROLL) & (~SWT.SINGLE) & (~SWT.MULTI) & (~SWT.FULL_SELECTION) & (~SWT.HIDE_SELECTION) & (~SWT.CHECK));

        // Create the controls
        createTable(style & (SWT.H_SCROLL | SWT.SINGLE | SWT.MULTI | SWT.FULL_SELECTION | SWT.HIDE_SELECTION | SWT.CHECK));
        createSlider(style & SWT.V_SCROLL);

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

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

        GridData tableGridData = new GridData(SWT.FILL, SWT.FILL, true, true);
        fTable.setLayoutData(tableGridData);

        GridData sliderGridData = new GridData(SWT.FILL, SWT.FILL, false, true);
        fSlider.setLayoutData(sliderGridData);

        // Add the listeners
        fTable.addMouseWheelListener(event -> {
            if (fTableItemCount <= fFullyVisibleRows || event.count == 0) {
                return;
            }
            fTableTopEventRank -= event.count;
            if (fTableTopEventRank < 0) {
                fTableTopEventRank = 0;
            }
            int latestFirstRowOffset = fTableItemCount - fFullyVisibleRows;
            if (fTableTopEventRank > latestFirstRowOffset) {
                fTableTopEventRank = latestFirstRowOffset;
            }

            fSlider.setSelection(fTableTopEventRank);
            refreshTable();
        });

        fTable.addListener(SWT.MouseWheel, event -> event.doit = false);

        fResizeListener = new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent event) {
                int tableHeight = Math.max(0, fTable.getClientArea().height - fTable.getHeaderHeight());
                fFullyVisibleRows = tableHeight / getItemHeight();
                if (fTableItemCount > 0) {
                    fSlider.setThumb(Math.max(1, Math.min(fTableRows, fFullyVisibleRows)));
                }
            }
        };
        fTable.addControlListener(fResizeListener);

        // Implement a "fake" tooltip
        final String TOOLTIP_DATA_KEY = "_TABLEITEM"; //$NON-NLS-1$
        final Listener labelListener = event -> {
            Label label = (Label) event.widget;
            Shell shell = label.getShell();
            switch (event.type) {
            case SWT.MouseDown:
                Event e = new Event();
                e.item = (TableItem) label.getData(TOOLTIP_DATA_KEY);
                // Assuming table is single select, set the selection as if
                // the mouse down event went through to the table
                fTable.setSelection(new TableItem [] {(TableItem) e.item});
                fTable.notifyListeners(SWT.Selection, e);
                shell.dispose();
                fTable.setFocus();
                break;
            case SWT.MouseExit:
            case SWT.MouseWheel:
                shell.dispose();
                break;
            default:
                break;
            }
        };

        Listener tableListener = new Listener() {
            Shell tip = null;
            Label label = null;
            @Override
            public void handleEvent(Event event) {
                switch (event.type) {
                case SWT.Dispose:
                case SWT.KeyDown:
                case SWT.MouseMove: {
                    if (tip == null) {
                        break;
                    }
                    tip.dispose();
                    tip = null;
                    label = null;
                    break;
                }
                case SWT.MouseHover: {
                    TableItem item = fTable.getItem(new Point(event.x, event.y));
                    if (item != null) {
                        for (int i = 0; i < fTable.getColumnCount(); i++) {
                            Rectangle bounds = item.getBounds(i);
                            if (bounds.contains(event.x, event.y)) {
                                if (tip != null && !tip.isDisposed()) {
                                    tip.dispose();
                                }
                                if (tooltipProvider == null) {
                                    return;
                                }
                                String tooltipText = tooltipProvider.getTooltip(i, item.getData());
                                if (tooltipText == null) {
                                    return;
                                }
                                tip = new Shell(fTable.getShell(), SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
                                tip.setBackground(PlatformUI.getWorkbench().getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND));
                                FillLayout layout = new FillLayout();
                                layout.marginWidth = 2;
                                tip.setLayout(layout);
                                label = new Label(tip, SWT.WRAP);
                                label.setForeground(PlatformUI.getWorkbench().getDisplay().getSystemColor(SWT.COLOR_INFO_FOREGROUND));
                                label.setBackground(PlatformUI.getWorkbench().getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND));
                                label.setData(TOOLTIP_DATA_KEY, item);
                                label.setText(tooltipText);

                                label.addListener(SWT.MouseExit, labelListener);
                                label.addListener(SWT.MouseDown, labelListener);
                                label.addListener(SWT.MouseWheel, labelListener);
                                Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                                Point pt = fTable.toDisplay(bounds.x, bounds.y);
                                tip.setBounds(pt.x, pt.y, size.x, size.y);
                                tip.setVisible(true);

                                // Item found, leave loop.
                                break;
                            }
                        }
                    }
                    break;
                }
                default:
                    break;
                }
            }
        };
        fTable.addListener(SWT.Dispose, tableListener);
        fTable.addListener(SWT.KeyDown, tableListener);
        fTable.addListener(SWT.MouseMove, tableListener);
        fTable.addListener(SWT.MouseHover, tableListener);
        addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent event) {
                resize();
                if (fTableItemCount > 0) {
                    fSlider.setThumb(Math.max(1, Math.min(fTableRows, fFullyVisibleRows)));
                }
            }
        });

        // And display
        refresh();
    }

    // ------------------------------------------------------------------------
    // Table handling
    // ------------------------------------------------------------------------

    /**
     * Create the table and add listeners
     * @param style int can be H_SCROLL, SINGLE, MULTI, FULL_SELECTION, HIDE_SELECTION, CHECK
     */
    private void createTable(int style) {
        fTable = new Table(this, style | SWT.NO_SCROLL);

        fTable.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent event) {
                if (event.item == null) {
                    // Override table selection from Select All action
                    refreshSelection();
                }
            }
        });

        fTable.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                handleTableMouseEvent(e);
            }
        });

        fTable.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent event) {
                handleTableKeyEvent(event);
            }
        });

        fTable.addListener(
                SWT.MouseDoubleClick, event -> {
                    if (doubleClickListener != null) {
                        TableItem item = fTable.getItem(new Point (event.x, event.y));
                        if (item != null) {
                            for (int i = 0; i < fTable.getColumnCount(); i++){
                                Rectangle bounds = item.getBounds(i);
                                if (bounds.contains(event.x, event.y)){
                                    doubleClickListener.handleDoubleClick(TmfVirtualTable.this, item, i);
                                    break;
                                }
                            }
                        }
                    }
                }
                );

        /*
         * Feature in Windows. When a partially visible table item is selected,
         * after ~500 ms the top index is changed to ensure the selected item is
         * fully visible. This leaves a blank space at the bottom of the virtual
         * table. The workaround is to reset the top index to 0 if it is not 0.
         * Also reset the top index to 0 if indicated by the flag that was set
         * at table selection of a partially visible table item.
         */
        fTable.addPaintListener(e -> {
            if (fTable.getTopIndex() != 0 || fResetTopIndex) {
                fTable.setTopIndex(0);
            }
            fResetTopIndex = false;
        });
    }

    /**
     * Handle mouse-based selection in table.
     *
     * @param event the mouse event
     */
    private void handleTableMouseEvent(MouseEvent event) {
        TableItem item = fTable.getItem(new Point(event.x, event.y));
        if (item == null) {
            return;
        }
        int selectedRow = indexOf(item);
        if (event.button == 1 || (event.button == 3 &&
                (selectedRow < Math.min(fSelectedBeginRank, fSelectedEventRank) ||
                        selectedRow > Math.max(fSelectedBeginRank, fSelectedEventRank)))) {
            if (selectedRow >= 0) {
                fSelectedEventRank = selectedRow;
            } else {
                fSelectedEventRank = -1;
            }
            if ((event.stateMask & SWT.SHIFT) == 0 || (fTable.getStyle() & SWT.MULTI) == 0 || fSelectedBeginRank == -1) {
                fSelectedBeginRank = fSelectedEventRank;
            }
        }
        refreshSelection();

        /*
         * Feature in Linux. When a partially visible table item is selected,
         * the origin is changed to ensure the selected item is fully visible.
         * This makes the first row partially visible. The solution is to force
         * reset the origin by setting the top index to 0. This should happen
         * only once at the next redraw by the paint listener.
         */
        if (selectedRow >= fFullyVisibleRows) {
            fResetTopIndex = true;
        }
    }

    /**
     * Handle key-based navigation in table.
     *
     * @param event the key event
     */
    private void handleTableKeyEvent(KeyEvent event) {

        int lastEventRank        = fTableItemCount - 1;
        int lastPageTopEntryRank = Math.max(0, fTableItemCount - fFullyVisibleRows);

        int previousSelectedEventRank = fSelectedEventRank;
        int previousSelectedBeginRank = fSelectedBeginRank;
        boolean needsRefresh = false;

        // In all case, perform the following steps:
        // - Update the selected entry rank (within valid range)
        // - Update the selected row
        // - Update the page's top entry if necessary (which also adjusts the selected row)
        // - If the top displayed entry was changed, table refresh is needed
        switch (event.keyCode) {

        case SWT.ARROW_DOWN: {
            event.doit = false;
            if (fSelectedEventRank < lastEventRank) {
                fSelectedEventRank++;
                int selectedRow = fSelectedEventRank - fTableTopEventRank;
                if (selectedRow == fFullyVisibleRows) {
                    fTableTopEventRank++;
                    needsRefresh = true;
                } else if (selectedRow < fFrozenRowCount || selectedRow > fFullyVisibleRows) {
                    fTableTopEventRank = Math.max(0, Math.min(fSelectedEventRank - fFrozenRowCount, lastPageTopEntryRank));
                    needsRefresh = true;
                }
            }
            break;
        }

        case SWT.ARROW_UP: {
            event.doit = false;
            if (fSelectedEventRank > 0) {
                fSelectedEventRank--;
                int selectedRow = fSelectedEventRank - fTableTopEventRank;
                if (selectedRow == fFrozenRowCount - 1 && fTableTopEventRank > 0) {
                    fTableTopEventRank--;
                    needsRefresh = true;
                } else if (selectedRow < fFrozenRowCount || selectedRow > fFullyVisibleRows) {
                    fTableTopEventRank = Math.max(0, Math.min(fSelectedEventRank - fFrozenRowCount, lastPageTopEntryRank));
                    needsRefresh = true;
                }
            }
            break;
        }

        case SWT.END: {
            event.doit = false;
            fTableTopEventRank = lastPageTopEntryRank;
            fSelectedEventRank = lastEventRank;
            needsRefresh = true;
            break;
        }

        case SWT.HOME: {
            event.doit = false;
            fSelectedEventRank = fFrozenRowCount;
            fTableTopEventRank = 0;
            needsRefresh = true;
            break;
        }

        case SWT.PAGE_DOWN: {
            event.doit = false;
            if (fSelectedEventRank < lastEventRank) {
                fSelectedEventRank += fFullyVisibleRows;
                if (fSelectedEventRank > lastEventRank) {
                    fSelectedEventRank = lastEventRank;
                }
                int selectedRow = fSelectedEventRank - fTableTopEventRank;
                if (selectedRow > fFullyVisibleRows + fFrozenRowCount - 1 && selectedRow < 2 * fFullyVisibleRows) {
                    fTableTopEventRank += fFullyVisibleRows;
                    if (fTableTopEventRank > lastPageTopEntryRank) {
                        fTableTopEventRank = lastPageTopEntryRank;
                    }
                    needsRefresh = true;
                } else if (selectedRow < fFrozenRowCount || selectedRow >= 2 * fFullyVisibleRows) {
                    fTableTopEventRank = Math.max(0, Math.min(fSelectedEventRank - fFrozenRowCount, lastPageTopEntryRank));
                    needsRefresh = true;
                }
            }
            break;
        }

        case SWT.PAGE_UP: {
            event.doit = false;
            if (fSelectedEventRank > 0) {
                fSelectedEventRank -= fFullyVisibleRows;
                if (fSelectedEventRank < fFrozenRowCount) {
                    fSelectedEventRank = fFrozenRowCount;
                }
                int selectedRow = fSelectedEventRank - fTableTopEventRank;
                if (selectedRow < fFrozenRowCount && selectedRow > -fFullyVisibleRows) {
                    fTableTopEventRank -= fFullyVisibleRows;
                    if (fTableTopEventRank < 0) {
                        fTableTopEventRank = 0;
                    }
                    needsRefresh = true;
                } else if (selectedRow <= -fFullyVisibleRows || selectedRow >= fFullyVisibleRows) {
                    fTableTopEventRank = Math.max(0, Math.min(fSelectedEventRank - fFrozenRowCount, lastPageTopEntryRank));
                    needsRefresh = true;
                }
            }
            break;
        }
        default: {
            return;
        }
        }

        if ((event.stateMask & SWT.SHIFT) == 0 || (fTable.getStyle() & SWT.MULTI) == 0 || fSelectedBeginRank == -1) {
            fSelectedBeginRank = fSelectedEventRank;
        }

        boolean done = true;
        if (needsRefresh) {
            done = refreshTable(); // false if table items not updated yet in this thread
        } else {
            refreshSelection();
        }

        if (fFullyVisibleRows < fTableItemCount) {
            fSlider.setSelection(fTableTopEventRank);
        }

        if (fSelectedEventRank != previousSelectedEventRank || fSelectedBeginRank != previousSelectedBeginRank) {
            if (done) {
                Event e = new Event();
                e.item = fTable.getItem(fSelectedEventRank - fTableTopEventRank);
                fTable.notifyListeners(SWT.Selection, e);
            } else {
                fPendingSelection = true;
            }
        }
    }

    /**
     * Method setDataItem.
     * @param index int
     * @param item TableItem
     * @return boolean
     */
    private boolean setDataItem(int index, TableItem item) {
        if (index != -1) {
            Event event = new Event();
            event.item  = item;
            if (index < fFrozenRowCount) {
                event.index = index;
            } else {
                event.index = index + fTableTopEventRank;
            }
            event.doit  = true;
            fTable.notifyListeners(SWT.SetData, event);
            return event.doit; // false if table item not updated yet in this thread
        }
        return true;
    }

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

    /**
     * Method createSlider.
     * @param style int
     */
    private void createSlider(int style) {
        fSlider = new Slider(this, SWT.VERTICAL | SWT.NO_FOCUS);
        fSlider.setMinimum(0);
        fSlider.setMaximum(0);
        if ((style & SWT.V_SCROLL) == 0) {
            fSlider.setVisible(false);
        }

        fSlider.addListener(SWT.Selection, event -> {
            switch (event.detail) {
            case SWT.ARROW_DOWN:
            case SWT.ARROW_UP:
            case SWT.END:
            case SWT.HOME:
            case SWT.PAGE_DOWN:
            case SWT.PAGE_UP: {
                fTableTopEventRank = fSlider.getSelection();
                refreshTable();
                break;
            }
            case SWT.DRAG:
            case SWT.NONE:
                /*
                 * 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.
                 */
                fTableTopEventRank = fSlider.getSelection();
                if (fSliderThrottler == null) {
                    fSliderThrottler = new SliderThrottler();
                    fSliderThrottler.start();
                }
                break;
            default:
                break;
            }
        });

        /*
         * When the mouse button is released, perform the refresh immediately
         * and interrupt and discard the slider throttler.
         */
        fSlider.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseUp(MouseEvent e) {
                if (fSliderThrottler != null) {
                    fSliderThrottler.interrupt();
                    fSliderThrottler = null;
                }
                fTableTopEventRank = fSlider.getSelection();
                refreshTable();
            }
        });

        /*
         * The SWT.NO_FOCUS style is only a hint and is not always respected. If
         * the slider gains focus, give it back to the table.
         */
        fSlider.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                fTable.setFocus();
            }
        });
    }

    // ------------------------------------------------------------------------
    // Simulated Table API
    // ------------------------------------------------------------------------

    /**
     * Constructs a new TableColumn instance given a style value describing its
     * alignment behavior. The column is added to the end of the columns
     * maintained by the table.
     *
     * @param style
     *            the alignment style
     * @return the new TableColumn
     *
     * @see SWT#LEFT
     * @see SWT#RIGHT
     * @see SWT#CENTER
     */
    public TableColumn newTableColumn(int style) {
        TableColumn column = new TableColumn(fTable, style);

        /*
         * In Linux the table does not receive a control resized event when
         * a table column resize causes the horizontal scroll bar to become
         * visible or invisible, so a resize listener must be added to every
         * table column to properly update the number of fully visible rows.
         */
        column.addControlListener(fResizeListener);

        return column;
    }

    /**
     * Method setHeaderVisible.
     * @param b boolean
     */
    public void setHeaderVisible(boolean b) {
        fTable.setHeaderVisible(b);
    }

    /**
     * Method setLinesVisible.
     * @param b boolean
     */
    public void setLinesVisible(boolean b) {
        fTable.setLinesVisible(b);
    }

    /**
     * Returns an array of <code>TableItem</code>s that are currently selected
     * in the receiver. The order of the items is unspecified. An empty array
     * indicates that no items are selected.
     * <p>
     * Note: This array only contains the visible selected items in the virtual
     * table. To get information about the full selection range, use
     * {@link #getSelectionIndices()}.
     * </p>
     *
     * @return an array representing the selection
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public TableItem[] getSelection() {
        return fTable.getSelection();
    }

    /**
     * Method addListener.
     * @param eventType int
     * @param listener Listener
     */
    @Override
    public void addListener(int eventType, Listener listener) {
        fTable.addListener(eventType, listener);
    }

    /**
     * Method addKeyListener.
     * @param listener KeyListener
     */
    @Override
    public void addKeyListener(KeyListener listener) {
        fTable.addKeyListener(listener);
    }

    /**
     * Method addMouseListener.
     * @param listener MouseListener
     */
    @Override
    public void addMouseListener(MouseListener listener) {
        fTable.addMouseListener(listener);
    }

    /**
     * Method addSelectionListener.
     * @param listener SelectionListener
     */
    public void addSelectionListener(SelectionListener listener) {
        fTable.addSelectionListener(listener);
    }

    /**
     * Method setMenu sets the menu
     * @param menu Menu the menu
     */
    @Override
    public void setMenu(Menu menu) {
        fTable.setMenu(menu);
    }

    /**
     * Gets the menu of this table
     * @return a Menu
     */
    @Override
    public Menu getMenu() {
        return fTable.getMenu();
    }

    /**
     * Method clearAll empties a table.
     */
    public void clearAll() {
        setItemCount(0);
    }

    /**
     * Method setItemCount
     * @param nbItems int the number of items in the table
     *
     */
    public void setItemCount(int nbItems) {
        final int nb = Math.max(0, nbItems);

        if (nb != fTableItemCount) {
            fTableItemCount = nb;
            fTable.remove(fTableItemCount, fTable.getItemCount() - 1);
            fSlider.setMaximum(nb);
            resize();
            int tableHeight = Math.max(0, fTable.getClientArea().height - fTable.getHeaderHeight());
            fFullyVisibleRows = tableHeight / getItemHeight();
            if (fTableItemCount > 0) {
                fSlider.setThumb(Math.max(1, Math.min(fTableRows, fFullyVisibleRows)));
            }
        }
    }

    /**
     * Method getItemCount.
     * @return int the number of items in the table
     */
    public int getItemCount() {
        return fTableItemCount;
    }

    /**
     * Method getItemHeight.
     * @return int the height of a table item in pixels. (may vary from one os to another)
     */
    public int getItemHeight() {
        /*
         * Bug in Linux.  The method getItemHeight doesn't always return the correct value.
         */
        if (fLinuxItemHeight >= 0 && System.getProperty("os.name").contains("Linux")) { //$NON-NLS-1$ //$NON-NLS-2$
            if (fLinuxItemHeight != 0) {
                return fLinuxItemHeight;
            }
            if (fTable.getItemCount() > 1) {
                int itemHeight = fTable.getItem(1).getBounds().y - fTable.getItem(0).getBounds().y;
                if (itemHeight > 0) {
                    fLinuxItemHeight = itemHeight;
                    return fLinuxItemHeight;
                }
            }
        } else {
            fLinuxItemHeight = -1; // Not Linux, don't perform os.name check anymore
        }
        return fTable.getItemHeight();
    }

    /**
     * Method getHeaderHeight.
     * @return int get the height of the header in pixels.
     */
    public int getHeaderHeight() {
        return fTable.getHeaderHeight();
    }

    /**
     * Method getTopIndex.
     * @return int get the first data item index, if you have a header it is offset.
     */
    public int getTopIndex() {
        return fTableTopEventRank + fFrozenRowCount;
    }

    /**
     * Method setTopIndex.
     * @param index int suggested top index for the table.
     */
    public void setTopIndex(int index) {
        if (fTableItemCount > 0) {
            int i = Math.min(index, fTableItemCount - 1);
            i = Math.max(i, fFrozenRowCount);

            fTableTopEventRank = i - fFrozenRowCount;
            if (fFullyVisibleRows < fTableItemCount) {
                fSlider.setSelection(fTableTopEventRank);
            }

            refreshTable();
        }
    }

    @Override
    public ScrollBar getHorizontalBar() {
        return fTable.getHorizontalBar();
    }

    @Override
    public ScrollBar getVerticalBar() {
        return fTable.getVerticalBar();
    }

    /**
     * Method indexOf. Return the index of a table item
     * @param ti TableItem the table item to search for in the table
     * @return int the index of the first match. (there should only be one match)
     */
    public int indexOf(TableItem ti) {
        int index = fTable.indexOf(ti);
        if (index < fFrozenRowCount) {
            return index;
        }
        return (index - fFrozenRowCount) + getTopIndex();
    }

    /**
     * Method getColumns.
     * @return TableColumn[] the table columns
     */
    public TableColumn[] getColumns() {
        return fTable.getColumns();
    }

    /**
     * Returns an array of zero-relative integers that map
     * the creation order of the receiver's columns to the
     * order in which they are currently being displayed.
     * <p>
     * Specifically, the indices of the returned array represent
     * the current visual order of the columns, and the contents
     * of the array represent the creation order of the columns.
     *
     * @return the current visual order of the receiver's columns
     */
    public int[] getColumnOrder() {
        return fTable.getColumnOrder();
    }

    /**
     * Sets the order that the columns in the receiver should
     * be displayed in to the given argument which is described
     * in terms of the zero-relative ordering of when the columns
     * were added.
     * <p>
     * Specifically, the contents of the array represent the
     * original position of each column at the time its creation.
     *
     * @param order the new order to display the columns
     */
    public void setColumnOrder(int[] order) {
        fTable.setColumnOrder(order);
    }

    /**
     * Returns the table item at the given point in the table or null if no such
     * item exists. The point is in coordinates relative to the table.
     *
     * @param point
     *            the point in coordinates relative to the table
     * @return the corresponding table item, or null
     */
    public TableItem getItem(Point point) {
        return fTable.getItem(point);
    }

    /**
     * Returns the table column at the given point in the table or null if no such
     * column exists. The point is in coordinates relative to the table.
     *
     * @param point
     *            the point in coordinates relative to the table
     * @return the corresponding table column, or null
     * @since 4.0
     */
    public TableColumn getColumn(Point point) {
        Rectangle clientArea = fTable.getClientArea();
        Point p = new Point(clientArea.x + point.x, clientArea.y + point.y);
        if (!clientArea.contains(p)) {
            return null;
        }
        int x = 0;
        for (int i : fTable.getColumnOrder()) {
            TableColumn column = fTable.getColumn(i);
            int width = column.getWidth();
            x += width;
            if (p.x < x && width > 0) {
                return column;
            }
        }
        return null;
    }

    /**
     * Method resize.
     */
    private void resize() {
        // Compute the numbers of rows that fit the new area
        int tableHeight = Math.max(0, getSize().y - fTable.getHeaderHeight());
        int itemHeight = getItemHeight();
        fTableRows = Math.min((tableHeight + itemHeight - 1) / itemHeight, fTableItemCount);

        if (fTableTopEventRank + fFullyVisibleRows > fTableItemCount) {
            // If we are at the end, get elements before to populate
            fTableTopEventRank = Math.max(0, fTableItemCount - fFullyVisibleRows);
            refreshTable();
        } else if (fTableRows > fTable.getItemCount() || fTableItemCount < fTable.getItemCount()) {
            // Only refresh if new table items are needed or if table items need to be deleted
            refreshTable();
        }

    }

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

    /**
     * Method setFocus.
     * @return boolean is this visible?
     */
    @Override
    public boolean setFocus() {
        boolean isVisible = isVisible();
        if (isVisible) {
            fTable.setFocus();
        }
        return isVisible;
    }

    /**
     * Method refresh.
     */
    public void refresh() {
        boolean done = refreshTable();
        if (!done) {
            return;
        }
        if (fPendingSelection) {
            fPendingSelection = false;
            TableItem item = null;
            if (fSelectedEventRank >= 0 && fSelectedEventRank < fFrozenRowCount) {
                item = fTable.getItem(fSelectedEventRank);
            } else if (fSelectedEventRank >= fTableTopEventRank + fFrozenRowCount && fSelectedEventRank - fTableTopEventRank < fTable.getItemCount()) {
                item = fTable.getItem(fSelectedEventRank - fTableTopEventRank);
            }
            if (item != null) {
                Event e = new Event();
                e.item = item;
                fTable.notifyListeners(SWT.Selection, e);
            }
        }
    }

    /**
     * Method removeAll.
     * @return int 0 the number of elements in the table
     */
    public int removeAll() {
        setItemCount(0);
        fSlider.setMaximum(0);
        fTable.removeAll();
        fSelectedEventRank = -1;
        fSelectedBeginRank = fSelectedEventRank;
        return 0;
    }

    /**
     * Method refreshTable.
     * @return true if all table items have been refreshed, false otherwise
     */
    private boolean refreshTable() {
        boolean done = true;
        for (int i = 0; i < fTableRows; i++) {
            if (i + fTableTopEventRank < fTableItemCount) {
                TableItem tableItem;
                if (i < fTable.getItemCount()) {
                    tableItem = fTable.getItem(i);
                } else {
                    tableItem = new TableItem(fTable, SWT.NONE);
                }
                done &= setDataItem(i, tableItem); // false if table item not updated yet in this thread
            } else {
                if (fTable.getItemCount() > fTableItemCount - fTableTopEventRank) {
                    fTable.remove(fTableItemCount - fTableTopEventRank);
                }
            }
        }
        if (done) {
            refreshSelection();
        } else {
            fTable.deselectAll();
        }
        return done;
    }

    private void refreshSelection() {
        int lastRowOffset = fTableTopEventRank + fTableRows - 1;
        int startRank = Math.min(fSelectedBeginRank, fSelectedEventRank);
        int endRank = Math.max(fSelectedBeginRank, fSelectedEventRank);
        int start = Integer.MAX_VALUE;
        int end = Integer.MIN_VALUE;
        if (startRank < fFrozenRowCount) {
            start = startRank;
        } else if (startRank < fTableTopEventRank + fFrozenRowCount) {
            start = fFrozenRowCount;
        } else if (startRank <= lastRowOffset) {
            start = startRank - fTableTopEventRank;
        }
        if (endRank < fFrozenRowCount) {
            end = endRank;
        } else if (endRank < fTableTopEventRank + fFrozenRowCount) {
            end = fFrozenRowCount - 1;
        } else if (endRank <= lastRowOffset) {
            end = endRank - fTableTopEventRank;
        } else {
            end = fTableRows - 1;
        }
        if (start <= end) {
            fTable.setSelection(start, end);
            /* Reset origin in case partially visible item selected */
            fTable.setTopIndex(0);
            if (startRank == fSelectedEventRank) {
                fTable.select(start);
            } else {
                fTable.select(end);
            }
        } else {
            /*
             * In GTK2, when the table is given focus, one table item is
             * highlighted even if there is no selection. In that case the
             * highlighted item is the last selected item. Make that last
             * selected item the top or bottom item depending on if the
             * out-of-range selection is above or below the visible items.
             */
            if (SWT.getPlatform().equals("gtk") && fTableRows > 0) { //$NON-NLS-1$
                fTable.setRedraw(false);
                if (start < Integer.MAX_VALUE) {
                    fTable.setSelection(0);
                } else {
                    fTable.setSelection(fTableRows - 1);
                    fTable.setTopIndex(0);
                }
                fTable.setRedraw(true);
            }
            fTable.deselectAll();
        }
    }

    /**
     * Selects the item at the given zero-relative index in the receiver.
     * The current selection is first cleared, then the new item is selected,
     * and if necessary the receiver is scrolled to make the new selection visible.
     *
     * @param index the index of the item to select
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void setSelection(int index) {
        setSelectionRange(index, index);
    }

    /**
     * Selects a range of items starting at the given zero-relative index in the
     * receiver. The current selection is first cleared, then the new items are
     * selected, and if necessary the receiver is scrolled to make the new
     * selection visible.
     *
     * @param beginIndex
     *            The index of the first item in the selection range
     * @param endIndex
     *            The index of the last item in the selection range
     *
     * @exception SWTException
     *                <ul>
     *                <li>ERROR_WIDGET_DISPOSED - if the receiver has been
     *                disposed</li>
     *                <li>ERROR_THREAD_INVALID_ACCESS - if not called from the
     *                thread that created the receiver</li>
     *                </ul>
     * @since 2.0
     */
    public void setSelectionRange(int beginIndex, int endIndex) {
        if (fTableItemCount > 0) {
            int begin = Math.max(Math.min(beginIndex, fTableItemCount - 1), 0);
            int end =   Math.max(Math.min(endIndex, fTableItemCount - 1), 0);
            int selection = fSelectedBeginRank != begin ? begin : end;

            fSelectedBeginRank = begin;
            fSelectedEventRank = end;
            if ((selection < fTableTopEventRank + fFrozenRowCount && selection >= fFrozenRowCount) ||
                    (selection >= fTableTopEventRank + fFullyVisibleRows)) {
                int lastPageTopEntryRank = Math.max(0, fTableItemCount - fFullyVisibleRows);
                fTableTopEventRank = Math.max(0, Math.min(lastPageTopEntryRank, selection - fFrozenRowCount - fFullyVisibleRows / 2));
            }
            if (fFullyVisibleRows < fTableItemCount) {
                fSlider.setSelection(fTableTopEventRank);
            }
            refreshTable();
        }
    }

    /**
     * Returns the zero-relative index of the item which is currently
     * selected in the receiver, or -1 if no item is selected.
     *
     * @return the index of the selected item
     */
    public int getSelectionIndex() {
        return fSelectedEventRank;
    }

    /**
     * Returns an index array representing the selection range. If there is a
     * single item selected the array holds one index. If there is a selected
     * range the first item in the array is the start index of the selection and
     * the second item is the end index of the selection, which is the item most
     * recently selected. The array is empty if no items are selected.
     * <p>
     * @return the array of indices of the selected items
     */
    public int[] getSelectionIndices() {
        if (fSelectedEventRank < 0 || fSelectedBeginRank < 0) {
            return new int[] {};
        } else if (fSelectedEventRank == fSelectedBeginRank) {
            return new int[] { fSelectedEventRank };
        }
        return new int[] { fSelectedBeginRank, fSelectedEventRank };
    }

    /**
     * Method setFrozenRowCount.
     * @param count int the number of rows to freeze from the top row
     */
    public void setFrozenRowCount(int count) {
        fFrozenRowCount = count;
        refreshTable();
    }

    /**
     * Method createTableEditor.
     * @return a TableEditor of the table
     */
    public TableEditor createTableEditor() {
        return new TableEditor(fTable);
    }

    /**
     * Method createTableEditorControl.
     * @param control Class<? extends Control>
     * @return Control
     */
    public Control createTableEditorControl(Class<? extends Control> control) {
        try {
            return control.getConstructor(Composite.class, int.class).newInstance(new Object[] {fTable, SWT.NONE});
        } catch (Exception e) {
            Activator.getDefault().logError("Error creating table editor control", e); //$NON-NLS-1$
        }
        return null;
    }

    /**
     * @return the tooltipProvider
     */
    public TooltipProvider getTooltipProvider() {
        return tooltipProvider;
    }

    /**
     * @param tooltipProvider the tooltipProvider to set
     */
    public void setTooltipProvider(TooltipProvider tooltipProvider) {
        this.tooltipProvider = tooltipProvider;
    }

    /**
     * @return the doubleClickListener
     */
    public IDoubleClickListener getDoubleClickListener() {
        return doubleClickListener;
    }

    /**
     * @param doubleClickListener the doubleClickListener to set
     */
    public void setDoubleClickListener(IDoubleClickListener doubleClickListener) {
        this.doubleClickListener = doubleClickListener;
    }

}
