/*******************************************************************************
 * Copyright (c) 2006, 2019 IBM Corporation and others.
 *
 * 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:
 *     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.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, event -> {
			if (!display.isDisposed()) {
				display.asyncExec(() -> {
					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<Point> result = new ArrayList<>();
		for (Rectangle rectangle : rectangles) {
			Rectangle intersect = widgetBounds.intersection(rectangle);
			if (intersect != null && intersect.height == rectangle.height) {
				result.add(new Point(intersect.x, intersect.y + intersect.height));
			}
		}
		return result.toArray(new Point[result.size()]);
	}

	private Point findBestLocation(Point[] points, Point relativeCursor) {
		Point result = null;
		double bestDist = Double.MAX_VALUE;
		for (Point point : points) {
			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 (int) gc.getFontMetrics().getAverageCharacterWidth();
		} 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.
	 */
	@Deprecated
	public void dispose() {
	}
}
