/*******************************************************************************
 * Copyright (c) 2003, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.presentations;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Item;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.misc.StringMatcher;
import org.eclipse.ui.internal.presentations.defaultpresentation.DefaultTabItem;

/**
 * @since 3.0
 */
public abstract class AbstractTableInformationControl {

    /**
     * The NamePatternFilter selects the elements which match the given string
     * patterns.
     */
    protected class NamePatternFilter extends ViewerFilter {

        public NamePatternFilter() {
            //no-op
        }

        /*
         * (non-Javadoc) Method declared on ViewerFilter.
         */
        public boolean select(Viewer viewer, Object parentElement,
                Object element) {
            StringMatcher matcher = getMatcher();
            if (matcher == null || !(viewer instanceof TableViewer))
                return true;
            TableViewer tableViewer = (TableViewer) viewer;

            String matchName = ((ILabelProvider) tableViewer.getLabelProvider())
                    .getText(element);
            
            if(matchName == null)
            	return false;
            // A dirty editor's label will start with dirty prefix, this prefix 
            // should not be taken in consideration when matching with a pattern
            String prefix = DefaultTabItem.DIRTY_PREFIX;
            if (matchName.startsWith(prefix))
                matchName = matchName.substring(prefix.length());
            return matchName != null && matcher.match(matchName);
        }
    }

    private static class BorderFillLayout extends Layout {

        /** The border widths. */
        final int fBorderSize;

        /**
         * Creates a fill layout with a border.
         */
        public BorderFillLayout(int borderSize) {
            if (borderSize < 0)
                throw new IllegalArgumentException();
            fBorderSize = borderSize;
        }

        /**
         * Returns the border size.
         */
        public int getBorderSize() {
            return fBorderSize;
        }

        /*
         * @see org.eclipse.swt.widgets.Layout#computeSize(org.eclipse.swt.widgets.Composite,
         *      int, int, boolean)
         */
        protected Point computeSize(Composite composite, int wHint, int hHint,
                boolean flushCache) {

            Control[] children = composite.getChildren();
            Point minSize = new Point(0, 0);

            if (children != null) {
                for (int i = 0; i < children.length; i++) {
                    Point size = children[i].computeSize(wHint, hHint,
                            flushCache);
                    minSize.x = Math.max(minSize.x, size.x);
                    minSize.y = Math.max(minSize.y, size.y);
                }
            }

            minSize.x += fBorderSize * 2 + RIGHT_MARGIN;
            minSize.y += fBorderSize * 2;

            return minSize;
        }

        /*
         * @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite,
         *      boolean)
         */
        protected void layout(Composite composite, boolean flushCache) {

            Control[] children = composite.getChildren();
            Point minSize = new Point(composite.getClientArea().width,
                    composite.getClientArea().height);

            if (children != null) {
                for (int i = 0; i < children.length; i++) {
                    Control child = children[i];
                    child.setSize(minSize.x - fBorderSize * 2, minSize.y
                            - fBorderSize * 2);
                    child.setLocation(fBorderSize, fBorderSize);
                }
            }
        }
    }

    /** Border thickness in pixels. */
    private static final int BORDER = 1;

    /** Right margin in pixels. */
    private static final int RIGHT_MARGIN = 3;

    /** The control's shell */
    private Shell fShell;

    /** The composite */
    protected Composite fComposite;

    /** The control's text widget */
    private Text fFilterText;

    /** The control's table widget */
    private TableViewer fTableViewer;

    /** The control width constraint */
    //private int fMaxWidth= -1;
    /** The control height constraint */
    //private int fMaxHeight= -1;
    /** The current string matcher */
    private StringMatcher fStringMatcher;

    /**
     * Creates an information control with the given shell as parent. The given
     * styles are applied to the shell and the table widget.
     * 
     * @param parent
     *            the parent shell
     * @param shellStyle
     *            the additional styles for the shell
     * @param controlStyle
     *            the additional styles for the control
     */
    public AbstractTableInformationControl(Shell parent, int shellStyle,
            int controlStyle) {
        fShell = new Shell(parent, shellStyle);
        Display display = fShell.getDisplay();
        fShell.setBackground(display.getSystemColor(SWT.COLOR_BLACK));

        // Composite for filter text and viewer

        fComposite = new Composite(fShell, SWT.RESIZE);
        GridLayout layout = new GridLayout(1, false);
        fComposite.setLayout(layout);
        //		fComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        createFilterText(fComposite);

        fTableViewer = createTableViewer(fComposite, controlStyle);

        final Table table = fTableViewer.getTable();
        table.addKeyListener(new KeyListener() {
            public void keyPressed(KeyEvent e) {
                if (e.character == SWT.ESC)
                    dispose();
                else if (e.character == SWT.DEL) {
                    removeSelectedItems();
                    e.character = SWT.NONE;
                    e.doit = false;
                }
            }

            public void keyReleased(KeyEvent e) {
                // do nothing
            }
        });

        table.addSelectionListener(new SelectionListener() {
            public void widgetSelected(SelectionEvent e) {
                // do nothing;
            }

            public void widgetDefaultSelected(SelectionEvent e) {
                gotoSelectedElement();
            }
        });

        /*
         * Bug in GTK, see SWT bug: 62405 Editor drop down performance slow on
         * Linux-GTK on mouse move.
         * Rather then removing the support altogether this feature has been 
         * worked around for GTK only as we expect that newer versions of GTK 
         * will no longer exhibit this quality and we will be able to have the 
         * desired support running on all platforms. See
         * comment https://bugs.eclipse.org/bugs/show_bug.cgi?id=62405#c22 
         * TODO: remove this code once bug 62405 is fixed for the mainstream GTK
         * version
         */
        final int ignoreEventCount = Platform.getWS().equals(Platform.WS_GTK) ? 4 : 1;
    
        table.addMouseMoveListener(new MouseMoveListener() {
            TableItem fLastItem = null;
            int lastY = 0;
            int lastX = 0;
            int itemHeightdiv4 = table.getItemHeight() / 4;
            int tableHeight = table.getBounds().height;
            Point tableLoc = table.toDisplay(0,0);
            int divCount = 0;
            public void mouseMove(MouseEvent e) {
                if (divCount == ignoreEventCount) divCount = 0;
                if (table.equals(e.getSource()) & ++divCount == ignoreEventCount) {
                    Object o = table.getItem(new Point(e.x, e.y));
                    if (o instanceof TableItem && lastY != e.y) {
                        lastY = e.y;
                        if (!o.equals(fLastItem)) {
                            fLastItem = (TableItem) o;
                            table.setSelection(new TableItem[] { fLastItem });
                        } else if (e.y < itemHeightdiv4) {
                            // Scroll up
                            Item item = fTableViewer.scrollUp(e.x + tableLoc.x, e.y + tableLoc.y);
                            if (item instanceof TableItem) {
                                fLastItem = (TableItem) item;
                                table.setSelection(new TableItem[] { fLastItem });
                            }
                        } else if (e.y > tableHeight - itemHeightdiv4) {
                            // Scroll down
                            Item item = fTableViewer.scrollDown(e.x + tableLoc.x, e.y + tableLoc.y);
                            if (item instanceof TableItem) {
                                fLastItem = (TableItem) item;
                                table.setSelection(new TableItem[] { fLastItem });
                            }
                        }
                    }
                }
            }
        });
    
        table.addMouseListener(new MouseAdapter() {
            public void mouseUp(MouseEvent e) {
                if (table.getSelectionCount() < 1)
                    return;

                if (e.button == 1)
                    if (table.equals(e.getSource())) {
                        Object o = table.getItem(new Point(e.x, e.y));
                        TableItem selection = table.getSelection()[0];
                        if (selection.equals(o))
                            gotoSelectedElement();
                    }
                if (e.button == 3) {
                    TableItem tItem = fTableViewer.getTable().getItem(
                            new Point(e.x, e.y));
                    if (tItem != null) {
                        Menu menu = new Menu(fTableViewer.getTable());
                        MenuItem mItem = new MenuItem(menu, SWT.NONE);
                        mItem.setText(WorkbenchMessages.PartPane_close);
                        mItem.addSelectionListener(new SelectionAdapter() {
                            public void widgetSelected(
                                    SelectionEvent selectionEvent) {
                                removeSelectedItems();
                            }
                        });
                        menu.setVisible(true);
                    }
                }
            }
        });

        int border = ((shellStyle & SWT.NO_TRIM) == 0) ? 0 : BORDER;
        fShell.setLayout(new BorderFillLayout(border));

        setInfoSystemColor();
        installFilter();
    }

    /**
     * Removes the selected items from the list and closes their corresponding tabs
     * Selects the next item in the list or disposes it if its presentation is disposed
     */
    protected void removeSelectedItems() {
        int selInd = fTableViewer.getTable().getSelectionIndex();
        if (deleteSelectedElements()) {
            return;
        }
        fTableViewer.refresh();
        if (selInd >= fTableViewer.getTable().getItemCount())
            selInd = fTableViewer.getTable().getItemCount() - 1;
        if (selInd >= 0)
            fTableViewer.getTable().setSelection(selInd);
    }

    protected abstract TableViewer createTableViewer(Composite parent, int style);

    public TableViewer getTableViewer() {
        return fTableViewer;
    }

    protected Text createFilterText(Composite parent) {
        fFilterText = new Text(parent, SWT.NONE);

        GridData data = new GridData();
        GC gc = new GC(parent);
        gc.setFont(parent.getFont());
        FontMetrics fontMetrics = gc.getFontMetrics();
        gc.dispose();

        data.heightHint = org.eclipse.jface.dialogs.Dialog
                .convertHeightInCharsToPixels(fontMetrics, 1);
        data.horizontalAlignment = GridData.FILL;
        data.verticalAlignment = GridData.BEGINNING;
        fFilterText.setLayoutData(data);

        fFilterText.addKeyListener(new KeyListener() {
            public void keyPressed(KeyEvent e) {
                if (e.keyCode == 0x0D) // return
                    gotoSelectedElement();
                if (e.keyCode == SWT.ARROW_DOWN) {
                    fTableViewer.getTable().setFocus();
                    fTableViewer.getTable().setSelection(0);
                }
                if (e.keyCode == SWT.ARROW_UP) {
                    fTableViewer.getTable().setFocus();
                    fTableViewer.getTable().setSelection(
                            fTableViewer.getTable().getItemCount() - 1);
                }
                if (e.character == 0x1B) // ESC
                    dispose();
            }

            public void keyReleased(KeyEvent e) {
                // do nothing
            }
        });

        // Horizontal separator line
        Label separator = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL
                | SWT.LINE_DOT);
        separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        return fFilterText;
    }

    private void setInfoSystemColor() {
        Display display = fShell.getDisplay();
        setForegroundColor(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
        setBackgroundColor(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
    }

    private void installFilter() {
        fFilterText.setText(""); //$NON-NLS-1$

        fFilterText.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                String text = ((Text) e.widget).getText();
                int length = text.length();
                if (length > 0 && text.charAt(length - 1) != '*') {
                    text = text + '*';
                }
                setMatcherString(text);
            }
        });
    }

    /**
     * The string matcher has been modified. The default implementation
     * refreshes the view and selects the first macthed element
     */
    protected void stringMatcherUpdated() {
        // refresh viewer to refilter
        fTableViewer.getControl().setRedraw(false);
        fTableViewer.refresh();
        selectFirstMatch();
        fTableViewer.getControl().setRedraw(true);
    }

    /**
     * Sets the patterns to filter out for the receiver.
     * <p>
     * The following characters have special meaning: ? => any character * =>
     * any string
     * </p>
     */
    protected void setMatcherString(String pattern) {
        if (pattern.length() == 0) {
            fStringMatcher = null;
        } else {
            boolean ignoreCase = pattern.toLowerCase().equals(pattern);
            fStringMatcher = new StringMatcher(pattern, ignoreCase, false);
        }
        stringMatcherUpdated();
    }

    protected StringMatcher getMatcher() {
        return fStringMatcher;
    }

    /**
     * Implementers can modify
     */
    protected Object getSelectedElement() {
        return ((IStructuredSelection) fTableViewer.getSelection())
                .getFirstElement();
    }

    /**
     * Implementers can modify
     */
    protected IStructuredSelection getSelectedElements() {
        return (IStructuredSelection) fTableViewer.getSelection();
    }

    protected abstract void gotoSelectedElement();

    /**
     * Delete all selected elements.
     *  
     * @return <code>true</code> if there are no elements left after deletion.
     */
    protected abstract boolean deleteSelectedElements();

    /**
     * Selects the first element in the table which matches the current filter
     * pattern.
     */
    protected void selectFirstMatch() {
        Table table = fTableViewer.getTable();
        Object element = findElement(table.getItems());
        if (element != null)
            fTableViewer.setSelection(new StructuredSelection(element), true);
        else
            fTableViewer.setSelection(StructuredSelection.EMPTY);
    }

    private Object findElement(TableItem[] items) {
        ILabelProvider labelProvider = (ILabelProvider) fTableViewer
                .getLabelProvider();
        for (int i = 0; i < items.length; i++) {
            Object element = items[i].getData();
            if (fStringMatcher == null)
                return element;

            if (element != null) {
                String label = labelProvider.getText(element);
                if(label == null)
                	return null;
                // remove the dirty prefix from the editor's label
                String prefix = DefaultTabItem.DIRTY_PREFIX;
                if (label.startsWith(prefix))
                    label = label.substring(prefix.length());
                if (fStringMatcher.match(label))
                    return element;
            }
        }
        return null;
    }

    public abstract void setInput(Object information);

    protected void inputChanged(Object newInput, Object newSelection) {
        fFilterText.setText(""); //$NON-NLS-1$
        fTableViewer.setInput(newInput);

        // Resize the table's height accordingly to the new input
        Table viewerTable = fTableViewer.getTable();
        Point tableSize = viewerTable.computeSize(SWT.DEFAULT, SWT.DEFAULT);
        int tableMaxHeight = fComposite.getDisplay().getBounds().height / 2;
        // removes padding if necessary
        int tableHeight = (tableSize.y <= tableMaxHeight) ? tableSize.y
                - viewerTable.getItemHeight() - viewerTable.getItemHeight() / 2
                : tableMaxHeight;
        ((GridData) viewerTable.getLayoutData()).heightHint = tableHeight;
        Point fCompSize = fComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT);
        fComposite.setSize(fCompSize);
        fComposite.getShell().setSize(fCompSize);
    }

    public void setVisible(boolean visible) {
        fShell.setVisible(visible);
    }

    public void dispose() {
        if (fShell != null) {
            if (!fShell.isDisposed())
                fShell.dispose();
            fShell = null;
            fTableViewer = null;
            fComposite = null;
            fFilterText = null;
        }
    }

    public boolean hasContents() {
        return fTableViewer != null && fTableViewer.getInput() != null;
    }

    public void setSizeConstraints(int maxWidth, int maxHeight) {
        //fMaxWidth= maxWidth;
        //fMaxHeight= maxHeight;
    }

    public Point computeSizeHint() {
        return fShell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
    }

    public void setLocation(Point location) {
        Rectangle trim = fShell.computeTrim(0, 0, 0, 0);
        Point textLocation = fComposite.getLocation();
        location.x += trim.x - textLocation.x;
        location.y += trim.y - textLocation.y;
        fShell.setLocation(location);
    }

    public void setSize(int width, int height) {
        fShell.setSize(width, height);
    }

    public void addDisposeListener(DisposeListener listener) {
        fShell.addDisposeListener(listener);
    }

    public void removeDisposeListener(DisposeListener listener) {
        fShell.removeDisposeListener(listener);
    }

    public void setForegroundColor(Color foreground) {
        fTableViewer.getTable().setForeground(foreground);
        fFilterText.setForeground(foreground);
        fComposite.setForeground(foreground);
    }

    public void setBackgroundColor(Color background) {
        fTableViewer.getTable().setBackground(background);
        fFilterText.setBackground(background);
        fComposite.setBackground(background);
    }

    public boolean isFocusControl() {
        return fTableViewer.getControl().isFocusControl()
                || fFilterText.isFocusControl();
    }

    public void setFocus() {
        fShell.forceFocus();
        fFilterText.setFocus();
    }

    public void addFocusListener(FocusListener listener) {
        fShell.addFocusListener(listener);
    }

    public void removeFocusListener(FocusListener listener) {
        fShell.removeFocusListener(listener);
    }
}
