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

import net.sourceforge.phpdt.core.ICompilationUnit;
import net.sourceforge.phpdt.core.IJavaElement;
import net.sourceforge.phpdt.core.IParent;
import net.sourceforge.phpdt.core.JavaModelException;
import net.sourceforge.phpdt.internal.ui.actions.OpenActionUtil;
import net.sourceforge.phpdt.internal.ui.util.StringMatcher;
import net.sourceforge.phpdt.internal.ui.viewsupport.AppearanceAwareLabelProvider;
import net.sourceforge.phpdt.internal.ui.viewsupport.DecoratingJavaLabelProvider;
import net.sourceforge.phpdt.internal.ui.viewsupport.JavaElementLabels;
import net.sourceforge.phpdt.ui.JavaElementSorter;
import net.sourceforge.phpdt.ui.StandardJavaElementContentProvider;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TreeViewer;
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.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.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

/**
 * @author dmegert
 * 
 * To change this generated comment edit the template variable "typecomment":
 * Window>Preferences>Java>Templates. To enable and disable the creation of type
 * comments go to Window>Preferences>Java>Code Generation.
 */
public class JavaOutlineInformationControl implements IInformationControl,
		IInformationControlExtension, IInformationControlExtension2 {

	/**
	 * The NamePatternFilter selects the elements which match the given string
	 * patterns.
	 * <p>
	 * The following characters have special meaning: ? => any character * =>
	 * any string
	 * </p>
	 * 
	 * @since 2.0
	 */
	private static class NamePatternFilter extends ViewerFilter {
		private String fPattern;

		private StringMatcher fMatcher;

		private ILabelProvider fLabelProvider;

		private Viewer fViewer;

		private StringMatcher getMatcher() {
			return fMatcher;
		}

		/*
		 * (non-Javadoc) Method declared on ViewerFilter.
		 */
		public boolean select(Viewer viewer, Object parentElement,
				Object element) {
			if (fMatcher == null)
				return true;

			ILabelProvider labelProvider = getLabelProvider(viewer);

			String matchName = null;
			if (labelProvider != null)
				matchName = ((ILabelProvider) labelProvider).getText(element);
			else if (element instanceof IJavaElement)
				matchName = ((IJavaElement) element).getElementName();

			if (matchName != null && fMatcher.match(matchName))
				return true;

			return hasUnfilteredChild(viewer, element);
		}

		private ILabelProvider getLabelProvider(Viewer viewer) {
			if (fViewer == viewer)
				return fLabelProvider;

			fLabelProvider = null;
			IBaseLabelProvider baseLabelProvider = null;
			if (viewer instanceof StructuredViewer)
				baseLabelProvider = ((StructuredViewer) viewer)
						.getLabelProvider();

			if (baseLabelProvider instanceof ILabelProvider)
				fLabelProvider = (ILabelProvider) baseLabelProvider;

			return fLabelProvider;
		}

		private boolean hasUnfilteredChild(Viewer viewer, Object element) {
			IJavaElement[] children;
			if (element instanceof IParent) {
				try {
					children = ((IParent) element).getChildren();
				} catch (JavaModelException ex) {
					return false;
				}
				for (int i = 0; i < children.length; i++)
					if (select(viewer, element, children[i]))
						return true;
			}
			return false;
		}

		/**
		 * Sets the patterns to filter out for the receiver.
		 * <p>
		 * The following characters have special meaning: ? => any character * =>
		 * any string
		 * </p>
		 */
		public void setPattern(String pattern) {
			fPattern = pattern;
			if (fPattern == null) {
				fMatcher = null;
				return;
			}
			boolean ignoreCase = pattern.toLowerCase().equals(pattern);
			fMatcher = new StringMatcher(pattern, ignoreCase, false);
		}
	}

	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 */
	Composite fComposite;

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

	/** The control's tree widget */
	private TreeViewer fTreeViewer;

	/** The control width constraint */
	private int fMaxWidth = -1;

	/** The control height constraint */
	private int fMaxHeight = -1;

	private StringMatcher fStringMatcher;

	/**
	 * Creates a tree information control with the given shell as parent. The
	 * given style is applied to the tree widget.
	 * 
	 * @param parent
	 *            the parent shell
	 * @param style
	 *            the additional styles for the tree widget
	 */
	public JavaOutlineInformationControl(Shell parent, int style) {
		this(parent, SWT.RESIZE, style);
	}

	/**
	 * Creates a tree information control with the given shell as parent. No
	 * additional styles are applied.
	 * 
	 * @param parent
	 *            the parent shell
	 */
	public JavaOutlineInformationControl(Shell parent) {
		this(parent, SWT.NONE);
	}

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

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

		createFilterText(fComposite);
		createTreeViewer(fComposite, treeStyle);

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

		setInfoSystemColor();
		installFilter();
	}

	private void createTreeViewer(Composite parent, int style) {
		Tree tree = new Tree(parent, SWT.SINGLE | (style & ~SWT.MULTI));
		GridData data = new GridData(GridData.FILL_BOTH);
		tree.setLayoutData(data);

		fTreeViewer = new TreeViewer(tree);

		// Hide import declartions but show the container
		// fTreeViewer.addFilter(new ViewerFilter() {
		// public boolean select(Viewer viewer, Object parentElement, Object
		// element) {
		// return !(element instanceof IImportDeclaration);
		// }
		// });

		fTreeViewer.setContentProvider(new StandardJavaElementContentProvider(
				true, true));
		fTreeViewer.setSorter(new JavaElementSorter());
		fTreeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);

		AppearanceAwareLabelProvider lprovider = new AppearanceAwareLabelProvider(
				AppearanceAwareLabelProvider.DEFAULT_TEXTFLAGS
						| JavaElementLabels.F_APP_TYPE_SIGNATURE,
				AppearanceAwareLabelProvider.DEFAULT_IMAGEFLAGS);
		fTreeViewer
				.setLabelProvider(new DecoratingJavaLabelProvider(lprovider));

		fTreeViewer.getTree().addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent e) {
				if (e.character == 0x1B) // ESC
					dispose();
			}

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

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

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

	private Text createFilterText(Composite parent) {
		fFilterText = new Text(parent, SWT.FLAT);

		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)
					fTreeViewer.getTree().setFocus();
				if (e.keyCode == SWT.ARROW_UP)
					fTreeViewer.getTree().setFocus();
				if (e.character == 0x1B) // ESC
					dispose();
			}

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

		// Horizonral 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() {
		final NamePatternFilter viewerFilter = new NamePatternFilter();
		fTreeViewer.addFilter(viewerFilter);
		fFilterText.setText(""); //$NON-NLS-1$

		fFilterText.addModifyListener(new ModifyListener() {
			public void modifyText(ModifyEvent e) {
				String pattern = fFilterText.getText();
				if (pattern != null) {
					int length = pattern.length();
					if (length == 0)
						pattern = null;
					else if (pattern.charAt(length - 1) != '*')
						pattern = pattern + '*';
				} else
					pattern = null;
				viewerFilter.setPattern(pattern);
				fStringMatcher = viewerFilter.getMatcher();
				fTreeViewer.getControl().setRedraw(false);
				fTreeViewer.refresh();
				fTreeViewer.expandAll();
				selectFirstMatch();
				fTreeViewer.getControl().setRedraw(true);
			}
		});
	}

	private void gotoSelectedElement() {
		Object selectedElement = ((IStructuredSelection) fTreeViewer
				.getSelection()).getFirstElement();
		if (selectedElement != null) {
			try {
				dispose();
				OpenActionUtil.open(selectedElement, true);
			} catch (CoreException ex) {
				PHPeclipsePlugin.log(ex);
			}
		}
	}

	/**
	 * Selects the first element in the tree which matches the current filter
	 * pattern.
	 */
	private void selectFirstMatch() {
		Tree tree = fTreeViewer.getTree();
		Object element = findElement(tree.getItems());
		if (element != null)
			fTreeViewer.setSelection(new StructuredSelection(element), true);
		else
			fTreeViewer.setSelection(StructuredSelection.EMPTY);
	}

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

			if (element != null) {
				String label = labelProvider.getText(element);
				if (fStringMatcher.match(label))
					return element;
			}

			element = findElement(items[i].getItems());
			if (element != null)
				return element;
		}
		return null;
	}

	/*
	 * @see IInformationControl#setInformation(String)
	 */
	public void setInformation(String information) {
		// this method is ignored, see IInformationControlExtension2
	}

	/*
	 * @see IInformationControlExtension2#setInput(Object)
	 */
	public void setInput(Object information) {
		fFilterText.setText(""); //$NON-NLS-1$
		if (information == null || information instanceof String) {
			setInput(null);
			return;
		}
		IJavaElement je = (IJavaElement) information;
		IJavaElement sel = null;
		ICompilationUnit cu = (ICompilationUnit) je
				.getAncestor(IJavaElement.COMPILATION_UNIT);
		if (cu != null)
			sel = cu;
		else
			sel = je.getAncestor(IJavaElement.CLASS_FILE);
		fTreeViewer.setInput(sel);
		fTreeViewer.setSelection(new StructuredSelection(information));
	}

	/*
	 * @see IInformationControl#setVisible(boolean)
	 */
	public void setVisible(boolean visible) {
		fShell.setVisible(visible);
	}

	/*
	 * @see IInformationControl#dispose()
	 */
	public void dispose() {
		if (fShell != null) {
			if (!fShell.isDisposed())
				fShell.dispose();
			fShell = null;
			fTreeViewer = null;
			fComposite = null;
			fFilterText = null;
		}
	}

	/*
	 * @see org.eclipse.jface.text.IInformationControlExtension#hasContents()
	 */
	public boolean hasContents() {
		return fTreeViewer != null && fTreeViewer.getInput() != null;
	}

	/*
	 * @see org.eclipse.jface.text.IInformationControl#setSizeConstraints(int,
	 *      int)
	 */
	public void setSizeConstraints(int maxWidth, int maxHeight) {
		fMaxWidth = maxWidth;
		fMaxHeight = maxHeight;
	}

	/*
	 * @see org.eclipse.jface.text.IInformationControl#computeSizeHint()
	 */
	public Point computeSizeHint() {
		return fShell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
	}

	/*
	 * @see IInformationControl#setLocation(Point)
	 */
	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);
	}

	/*
	 * @see IInformationControl#setSize(int, int)
	 */
	public void setSize(int width, int height) {
		fShell.setSize(width, height);
	}

	/*
	 * @see IInformationControl#addDisposeListener(DisposeListener)
	 */
	public void addDisposeListener(DisposeListener listener) {
		fShell.addDisposeListener(listener);
	}

	/*
	 * @see IInformationControl#removeDisposeListener(DisposeListener)
	 */
	public void removeDisposeListener(DisposeListener listener) {
		fShell.removeDisposeListener(listener);
	}

	/*
	 * @see IInformationControl#setForegroundColor(Color)
	 */
	public void setForegroundColor(Color foreground) {
		fTreeViewer.getTree().setForeground(foreground);
		fFilterText.setForeground(foreground);
		fComposite.setForeground(foreground);
	}

	/*
	 * @see IInformationControl#setBackgroundColor(Color)
	 */
	public void setBackgroundColor(Color background) {
		fTreeViewer.getTree().setBackground(background);
		fFilterText.setBackground(background);
		fComposite.setBackground(background);
	}

	/*
	 * @see IInformationControl#isFocusControl()
	 */
	public boolean isFocusControl() {
		return fTreeViewer.getControl().isFocusControl()
				|| fFilterText.isFocusControl();
	}

	/*
	 * @see IInformationControl#setFocus()
	 */
	public void setFocus() {
		fShell.forceFocus();
		fFilterText.setFocus();
	}

	/*
	 * @see IInformationControl#addFocusListener(FocusListener)
	 */
	public void addFocusListener(FocusListener listener) {
		fShell.addFocusListener(listener);
	}

	/*
	 * @see IInformationControl#removeFocusListener(FocusListener)
	 */
	public void removeFocusListener(FocusListener listener) {
		fShell.removeFocusListener(listener);
	}
}
