/*******************************************************************************
 * Copyright (c) 2000, 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
 *     Remy Chi Jian Suen <remy.suen@gmail.com> - Bug 43573 [Contributions] Support icon in <menu>
 *     Alexander Fedorov <alexander.fedorov@arsysop.ru> - Bug 548799
 *******************************************************************************/
package org.eclipse.ui.internal;

import java.util.ArrayList;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.action.AbstractGroupMarker;
import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IContributionManager;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ResourceLocator;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.internal.registry.RegistryReader;

/**
 * This class contains shared functionality for reading action contributions
 * from plugins into workbench parts (both editors and views).
 */
public abstract class PluginActionBuilder extends RegistryReader {
	protected String targetID;

	protected String targetContributionTag;

	protected BasicContribution currentContribution;

	protected ArrayList<Object> cache;

	/**
	 * The default constructor.
	 */
	public PluginActionBuilder() {
	}

	/**
	 * Contributes submenus and/or actions into the provided menu and tool bar
	 * managers.
	 *
	 * @param menu            the menu to contribute to
	 * @param toolbar         the toolbar to contribute to
	 * @param appendIfMissing append containers if missing
	 */
	public final void contribute(IMenuManager menu, IToolBarManager toolbar, boolean appendIfMissing) {
		if (cache == null) {
			return;
		}

		for (Object element : cache) {
			BasicContribution contribution = (BasicContribution) element;
			contribution.contribute(menu, appendIfMissing, toolbar, appendIfMissing);
		}
	}

	/**
	 * This factory method returns a new ActionDescriptor for the configuration
	 * element. It should be implemented by subclasses.
	 */
	protected abstract ActionDescriptor createActionDescriptor(IConfigurationElement element);

	/**
	 * Factory method to create the helper contribution class that will hold onto
	 * the menus and actions contributed.
	 */
	protected BasicContribution createContribution() {
		return new BasicContribution();
	}

	/**
	 * Returns the name of the part ID attribute that is expected in the target
	 * extension.
	 */
	protected String getTargetID(IConfigurationElement element) {
		String value = element.getAttribute(IWorkbenchRegistryConstants.ATT_TARGET_ID);
		return value != null ? value : "???"; //$NON-NLS-1$
	}

	/**
	 * Returns the id of this contributions.
	 */
	protected String getID(IConfigurationElement element) {
		String value = element.getAttribute(IWorkbenchRegistryConstants.ATT_ID);
		return value != null ? value : "???"; //$NON-NLS-1$
	}

	/**
	 * Reads the contributions from the registry for the provided workbench part and
	 * the provided extension point ID.
	 */
	protected void readContributions(String id, String tag, String extensionPoint) {
		cache = null;
		currentContribution = null;
		targetID = id;
		targetContributionTag = tag;
		readRegistry(Platform.getExtensionRegistry(), PlatformUI.PLUGIN_ID, extensionPoint);
	}

	/**
	 * Implements abstract method to handle the provided XML element in the
	 * registry.
	 */
	@Override
	protected boolean readElement(IConfigurationElement element) {
		String tag = element.getName();

		// Ignore all object contributions element as these
		// are handled by the ObjectActionContributorReader.
		if (tag.equals(IWorkbenchRegistryConstants.TAG_OBJECT_CONTRIBUTION)) {
			return true;
		}

		// Found top level contribution element
		if (tag.equals(targetContributionTag)) {
			if (targetID != null) {
				// Ignore contributions not matching target id
				String id = getTargetID(element);
				if (id == null || !id.equals(targetID)) {
					return true;
				}
			}

			// Read its sub-elements
			currentContribution = createContribution();
			readElementChildren(element);
			if (cache == null) {
				cache = new ArrayList<>(4);
			}
			cache.add(currentContribution);
			currentContribution = null;
			return true;
		}

		// Found menu contribution sub-element
		if (tag.equals(IWorkbenchRegistryConstants.TAG_MENU)) {
			currentContribution.addMenu(element);
			return true;
		}

		// Found action contribution sub-element
		if (tag.equals(IWorkbenchRegistryConstants.TAG_ACTION)) {
			currentContribution.addAction(createActionDescriptor(element));
			return true;
		}

		return false;
	}

	/**
	 * Helper class to collect the menus and actions defined within a contribution
	 * element.
	 */
	protected static class BasicContribution {
		protected ArrayList<IConfigurationElement> menus;

		protected ArrayList<ActionDescriptor> actions;

		/**
		 * Add a menu.
		 *
		 * @param element the element to base the menu on
		 */
		public void addMenu(IConfigurationElement element) {
			if (menus == null) {
				menus = new ArrayList<>(1);
			}
			menus.add(element);
		}

		/**
		 * Add an action.
		 *
		 * @param desc the descriptor
		 */
		public void addAction(ActionDescriptor desc) {
			if (actions == null) {
				actions = new ArrayList<>(3);
			}
			actions.add(desc);
		}

		/**
		 * Contributes submenus and/or actions into the provided menu and tool bar
		 * managers.
		 *
		 * The elements added are filtered based on activity enablement.
		 *
		 * @param menu                the menu to contribute to
		 * @param menuAppendIfMissing whether to append missing groups to menus
		 * @param toolbar             the toolbar to contribute to
		 * @param toolAppendIfMissing whether to append missing groups to toolbars
		 */
		public void contribute(IMenuManager menu, boolean menuAppendIfMissing, IToolBarManager toolbar,
				boolean toolAppendIfMissing) {
			if (menus != null && menu != null) {
				for (IConfigurationElement menuElement : menus) {
					contributeMenu(menuElement, menu, menuAppendIfMissing);
				}
			}

			if (actions != null) {
				for (ActionDescriptor ad : actions) {
					if (menu != null) {
						contributeMenuAction(ad, menu, menuAppendIfMissing);
					}
					if (toolbar != null) {
						contributeToolbarAction(ad, toolbar, toolAppendIfMissing);
					}
				}
			}
		}

		/**
		 * Creates a menu from the information in the menu configuration element and
		 * adds it into the provided menu manager. If 'appendIfMissing' is true, and
		 * menu path slot is not found, it will be created and menu will be added into
		 * it. Otherwise, add operation will fail.
		 */
		protected void contributeMenu(IConfigurationElement menuElement, IMenuManager mng, boolean appendIfMissing) {
			// Get config data.
			String id = menuElement.getAttribute(IWorkbenchRegistryConstants.ATT_ID);
			String label = menuElement.getAttribute(IWorkbenchRegistryConstants.ATT_LABEL);
			String path = menuElement.getAttribute(IWorkbenchRegistryConstants.ATT_PATH);
			String icon = menuElement.getAttribute(IWorkbenchRegistryConstants.ATT_ICON);
			ImageDescriptor image = null;
			if (icon != null) {
				String extendingPluginId = menuElement.getDeclaringExtension().getContributor().getName();
				image = ResourceLocator.imageDescriptorFromBundle(extendingPluginId, icon).orElse(null);
			}
			if (label == null) {
				WorkbenchPlugin.log("Plugin \'" //$NON-NLS-1$
						+ menuElement.getContributor().getName() + "\' invalid Menu Extension (label == null): " + id); //$NON-NLS-1$
				return;
			}

			// Calculate menu path and group.
			String group = null;
			if (path != null) {
				int loc = path.lastIndexOf('/');
				if (loc != -1) {
					group = path.substring(loc + 1);
					path = path.substring(0, loc);
				} else {
					// assume that path represents a slot
					// so actual path portion should be null
					group = path;
					path = null;
				}
			}

			// Find parent menu.
			IMenuManager parent = mng;
			if (path != null) {
				parent = mng.findMenuUsingPath(path);
				if (parent == null) {
					ideLog("Plugin \'" //$NON-NLS-1$
							+ menuElement.getContributor().getName() + "\' invalid Menu Extension (Path \'" //$NON-NLS-1$
							+ path + "\' is invalid): " + id); //$NON-NLS-1$
					return;
				}
			}

			// Find reference group.
			if (group == null) {
				group = IWorkbenchActionConstants.MB_ADDITIONS;
			}
			IContributionItem sep = parent.find(group);
			if (sep == null) {
				if (appendIfMissing) {
					addGroup(parent, group);
				} else {
					WorkbenchPlugin.log("Plugin \'" //$NON-NLS-1$
							+ menuElement.getContributor().getName() + "\' invalid Menu Extension (Group \'" //$NON-NLS-1$
							+ group + "\' is invalid): " + id); //$NON-NLS-1$
					return;
				}
			}

			// If the menu does not exist create it.
			IMenuManager newMenu = parent.findMenuUsingPath(id);
			if (newMenu == null) {
				newMenu = new MenuManager(label, image, id);
			}

			// Add the menu
			try {
				insertAfter(parent, group, newMenu);
			} catch (IllegalArgumentException e) {
				WorkbenchPlugin.log("Plugin \'" //$NON-NLS-1$
						+ menuElement.getContributor().getName() + "\' invalid Menu Extension (Group \'" //$NON-NLS-1$
						+ group + "\' is missing): " + id); //$NON-NLS-1$
			}

			// Get the menu again as it may be wrapped, otherwise adding
			// the separators and group markers below will not be wrapped
			// properly if the menu was just created.
			newMenu = parent.findMenuUsingPath(id);
			if (newMenu == null) {
				WorkbenchPlugin.log("Could not find new menu: " + id); //$NON-NLS-1$
			}

			// Create separators.
			IConfigurationElement[] children = menuElement.getChildren();
			for (IConfigurationElement element : children) {
				String childName = element.getName();
				if (childName.equals(IWorkbenchRegistryConstants.TAG_SEPARATOR)) {
					contributeSeparator(newMenu, element);
				} else if (childName.equals(IWorkbenchRegistryConstants.TAG_GROUP_MARKER)) {
					contributeGroupMarker(newMenu, element);
				}
			}
		}

		/**
		 * Contributes action from action descriptor into the provided menu manager.
		 */
		protected void contributeMenuAction(ActionDescriptor ad, IMenuManager menu, boolean appendIfMissing) {
			// Get config data.
			String mpath = ad.getMenuPath();
			String mgroup = ad.getMenuGroup();
			if (mpath == null && mgroup == null) {
				return;
			}
			// Find parent menu.
			IMenuManager parent = menu;
			if (mpath != null) {
				parent = parent.findMenuUsingPath(mpath);
				if (parent == null) {
					ideLog("Plug-in '" + ad.getPluginId() + "' contributed an invalid Menu Extension (Path: '" + mpath //$NON-NLS-1$ //$NON-NLS-2$
							+ "' is invalid): " + ad.getId()); //$NON-NLS-1$
					return;
				}
			}

			// Find reference group.
			if (mgroup == null) {
				mgroup = IWorkbenchActionConstants.MB_ADDITIONS;
			}
			IContributionItem sep = parent.find(mgroup);
			if (sep == null) {
				if (appendIfMissing) {
					addGroup(parent, mgroup);
				} else {
					WorkbenchPlugin
							.log("Plug-in '" + ad.getPluginId() + "' contributed an invalid Menu Extension (Group: '" //$NON-NLS-1$ //$NON-NLS-2$
									+ mgroup + "' is invalid): " + ad.getId()); //$NON-NLS-1$
					return;
				}
			}

			// Add action.
			try {
				insertAfter(parent, mgroup, ad);
			} catch (IllegalArgumentException e) {
				WorkbenchPlugin.log("Plug-in '" + ad.getPluginId() + "' contributed an invalid Menu Extension (Group: '" //$NON-NLS-1$ //$NON-NLS-2$
						+ mgroup + "' is missing): " + ad.getId()); //$NON-NLS-1$
			}
		}

		/**
		 * Creates a named menu separator from the information in the configuration
		 * element. If the separator already exists do not create a second.
		 */
		protected void contributeSeparator(IMenuManager menu, IConfigurationElement element) {
			String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
			if (id == null || id.length() <= 0) {
				return;
			}
			IContributionItem sep = menu.find(id);
			if (sep != null) {
				return;
			}
			insertMenuGroup(menu, new Separator(id));
		}

		/**
		 * Creates a named menu group marker from the information in the configuration
		 * element. If the marker already exists do not create a second.
		 */
		protected void contributeGroupMarker(IMenuManager menu, IConfigurationElement element) {
			String id = element.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
			if (id == null || id.length() <= 0) {
				return;
			}
			IContributionItem marker = menu.find(id);
			if (marker != null) {
				return;
			}
			insertMenuGroup(menu, new GroupMarker(id));
		}

		/**
		 * Contributes action from the action descriptor into the provided tool bar
		 * manager.
		 */
		protected void contributeToolbarAction(ActionDescriptor ad, IToolBarManager toolbar, boolean appendIfMissing) {
			// Get config data.
			String tId = ad.getToolbarId();
			String tgroup = ad.getToolbarGroupId();
			if (tId == null && tgroup == null) {
				return;
			}

			// Find reference group.
			if (tgroup == null) {
				tgroup = IWorkbenchActionConstants.MB_ADDITIONS;
			}
			IContributionItem sep = null;
			sep = toolbar.find(tgroup);
			if (sep == null) {
				if (appendIfMissing) {
					addGroup(toolbar, tgroup);
				} else {
					WorkbenchPlugin.log("Plug-in '" + ad.getPluginId() //$NON-NLS-1$
							+ "' invalid Toolbar Extension (Group \'" //$NON-NLS-1$
							+ tgroup + "\' is invalid): " + ad.getId()); //$NON-NLS-1$
					return;
				}
			}
			// Add action to tool bar.
			try {
				insertAfter(toolbar, tgroup, ad);
			} catch (IllegalArgumentException e) {
				WorkbenchPlugin.log("Plug-in '" + ad.getPluginId() //$NON-NLS-1$
						+ "' invalid Toolbar Extension (Group \'" //$NON-NLS-1$
						+ tgroup + "\' is missing): " + ad.getId()); //$NON-NLS-1$
			}
		}

		/**
		 * Inserts the separator or group marker into the menu. Subclasses may override.
		 */
		protected void insertMenuGroup(IMenuManager menu, AbstractGroupMarker marker) {
			menu.add(marker);
		}

		/**
		 * Inserts an action after another named contribution item. Subclasses may
		 * override.
		 */
		protected void insertAfter(IContributionManager mgr, String refId, ActionDescriptor desc) {
			final PluginActionContributionItem item = new PluginActionContributionItem(desc.getAction());
			item.setMode(desc.getMode());
			insertAfter(mgr, refId, item);
		}

		/**
		 * Inserts a contribution item after another named contribution item. Subclasses
		 * may override.
		 */
		protected void insertAfter(IContributionManager mgr, String refId, IContributionItem item) {
			mgr.insertAfter(refId, item);
		}

		/**
		 * Adds a group to a contribution manager. Subclasses may override.
		 */
		protected void addGroup(IContributionManager mgr, String name) {
			mgr.add(new Separator(name));
		}

		/**
		 * Disposes this contribution.
		 *
		 * @since 3.1
		 */
		public void dispose() {
			// do nothing
		}

		/**
		 * Disposes the actions.
		 *
		 * @since 3.1
		 */
		protected void disposeActions() {
			if (actions != null) {
				for (ActionDescriptor action : actions) {
					PluginAction proxy = action.getAction();
					proxy.dispose();
				}
				actions = null;
			}
		}
	}

	private static boolean allowIdeLogging = false;

	/**
	 * If set to <code>false</code>, some of the logs that can be caused by use IDE
	 * plugins from an RCP app will be ignored.
	 *
	 * @param b Log the errors or not.
	 * @since 3.3
	 */
	public static void setAllowIdeLogging(boolean b) {
		allowIdeLogging = b;
	}

	/**
	 * These are log messages that should be ignored by RCP apps when using the IDE
	 * plugins.
	 *
	 * @param msg
	 * @since 3.3
	 */
	private static void ideLog(String msg) {
		if (allowIdeLogging) {
			WorkbenchPlugin.log(msg);
		}
	}
}
