/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.gef.internal.ui.palette;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Item;
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.jface.action.Action;
import org.eclipse.jface.action.ContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionManagerOverrides;
import org.eclipse.jface.action.IMenuCreator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

/**
 * A contribution item which delegates to an action.
 * <p>
 * This class may be instantiated; it is not intended to be subclassed.
 * </p>
 */
public class ToolbarDropdownContributionItem extends ContributionItem {

	private static ImageCache globalImageCache;

	/**
	 * The action.
	 */
	private IAction action;

	/**
	 * The widget created for this item; <code>null</code> before creation and
	 * after disposal.
	 */
	private Widget widget = null;

	/**
	 * Nested class handles notification from SWT widget and from Action, to
	 * avoid polluting ToolbarDropdownContributionItem with public listener
	 * methods.
	 */
	private class ActionListener implements Listener, IPropertyChangeListener {
		public void handleEvent(Event event) {
			ToolbarDropdownContributionItem.this.handleWidgetEvent(event);
		}

		public void propertyChange(PropertyChangeEvent event) {
			ToolbarDropdownContributionItem.this.actionPropertyChange(event);
		}
	}

	private ActionListener listener = new ActionListener();

	private class ImageCache {
		/** Map from ImageDescriptor to Entry */
		private Map entries = new HashMap(11);
		private Image missingImage;

		private class Entry {
			protected Image image;
			protected Image grayImage;

			void dispose() {
				if (image != null) {
					image.dispose();
					image = null;
				}
				if (grayImage != null) {
					grayImage.dispose();
					grayImage = null;
				}
			}
		}

		Entry getEntry(ImageDescriptor desc) {
			Entry entry = (Entry) entries.get(desc);
			if (entry == null) {
				entry = new Entry();
				entries.put(desc, entry);
			}
			return entry;
		}

		Image getImage(ImageDescriptor desc) {
			if (desc == null) {
				return null;
			}
			Entry entry = getEntry(desc);
			if (entry.image == null) {
				entry.image = desc.createImage();
			}
			return entry.image;
		}

		Image getGrayImage(ImageDescriptor desc) {
			if (desc == null) {
				return null;
			}
			Entry entry = getEntry(desc);
			if (entry.grayImage == null) {
				Image image = getImage(desc);
				if (image != null) {
					entry.grayImage = new Image(null, image,
							org.eclipse.draw2d.rap.swt.SWT.IMAGE_GRAY);
				}
			}
			return entry.grayImage;
		}

		Image getMissingImage() {
			if (missingImage == null) {
				missingImage = getImage(ImageDescriptor
						.getMissingImageDescriptor());
			}
			return missingImage;
		}

		void dispose() {
			for (Iterator i = entries.values().iterator(); i.hasNext();) {
				Entry entry = (Entry) i.next();
				entry.dispose();
			}
			entries.clear();
		}
	}

	/**
	 * Creates a new contribution item from the given action. The id of the
	 * action is used as the id of the item.
	 * 
	 * @param action
	 *            the action
	 */
	public ToolbarDropdownContributionItem(IAction action) {
		super(action.getId());
		this.action = action;
	}

	/**
	 * Handles a property change event on the action (forwarded by nested
	 * listener).
	 */
	private void actionPropertyChange(final PropertyChangeEvent e) {
		// This code should be removed. Avoid using free asyncExec

		if (isVisible() && widget != null) {
			Display display = widget.getDisplay();
			if (display.getThread() == Thread.currentThread()) {
				update(e.getProperty());
			} else {
				display.asyncExec(new Runnable() {
					public void run() {
						update(e.getProperty());
					}
				});
			}

		}
	}

	/**
	 * Checks whether the given menu item belongs to a context menu (the one
	 * that pops up if the user presses the right mouse button).
	 */
	private static boolean belongsToContextMenu(MenuItem item) {
		Menu menu = item.getParent();
		if (menu == null)
			return false;
		while (menu.getParentMenu() != null)
			menu = menu.getParentMenu();
		return (menu.getStyle() & SWT.BAR) == 0;
	}

	/**
	 * Compares this action contribution item with another object. Two action
	 * contribution items are equal if they refer to the identical Action.
	 */
	public boolean equals(Object o) {
		if (!(o instanceof ToolbarDropdownContributionItem)) {
			return false;
		}
		return action.equals(((ToolbarDropdownContributionItem) o).action);
	}

	/**
	 * The <code>ToolbarDropdownContributionItem</code> implementation of this
	 * <code>IContributionItem</code> method creates a SWT Button for the
	 * action. If the action's checked property has been set, a toggle button is
	 * created and primed to the value of the checked property.
	 */
	public void fill(Composite parent) {
		if (widget == null && parent != null) {
			int flags = SWT.PUSH;

			if (action != null) {
				if (action.getStyle() == IAction.AS_CHECK_BOX)
					flags = SWT.TOGGLE;
			}

			Button b = new Button(parent, flags);
			b.setData(this);
			b.addListener(SWT.Dispose, listener);
			// Don't hook a dispose listener on the parent
			b.addListener(SWT.Selection, listener);
			if (action.getHelpListener() != null)
				b.addHelpListener(action.getHelpListener());
			widget = b;

			update(null);

			action.addPropertyChangeListener(listener);
		}
	}

	/**
	 * The <code>ToolbarDropdownContributionItem</code> implementation of this
	 * <code>IContributionItem</code> method creates a SWT MenuItem for the
	 * action. If the action's checked property has been set, a toggle button is
	 * created and primed to the value of the checked property. If the action's
	 * menu creator property has been set, a cascading submenu is created.
	 */
	public void fill(Menu parent, int index) {
		if (widget == null && parent != null) {
			int flags = SWT.PUSH;
			Menu subMenu = null;

			if (action != null) {
				int style = action.getStyle();
				if (style == IAction.AS_CHECK_BOX)
					flags = SWT.CHECK;
				else if (style == IAction.AS_DROP_DOWN_MENU) {
					IMenuCreator mc = action.getMenuCreator();
					subMenu = mc.getMenu(parent);
					flags = SWT.CASCADE;
				}
			}

			MenuItem mi = null;
			if (index >= 0)
				mi = new MenuItem(parent, flags, index);
			else
				mi = new MenuItem(parent, flags);
			widget = mi;

			mi.setData(this);
			mi.addListener(SWT.Arm, listener);
			mi.addListener(SWT.Dispose, listener);
			mi.addListener(SWT.Selection, listener);
			if (action.getHelpListener() != null)
				mi.addHelpListener(action.getHelpListener());

			if (subMenu != null)
				mi.setMenu(subMenu);

			update(null);

			action.addPropertyChangeListener(listener);
		}
	}

	/**
	 * The <code>ToolbarDropdownContributionItem</code> implementation of this
	 * <code>IContributionItem</code> method creates a SWT ToolItem for the
	 * action. If the action's checked property has been set, a toggle button is
	 * created and primed to the value of the checked property. If the action's
	 * menu creator property has been set, a drop-down tool item is created.
	 */
	public void fill(ToolBar parent, int index) {
		if (widget == null && parent != null) {
			int flags = SWT.PUSH;

			if (action != null) {
				int style = action.getStyle();
				if (style == IAction.AS_CHECK_BOX)
					flags = SWT.CHECK;
				else if (style == IAction.AS_DROP_DOWN_MENU)
					flags = SWT.DROP_DOWN;
			}

			ToolItem ti = null;
			if (index >= 0)
				ti = new ToolItem(parent, flags, index);
			else
				ti = new ToolItem(parent, flags);
			ti.setData(this);
			ti.addListener(SWT.Selection, listener);
			ti.addListener(SWT.Dispose, listener);

			widget = ti;

			update(null);

			action.addPropertyChangeListener(listener);
		}
	}

	/**
	 * Returns the action associated with this contribution item.
	 * 
	 * @return the action
	 */
	public IAction getAction() {
		return action;
	}

	/**
	 * Returns the image cache. The cache is global, and is shared by all action
	 * contribution items. This has the disadvantage that once an image is
	 * allocated, it is never freed until the display is disposed. However, it
	 * has the advantage that the same image in different contribution managers
	 * is only ever created once.
	 */
	private ImageCache getImageCache() {
		ImageCache cache = globalImageCache;
		if (cache == null) {
			globalImageCache = cache = new ImageCache();
			Display display = Display.getDefault();
			if (display != null) {
				display.disposeExec(new Runnable() {
					public void run() {
						if (globalImageCache != null) {
							globalImageCache.dispose();
							globalImageCache = null;
						}
					}
				});
			}
		}
		return cache;
	}

	/**
	 * Handles a widget arm event.
	 */
	private void handleWidgetArm(Event e) {
		/*
		 * String description= null; if (fAction instanceof Action) //
		 * getDescription should go into IAction description=
		 * ((Action)fAction).getDescription(); if (description != null)
		 * ApplicationWindow.showDescription(e.widget, description); else
		 * ApplicationWindow.resetDescription(e.widget);
		 */
	}

	/**
	 * Handles a widget dispose event for the widget corresponding to this item.
	 */
	private void handleWidgetDispose(Event e) {
		if (e.widget == widget) {
			// the item is being disposed
			if (action.getStyle() == IAction.AS_DROP_DOWN_MENU) {
				action.getMenuCreator().dispose();
			}
			action.removePropertyChangeListener(listener);
			widget = null;
		}
	}

	/**
	 * Handles an event from the widget (forwarded from nested listener).
	 */
	private void handleWidgetEvent(Event e) {
		switch (e.type) {
		case SWT.Arm:
			handleWidgetArm(e);
			break;
		case SWT.Dispose:
			handleWidgetDispose(e);
			break;
		case SWT.Selection:
			handleWidgetSelection(e);
			break;
		}
	}

	/**
	 * Handles a widget selection event.
	 */
	private void handleWidgetSelection(Event e) {
		Widget item = e.widget;
		if (item != null) {

			int style = item.getStyle();

			if ((style & (SWT.TOGGLE | SWT.CHECK)) != 0) {
				if (action.getStyle() == IAction.AS_CHECK_BOX) {
					action.setChecked(!action.isChecked());
				}

			} else if ((style & SWT.DROP_DOWN) != 0) {
				/*
				 * Added by Pratik Shah Do this regardless of whether the down
				 * arrow button on the side was clicked, or the main button
				 * itself
				 */
				if (action.getStyle() == IAction.AS_DROP_DOWN_MENU) {
					IMenuCreator mc = action.getMenuCreator();
					ToolItem ti = (ToolItem) item;
					// we create the menu as a sub-menu of "dummy" so that we
					// can use
					// it in a cascading menu too.
					// If created on a SWT control we would get an SWT error...
					// Menu dummy= new Menu(ti.getParent());
					// Menu m= mc.getMenu(dummy);
					// dummy.dispose();

					Menu m = mc.getMenu(ti.getParent());
					if (m != null) {
						// position the menu below the drop down item
						Rectangle b = ti.getBounds();
						Point p = ti.getParent().toDisplay(
								new Point(b.x, b.y + b.height));
						m.setLocation(p.x, p.y); // waiting for SWT 0.42
						m.setVisible(true);
						return; // we don't fire the action
					}
				}
			}

			// Ensure action is enabled first.
			// See 1GAN3M6: ITPUI:WINNT - Any IAction in the workbench can be
			// executed
			// while disabled.
			if (action.isEnabled()) {
				action.runWithEvent(e);
			}
		}
	}

	/*
	 * (non-Javadoc) Method declared on Object.
	 */
	public int hashCode() {
		return action.hashCode();
	}

	/*
	 * (non-Javadoc) Method declared on IContributionItem.
	 */
	public boolean isEnabled() {
		return action != null && action.isEnabled();
	}

	/**
	 * The action item implementation of this <code>IContributionItem</code>
	 * method returns <code>true</code> for menu items and <code>false</code>
	 * for everything else.
	 */
	public boolean isDynamic() {
		if (widget instanceof MenuItem) {
			// Optimization. Only recreate the item is the check style has
			// changed.
			boolean itemIsCheck = (widget.getStyle() & SWT.CHECK) != 0;
			boolean actionIsCheck = getAction() != null
					&& getAction().getStyle() == IAction.AS_CHECK_BOX;
			return itemIsCheck != actionIsCheck;
		}
		return false;
	}

	/**
	 * Returns <code>true</code> if this item is allowed to enable,
	 * <code>false</code> otherwise.
	 * 
	 * @return if this item is allowed to be enabled
	 * @since 2.0
	 */
	protected boolean isEnabledAllowed() {
		if (getParent() == null)
			return true;
		Boolean value = getParent().getOverrides().getEnabled(this);
		return (value == null) ? true : value.booleanValue();
	}

	/**
	 * The action item implementation of this <code>IContributionItem</code>
	 * method calls <code>update(null)</code>.
	 */
	public final void update() {
		update(null);
	}

	/**
	 * Synchronizes the UI with the given property.
	 * 
	 * @param propertyName
	 *            the name of the property, or <code>null</code> meaning all
	 *            applicable properties
	 */
	public void update(String propertyName) {
		if (widget != null) {

			// determine what to do
			boolean textChanged = propertyName == null
					|| propertyName.equals(Action.TEXT);
			boolean imageChanged = propertyName == null
					|| propertyName.equals(Action.IMAGE);
			boolean tooltipTextChanged = propertyName == null
					|| propertyName.equals(Action.TOOL_TIP_TEXT);
			boolean enableStateChanged = propertyName == null
					|| propertyName.equals(Action.ENABLED)
					|| propertyName
							.equals(IContributionManagerOverrides.P_ENABLED);
			boolean checkChanged = (action.getStyle() == IAction.AS_CHECK_BOX)
					&& (propertyName == null || propertyName
							.equals(Action.CHECKED));

			if (widget instanceof ToolItem) {
				ToolItem ti = (ToolItem) widget;
				if (imageChanged) {
					updateImages(true);
				}
				if (tooltipTextChanged)
					ti.setToolTipText(action.getToolTipText());

				if (enableStateChanged) {
					boolean shouldBeEnabled = action.isEnabled()
							&& isEnabledAllowed();
					if (ti.getEnabled() != shouldBeEnabled)
						ti.setEnabled(shouldBeEnabled);
				}

				if (checkChanged) {
					boolean bv = action.isChecked();
					if (ti.getSelection() != bv)
						ti.setSelection(bv);
				}
				return;
			}

			if (widget instanceof MenuItem) {
				MenuItem mi = (MenuItem) widget;
				boolean isContextMenu = belongsToContextMenu(mi);

				// We only install an accelerator if the menu item doesn't
				// belong to a context menu (right mouse button menu).
				if (textChanged) {
					if (isContextMenu) {
						String text = action.getText();
						if (text != null) {
							text = Action.removeAcceleratorText(text);
							mi.setText(text);
						}
					} else {
						String text = null;
						IContributionManagerOverrides overrides = null;
						if (getParent() != null)
							overrides = getParent().getOverrides();
						if (overrides != null)
							text = getParent().getOverrides().getText(this);
						if (text == null)
							text = action.getText();
						if (text != null) {
							String label = Action.removeAcceleratorText(text);
							String accText = null;
							Integer acc = null;
							if (overrides != null) {
								accText = overrides.getAcceleratorText(this);
								acc = overrides.getAccelerator(this);
							}
							if ((accText == null)
									&& (label.length() + 1 < text.length()))
								accText = text.substring(label.length() + 1);
							if (acc == null)
								acc = new Integer(action.getAccelerator());
							// UNSUPPORTED - api not implemented in RAP
							// if (acc.intValue() >= 0)
							// mi.setAccelerator(acc.intValue());
							if (accText == null)
								mi.setText(label);
							else
								mi.setText(label + '\t' + accText);
						}
					}
				}
				if (imageChanged) {
					updateImages(false);
				}
				if (enableStateChanged) {
					boolean shouldBeEnabled = action.isEnabled()
							&& isEnabledAllowed();
					if (mi.getEnabled() != shouldBeEnabled)
						mi.setEnabled(shouldBeEnabled);
				}

				if (checkChanged) {
					boolean bv = action.isChecked();
					if (mi.getSelection() != bv)
						mi.setSelection(bv);
				}
				return;
			}

			if (widget instanceof Button) {
				Button button = (Button) widget;
				if (imageChanged) {
					if (updateImages(false)) {
						// don't update text if it has an image
						textChanged = false;
					}
				}
				if (textChanged) {
					String text = action.getText();
					if (text != null)
						button.setText(text);
				}
				if (tooltipTextChanged)
					button.setToolTipText(action.getToolTipText());

				if (enableStateChanged) {
					boolean shouldBeEnabled = action.isEnabled()
							&& isEnabledAllowed();
					if (button.getEnabled() != shouldBeEnabled)
						button.setEnabled(shouldBeEnabled);
				}

				if (checkChanged) {
					boolean bv = action.isChecked();
					if (button.getSelection() != bv)
						button.setSelection(bv);
				}
				return;
			}
		}
	}

	/**
	 * Updates the images for this action.
	 * 
	 * @param forceImage
	 *            <code>true</code> if some form of image is compulsory, and
	 *            <code>false</code> if it is acceptable for this item to have
	 *            no image
	 * @return <code>true</code> if there are images for this action,
	 *         <code>false</code> if not
	 */
	private boolean updateImages(boolean forceImage) {

		ImageCache cache = getImageCache();

		if (widget instanceof ToolItem) {
			Image image = cache.getImage(action.getImageDescriptor());
			Image hoverImage = cache.getImage(action.getHoverImageDescriptor());
			Image disabledImage = cache.getImage(action
					.getDisabledImageDescriptor());

			// If there is no regular image, but there is a hover image,
			// convert the hover image to gray and use it as the regular image.
			if (image == null && hoverImage != null) {
				image = cache.getGrayImage(action.getHoverImageDescriptor());
			} else {
				// If there is no hover image, use the regular image as the
				// hover image,
				// and convert the regular image to gray
				if (hoverImage == null && image != null) {
					hoverImage = image;
					image = cache.getGrayImage(action.getImageDescriptor());
				}
			}

			// Make sure there is a valid image.
			if (hoverImage == null && image == null && forceImage) {
				image = cache.getMissingImage();
			}

			// performance: more efficient in SWT to set disabled and hot image
			// before
			// regular image
			if (disabledImage != null) {
				// Set the disabled image if we were able to create one.
				// Assumes that SWT.ToolItem will use platform's default
				// behavior to show item when it is disabled and a disabled
				// image has not been set.
				((ToolItem) widget).setDisabledImage(disabledImage);
			}
			((ToolItem) widget).setHotImage(hoverImage);
			((ToolItem) widget).setImage(image);

			return image != null;
		} else if (widget instanceof Item || widget instanceof Button) {
			// Use hover image if there is one, otherwise use regular image.
			Image image = cache.getImage(action.getHoverImageDescriptor());
			if (image == null) {
				image = cache.getImage(action.getImageDescriptor());
			}
			// Make sure there is a valid image.
			if (image == null && forceImage) {
				image = cache.getMissingImage();
			}
			if (widget instanceof Item) {
				((Item) widget).setImage(image);
			} else if (widget instanceof Button) {
				((Button) widget).setImage(image);
			}
			return image != null;
		}
		return false;
	}
}
