/*******************************************************************************
 * Copyright (c) 2010, 2018 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@vogella.com - Bug 454712, 485851
 *     dirk.fauth@googlemail.com - Bug 446095
 ******************************************************************************/
package org.eclipse.e4.ui.workbench.addons.minmax;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.di.UIEventTopic;
import org.eclipse.e4.ui.internal.workbench.swt.CSSRenderingUtils;
import org.eclipse.e4.ui.internal.workbench.swt.ShellActivationListener;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.MGenericStack;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.e4.ui.model.application.ui.MUILabel;
import org.eclipse.e4.ui.model.application.ui.SideValue;
import org.eclipse.e4.ui.model.application.ui.advanced.MArea;
import org.eclipse.e4.ui.model.application.ui.advanced.MPerspective;
import org.eclipse.e4.ui.model.application.ui.advanced.MPerspectiveStack;
import org.eclipse.e4.ui.model.application.ui.advanced.MPlaceholder;
import org.eclipse.e4.ui.model.application.ui.basic.MCompositePart;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.basic.MPartStack;
import org.eclipse.e4.ui.model.application.ui.basic.MStackElement;
import org.eclipse.e4.ui.model.application.ui.basic.MTrimBar;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.model.application.ui.menu.MToolControl;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.IResourceUtilities;
import org.eclipse.e4.ui.workbench.UIEvents;
import org.eclipse.e4.ui.workbench.UIEvents.EventTags;
import org.eclipse.e4.ui.workbench.addons.minmax.TrimStackIdHelper.TrimStackIdPart;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.e4.ui.workbench.modeling.EPartService;
import org.eclipse.e4.ui.workbench.renderers.swt.TrimmedPartLayout;
import org.eclipse.emf.common.util.URI;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
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.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.osgi.service.event.EventHandler;


/**
 * Class for representing window trim containing minimized views and shared areas
 */
public class TrimStack {

	/**
	 * Contribution URI for this class
	 */
	public static String CONTRIBUTION_URI = "bundleclass://org.eclipse.e4.ui.workbench.addons.swt/org.eclipse.e4.ui.workbench.addons.minmax.TrimStack"; //$NON-NLS-1$

	private static final String LAYOUT_ICON_URI = "platform:/plugin/org.eclipse.e4.ui.workbench.addons.swt/icons/full/obj16/layout_co.png"; //$NON-NLS-1$

	private static final String RESTORE_ICON_URI = "platform:/plugin/org.eclipse.e4.ui.workbench.addons.swt/icons/full/etool16/fastview_restore.png"; //$NON-NLS-1$

	public static final String USE_OVERLAYS_KEY = "UseOverlays"; //$NON-NLS-1$

	static final String STATE_XSIZE = "XSize"; //$NON-NLS-1$

	static final String STATE_YSIZE = "YSize"; //$NON-NLS-1$

	public static final String MINIMIZED_AND_SHOWING = "MinimizedAndShowing"; //$NON-NLS-1$

	private Image layoutImage;

	private Image restoreImage;

	private ToolBar trimStackTB;

	/**
	 * The context menu for this trim stack's items.
	 */
	private Menu trimStackMenu;

	private boolean cachedUseOverlays = true;
	private boolean isShowing = false;
	private MUIElement minimizedElement;
	// private Composite clientAreaComposite;
	private Composite hostPane;

	@Inject
	@Named("org.eclipse.e4.ui.workbench.IResourceUtilities")
	private IResourceUtilities<ImageDescriptor> resUtils;

	/**
	 * A map of created images from a part's icon URI path.
	 */
	private Map<String, Image> imageMap = new HashMap<>();

	ControlListener caResizeListener = new ControlAdapter() {
		@Override
		public void controlResized(ControlEvent e) {
			if (hostPane != null && hostPane.isVisible()) {
				setPaneLocation();
			}
		}
	};


	@Inject
	EModelService modelService;

	@Inject
	EPartService partService;

	@Inject
	MWindow window;

	@Inject
	MToolControl toolControl;

	@Inject
	protected IEventBroker eventBroker;

	// Listens to ESC and closes the active fast view
	private Listener escapeListener = event -> {
		if (event.character == SWT.ESC) {
			showStack(false);
			partService.requestActivation();
		}
	};

	@Inject
	@Optional
	private void subscribeTopicTagsChanged(
			@UIEventTopic(UIEvents.ApplicationElement.TOPIC_TAGS) org.osgi.service.event.Event event) {
		Object changedObj = event.getProperty(EventTags.ELEMENT);

		if (!(changedObj instanceof MToolControl)) {
			return;
		}

		final MToolControl changedElement = (MToolControl) changedObj;
		if (changedElement.getObject() != this) {
			return;
		}

		if (UIEvents.isREMOVE(event)) {
			if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE, MINIMIZED_AND_SHOWING)) {
				showStack(false);
			}
		}
	}

	private Image getOverrideImage(MUIElement element) {
		Image result = null;

		Object imageObject = element.getTransientData().get(
				IPresentationEngine.OVERRIDE_ICON_IMAGE_KEY);
		if (imageObject != null && imageObject instanceof Image
				&& !((Image) imageObject).isDisposed()) {
			result = (Image) imageObject;
		}
		return result;
	}

	private String getOverrideTitleToolTip(MUIElement element) {
		String result = null;

		Object stringObject = element.getTransientData().get(
				IPresentationEngine.OVERRIDE_TITLE_TOOL_TIP_KEY);
		if (stringObject != null && stringObject instanceof String) {
			result = (String) stringObject;
		}

		if (result == null || result.length() == 0) {
			return null;
		}

		if (element instanceof MUILabel) {
			String label = ((MUILabel)element).getLocalizedLabel();
			if (label != null && label.length() > 0) {
				result = label + ' ' + '(' + result + ')';
			}
		}

		return result;
	}

	/**
	 * This is the new way to handle UIEvents (as opposed to subscring and unsubscribing them with
	 * the event broker.
	 *
	 * The method is described in detail at http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	@SuppressWarnings("unchecked")
	@Inject
	@Optional
	private void handleTransientDataEvents(
			@UIEventTopic(UIEvents.ApplicationElement.TOPIC_TRANSIENTDATA) org.osgi.service.event.Event event) {
		// Prevent exceptions on shutdown
		if (trimStackTB == null || trimStackTB.isDisposed() || minimizedElement.getWidget() == null) {
			return;
		}

		Object changedElement = event.getProperty(UIEvents.EventTags.ELEMENT);
		if (!(changedElement instanceof MUIElement)) {
			return;
		}

		String key;
		if (UIEvents.isREMOVE(event)) {
			key = ((Entry<String, Object>) event.getProperty(UIEvents.EventTags.OLD_VALUE))
					.getKey();
		} else {
			key = ((Entry<String, Object>) event.getProperty(UIEvents.EventTags.NEW_VALUE))
					.getKey();
		}

		if (key.equals(IPresentationEngine.OVERRIDE_ICON_IMAGE_KEY)) {
			ToolItem toolItem = getChangedToolItem((MUIElement) changedElement);
			if (toolItem != null) {
				toolItem.setImage(getImage((MUILabel) toolItem.getData()));
			}
		} else if (key.equals(IPresentationEngine.OVERRIDE_TITLE_TOOL_TIP_KEY)) {
			ToolItem toolItem = getChangedToolItem((MUIElement) changedElement);
			if (toolItem != null) {
				toolItem.setToolTipText(getLabelText((MUILabel) toolItem.getData()));
			}
		}
	}

	private ToolItem getChangedToolItem(MUIElement changedElement) {
		ToolItem[] toolItems = trimStackTB.getItems();
		for (ToolItem toolItem : toolItems) {
			if (changedElement.equals(toolItem.getData())) {
				return toolItem;
			}
		}
		return null;
	}

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	private EventHandler closeHandler = event -> {
		if (!isShowing) {
			return;
		}

		// The only time we don't close is if I've selected my tab.
		MUIElement changedElement = (MUIElement) event.getProperty(UIEvents.EventTags.ELEMENT);

		// Perspective changed, close the visible stacks
		if (changedElement instanceof MPerspectiveStack) {
			showStack(false);
			return;
		}

		if (changedElement instanceof MCompositePart) {
			MPart innerPart = getLeafPart(changedElement);
			if (innerPart != null) {
				fixToolItemSelection();
				return;
			}
		}

		if (changedElement == getLeafPart(minimizedElement)) {
			fixToolItemSelection();
			return;
		}

		showStack(false);
	};

	// Close any open stacks before shutting down
	private EventHandler shutdownHandler = event -> showStack(false);

	private void fixToolItemSelection() {
		if (trimStackTB == null || trimStackTB.isDisposed()) {
			return;
		}

		if (!isShowing) {
			// Not open...no selection
			for (ToolItem item : trimStackTB.getItems()) {
				item.setSelection(false);
			}
		} else {
			if (isEditorStack() || minimizedElement instanceof MPlaceholder) {
				trimStackTB.getItem(1).setSelection(true);
			} else if (isPerspectiveStack()) {
				MPerspectiveStack pStack = (MPerspectiveStack) minimizedElement;
				MUIElement selElement = pStack.getSelectedElement();
				for (ToolItem item : trimStackTB.getItems()) {
					item.setSelection(item.getData() == selElement);
				}
			} else {
				MPartStack partStack = (MPartStack) minimizedElement;
				MUIElement selElement = partStack.getSelectedElement();
				if (selElement instanceof MPlaceholder) {
					selElement = ((MPlaceholder) selElement).getRef();
				}

				for (ToolItem item : trimStackTB.getItems()) {
					boolean isSel = item.getData() == selElement;
					item.setSelection(isSel);
				}
			}
		}
	}

	private boolean isEditorStack() {
		if (!(minimizedElement instanceof MPlaceholder)) {
			return false;
		}

		MPlaceholder ph = (MPlaceholder) minimizedElement;
		return ph.getRef() instanceof MArea;
	}

	private boolean isPerspectiveStack() {
		return minimizedElement instanceof MPerspectiveStack;
	}

	private MPart getLeafPart(MUIElement element) {
		if (element instanceof MPlaceholder) {
			return getLeafPart(((MPlaceholder) element).getRef());
		}

		if (element instanceof MElementContainer<?>) {
			return getLeafPart(((MElementContainer<?>) element).getSelectedElement());
		}

		if (element instanceof MPart) {
			return (MPart) element;
		}

		return null;
	}

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	private EventHandler openHandler = event -> {
		if (isShowing) {
			return;
		}

		MUIElement changedElement = (MUIElement) event.getProperty(UIEvents.EventTags.ELEMENT);

		// Open if shared area
		if (getLeafPart(minimizedElement) == changedElement && !(minimizedElement instanceof MPerspectiveStack)) {
			showStack(true);
			return;
		}

		MUIElement selectedElement = null;

		if (minimizedElement instanceof MPlaceholder) {
			selectedElement = ((MPlaceholder) minimizedElement).getRef();
		} else if (minimizedElement instanceof MPartStack) {
			selectedElement = ((MPartStack) minimizedElement).getSelectedElement();
		}

		if (selectedElement == null) {
			return;
		}

		if (selectedElement instanceof MPlaceholder) {
			selectedElement = ((MPlaceholder) selectedElement).getRef();
		}

		if (changedElement != selectedElement) {
			return;
		}

		showStack(true);
	};

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	private EventHandler toBeRenderedHandler = event -> {
		if (minimizedElement == null || trimStackTB == null) {
			return;
		}

		MUIElement changedElement = (MUIElement) event.getProperty(UIEvents.EventTags.ELEMENT);

		// if our stack is going away, so should we
		if (changedElement == minimizedElement && !minimizedElement.isToBeRendered()) {
			restoreStack();
			return;
		}

		// if one of the kids changes state, re-scrape the CTF
		MUIElement parentElement = changedElement.getParent();
		if (parentElement == minimizedElement) {
			trimStackTB.getDisplay().asyncExec(() -> updateTrimStackItems());
		}
	};

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	private EventHandler childrenHandler = event -> {
		if (minimizedElement == null || trimStackTB == null) {
			return;
		}

		Object changedObj = event.getProperty(UIEvents.EventTags.ELEMENT);

		// if a child has been added or removed, re-scape the CTF
		if (changedObj == minimizedElement) {
			trimStackTB.getDisplay().asyncExec(() -> updateTrimStackItems());
		}
	};

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	private EventHandler widgetHandler = event -> {
		Object changedObj = event.getProperty(UIEvents.EventTags.ELEMENT);
		if (changedObj != minimizedElement) {
			return;
		}

		if (minimizedElement.getWidget() != null) {
			trimStackTB.getDisplay().asyncExec(() -> updateTrimStackItems());
		}
	};

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	// Listener attached to every ToolItem in a TrimStack. Responsible for activating the
	// appropriate part.
	private SelectionListener toolItemSelectionListener = new SelectionListener() {
		@Override
		public void widgetSelected(SelectionEvent e) {
			ToolItem toolItem = (ToolItem) e.widget;
			MUIElement uiElement = (MUIElement) toolItem.getData();

			// Clicking on the already showing item ? NOTE: the selection will already have been
			// turned off by the time the event arrives
			if (!toolItem.getSelection()) {
				partService.requestActivation();
				showStack(false);
				return;
			}

			if (uiElement instanceof MPart) {
				partService.activate((MPart) uiElement);
			} else if (uiElement instanceof MPerspective) {
				uiElement.getParent().setSelectedElement(uiElement);
			}
			showStack(true);
		}

		@Override
		public void widgetDefaultSelected(SelectionEvent e) {
			widgetSelected(e);
		}
	};

	// private MTrimBar bar;

	private int fixedSides;

	private Composite originalParent;

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	@PostConstruct
	void addListeners() {
		eventBroker.subscribe(UIEvents.ElementContainer.TOPIC_CHILDREN, childrenHandler);
		eventBroker.subscribe(UIEvents.UIElement.TOPIC_TOBERENDERED, toBeRenderedHandler);
		eventBroker.subscribe(UIEvents.UIElement.TOPIC_WIDGET, widgetHandler);
		eventBroker.subscribe(UIEvents.UILifeCycle.BRINGTOTOP, openHandler);
		eventBroker.subscribe(UIEvents.UILifeCycle.ACTIVATE, closeHandler);
		eventBroker.subscribe(UIEvents.UILifeCycle.APP_SHUTDOWN_STARTED, shutdownHandler);
	}

	private Composite getCAComposite() {
		if (trimStackTB == null) {
			return null;
		}

		// Get the shell's client area composite
		Shell theShell = trimStackTB.getShell();
		if (theShell.getLayout() instanceof TrimmedPartLayout) {
			TrimmedPartLayout tpl = (TrimmedPartLayout) theShell.getLayout();
			if (!tpl.clientArea.isDisposed()) {
				return tpl.clientArea;
			}
		}
		return null;
	}

	/**
	 * This is the old way to subscribe to UIEvents. You should consider using the new way as shown
	 * by handleTransientDataEvents() and described in the article at
	 * http://wiki.eclipse.org/Eclipse4/RCP/Event_Model
	 */
	@PreDestroy
	void removeListeners() {
		eventBroker.unsubscribe(toBeRenderedHandler);
		eventBroker.unsubscribe(childrenHandler);
		eventBroker.unsubscribe(widgetHandler);
		eventBroker.unsubscribe(openHandler);
		eventBroker.unsubscribe(closeHandler);
	}

	@PostConstruct
	void createWidget(Composite parent, MToolControl me, CSSRenderingUtils cssUtils) {
		if (minimizedElement == null) {
			minimizedElement = findElement();
		}

		MUIElement meParent = me.getParent();
		int orientation = SWT.HORIZONTAL;
		if (meParent instanceof MTrimBar) {
			MTrimBar bar = (MTrimBar) meParent;
			if (bar.getSide() == SideValue.RIGHT || bar.getSide() == SideValue.LEFT) {
				orientation = SWT.VERTICAL;
			}
			// TrimStacks are draggable by default
			 me.getTags().add(IPresentationEngine.DRAGGABLE);
		}
		trimStackTB = new ToolBar(parent, orientation | SWT.FLAT | SWT.WRAP);
		trimStackTB.addDisposeListener(e -> {
			showStack(false);

			trimStackTB = null;
			trimStackMenu = null;
		});

		trimStackTB.addListener(SWT.MenuDetect, event -> {
			// Clear any existing menus
			while (trimStackMenu.getItemCount() > 0) {
				trimStackMenu.getItem(0).dispose();
			}

			// Only open the menu if a tool item is selected
			Point point = trimStackTB.getDisplay().map(null, trimStackTB, new Point(event.x, event.y));
			ToolItem selectedToolItem = trimStackTB.getItem(point);
			if (selectedToolItem == null) {
				return;
			}

			// Are we hovering over a valid tool item (vs restore button)
			Object data = selectedToolItem.getData();
			if (data instanceof MPart) {
				// A part on a stack or editor area
				createPartMenu((MPart) data);
			} else if (data instanceof MPerspective) {
				// A perspective in a perspective stack (for now we just support restore)
				createEmtpyEditorAreaMenu();
			} else if (isEditorStack()) {
				// An empty editor area
				createEmtpyEditorAreaMenu();
			} else {
				createUseOverlaysMenu();
			}
		});

		trimStackMenu = new Menu(trimStackTB);
		trimStackTB.setMenu(trimStackMenu);

		ToolItem restoreBtn = new ToolItem(trimStackTB, SWT.PUSH);
		restoreBtn.setToolTipText(Messages.TrimStack_RestoreText);
		restoreBtn.setImage(getRestoreImage());
		restoreBtn.addSelectionListener(SelectionListener
				.widgetSelectedAdapter(e -> minimizedElement.getTags().remove(IPresentationEngine.MINIMIZED)));

		updateTrimStackItems();
	}

	/**
	 * Creates a restore menu item that removes the minimized tag from the {@link #minimizedElement}
	 */
	private void createEmtpyEditorAreaMenu() {
		MenuItem restoreItem = new MenuItem(trimStackMenu, SWT.NONE);
		restoreItem.setText(Messages.TrimStack_RestoreText);
		restoreItem.addListener(SWT.Selection,
				event -> minimizedElement.getTags().remove(IPresentationEngine.MINIMIZED));
	}

	/**
	 * Creates a restore menu item that removes the minimized tag from the {@link #minimizedElement}
	 */
	private void createUseOverlaysMenu() {
		MenuItem useOverlaysItem = new MenuItem(trimStackMenu, SWT.CHECK);
		useOverlaysItem.setText(Messages.TrimStack_Show_In_Original_Location);
		useOverlaysItem.setSelection(!useOverlays());
		useOverlaysItem.addListener(SWT.Selection, event -> {
			if (toolControl != null) {
				toolControl.getPersistedState().put(USE_OVERLAYS_KEY, Boolean.toString(!useOverlays()));
			}
		});
	}

	/**
	 * Creates a series of menu items when a part is selected. The orientation submenu changes the
	 * layout tags on the {@link #minimizedElement}. The restore item will remove the minimized tag.
	 * The close item is not available on the editor stack, but will ask the part service to hide
	 * the part.
	 *
	 * @param selectedPart
	 *            the part from the data of the selected tool item
	 */
	private void createPartMenu(final MPart selectedPart) {
		MenuItem orientationItem = new MenuItem(trimStackMenu, SWT.CASCADE);
		orientationItem.setText(Messages.TrimStack_OrientationMenu);
		Menu orientationMenu = new Menu(orientationItem);
		orientationItem.setMenu(orientationMenu);

		MenuItem defaultItem = new MenuItem(orientationMenu, SWT.RADIO);
		defaultItem.setText(Messages.TrimStack_DefaultOrientationItem);
		defaultItem.addListener(SWT.Selection, event -> {
			boolean doRefresh = minimizedElement.getTags().remove(IPresentationEngine.ORIENTATION_HORIZONTAL);
			doRefresh |= minimizedElement.getTags().remove(IPresentationEngine.ORIENTATION_VERTICAL);
			if (isShowing && doRefresh) {
				setPaneLocation();
			}
		});

		MenuItem horizontalItem = new MenuItem(orientationMenu, SWT.RADIO);
		horizontalItem.setText(Messages.TrimStack_Horizontal);
		horizontalItem.addListener(SWT.Selection, event -> {
			if (!minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_HORIZONTAL)) {
				minimizedElement.getTags().remove(IPresentationEngine.ORIENTATION_VERTICAL);
				minimizedElement.getTags().add(IPresentationEngine.ORIENTATION_HORIZONTAL);
				if (isShowing) {
					setPaneLocation();
				}
			}
		});

		MenuItem verticalItem = new MenuItem(orientationMenu, SWT.RADIO);
		verticalItem.setText(Messages.TrimStack_Vertical);
		verticalItem.addListener(SWT.Selection, event -> {
			if (!minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_VERTICAL)) {
				minimizedElement.getTags().remove(IPresentationEngine.ORIENTATION_HORIZONTAL);
				minimizedElement.getTags().add(IPresentationEngine.ORIENTATION_VERTICAL);
				if (isShowing) {
					setPaneLocation();
				}
			}
		});

		// Set initial orientation selection
		if (minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_HORIZONTAL)) {
			horizontalItem.setSelection(true);
		} else if (minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_VERTICAL)) {
			verticalItem.setSelection(true);
		} else {
			defaultItem.setSelection(true);
		}

		MenuItem restoreItem = new MenuItem(trimStackMenu, SWT.NONE);
		restoreItem.setText(Messages.TrimStack_RestoreText);
		restoreItem.addListener(SWT.Selection, event -> {
			minimizedElement.getTags().remove(IPresentationEngine.MINIMIZED);
			partService.activate(selectedPart);
		});

		// Do not allow the shared editor area to be closed
		if (!isEditorStack()) {
			MenuItem closeItem = new MenuItem(trimStackMenu, SWT.NONE);
			closeItem.setText(Messages.TrimStack_CloseText);
			closeItem.addListener(SWT.Selection, event -> partService.hidePart(selectedPart));
		}
	}

	@PreDestroy
	void destroy() {
		for (Image image : imageMap.values()) {
			image.dispose();
		}

		if (layoutImage != null) {
			layoutImage.dispose();
			layoutImage = null;
		}

		if (restoreImage != null) {
			restoreImage.dispose();
			restoreImage = null;
		}
	}

	public MUIElement getMinimizedElement() {
		return minimizedElement;
	}

	private MUIElement findElement() {
		MUIElement result;
		List<MPerspectiveStack> ps = modelService.findElements(window, null, MPerspectiveStack.class);
		if (ps.isEmpty()) {
			String toolControlId = toolControl.getElementId();
			int index = toolControlId.indexOf('(');
			String stackId = toolControlId.substring(0, index);
			result = modelService.find(stackId, window);
		} else {
			String toolControlId = toolControl.getElementId();
			Map<TrimStackIdPart, String> parsedIds = TrimStackIdHelper.parseTrimStackId(toolControlId);

			String stackId = parsedIds.get(TrimStackIdPart.ELEMENT_ID);
			String perspId = parsedIds.get(TrimStackIdPart.PERSPECTIVE_ID);

			MPerspective persp = null;
			if (perspId != null) {
				List<MPerspective> perspectives = modelService.findElements(ps.get(0), perspId, MPerspective.class);
				if (perspectives != null && !perspectives.isEmpty()) {
					persp = perspectives.get(0);
				}
			}

			if (persp != null) {
				result = modelService.find(stackId, persp);
			} else {
				result = modelService.find(stackId, window);
			}
		}

		return result;
	}

	private String getLabelText(MUILabel label) {
		// Use override text if available
		if (label instanceof MUIElement) {
			String text = getOverrideTitleToolTip((MUIElement) label);
			if (text != null && text.length() > 0) {
				return text;
			}
		}

		String string = label.getLocalizedLabel();
		return string == null ? "" : string; //$NON-NLS-1$
	}

	private Image getImage(MUILabel element) {
		// Use override image if available
		if (element instanceof MUIElement) {
			Image image = getOverrideImage((MUIElement) element);
			if (image != null) {
				return image;
			}
		}

		String iconURI = element.getIconURI();
		if (iconURI != null && iconURI.length() > 0) {
			Image image = imageMap.get(iconURI);
			if (image == null) {
				image = resUtils.imageDescriptorFromURI(URI.createURI(iconURI)).createImage();
				imageMap.put(iconURI, image);
			}
			return image;
		}

		return null;
	}

	private MUILabel getLabelElement(MUIElement element) {
		if (element instanceof MPlaceholder) {
			element = ((MPlaceholder) element).getRef();
		}

		return (MUILabel) (element instanceof MUILabel ? element : null);
	}

	private void updateTrimStackItems() {
		// Prevent exceptions on shutdown
		if (trimStackTB == null || trimStackTB.isDisposed() || minimizedElement.getWidget() == null) {
			return;
		}

		// Remove any current items except the 'restore' button
		while (trimStackTB.getItemCount() > 1) {
			trimStackTB.getItem(trimStackTB.getItemCount() - 1).dispose();
		}

		if (isEditorStack() && trimStackTB.getItemCount() == 1) {
			ToolItem ti = new ToolItem(trimStackTB, SWT.CHECK);
			ti.setToolTipText(Messages.TrimStack_SharedAreaTooltip);
			ti.setImage(getLayoutImage());
			ti.addSelectionListener(toolItemSelectionListener);
		} else if (minimizedElement instanceof MPlaceholder) {
			MPlaceholder ph = (MPlaceholder) minimizedElement;
			if (ph.getRef() instanceof MPart) {
				MPart part = (MPart) ph.getRef();
				ToolItem ti = new ToolItem(trimStackTB, SWT.CHECK);
				ti.setData(part);
				ti.setImage(getImage(part));
				ti.setToolTipText(getLabelText(part));
				ti.addSelectionListener(toolItemSelectionListener);
			}
		} else if (minimizedElement instanceof MGenericStack<?>) {
			// Handle *both* PartStacks and PerspectiveStacks here...
			MGenericStack<?> theStack = (MGenericStack<?>) minimizedElement;

			// check to see if this stack has any valid elements
			boolean hasRenderedElements = false;
			for (MUIElement stackElement : theStack.getChildren()) {
				if (stackElement.isToBeRendered()) {
					hasRenderedElements = true;
					break;
				}
			}

			if (hasRenderedElements) {
				for (MUIElement stackElement : theStack.getChildren()) {
					if (!stackElement.isToBeRendered()) {
						continue;
					}

					MUILabel labelElement = getLabelElement(stackElement);
					ToolItem newItem = new ToolItem(trimStackTB, SWT.CHECK);
					newItem.setData(labelElement);
					newItem.setImage(getImage(labelElement));
					newItem.setToolTipText(getLabelText(labelElement));
					newItem.addSelectionListener(toolItemSelectionListener);
				}
			} else if (theStack.getTags().contains(IPresentationEngine.NO_AUTO_COLLAPSE)) {
				// OK to be empty and still minimized
				ToolItem ti = new ToolItem(trimStackTB, SWT.CHECK);
				ti.setToolTipText(Messages.TrimStack_EmptyStackTooltip);
				ti.setImage(getLayoutImage());
				ti.addSelectionListener(toolItemSelectionListener);
			} else {
				// doesn't have any children that's showing, place it back in the presentation
				restoreStack();
				return;
			}
		}
		trimStackTB.pack();
		trimStackTB.requestLayout();
	}

	void restoreStack() {
		showStack(false);

		minimizedElement.setVisible(true);
		minimizedElement.getTags().remove(IPresentationEngine.MINIMIZED);

		// Activate the part that is being brought up...
		if (minimizedElement instanceof MPartStack) {
			MPartStack theStack = (MPartStack) minimizedElement;
			MStackElement curSel = theStack.getSelectedElement();
			Control ctrl = (Control) minimizedElement.getWidget();

			// Hack for elems that are lazy initialized
			if (ctrl instanceof CTabFolder && ((CTabFolder) ctrl).getSelection() == null) {
				theStack.setSelectedElement(null);
				theStack.setSelectedElement(curSel);
			}
		}

		toolControl.setToBeRendered(false);

		if (hostPane != null && !hostPane.isDisposed()) {
			hostPane.dispose();
		}
		hostPane = null;
	}

	/**
	 * Sets whether this stack should be visible or hidden
	 *
	 * @param show
	 *            whether the stack should be visible
	 */
	public void showStack(boolean show) {
		Control ctrl = (Control) minimizedElement.getWidget();
		if (ctrl == null) {
			return;
		}
		CTabFolder ctf = ctrl instanceof CTabFolder? (CTabFolder) ctrl: null;

		Composite clientAreaComposite = getCAComposite();
		if (clientAreaComposite == null || clientAreaComposite.isDisposed()) {
			return;
		}

		if (show && !isShowing) {
			if (useOverlays()) {
				hostPane = getHostPane();
				originalParent = ctrl.getParent();
				ctrl.setParent(hostPane);

				// Hack ! Force a resize of the CTF to make sure the hosted
				// view is the correct size...see bug 434062 for details
				if (ctf != null) {
					Rectangle bb = ctf.getBounds();
					bb.width--;
					ctf.setBounds(bb);
				}

				clientAreaComposite.addControlListener(caResizeListener);

				// Set the initial location
				setPaneLocation();

				hostPane.addListener(SWT.Traverse, escapeListener);

				hostPane.layout(true);
				hostPane.moveAbove(null);
				hostPane.setVisible(true);

				// Cache the value to ensure that a stack is hidden using the same mode it was
				// opened in
				cachedUseOverlays = true;
			} else {
				minimizedElement.setVisible(true);
				ctrl.addListener(SWT.Traverse, escapeListener);

				// Cache the value to ensure that a stack is hidden using the same mode it was
				// opened in
				cachedUseOverlays = false;
			}

			isShowing = true;
			toolControl.getTags().add(MINIMIZED_AND_SHOWING);

			// Activate the part that is being brought up...
			if (minimizedElement instanceof MPartStack) {
				MPartStack theStack = (MPartStack) minimizedElement;
				MStackElement curSel = theStack.getSelectedElement();

				// Hack for elems that are lazy initialized
				if (ctf != null && ctf.getSelection() == null) {
					theStack.setSelectedElement(null);
					theStack.setSelectedElement(curSel);
				}

				if (curSel instanceof MPart) {
					partService.activate((MPart) curSel);
				} else if (curSel instanceof MPlaceholder) {
					MPlaceholder ph = (MPlaceholder) curSel;
					if (ph.getRef() instanceof MPart) {
						partService.activate((MPart) ph.getRef());
					}
				}
			} else if (isEditorStack()) {
				MArea area = (MArea) ((MPlaceholder) minimizedElement).getRef();

				// See if we can find an element to activate...
				MPart partToActivate = null;
				MUIElement selectedElement = area.getSelectedElement();
				while (partToActivate == null && selectedElement != null) {
					if (selectedElement instanceof MPart) {
						partToActivate = (MPart) selectedElement;
					} else if (selectedElement instanceof MPlaceholder) {
						MPlaceholder ph = (MPlaceholder) selectedElement;
						if (ph.getRef() instanceof MPart) {
							partToActivate = (MPart) ph.getRef();
						} else {
							selectedElement = null;
						}
					} else if (selectedElement instanceof MElementContainer<?>) {
						MElementContainer<?> container = (MElementContainer<?>) selectedElement;
						selectedElement = container.getSelectedElement();
					}
				}

				// If we haven't found one then use the first
				if (partToActivate == null) {
					List<MPart> parts = modelService.findElements(area, null, MPart.class, null);
					for (MPart part : parts) {
						if (partService.isPartVisible(part)) {
							partToActivate = part;
							break;
						}
					}
				}

				if (partToActivate != null) {
					partService.activate(partToActivate);
				}
			} else if (minimizedElement instanceof MPlaceholder) {
				MPlaceholder ph = (MPlaceholder) minimizedElement;
				if (ph.getRef() instanceof MPart) {
					MPart part = (MPart) ph.getRef();
					partService.activate(part);
				}
			}

			fixToolItemSelection();
		} else if (!show && isShowing) {
			if (cachedUseOverlays) {
				clientAreaComposite.removeControlListener(caResizeListener);

				ctrl.setParent(originalParent);

				hostPane.dispose();
				hostPane = null;
			} else {
				if (ctrl != null && !ctrl.isDisposed()) {
					ctrl.removeListener(SWT.Traverse, escapeListener);
				}
				minimizedElement.setVisible(false);
			}

			isShowing = false;
			toolControl.getTags().remove(MINIMIZED_AND_SHOWING);

			fixToolItemSelection();
		}
	}

	/**
	 * @return 'true' iff the minimized stack should overlay the current presentation, 'false' means
	 *         to temporarily restore the stack into the current presentation.
	 */
	private boolean useOverlays() {
		if (toolControl == null) {
			return true;
		}

		String useOverlays = toolControl.getPersistedState().get(USE_OVERLAYS_KEY);
		if (useOverlays == null)
		 {
			useOverlays = "true"; //$NON-NLS-1$
		}
		return Boolean.parseBoolean(useOverlays);
	}

	private void setPaneLocation() {
		Composite clientAreaComposite = getCAComposite();
		if (clientAreaComposite == null || clientAreaComposite.isDisposed()) {
			return;
		}

		Rectangle caRect = clientAreaComposite.getBounds();

		// NOTE: always starts in the persisted (or default) size
		Point paneSize = hostPane.getSize();

		// Ensure it's not clipped
		if (paneSize.x > caRect.width) {
			paneSize.x = caRect.width;
		}
		if (paneSize.y > caRect.height) {
			paneSize.y = caRect.height;
		}

		if (minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_HORIZONTAL)) {
			paneSize.x = caRect.width;
		}
		if (minimizedElement.getTags().contains(IPresentationEngine.ORIENTATION_VERTICAL)) {
			paneSize.y = caRect.height;
		}

		Point loc = new Point(0, 0);
		if (isFixed(SWT.LEFT)) {
			loc.x = caRect.x;
		} else {
			loc.x = (caRect.x + caRect.width) - paneSize.x;
		}

		if (isFixed(SWT.TOP)) {
			loc.y = caRect.y;
		} else {
			loc.y = (caRect.y + caRect.height) - paneSize.y;
		}

		hostPane.setSize(paneSize);
		hostPane.setLocation(loc);
	}

	private void setHostSize() {
		if (hostPane == null || hostPane.isDisposed()) {
			return;
		}

		int xSize = 600;
		String xSizeStr = toolControl.getPersistedState().get(STATE_XSIZE);
		if (xSizeStr != null) {
			xSize = Integer.parseInt(xSizeStr);
		}
		int ySize = 400;
		String ySizeStr = toolControl.getPersistedState().get(STATE_YSIZE);
		if (ySizeStr != null) {
			ySize = Integer.parseInt(ySizeStr);
		}
		hostPane.setSize(xSize, ySize);
	}

	private Composite getHostPane() {
		// Create one
		hostPane = new Composite(trimStackTB.getShell(), SWT.NONE);
		hostPane.setData(ShellActivationListener.DIALOG_IGNORE_KEY, Boolean.TRUE);

		hostPane.addDisposeListener(e -> hostPane = null);

		setHostSize();

		// Set a special layout that allows resizing
		fixedSides = getFixedSides();
		hostPane.setLayout(new TrimPaneLayout(toolControl, fixedSides));

		return hostPane;
	}

	private int getFixedSides() {
		MUIElement tcParent = toolControl.getParent();
		if (!(tcParent instanceof MTrimBar)) {
			return 0;
		}
		MTrimBar bar = (MTrimBar) tcParent;
		Composite trimComp = (Composite) bar.getWidget();
		Rectangle trimBounds = trimComp.getBounds();
		Point trimCenter = new Point(trimBounds.width / 2, trimBounds.height / 2); // adjusted to
																					// (0,0)

		Control trimCtrl = (Control) toolControl.getWidget();
		Rectangle ctrlBounds = trimCtrl.getBounds();
		Point ctrlCenter = new Point(ctrlBounds.x + (ctrlBounds.width / 2), ctrlBounds.y
				+ (ctrlBounds.height / 2));

		if (bar.getSide() == SideValue.LEFT) {
			int verticalValue = ctrlCenter.y < trimCenter.y ? SWT.TOP : SWT.BOTTOM;
			return SWT.LEFT | verticalValue;
		} else if (bar.getSide() == SideValue.RIGHT) {
			int verticalValue = ctrlCenter.y < trimCenter.y ? SWT.TOP : SWT.BOTTOM;
			return SWT.RIGHT | verticalValue;
		} else if (bar.getSide() == SideValue.TOP) {
			int horizontalValue = ctrlCenter.x < trimCenter.x ? SWT.LEFT : SWT.RIGHT;
			return SWT.TOP | horizontalValue;
		} else if (bar.getSide() == SideValue.BOTTOM) {
			int horizontalValue = ctrlCenter.x < trimCenter.x ? SWT.LEFT : SWT.RIGHT;
			return SWT.BOTTOM | horizontalValue;
		}

		return SWT.BOTTOM | SWT.RIGHT;
	}

	private Image getLayoutImage() {
		if (layoutImage == null) {
			layoutImage = resUtils.imageDescriptorFromURI(URI.createURI(LAYOUT_ICON_URI))
					.createImage();
		}
		return layoutImage;
	}

	private Image getRestoreImage() {
		if (restoreImage == null) {
			restoreImage = resUtils.imageDescriptorFromURI(URI.createURI(RESTORE_ICON_URI))
					.createImage();
		}
		return restoreImage;
	}

	private boolean isFixed(int swtSide) {
		return (fixedSides & swtSide) != 0;
	}
}
