/*******************************************************************************
 * Copyright (c) 2005, 2015 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@gmail.com> - Bug 440810
 *******************************************************************************/

package org.eclipse.ui.internal.menus;

import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.CommandException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.menus.IWidget;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.CoolBar;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.IWorkbenchWindowPulldownDelegate;
import org.eclipse.ui.IWorkbenchWindowPulldownDelegate2;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.services.IServiceLocator;

/**
 * <p>
 * A proxy for a {@link IWorkbenchWindowPulldownDelegate} on a pulldown action
 * set action. This delays the class loading until the delegate is really asked
 * for information. Asking a proxy for anything (except disposing) will cause
 * the proxy to instantiate the proxied delegate.
 * </p>
 * <p>
 * This class is not intended for use outside of the
 * <code>org.eclipse.ui.workbench</code> plug-in.
 * </p>
 *
 * @since 3.2
 */
final class PulldownDelegateWidgetProxy implements IWidget {

	/**
	 * A wrapper for loading the menu that defends against possible exceptions
	 * triggered outside of the workbench.
	 */
	private static final class MenuLoader implements ISafeRunnable {

		/**
		 * The parent for the menu to be created. This value is
		 * <code>null</code> if the parent is a menu.
		 */
		private final Control control;

		/**
		 * The delegate from which to load the menu.
		 */
		private final IWorkbenchWindowPulldownDelegate delegate;

		/**
		 * The loaded menu. This value is <code>null</code> if the load
		 * failed, or if it hasn't been loaded yet.
		 */
		private Menu menu = null;

		/**
		 * The parent for the menu to be created. This value is
		 * <code>null</code> if the parent is a control.
		 */
		private final Menu parent;

		/**
		 * Constructs a new instance of <code>MenuLoader</code>
		 *
		 * @param delegate
		 *            The delegate from which the menu will be loaded; this
		 *            value must not be <code>null</code>.
		 * @param parent
		 *            The parent of the menu to be loaded; this value must not
		 *            be <code>null</code>.
		 */
		private MenuLoader(final IWorkbenchWindowPulldownDelegate delegate,
				final Control parent) {
			this.delegate = delegate;
			this.parent = null;
			this.control = parent;
		}

		/**
		 * Constructs a new instance of <code>MenuLoader</code>
		 *
		 * @param delegate
		 *            The delegate from which the menu will be loaded; this
		 *            value must not be <code>null</code>.
		 * @param parent
		 *            The parent of the menu to be loaded; this value must not
		 *            be <code>null</code>.
		 */
		private MenuLoader(final IWorkbenchWindowPulldownDelegate2 delegate,
				final Menu parent) {
			this.delegate = delegate;
			this.parent = parent;
			this.control = null;
		}

		/**
		 * Returns the menu loaded, if any.
		 *
		 * @return the loaded menu, or <code>null</code> if none.
		 */
		private Menu getMenu() {
			return menu;
		}

		/**
		 * @see ISafeRunnable#handleException(java.lang.Throwable)
		 */
		@Override
		public void handleException(Throwable exception) {
			// Do nothing
		}

		/**
		 * @see ISafeRunnable#run()
		 */
		@Override
		public void run() throws Exception {
			if (parent == null) {
				menu = delegate.getMenu(control);
			} else {
				menu = ((IWorkbenchWindowPulldownDelegate2) delegate)
						.getMenu(parent);
			}
		}
	}

	/**
	 * The command to execute when the pulldown delegate appears in a tool bar,
	 * and the arrow is <em>not</em> clicked. This also carries a help context
	 * identifier. This value must be <code>null</code>.
	 */
	private final ParameterizedCommand command;

	/**
	 * The configuration element from which the delegate can be created. This
	 * value will exist until the element is converted into a real class -- at
	 * which point this value will be set to <code>null</code>.
	 */
	private IConfigurationElement configurationElement;

	/**
	 * The real delegate. This value is <code>null</code> until the proxy is
	 * forced to load the real delegate. At this point, the configuration
	 * element is converted, nulled out, and this delegate gains a reference.
	 */
	private IWorkbenchWindowPulldownDelegate delegate = null;

	/**
	 * The name of the configuration element attribute which contains the
	 * information necessary to instantiate the real delegate.
	 */
	private final String delegateAttributeName;

	private final DisposeListener disposeListener = new DisposeListener() {
		@Override
		public void widgetDisposed(DisposeEvent e) {
			if (e.widget == widget) {
				dispose();
				widget = null;

				// TODO Is this necessary?
				// disposeOldImages();
			}
		}
	};

	/**
	 * The service locator from which a handler service can be retrieved. This
	 * is needed if the pulldown appears in the tool bar, and the drop-down
	 * arrow is <em>not</em> clicked. This value must not be <code>null</code>.
	 */
	private final IServiceLocator locator;

	private final Listener selectionListener = new Listener() {
		@Override
		public void handleEvent(final Event event) {
			final Widget item = event.widget;
			if (item == null) {
				return;
			}

			final int style = item.getStyle();
			if (((style & SWT.DROP_DOWN) != 0) && (event.detail == SWT.ARROW)
					&& (item instanceof ToolItem)) {
				// Create the submenu.
				final ToolItem toolItem = (ToolItem) item;
				final ToolBar toolBar = toolItem.getParent();
				if (loadDelegate()
						&& (delegate instanceof IWorkbenchWindowPulldownDelegate2)) {
					final IWorkbenchWindowPulldownDelegate2 delegate2 = (IWorkbenchWindowPulldownDelegate2) delegate;
					final MenuLoader loader = new MenuLoader(delegate2, toolBar);
					SafeRunner.run(loader);
					final Menu subMenu = loader.getMenu();
					if (subMenu != null) {
						// position the menu below the drop down item
						final Rectangle bounds = toolItem.getBounds();
						final Point location = toolBar.toDisplay(new Point(
								bounds.x, bounds.y + bounds.height));
						subMenu.setLocation(location);
						subMenu.setVisible(true);
						return; // we don't fire the command
					}
				}
			}

			final IHandlerService service = locator
					.getService(IHandlerService.class);
			try {
				service.executeCommand(command, event);
			} catch (final CommandException e) {
				/*
				 * TODO There should be an API on IHandlerService that handles
				 * the exceptions.
				 */
			}
		}

	};

	/**
	 * The widget created for this pulldown delegate. If this proxy has not been
	 * asked to fill or it has been disposed, then this value is
	 * <code>null</code>.
	 */
	private Widget widget = null;

	/**
	 * Constructs a new instance of <code>PulldownDelegateWidgetProxy</code>
	 * with all the information it needs to try to avoid loading until it is
	 * needed.
	 *
	 * @param configurationElement
	 *            The configuration element from which the real class can be
	 *            loaded at run-time; must not be <code>null</code>.
	 * @param delegateAttributeName
	 *            The name of the attibute or element containing the delegate;
	 *            must not be <code>null</code>.
	 * @param command
	 *            The command to execute if this the pulldown is not shown; must
	 *            not be <code>null</code>.
	 * @param locator
	 *            A service locator from which a handler service can be
	 *            retrieved; must not be <code>null</code>.
	 */
	public PulldownDelegateWidgetProxy(
			final IConfigurationElement configurationElement,
			final String delegateAttributeName,
			final ParameterizedCommand command, final IServiceLocator locator) {
		if (configurationElement == null) {
			throw new NullPointerException(
					"The configuration element backing a handler proxy cannot be null"); //$NON-NLS-1$
		}

		if (delegateAttributeName == null) {
			throw new NullPointerException(
					"The attribute containing the handler class must be known"); //$NON-NLS-1$
		}

		if (command == null) {
			throw new NullPointerException("The command cannot be null"); //$NON-NLS-1$
		}

		this.configurationElement = configurationElement;
		this.delegateAttributeName = delegateAttributeName;
		this.command = command;
		this.locator = locator;
	}

	/**
	 * Passes the dipose on to the proxied handler, if it has been loaded.
	 */
	@Override
	public void dispose() {
		if (delegate != null) {
			delegate.dispose();
		}
	}

	@Override
	public void fill(final Composite parent) {
		// This does not need to be supported.
	}

	@Override
	public void fill(CoolBar parent, final int index) {
		// This does not need to be supported.
	}

	@Override
	public void fill(final Menu parent, final int index) {
		if ((widget != null) || (parent == null)) {
			return;
		}

		// Create the menu item.
		final MenuItem menuItem;
		if (index >= 0) {
			menuItem = new MenuItem(parent, SWT.CASCADE, index);
		} else {
			menuItem = new MenuItem(parent, SWT.CASCADE);
		}
		menuItem.setData(this);
		widget = menuItem;

		// Create the submenu.
		if (loadDelegate()
				&& (delegate instanceof IWorkbenchWindowPulldownDelegate2)) {
			final IWorkbenchWindowPulldownDelegate2 delegate2 = (IWorkbenchWindowPulldownDelegate2) delegate;
			final MenuLoader loader = new MenuLoader(delegate2, parent);
			SafeRunner.run(loader);
			final Menu subMenu = loader.getMenu();
			if (subMenu != null) {
				menuItem.setMenu(subMenu);
			}
		}

		menuItem.addDisposeListener(disposeListener);
		menuItem.addListener(SWT.Selection, selectionListener);

		// TODO Needs a way to be linked to a command.
		// if (action.getHelpListener() != null)
		// menuItem.addHelpListener(action.getHelpListener());

		// TODO Needs a way of updating itself
		// update(null);
	}

	@Override
	public void fill(final ToolBar parent, final int index) {
		if ((widget != null) && (parent == null)) {
			return;
		}

		final ToolItem toolItem;
		if (index >= 0) {
			toolItem = new ToolItem(parent, SWT.DROP_DOWN, index);
		} else {
			toolItem = new ToolItem(parent, SWT.DROP_DOWN);
		}
		toolItem.setData(this);
		widget = toolItem;

		// Attach some listeners.
		toolItem.addDisposeListener(disposeListener);
		toolItem.addListener(SWT.Selection, selectionListener);

		// TODO Needs a way to be linked to a command.
		// toolItem.addListener(SWT.Selection, getToolItemListener());
		// action.addPropertyChangeListener(propertyListener);
		// if (action != null) {
		// String commandId = action.getActionDefinitionId();
		// ExternalActionManager.ICallback callback = ExternalActionManager
		// .getInstance().getCallback();
		//
		// if ((callback != null) && (commandId != null)) {
		// callback.addPropertyChangeListener(commandId,
		// actionTextListener);
		// }
		// }

		// TODO Needs a way of updating itself
		// update(null);
	}

	/**
	 * Loads the delegate, if possible. If the delegate is loaded, then the
	 * member variables are updated accordingly.
	 *
	 * @return <code>true</code> if the delegate is now non-null;
	 *         <code>false</code> otherwise.
	 */
	private boolean loadDelegate() {
		if (delegate == null) {
			// Load the handler.
			try {
				delegate = (IWorkbenchWindowPulldownDelegate) configurationElement
						.createExecutableExtension(delegateAttributeName);
				configurationElement = null;
				return true;

			} catch (final ClassCastException e) {
				final String message = "The proxied delegate was the wrong class"; //$NON-NLS-1$
				final IStatus status = new Status(IStatus.ERROR,
						WorkbenchPlugin.PI_WORKBENCH, 0, message, e);
				WorkbenchPlugin.log(message, status);
				return false;

			} catch (final CoreException e) {
				final String message = "The proxied delegate for '" + configurationElement.getAttribute(delegateAttributeName) //$NON-NLS-1$
						+ "' could not be loaded"; //$NON-NLS-1$
				IStatus status = new Status(IStatus.ERROR,
						WorkbenchPlugin.PI_WORKBENCH, 0, message, e);
				WorkbenchPlugin.log(message, status);
				return false;
			}
		}

		return true;
	}

	@Override
	public String toString() {
		if (delegate == null) {
			return configurationElement.getAttribute(delegateAttributeName);
		}

		return delegate.toString();
	}
}
