/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     Andrey Loskutov <loskutov@gmx.de> - Bug 445538
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 *     Patrik Suzzi <psuzzi@gmail.com> - Bug 489250
 *******************************************************************************/
package org.eclipse.ui.internal.dialogs.cpd;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.e4.ui.workbench.renderers.swt.HandledContributionItem;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.preference.PreferenceDialog;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.dialogs.PreferencesUtil;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchWindow;
import org.eclipse.ui.internal.dialogs.cpd.CustomizePerspectiveDialog.ActionSet;
import org.eclipse.ui.internal.dialogs.cpd.CustomizePerspectiveDialog.DisplayItem;
import org.eclipse.ui.internal.dialogs.cpd.CustomizePerspectiveDialog.DynamicContributionItem;
import org.eclipse.ui.internal.dialogs.cpd.CustomizePerspectiveDialog.ShortcutItem;
import org.eclipse.ui.internal.dialogs.cpd.TreeManager.TreeItem;
import org.eclipse.ui.internal.keys.BindingService;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.keys.IBindingService;

/**
 * A tooltip with useful information based on the type of ContributionItem the
 * cursor hovers over in a Tree. In addition to the content provided by the
 * {@link NameAndDescriptionToolTip} this includes action set information and
 * key binding data.
 *
 * @since 3.5
 */
class ItemDetailToolTip extends NameAndDescriptionToolTip {
	private Tree tree;
	private boolean showActionSet;
	private boolean showKeyBindings;
	private ViewerFilter filter;
	private TreeViewer v;
	private CustomizePerspectiveDialog dialog;

	/**
	 * @param dialog
	 * @param tree   The tree for the tooltip to hover over
	 */
	ItemDetailToolTip(CustomizePerspectiveDialog dialog, TreeViewer v, Tree tree, boolean showActionSet,
			boolean showKeyBindings, ViewerFilter filter) {
		super(tree, NO_RECREATE);
		this.dialog = dialog;
		this.tree = tree;
		this.v = v;
		this.showActionSet = showActionSet;
		this.showKeyBindings = showKeyBindings;
		this.filter = filter;
		this.setHideOnMouseDown(false);
	}

	@Override
	public Point getLocation(Point tipSize, Event event) {
		// try to position the tooltip at the bottom of the cell
		ViewerCell cell = v.getCell(new Point(event.x, event.y));

		if (cell != null) {
			return tree.toDisplay(event.x, cell.getBounds().y + cell.getBounds().height);
		}

		return super.getLocation(tipSize, event);
	}

	@Override
	protected Object getToolTipArea(Event event) {
		// Ensure that the tooltip is hidden when the cell is left
		return v.getCell(new Point(event.x, event.y));
	}

	@Override
	protected void addContent(Composite destination, Object modelElement) {
		final DisplayItem item = (DisplayItem) modelElement;

		// Show any relevant action set info
		if (showActionSet) {
			String text = null;

			boolean effectivelyAvailable = CustomizePerspectiveDialog.isEffectivelyAvailable(item, filter);
			if (effectivelyAvailable) {
				if (item.actionSet != null) {
					// give information on which command group the item is in

					final String actionSetName = item.getActionSet().descriptor.getLabel();

					text = NLS.bind(WorkbenchMessages.HideItems_itemInActionSet, actionSetName);
				}
			} else {
				// give feedback on why item is unavailable

				if (item.getChildren().isEmpty() && item.getActionSet() != null) {
					// i.e. is a leaf

					final String actionSetName = item.getActionSet().descriptor.getLabel();

					text = NLS.bind(WorkbenchMessages.HideItems_itemInUnavailableActionSet, actionSetName);

				} else if (item.getChildren().isEmpty() && item.getActionSet() == null
						&& item.getIContributionItem() instanceof HandledContributionItem) {
					text = WorkbenchMessages.HideItems_itemInUnavailableCommand;
				} else {
					// i.e. has children
					Set<ActionSet> actionGroup = new LinkedHashSet<>();
					ItemDetailToolTip.collectDescendantCommandGroups(actionGroup, item, filter);

					if (actionGroup.size() == 1) {
						// i.e. only one child
						ActionSet actionSet = actionGroup.iterator().next();
						text = NLS.bind(WorkbenchMessages.HideItems_unavailableChildCommandGroup,
								actionSet.descriptor.getId(), actionSet.descriptor.getLabel());
					} else {
						// i.e. multiple children
						String commandGroupList = null;

						for (ActionSet actionSet : actionGroup) {
							// For each action set, make a link for it, set
							// the href to its id
							String commandGroupLink = MessageFormat.format("<a href=\"{0}\">{1}</a>", //$NON-NLS-1$
									actionSet.descriptor.getId(), actionSet.descriptor.getLabel());

							if (commandGroupList == null) {
								commandGroupList = commandGroupLink;
							} else {
								commandGroupList = Util.createList(commandGroupList, commandGroupLink);
							}
						}

						commandGroupList = NLS.bind("{0}{1}", //$NON-NLS-1$
								new Object[] { CustomizePerspectiveDialog.NEW_LINE, commandGroupList });
						text = NLS.bind(WorkbenchMessages.HideItems_unavailableChildCommandGroups, commandGroupList);
					}
				}
			}

			if (text != null) {
				Image warningImage;
				if (!effectivelyAvailable) {
					warningImage = dialog.warningImageDescriptor.createImage();
					destination.addDisposeListener(e -> warningImage.dispose());
				} else {
					warningImage = null;
				}
				Link link = createEntryWithLink(destination, warningImage, text);
				link.addSelectionListener(new SelectionListener() {
					@Override
					public void widgetDefaultSelected(SelectionEvent e) {
						widgetSelected(e);
					}

					@Override
					public void widgetSelected(SelectionEvent e) {
						ActionSet actionSet = dialog.idToActionSet.get(e.text);
						if (actionSet == null) {
							hide();
							dialog.showActionSet(item);
						} else {
							hide();
							dialog.showActionSet(actionSet);
						}
					}
				});
			}
		}

		// Show key binding info
		if (showKeyBindings && CustomizePerspectiveDialog.getCommandID(item) != null) {
			// See if there is a command associated with the command id
			ICommandService commandService = dialog.window.getService(ICommandService.class);
			Command command = commandService.getCommand(CustomizePerspectiveDialog.getCommandID(item));

			if (command != null && command.isDefined()) {
				// Find the bindings and list them as a string
				Binding[] bindings = ItemDetailToolTip.getKeyBindings(dialog.window, item);
				String keybindings = ItemDetailToolTip.keyBindingsAsString(bindings);

				String text = null;

				// Is it possible for this item to be visible?
				final boolean available = (item.getActionSet() == null) || (item.getActionSet().isActive());

				if (bindings.length > 0) {
					if (available) {
						text = NLS.bind(WorkbenchMessages.HideItems_keyBindings, keybindings);
					} else {
						text = NLS.bind(WorkbenchMessages.HideItems_keyBindingsActionSetUnavailable, keybindings);
					}
				} else if (available) {
					text = WorkbenchMessages.HideItems_noKeyBindings;
				} else {
					text = WorkbenchMessages.HideItems_noKeyBindingsActionSetUnavailable;
				}

				// Construct link to go to the preferences page for key
				// bindings
				final Object highlight;
				if (bindings.length == 0) {
					Map<String, String> parameters = new HashMap<>();

					// If item is a shortcut, need to add a parameter to go
					// to
					// the correct item
					if (item instanceof ShortcutItem) {
						if (CustomizePerspectiveDialog.isNewWizard(item)) {
							parameters.put(IWorkbenchCommandConstants.FILE_NEW_PARM_WIZARDID,
									CustomizePerspectiveDialog.getParamID(item));
						} else if (CustomizePerspectiveDialog.isShowPerspective(item)) {
							parameters.put(IWorkbenchCommandConstants.PERSPECTIVES_SHOW_PERSPECTIVE_PARM_ID,
									CustomizePerspectiveDialog.getParamID(item));
						} else if (CustomizePerspectiveDialog.isShowView(item)) {
							parameters.put(IWorkbenchCommandConstants.VIEWS_SHOW_VIEW_PARM_ID,
									CustomizePerspectiveDialog.getParamID(item));
						}
					}

					ParameterizedCommand pc = ParameterizedCommand.generateCommand(command, parameters);
					highlight = pc;
				} else {
					highlight = bindings[0];
				}

				Link bindingLink = createEntryWithLink(destination, null, text);

				bindingLink.addSelectionListener(new SelectionListener() {
					@Override
					public void widgetDefaultSelected(SelectionEvent e) {
						widgetSelected(e);
					}

					@Override
					public void widgetSelected(SelectionEvent e) {
						PreferenceDialog pdialog = PreferencesUtil.createPreferenceDialogOn(dialog.getShell(),
								CustomizePerspectiveDialog.KEYS_PREFERENCE_PAGE_ID, new String[0], highlight);
						hide();
						pdialog.open();
					}
				});
			}
		}

		// Show dynamic menu item info
		if (item instanceof DynamicContributionItem) {
			DynamicContributionItem dynamic = ((DynamicContributionItem) item);
			StringBuilder text = new StringBuilder();
			final List<MenuItem> currentItems = dynamic.getCurrentItems();

			if (currentItems.size() > 0) {
				// Create a list of the currently displayed items
				text.append(WorkbenchMessages.HideItems_dynamicItemList);
				for (MenuItem menuItem : currentItems) {
					text.append(CustomizePerspectiveDialog.NEW_LINE).append("- ") //$NON-NLS-1$
							.append(menuItem.getText());
				}
			} else {
				text.append(WorkbenchMessages.HideItems_dynamicItemEmptyList);
			}
			createEntry(destination, null, text.toString());
		}
	}

	@Override
	protected Object getModelElement(Event event) {
		org.eclipse.swt.widgets.TreeItem treeItem = tree.getItem(new Point(event.x, event.y));
		if (treeItem == null) {
			return null;
		}
		return treeItem.getData();
	}

	/**
	 * @param collection a collection, into which all command groups (action sets)
	 *                   which contribute <code>item</code> or its descendants will
	 *                   be placed
	 * @param item       the item to collect descendants of
	 * @param filter     the filter currently being used
	 */
	static void collectDescendantCommandGroups(Collection<ActionSet> collection, DisplayItem item,
			ViewerFilter filter) {
		List<TreeItem> children = item.getChildren();
		for (TreeItem treeItem : children) {
			DisplayItem child = (DisplayItem) treeItem;
			if ((filter == null || filter.select(null, null, child)) && child.getActionSet() != null) {
				collection.add(child.getActionSet());
			}
			collectDescendantCommandGroups(collection, child, filter);
		}
	}

	/**
	 * @param bindings
	 * @return a String representing the key bindings in <code>bindings</code>
	 */
	static String keyBindingsAsString(Binding[] bindings) {
		String keybindings = null;
		for (int i = 0; i < bindings.length; i++) {
			// Unfortunately, bindings may be reported more than once:
			// check to see if this one has already been recorded.
			boolean alreadyRecorded = false;
			for (int j = 0; j < i && !alreadyRecorded; j++) {
				if (bindings[i].getTriggerSequence().equals(bindings[j].getTriggerSequence())) {
					alreadyRecorded = true;
				}
			}
			if (!alreadyRecorded) {
				String keybinding = bindings[i].getTriggerSequence().format();
				if (i == 0) {
					keybindings = keybinding;
				} else {
					keybindings = Util.createList(keybindings, keybinding);
				}
			}
		}
		return keybindings;
	}

	/**
	 * Gets the keybindings associated with a ContributionItem.
	 */
	static Binding[] getKeyBindings(WorkbenchWindow window, DisplayItem item) {
		IBindingService bindingService = window.getService(IBindingService.class);

		if (!(bindingService instanceof BindingService)) {
			return new Binding[0];
		}

		String id = CustomizePerspectiveDialog.getCommandID(item);
		String param = CustomizePerspectiveDialog.getParamID(item);

		BindingManager bindingManager = ((BindingService) bindingService).getBindingManager();

		Collection<?> allBindings = bindingManager.getActiveBindingsDisregardingContextFlat();

		List<Binding> foundBindings = new ArrayList<>(2);

		for (Object name : allBindings) {
			Binding binding = (Binding) name;
			if (binding.getParameterizedCommand() == null) {
				continue;
			}
			if (binding.getParameterizedCommand().getId() == null) {
				continue;
			}
			if (binding.getParameterizedCommand().getId().equals(id)) {
				if (param == null) {
					// We found it!
					foundBindings.add(binding);
				} else {
					// command parameters are only used in the shortcuts
					Map<?, ?> m = binding.getParameterizedCommand().getParameterMap();
					String key = null;
					if (CustomizePerspectiveDialog.isNewWizard(item)) {
						key = IWorkbenchCommandConstants.FILE_NEW_PARM_WIZARDID;
					} else if (CustomizePerspectiveDialog.isShowView(item)) {
						key = IWorkbenchCommandConstants.VIEWS_SHOW_VIEW_PARM_ID;
					} else if (CustomizePerspectiveDialog.isShowPerspective(item)) {
						key = IWorkbenchCommandConstants.PERSPECTIVES_SHOW_PERSPECTIVE_PARM_ID;
					}

					if (key != null) {
						if (param.equals(m.get(key))) {
							foundBindings.add(binding);
						}
					}
				}
			}
		}

		return foundBindings.toArray(new Binding[foundBindings.size()]);
	}
}