/*******************************************************************************
 * Copyright (c) 2006, 2009 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.actions;

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

import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

/**
 * Abstract class that is capable of creating a context menu. It will try and
 * open the menu close to the current selection, or under the mouse pointer if
 * that's not possible.
 * 
 * @since 3.3
 */
public abstract class QuickMenuCreator {

	private static final int CHAR_INDENT = 3;

	/**
	 * Create and open the context menu.
	 */
	public void createMenu() {
		final Display display = Display.getCurrent();
		if (display == null) {
			return;
		}
		Control focus = display.getFocusControl();
		if (focus == null || focus.isDisposed()) {
			return;
		}

		MenuManager menu = new MenuManager();
		fillMenu(menu);
		final Menu quickMenu = menu.createContextMenu(focus.getShell());
		Point location = computeMenuLocation(focus);
		if (location == null) {
			return;
		}
		quickMenu.setLocation(location);
		quickMenu.addListener(SWT.Hide, new Listener() {
			public void handleEvent(Event event) {
				if (!display.isDisposed()) {
					display.asyncExec(new Runnable() {
						public void run() {
							if (!quickMenu.isDisposed()) {
								quickMenu.dispose();
							}
						}
					});
				}
			}
		});
		quickMenu.setVisible(true);
	}

	/**
	 * Create the contents of the context menu.
	 * 
	 * @param menu
	 *            the menu to fill
	 */
	protected abstract void fillMenu(IMenuManager menu);

	/**
	 * Determine the optimal point for this menu to appear.
	 * 
	 * @param focus
	 *            the focus control
	 * @return the optimal placement
	 * @since 3.5
	 */
	protected Point computeMenuLocation(Control focus) {
		Point cursorLocation = focus.getDisplay().getCursorLocation();
		Rectangle clientArea = null;
		Point result = null;
		if (focus instanceof StyledText) {
			StyledText styledText = (StyledText) focus;
			clientArea = styledText.getClientArea();
			result = computeMenuLocation(styledText);
		} else if (focus instanceof Tree) {
			Tree tree = (Tree) focus;
			clientArea = tree.getClientArea();
			result = computeMenuLocation(tree);
		} else if (focus instanceof Table) {
			Table table = (Table) focus;
			clientArea = table.getClientArea();
			result = computeMenuLocation(table);
		}
		if (result == null) {
			result = focus.toControl(cursorLocation);
		}
		if (clientArea != null && !clientArea.contains(result)) {
			result = new Point(clientArea.x + clientArea.width / 2,
					clientArea.y + clientArea.height / 2);
		}
		Rectangle shellArea = focus.getShell().getClientArea();
		if (!shellArea.contains(focus.getShell().toControl(
				focus.toDisplay(result)))) {
			result = new Point(shellArea.x + shellArea.width / 2, shellArea.y
					+ shellArea.height / 2);
		}
		return focus.toDisplay(result);
	}

	/**
	 * Hook to compute the menu location if the focus widget is a styled text
	 * widget.
	 * 
	 * @param text
	 *            the styled text widget that has the focus
	 * 
	 * @return a widget relative position of the menu to pop up or
	 *         <code>null</code> if now position inside the widget can be
	 *         computed
	 * @since 3.5
	 */
	protected Point computeMenuLocation(StyledText text) {
		Point result = text.getLocationAtOffset(text.getCaretOffset());
		result.y += text.getLineHeight(text.getCaretOffset());
		if (!text.getClientArea().contains(result)) {
			return null;
		}
		return result;
	}

	/**
	 * Hook to compute the menu location if the focus widget is a tree widget.
	 * 
	 * @param tree
	 *            the tree widget that has the focus
	 * 
	 * @return a widget relative position of the menu to pop up or
	 *         <code>null</code> if now position inside the widget can be
	 *         computed
	 * @since 3.5
	 */
	protected Point computeMenuLocation(Tree tree) {
		TreeItem[] items = tree.getSelection();
		Rectangle clientArea = tree.getClientArea();
		switch (items.length) {
		case 0:
			return null;
		case 1:
			Rectangle bounds = items[0].getBounds();
			Rectangle intersect = clientArea.intersection(bounds);
			if (intersect != null && intersect.height == bounds.height) {
				return new Point(Math.max(0, bounds.x
						+ getAvarageCharWith(tree) * CHAR_INDENT), bounds.y
						+ bounds.height);
			}
			return null;

		default:
			Rectangle[] rectangles = new Rectangle[items.length];
			for (int i = 0; i < rectangles.length; i++) {
				rectangles[i] = items[i].getBounds();
			}
			Point cursorLocation = tree.getDisplay().getCursorLocation();
			Point result = findBestLocation(getIncludedPositions(rectangles,
					clientArea), tree.toControl(cursorLocation));
			if (result != null) {
				result.x = result.x + getAvarageCharWith(tree) * CHAR_INDENT;
			}
			return result;
		}
	}

	/**
	 * Hook to compute the menu location if the focus widget is a table widget.
	 * 
	 * @param table
	 *            the table widget that has the focus
	 * 
	 * @return a widget relative position of the menu to pop up or
	 *         <code>null</code> if now position inside the widget can be
	 *         computed
	 * @since 3.5
	 */
	protected Point computeMenuLocation(Table table) {
		TableItem[] items = table.getSelection();
		Rectangle clientArea = table.getClientArea();
		switch (items.length) {
		case 0: {
			return null;
		}
		case 1: {
			Rectangle bounds = items[0].getBounds(0);
			Rectangle iBounds = items[0].getImageBounds(0);
			Rectangle intersect = clientArea.intersection(bounds);
			if (intersect != null && intersect.height == bounds.height) {
				return new Point(Math.max(0, bounds.x + iBounds.width
						+ getAvarageCharWith(table) * CHAR_INDENT), bounds.y
						+ bounds.height);
			}
			return null;

		}
		default: {
			Rectangle[] rectangles = new Rectangle[items.length];
			for (int i = 0; i < rectangles.length; i++) {
				rectangles[i] = items[i].getBounds(0);
			}
			Rectangle iBounds = items[0].getImageBounds(0);
			Point cursorLocation = table.getDisplay().getCursorLocation();
			Point result = findBestLocation(getIncludedPositions(rectangles,
					clientArea), table.toControl(cursorLocation));
			if (result != null) {
				result.x = result.x + iBounds.width + getAvarageCharWith(table)
						* CHAR_INDENT;
			}
			return result;
		}
		}
	}

	private Point[] getIncludedPositions(Rectangle[] rectangles,
			Rectangle widgetBounds) {
		List result = new ArrayList();
		for (int i = 0; i < rectangles.length; i++) {
			Rectangle rectangle = rectangles[i];
			Rectangle intersect = widgetBounds.intersection(rectangle);
			if (intersect != null && intersect.height == rectangle.height) {
				result.add(new Point(intersect.x, intersect.y
						+ intersect.height));
			}
		}
		return (Point[]) result.toArray(new Point[result.size()]);
	}

	private Point findBestLocation(Point[] points, Point relativeCursor) {
		Point result = null;
		double bestDist = Double.MAX_VALUE;
		for (int i = 0; i < points.length; i++) {
			Point point = points[i];
			int a = 0;
			int b = 0;
			if (point.x > relativeCursor.x) {
				a = point.x - relativeCursor.x;
			} else {
				a = relativeCursor.x - point.x;
			}
			if (point.y > relativeCursor.y) {
				b = point.y - relativeCursor.y;
			} else {
				b = relativeCursor.y - point.y;
			}
			double dist = Math.sqrt(a * a + b * b);
			if (dist < bestDist) {
				result = point;
				bestDist = dist;
			}
		}
		return result;
	}

	private int getAvarageCharWith(Control control) {
		GC gc = null;
		try {
			gc = new GC(control);
			return gc.getFontMetrics().getAverageCharWidth();
		} finally {
			if (gc != null) {
				gc.dispose();
			}
		}
	}

	/**
	 * Dispose of this quick menu creator. Subclasses should ensure that they
	 * call this method.
	 * 
	 * @deprecated As of 3.5 this is not necessary as the SWT Menu created in
	 *             {@link #createMenu()} will be disposed shortly after the
	 *             SWT.Hide event.
	 */
	public void dispose() {
	}
}
