/*******************************************************************************
 * Copyright (c) 2010, 2022 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 180308, 472654
 *     Daniel Kruegler - #399, #401
 *******************************************************************************/
package org.eclipse.ui.internal.menus;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.expressions.EvaluationResult;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionConverter;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.e4.core.commands.EHandlerService;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.services.adapter.Adapter;
import org.eclipse.e4.ui.internal.workbench.swt.Policy;
import org.eclipse.e4.ui.internal.workbench.swt.WorkbenchSWTActivator;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.commands.MCommand;
import org.eclipse.e4.ui.model.application.ui.MCoreExpression;
import org.eclipse.e4.ui.model.application.ui.MExpression;
import org.eclipse.e4.ui.model.application.ui.impl.UiFactoryImpl;
import org.eclipse.e4.ui.model.application.ui.menu.ItemType;
import org.eclipse.e4.ui.model.application.ui.menu.MHandledToolItem;
import org.eclipse.e4.ui.model.application.ui.menu.MMenu;
import org.eclipse.e4.ui.model.application.ui.menu.MToolItem;
import org.eclipse.e4.ui.model.application.ui.menu.impl.MenuFactoryImpl;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.IWorkbench;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.ui.commands.ICommandImageService;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.WorkbenchWindow;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;

public class MenuHelper {

	public static void trace(String msg, Throwable error) {
		WorkbenchSWTActivator.trace(Policy.DEBUG_MENUS_FLAG, msg, error);
	}

	private static final Pattern SCHEME_PATTERN = Pattern.compile("\\p{Alpha}[\\p{Alnum}+.-]*:.*"); //$NON-NLS-1$

	public static String getImageUrl(ImageDescriptor imageDescriptor) {
		return getIconURI(imageDescriptor, null);
	}

	static MExpression getVisibleWhen(final IConfigurationElement commandAddition) {
		try {
			IConfigurationElement[] visibleConfig = commandAddition
					.getChildren(IWorkbenchRegistryConstants.TAG_VISIBLE_WHEN);
			if (visibleConfig.length > 0 && visibleConfig.length < 2) {
				IConfigurationElement[] visibleChild = visibleConfig[0].getChildren();
				if (visibleChild.length == 0) {
					String checkEnabled = visibleConfig[0].getAttribute(IWorkbenchRegistryConstants.ATT_CHECK_ENABLED);
					if (Boolean.parseBoolean(checkEnabled)) {
						final String commandId = getCommandId(commandAddition);
						if (commandId == null) {
							return null;
						}

						Expression visWhen = new Expression() {
							@Override
							public EvaluationResult evaluate(IEvaluationContext context) {
								EHandlerService service = getFromContext(context, EHandlerService.class);
								ICommandService commandService = getFromContext(context, ICommandService.class);
								if (service == null || commandService == null) {
									WorkbenchPlugin.log(
											"Could not retrieve EHandlerService or ICommandService from context evaluation context for" //$NON-NLS-1$
													+ commandId);
									return EvaluationResult.FALSE;
								}
								Command c = commandService.getCommand(commandId);
								ParameterizedCommand generateCommand = ParameterizedCommand.generateCommand(c,
										Collections.EMPTY_MAP);
								return EvaluationResult.valueOf(service.canExecute(generateCommand));
							}
						};
						MCoreExpression exp = UiFactoryImpl.eINSTANCE.createCoreExpression();
						exp.setCoreExpressionId("programmatic.value"); //$NON-NLS-1$
						exp.setCoreExpression(visWhen);
						return exp;
					}
				} else if (visibleChild.length > 0) {
					Expression visWhen = ExpressionConverter.getDefault().perform(visibleChild[0]);
					MCoreExpression exp = UiFactoryImpl.eINSTANCE.createCoreExpression();
					exp.setCoreExpressionId("programmatic.value"); //$NON-NLS-1$
					exp.setCoreExpression(visWhen);
					return exp;
					// visWhenMap.put(configElement, visWhen);
				}
			}
		} catch (InvalidRegistryObjectException | CoreException e) {
			// visWhenMap.put(configElement, null);
			WorkbenchPlugin.log(e);
		}
		return null;
	}

	/**
	 * Do a type-safe extraction of an object from the evalation context
	 *
	 * @param context      the evaluation context
	 * @param expectedType the expected type
	 * @return an object of the expected type or <code>null</code>
	 * @throws NullPointerException if either argument is <code>null</code>
	 */
	protected static <T> T getFromContext(IEvaluationContext context, Class<T> expectedType) {
		if (context == null || expectedType == null) {
			throw new NullPointerException();
		}
		final Object rawValue = context.getVariable(expectedType.getName());
		return (expectedType.isInstance(rawValue)) ? expectedType.cast(rawValue) : null;
	}

	/**
	 * Returns id attribute of the element or unique string computed from the
	 * element registry handle
	 *
	 * @param element non null
	 * @return non null id
	 */
	public static String getId(IConfigurationElement element) {
		String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_ID);

		// For sub-menu management -all- items must be id'd so enforce this
		// here (we could optimize by checking the 'name' of the config
		// element == "menu"
		if (id == null || id.isEmpty()) {
			id = getCommandId(element);
		}
		if (id == null || id.isEmpty()) {
			id = getConfigurationHandleId(element);
		}
		return id;
	}

	/**
	 * @return unique string computed from the element registry handle
	 */
	private static String getConfigurationHandleId(IConfigurationElement element) {
		return String.valueOf(element.getHandleId());
	}

	static String getName(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
	}

	static int getMode(IConfigurationElement element) {
		if ("FORCE_TEXT".equals(element.getAttribute(IWorkbenchRegistryConstants.ATT_MODE))) { //$NON-NLS-1$
			return CommandContributionItem.MODE_FORCE_TEXT;
		}
		return 0;
	}

	static String getLabel(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_LABEL);
	}

	static String getMnemonic(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_MNEMONIC);
	}

	static String getTooltip(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_TOOLTIP);
	}

	public static String getIconURI(IConfigurationElement element, String attr) {
		String iconPath = element.getAttribute(attr);
		if (iconPath == null) {
			return null;
		}

		// If iconPath doesn't specify a scheme, then try to transform to a URL
		// RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
		// This allows using data:, http:, or other custom URL schemes
		if (!SCHEME_PATTERN.matcher(iconPath).matches()) {
			// First attempt to resolve in ISharedImages (e.g. "IMG_OBJ_FOLDER")
			// as per bug 391232 & AbstractUIPlugin.imageDescriptorFromPlugin().
			ImageDescriptor d = WorkbenchPlugin.getDefault().getSharedImages().getImageDescriptor(iconPath);
			if (d != null) {
				return getImageUrl(d);
			}
			String extendingPluginId = element.getDeclaringExtension().getContributor().getName();
			iconPath = "platform:/plugin/" + extendingPluginId + "/" + iconPath; //$NON-NLS-1$//$NON-NLS-2$
		}
		URL url = null;
		try {
			url = FileLocator.find(new URL(iconPath));
		} catch (MalformedURLException e) {
			/* IGNORE */
		}
		return url == null ? iconPath : rewriteDurableURL(url.toString());
	}

	static String getHelpContextId(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_HELP_CONTEXT_ID);
	}

	public static boolean isSeparatorVisible(IConfigurationElement element) {
		String val = element.getAttribute(IWorkbenchRegistryConstants.ATT_VISIBLE);
		return Boolean.parseBoolean(val);
	}

	public static String getCommandId(IConfigurationElement element) {
		return element.getAttribute(IWorkbenchRegistryConstants.ATT_COMMAND_ID);
	}

	public static ItemType getStyle(IConfigurationElement element) {
		String style = element.getAttribute(IWorkbenchRegistryConstants.ATT_STYLE);
		if (style == null || style.isEmpty()) {
			return ItemType.PUSH;
		}
		if (IWorkbenchRegistryConstants.STYLE_TOGGLE.equals(style)) {
			return ItemType.CHECK;
		}
		if (IWorkbenchRegistryConstants.STYLE_RADIO.equals(style)) {
			return ItemType.RADIO;
		}
		if (IWorkbenchRegistryConstants.STYLE_PULLDOWN.equals(style)) {
			if (Policy.DEBUG_MENUS) {
				trace("Failed to get style for " + IWorkbenchRegistryConstants.STYLE_PULLDOWN, null); //$NON-NLS-1$
				// return CommandContributionItem.STYLE_PULLDOWN;
			}
		}
		return ItemType.PUSH;
	}

	public static boolean hasPulldownStyle(IConfigurationElement element) {
		String style = element.getAttribute(IWorkbenchRegistryConstants.ATT_STYLE);
		return IWorkbenchRegistryConstants.STYLE_PULLDOWN.equals(style);
	}

	public static Map<String, String> getParameters(IConfigurationElement element) {
		HashMap<String, String> map = new HashMap<>();
		IConfigurationElement[] parameters = element.getChildren(IWorkbenchRegistryConstants.TAG_PARAMETER);
		for (IConfigurationElement parameter : parameters) {
			String name = parameter.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
			String value = parameter.getAttribute(IWorkbenchRegistryConstants.ATT_VALUE);
			if (name != null && value != null) {
				map.put(name, value);
			}
		}
		return map;
	}

	public static MMenu createMenuAddition(IConfigurationElement menuAddition) {
		MMenu element = MenuFactoryImpl.eINSTANCE.createMenu();
		element.getPersistedState().put(IWorkbench.PERSIST_STATE, Boolean.FALSE.toString());
		String id = MenuHelper.getId(menuAddition);
		element.setElementId(id);
		String text = MenuHelper.getLabel(menuAddition);
		String mnemonic = MenuHelper.getMnemonic(menuAddition);
		if (text != null && mnemonic != null) {
			int idx = text.indexOf(mnemonic);
			if (idx != -1) {
				text = text.substring(0, idx) + '&' + text.substring(idx);
			}
		}
		element.setVisibleWhen(getVisibleWhen(menuAddition));
		element.setIconURI(MenuHelper.getIconURI(menuAddition, IWorkbenchRegistryConstants.ATT_ICON));
		element.setLabel(Util.safeString(text));

		return element;
	}

	public static MMenu createMenu(MenuManager manager) {
		MMenu subMenu = MenuFactoryImpl.eINSTANCE.createMenu();
		subMenu.setLabel(manager.getMenuText());
		subMenu.setElementId(manager.getId());
		return subMenu;
	}

	public static MHandledToolItem createToolItem(MApplication application, CommandContributionItem cci) {
		MCommand command = getMCommand(application, cci);
		if (command != null) {
			CommandContributionItemParameter data = cci.getData();
			MHandledToolItem toolItem = MenuFactoryImpl.eINSTANCE.createHandledToolItem();
			toolItem.setCommand(command);
			toolItem.setContributorURI(command.getContributorURI());
			toolItem.setVisible(cci.isVisible());

			String iconURI = null;
			String disabledIconURI = null;

			toolItem.setType(ItemType.PUSH);
			if (data.style == CommandContributionItem.STYLE_CHECK)
				toolItem.setType(ItemType.CHECK);
			else if (data.style == CommandContributionItem.STYLE_RADIO)
				toolItem.setType(ItemType.RADIO);

			if (data.icon != null) {
				iconURI = getIconURI(data.icon, application.getContext());
			}
			if (iconURI == null) {
				iconURI = getIconURI(command.getElementId(), application.getContext(),
						ICommandImageService.TYPE_DEFAULT);
			}
			if (iconURI == null) {
				toolItem.setLabel(command.getCommandName());
			} else {
				toolItem.setIconURI(iconURI);
			}

			if (data.disabledIcon != null) {
				disabledIconURI = getIconURI(data.disabledIcon, application.getContext());
			}

			if (disabledIconURI == null) {
				disabledIconURI = getIconURI(command.getElementId(), application.getContext(),
						ICommandImageService.TYPE_DISABLED);
			}

			if (disabledIconURI != null) {
				setDisabledIconURI(toolItem, disabledIconURI);
			}

			if (data.tooltip != null) {
				toolItem.setTooltip(data.tooltip);
			} else if (data.label != null) {
				toolItem.setTooltip(data.label);
			} else {
				toolItem.setTooltip(command.getDescription());
			}

			String itemId = cci.getId();
			toolItem.setElementId(itemId == null ? command.getElementId() : itemId);
			return toolItem;
		}
		return null;
	}

	public static MCommand getMCommand(MApplication application, CommandContributionItem contribution) {
		ParameterizedCommand command = contribution.getCommand();
		if (command != null) {
			for (MCommand mcommand : application.getCommands()) {
				if (mcommand.getElementId().equals(command.getId())) {
					return mcommand;
				}
			}
		}
		return null;
	}

	public static String getIconURI(ImageDescriptor descriptor, IEclipseContext context) {
		if (descriptor == null) {
			return null;
		}

		// Attempt to retrieve URIs from the descriptor and convert into a more
		// durable form in case it's to be persisted
		Adapter adapter = context != null ? context.get(Adapter.class) : null;
		if (adapter != null) {
			Object o = adapter.adapt(descriptor, URL.class);
			if (o != null) {
				return rewriteDurableURL(o.toString());
			}
			o = adapter.adapt(descriptor, URI.class);
			if (o != null) {
				return rewriteDurableURL(o.toString());
			}
		}
		else {
			Object o = Adapters.adapt(descriptor, URL.class);
			if (o != null) {
				return rewriteDurableURL(o.toString());
			}
			o = Adapters.adapt(descriptor, URI.class);
			if (o != null) {
				return rewriteDurableURL(o.toString());
			}
		}
		return null;
	}

	/**
	 * Rewrite certain types of URLs to more durable forms, as these URLs may may be
	 * persisted in the model.
	 *
	 * @param url the url
	 * @return the rewritten URL
	 */
	private static String rewriteDurableURL(String url) {
		// Rewrite bundleentry and bundleresource entries as they are
		// invalidated on -clean or a bundle remove, . These Platform URIs are
		// of the form:
		// bundleentry://<bundle-id>.XXX/path/to/file
		// bundleresource://<bundle-id>.XXX/path/to/file
		if (!url.startsWith("bundleentry:") && !url.startsWith("bundleresource:")) { //$NON-NLS-1$ //$NON-NLS-2$
			return url;
		}

		BundleContext ctxt = FrameworkUtil.getBundle(WorkbenchWindow.class).getBundleContext();
		try {
			URI uri = new URI(url);
			String host = uri.getHost();
			String bundleId = host.substring(0, host.indexOf('.'));
			Bundle bundle = ctxt.getBundle(Long.parseLong(bundleId));
			StringBuilder builder = new StringBuilder("platform:/plugin/"); //$NON-NLS-1$
			builder.append(bundle.getSymbolicName());
			builder.append(uri.getPath());
			return builder.toString();
		} catch (URISyntaxException e) {
			return url;
		}
	}

	private static String getIconURI(String commandId, IEclipseContext workbench, int type) {
		if (commandId == null) {
			return null;
		}

		ICommandImageService imageService = workbench.get(ICommandImageService.class);
		ImageDescriptor descriptor = imageService.getImageDescriptor(commandId, type);
		return getIconURI(descriptor, workbench);
	}

	/**
	 * @param item
	 * @param disabledIconURI
	 */
	public static void setDisabledIconURI(MToolItem item, String disabledIconURI) {
		item.getTransientData().put(IPresentationEngine.DISABLED_ICON_IMAGE_KEY, disabledIconURI);
	}
}
