/*******************************************************************************
 * Copyright (c) 2011, 2020 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 331690
 *     Dirk Fauth (dirk.fauth@googlemail.com) - Bug 459285
 *     Eugen Neufeld (eneufeld@eclipsesource.com) - Bug 432466, Bug 455568
 *     Christoph Läubrich - Bug 365525
 *     Pierre-Yves B. (pyvesdev@gmail.com) - Bug 562747
 ******************************************************************************/

package org.eclipse.e4.ui.workbench.addons.minmax;

import static java.util.Collections.singletonList;

import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.eclipse.e4.core.contexts.IEclipseContext;
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.AbstractPartRenderer;
import org.eclipse.e4.ui.model.application.MAddon;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
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.MPartStack;
import org.eclipse.e4.ui.model.application.ui.basic.MTrimBar;
import org.eclipse.e4.ui.model.application.ui.basic.MTrimElement;
import org.eclipse.e4.ui.model.application.ui.basic.MTrimmedWindow;
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.model.application.ui.menu.impl.MenuFactoryImpl;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.UIEvents;
import org.eclipse.e4.ui.workbench.UIEvents.EventTags;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.e4.ui.workbench.modeling.EPartService;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabFolder2Adapter;
import org.eclipse.swt.custom.CTabFolderEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.osgi.service.event.Event;

/**
 * Workbench addon that provides methods to minimize, maximize and restore parts in the window
 */
public class MinMaxAddon {

	private static final String DISABLE_MINMAX_ADDON = "DisableMinMaxAddon";

	private static final String MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG = IPresentationEngine.MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG;

	/**
	 * The identifier for the shared area in the Eclipse Platform. This value should be identical to
	 * the value defined in org.eclipse.ui.IPageLayout.ID_EDITOR_AREA.
	 */
	private static final String ID_EDITOR_AREA = "org.eclipse.ui.editorss"; //$NON-NLS-1$

	private static final String GLOBAL_CACHE_ID = "Global"; //$NON-NLS-1$

	// tags representing the min/max state (h
	private static final String MINIMIZED = IPresentationEngine.MINIMIZED;
	private static final String MAXIMIZED = IPresentationEngine.MAXIMIZED;
	private static final String MINIMIZED_BY_ZOOM = IPresentationEngine.MINIMIZED_BY_ZOOM;

	@Inject
	IEventBroker eventBroker;

	@Inject
	EModelService modelService;

	@Inject
	private IEclipseContext context;

	// Allow 'local' changes to the tags
	private boolean ignoreTagChanges = false;

	@Inject
	MAddon minMaxAddon;

	@Inject
	MApplication app;

	private CTabFolder2Adapter CTFButtonListener = new CTabFolder2Adapter() {
		private MUIElement getElementToChange(CTabFolderEvent event) {
			CTabFolder ctf = (CTabFolder) event.widget;
			MUIElement element = (MUIElement) ctf.getData(AbstractPartRenderer.OWNING_ME);
			if (element instanceof MArea) {
				return element.getCurSharedRef();
			}

			MUIElement parentElement = element.getParent();
			while (parentElement != null && !(parentElement instanceof MArea)) {
				parentElement = parentElement.getParent();
			}

			if (parentElement!=null && MinMaxAddonUtil.isMinMaxChildrenAreaWithMultipleVisibleChildren(parentElement)) {
				return element;
			}

			return parentElement != null ? parentElement.getCurSharedRef() : element;
		}

		@Override
		public void maximize(CTabFolderEvent event) {
			setState(getElementToChange(event), MAXIMIZED);
		}

		@Override
		public void minimize(CTabFolderEvent event) {
			setState(getElementToChange(event), MINIMIZED);
		}

		@Override
		public void restore(CTabFolderEvent event) {
			setState(getElementToChange(event), null);
		}
	};

	private MouseListener CTFDblClickListener = new MouseAdapter() {
		@Override
		public void mouseDown(MouseEvent e) {
			// HACK! If this is an empty stack treat it as though it was the editor area
			// and tear down any open trim stacks (see bug 384814)
			CTabFolder ctf = (CTabFolder) e.widget;
			MUIElement element = (MUIElement) ctf.getData(AbstractPartRenderer.OWNING_ME);
			if (element instanceof MPartStack && ctf.getItemCount() == 0) {
				MWindow window = modelService.getTopLevelWindowFor(element);
				if (window != null) {
					List<MToolControl> tcList = modelService.findElements(window, null,
							MToolControl.class, null);
					for (MToolControl tc : tcList) {
						if (tc.getObject() instanceof TrimStack) {
							TrimStack ts = (TrimStack) tc.getObject();
							ts.showStack(false);
						}
					}
				}
			}
		}

		private MUIElement getElementToChange(MouseEvent event) {
			CTabFolder ctf = (CTabFolder) event.widget;
			MUIElement element = (MUIElement) ctf.getData(AbstractPartRenderer.OWNING_ME);
			if (element instanceof MArea) {
				// set the state on the placeholder
				return element.getCurSharedRef();
			}

			MUIElement parentElement = element.getParent();
			while (parentElement != null
					&& (!(parentElement instanceof MArea) || parentElement.getCurSharedRef() == null)) {
				parentElement = parentElement.getParent();
			}

			if (parentElement != null
					&& MinMaxAddonUtil.isMinMaxChildrenAreaWithMultipleVisibleChildren(parentElement)) {
				return element;
			}

			return parentElement != null ? parentElement.getCurSharedRef() : element;
		}

		@Override
		public void mouseDoubleClick(MouseEvent e) {
			// only maximize if the primary mouse button was used
			if (e.button == 1) {

				MUIElement elementToChange = getElementToChange(e);
				CTabFolder ctf = (CTabFolder) e.widget;

				// Only fire if we're in the 'tab' area
				if (e.y > ctf.getTabHeight()) {
					return;
				}

				if (!elementToChange.getTags().contains(MAXIMIZED)) {
					setState(elementToChange, MAXIMIZED);
				} else {
					setState(elementToChange, null);
				}
			}
		}
	};

	private void setState(MUIElement element, String state) {
		if (element == null) {
			return;
		}
		if (MINIMIZED.equals(state)) {
			element.getTags().remove(MAXIMIZED);
			element.getTags().add(MINIMIZED);
		} else if (MAXIMIZED.equals(state)) {
			element.getTags().remove(MINIMIZED);
			element.getTags().add(MAXIMIZED);
		} else {
			element.getTags().remove(MINIMIZED);
			element.getTags().remove(MAXIMIZED);
		}

	}

	@Inject
	@Optional
	private void subscribeTopicWidget(@UIEventTopic(UIEvents.UIElement.TOPIC_WIDGET) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		final MUIElement changedElement = (MUIElement) event.getProperty(EventTags.ELEMENT);
		if (!(changedElement instanceof MPartStack) && !(changedElement instanceof MArea)) {
			return;
		}

		final CTabFolder ctf = getCTFFor(changedElement);
		if (ctf == null) {
			return;
		}

		MUIElement stateElement = changedElement;
		if (changedElement instanceof MPartStack) {
			MPartStack stack = (MPartStack) changedElement;
			MArea area = MinMaxAddonUtil.getAreaFor(stack);
			if (area != null && !(area.getWidget() instanceof CTabFolder)) {
				stateElement = area.getCurSharedRef();
			}
		} else if (changedElement instanceof MArea) {
			stateElement = changedElement.getCurSharedRef();
		}

		adjustCTFButtons(stateElement);

		ctf.removeCTabFolder2Listener(CTFButtonListener); // Prevent multiple instances
		ctf.addCTabFolder2Listener(CTFButtonListener);

		ctf.removeMouseListener(CTFDblClickListener); // Prevent multiple instances
		ctf.addMouseListener(CTFDblClickListener);
	}

	/**
	 * Handles removals from the perspective
	 *
	 * @param event
	 */

	@Inject
	@Optional
	private void subscribeTopicChildren(
			@UIEventTopic(UIEvents.ElementContainer.TOPIC_CHILDREN) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		final MUIElement changedElement = (MUIElement) event.getProperty(EventTags.ELEMENT);
		MWindow window = modelService.getTopLevelWindowFor(changedElement);

		// this method is intended to update the minimized stacks in a trim
		// if the removed element is no perspective and the top level window
		// is not a trimmed window, we don't need to do anything here
		if (!(changedElement instanceof MPerspectiveStack) || window == null
				|| !(window instanceof MTrimmedWindow)) {
			return;
		}

		if (UIEvents.isREMOVE(event)) {
			for (Object removedElement : UIEvents.asIterable(event, UIEvents.EventTags.OLD_VALUE)) {
				MUIElement removed = (MUIElement) removedElement;
				String perspectiveId = removed.getElementId();

				MTrimBar bar = modelService.getTrim((MTrimmedWindow) window, SideValue.TOP);

				// gather up any minimized stacks for this perspective...
				List<MToolControl> toRemove = new ArrayList<>();
				for (MTrimElement child : bar.getChildren()) {
					String trimElementId = child.getElementId();
					if (child instanceof MToolControl && trimElementId.contains(perspectiveId)) {
						toRemove.add((MToolControl) child);
					}
				}

				// ...and remove them
				for (MToolControl minStack : toRemove) {
					minStack.setToBeRendered(false);
					bar.getChildren().remove(minStack);
				}
			}
		}
	}

	/**
	 * Handles changes of the perspective
	 *
	 * @param event
	 */

	@Inject
	@Optional
	private void subscribeTopicSelectedElement(
			@UIEventTopic(UIEvents.ElementContainer.TOPIC_SELECTEDELEMENT) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

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

		MPerspectiveStack ps = (MPerspectiveStack) changedElement;
		MWindow window = modelService.getTopLevelWindowFor(ps);
		Object widget = window.getWidget();
		if (!(widget instanceof Shell)) {
			return;
		}
		final Shell winShell = (Shell) widget;
		List<MToolControl> tcList = modelService.findElements(window, null, MToolControl.class);

		final MPerspective curPersp = ps.getSelectedElement();
		if (curPersp != null) {
			List<MUIElement> minimizedElements = modelService.findElements(curPersp, null, MUIElement.class,
					singletonList(IPresentationEngine.MINIMIZED));
			// Show any minimized stack from the current perspective
			for (MUIElement ele : minimizedElements) {
				String fullId = TrimStackIdHelper.createTrimStackId(ele, curPersp, window);

				for (MToolControl tc : tcList) {
					if (fullId.equals(tc.getElementId())) {
						tc.setToBeRendered(true);
					}
				}
			}

			// Find the editor 'area'
			MUIElement eaPlaceholder = modelService.find(ID_EDITOR_AREA, curPersp);
			adjustCTFButtons(eaPlaceholder);
		}

		// Hide any minimized stacks from the old perspective
		if (event.getProperty(EventTags.OLD_VALUE) instanceof MPerspective) {
			MPerspective oldPersp = (MPerspective) event.getProperty(EventTags.OLD_VALUE);
			String perspId = '(' + oldPersp.getElementId() + ')';
			for (MToolControl tc : tcList) {
				if (tc.getObject() instanceof TrimStack && tc.getElementId().contains(perspId)) {
					TrimStack ts = (TrimStack) tc.getObject();
					ts.showStack(false);
					tc.setToBeRendered(false);
				}
			}
		}

		winShell.getDisplay().asyncExec(() -> {
			if (!winShell.isDisposed()) {
				winShell.layout(true, true);
			}
		});
	}

	/**
	 * Handles changes in tags
	 *
	 * @param event
	 */

	@Inject
	@Optional
	private void subscribeTopicTagsChanged(
			@UIEventTopic(UIEvents.ApplicationElement.TOPIC_TAGS) Event event) {
		if (ignoreTagChanges) {
			return;
		}

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

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

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

		final MUIElement changedElement = (MUIElement) changedObj;

		if (UIEvents.isADD(event)) {
			if (UIEvents.contains(event, UIEvents.EventTags.NEW_VALUE, MINIMIZED)) {
				minimize(changedElement);
			} else if (UIEvents.contains(event, UIEvents.EventTags.NEW_VALUE, MAXIMIZED)) {
				maximize(changedElement);
			}
		} else if (UIEvents.isREMOVE(event)) {
			if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE, MINIMIZED)) {
				restore(changedElement);
			} else if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE, MAXIMIZED)) {
				unzoom(changedElement);
			}
		}
	}

	/**
	 * Handles changes in the id of the element If a perspective ID changes fix any TrimStacks that
	 * reference the old id to point at the new id.
	 *
	 * This keeps trim stacks attached to the correct perspective when a perspective is saved with a
	 * new name.
	 *
	 * @param event
	 */

	@Inject
	@Optional
	private void subscribeTopicElementId(
			@UIEventTopic(UIEvents.ApplicationElement.TOPIC_ELEMENTID) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		Object changedObject = event.getProperty(EventTags.ELEMENT);

		// Only care about MPerspective id changes
		if (!(changedObject instanceof MPerspective)) {
			return;
		}

		MPerspective perspective = (MPerspective) changedObject;

		String newID = (String) event.getProperty(UIEvents.EventTags.NEW_VALUE);
		String oldID = (String) event.getProperty(UIEvents.EventTags.OLD_VALUE);

		// pattern is trimStackID(perspectiveID)
		newID = '(' + newID + ')';
		oldID = '(' + oldID + ')';

		// Search the trim for the window containing the perspective
		MWindow perspWin = modelService.getTopLevelWindowFor(perspective);
		if (perspWin == null) {
			return;
		}

		List<MToolControl> trimStacks = modelService.findElements(perspWin, null, MToolControl.class);
		for (MToolControl trimStack : trimStacks) {
			// Only care about MToolControls that are TrimStacks
			if (TrimStack.CONTRIBUTION_URI.equals(trimStack.getContributionURI())) {
				trimStack.setElementId(trimStack.getElementId().replace(oldID, newID));
			}
		}
	}

	/**
	 * Handles the event that the perspective is saved
	 *
	 * @param event
	 */

	@Inject
	@Optional
	private void subscribeTopicPerspSaved(
			@UIEventTopic(UIEvents.UILifeCycle.PERSPECTIVE_SAVED) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		final MPerspective savedPersp = (MPerspective) event.getProperty(EventTags.ELEMENT);
		String cache = getTrimCache(savedPersp);
		minMaxAddon.getPersistedState().put(savedPersp.getElementId(), cache);
	}

	private String getTrimCache(MPerspective savedPersp) {
		MWindow topWin = modelService.getTopLevelWindowFor(savedPersp);
		StringBuilder cache = new StringBuilder(getWinCache(topWin, savedPersp));
		for (MWindow dw : savedPersp.getWindows()) {
			cache.append(getWinCache(dw, savedPersp));
		}
		return cache.toString();
	}

	private String getWinCache(MWindow win, MPerspective perspective) {
		StringBuilder winStrBuilder = new StringBuilder();
		List<MPartStack> stackList = modelService.findElements(win, null, MPartStack.class);
		for (MPartStack stack : stackList) {
			winStrBuilder.append(getStackTrimLoc(stack, perspective));
		}
		return winStrBuilder.toString();
	}

	private String getStackTrimLoc(MPartStack stack, MPerspective perspective) {
		MWindow stackWin = modelService.getTopLevelWindowFor(stack);
		MUIElement tcElement = modelService.find(TrimStackIdHelper.createTrimStackId(stack, perspective, stackWin),
				stackWin);
		if (tcElement == null)
		 {
			return ""; //$NON-NLS-1$
		}

		MTrimBar bar = (MTrimBar) ((MUIElement) tcElement.getParent());
		int sideVal = bar.getSide().getValue();
		int index = bar.getChildren().indexOf(tcElement);
		return stack.getElementId() + ' ' + sideVal + ' ' + index + "#"; //$NON-NLS-1$
	}

	/**
	 * Handles the event that the perspective is reset
	 *
	 * @param event
	 */
	@Inject
	@Optional
	private void subscribeTopicPerspReset(
			@UIEventTopic(UIEvents.UILifeCycle.PERSPECTIVE_RESET) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		final MPerspective resetPersp = (MPerspective) event.getProperty(EventTags.ELEMENT);

		// Find any minimized stacks and show their trim
		List<MUIElement> minimizedElements = modelService.findElements(resetPersp, null,
				MUIElement.class, singletonList(IPresentationEngine.MINIMIZED));
		for (MUIElement element : minimizedElements) {
			createTrim(element);
		}
	}

	/**
	 * Handles the event that the perspective is opened
	 *
	 * @param event
	 */
	@Inject
	@Optional
	private void subscribeTopicPerspOpened(
			@UIEventTopic(UIEvents.UILifeCycle.PERSPECTIVE_OPENED) Event event) {

		if (app.getTags().contains(DISABLE_MINMAX_ADDON)) {
			return;
		}

		final MPerspective openedPersp = (MPerspective) event.getProperty(EventTags.ELEMENT);

		// Find any minimized stacks and show their trim
		List<MUIElement> minimizedElements = modelService.findElements(openedPersp, null,
				MUIElement.class, singletonList(IPresentationEngine.MINIMIZED));
		for (MUIElement element : minimizedElements) {
			createTrim(element);
		}
	}



	private void setCTFButtons(CTabFolder ctf, MUIElement stateElement, boolean hideButtons) {
		if (hideButtons) {
			ctf.setMinimizeVisible(false);
			ctf.setMaximizeVisible(false);
		} else if (stateElement.getTags().contains(MINIMIZED)) {
			ctf.setMinimizeVisible(false);
			ctf.setMaximizeVisible(true);
			ctf.setMaximized(true);
		} else if (stateElement.getTags().contains(MAXIMIZED)) {
			ctf.setMinimizeVisible(true);
			ctf.setMaximizeVisible(true);
			ctf.setMaximized(true);
		} else {
			ctf.setMinimizeVisible(true);
			ctf.setMaximizeVisible(true);
			ctf.setMinimized(false);
			ctf.setMaximized(false);
			ctf.requestLayout();
		}
	}

	/**
	 * Set the state of the min / max buttons on the CTF based on the model element's state. The
	 * input is expected to be the element that contains the min/max state info which should either
	 * be an MPartStack or an MPlaceholder for the shared area.
	 *
	 * @param element
	 *            The element to test
	 */
	void adjustCTFButtons(MUIElement element) {
		if (!(element instanceof MPartStack) && !(element instanceof MPlaceholder)) {
			return;
		}

		CTabFolder ctf = getCTFFor(element);
		if (ctf == null) {
			return;
		}

		if (element instanceof MPlaceholder) {
			setCTFButtons(ctf, element, false);
		} else {
			MArea area = MinMaxAddonUtil.getAreaFor((MPartStack) element);
			if (area == null) {
				setCTFButtons(ctf, element, false);
			}
 else if (area.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				setCTFButtons(ctf, element, false);
			}
		}
	}

	private CTabFolder getCTFFor(MUIElement element) {
		if (element instanceof MArea) {
			if (element.getWidget() instanceof CTabFolder) {
				return (CTabFolder) element.getWidget();
			}
			List<MPartStack> stacks = modelService.findElements(element, null, MPartStack.class);
			for (MPartStack stack : stacks) {
				if (stack.getWidget() instanceof CTabFolder) {
					return (CTabFolder) stack.getWidget();
				}
			}
		} else if (element.getWidget() instanceof CTabFolder) {
			return (CTabFolder) element.getWidget();
		} else if (element instanceof MPlaceholder) {
			MPlaceholder ph = (MPlaceholder) element;
			if (ph.getRef() instanceof MArea) {
				return getCTFFor(ph.getRef());
			}
		}
		return null;
	}

	boolean isEmptyPerspectiveStack(MUIElement element) {
		if (!(element instanceof MPerspectiveStack)) {
			return false;
		}
		MPerspectiveStack ps = (MPerspectiveStack) element;
		return ps.getChildren().isEmpty();
	}

	void minimize(MUIElement element) {
		// Can't minimize a non-rendered element
		if (!element.isToBeRendered()) {
			return;
		}

		if (isEmptyPerspectiveStack(element)) {
			element.setVisible(false);
			return;
		}

		createTrim(element);
		element.setVisible(false);
		adjustCTFButtons(element);
		// Activate a part other than the trimStack so that if the tool item is pressed
		// immediately it will still open the stack.
		MWindow win = MinMaxAddonUtil.getWindowFor(element);
		EPartService partService = win.getContext().get(EPartService.class);
		partService.requestActivation();
	}

	void restore(MUIElement element) {
		MWindow window = modelService.getTopLevelWindowFor(element);
		String trimId = getTrimId(element, MinMaxAddonUtil.getWindowFor(element));
		MToolControl trimStack = (MToolControl) modelService.find(trimId, window);
		if (trimStack == null || trimStack.getObject() == null) {
			// try legacy id
			trimId = TrimStackIdHelper.createTrimStackId(element, modelService.getPerspectiveFor(element), null);
			trimStack = (MToolControl) modelService.find(trimId, window);
			if (trimStack == null || trimStack.getObject() == null) {
				if (element instanceof MPerspectiveStack) {
					element.setVisible(true);
				}
				return;
			}
		}

		TrimStack ts = (TrimStack) trimStack.getObject();
		ts.restoreStack();

		adjustCTFButtons(element);

		List<String> maximizeTag = singletonList(IPresentationEngine.MAXIMIZED);
		List<MUIElement> curMax = modelService.findElements(window, null, MUIElement.class,
				maximizeTag, EModelService.PRESENTATION);

		MinMaxAddonUtil.ignoreChildrenOfMinMaxChildrenArea(modelService, element, curMax);

		if (curMax.size() > 0) {
			MUIElement maxElement = curMax.get(0);
			List<MUIElement> elementsLeftToRestore = getElementsToRestore(maxElement);

			// Are any stacks still minimized ?
			boolean unMax = true;
			for (MUIElement toRestore : elementsLeftToRestore) {
				if (!toRestore.isVisible()) {
					unMax = false;
				}
			}
			if (unMax) {
				maxElement.getTags().remove(IPresentationEngine.MAXIMIZED);
			}
		}
		MinMaxAddonUtil.restoreStacksOfMinMaxChildrenArea(this, element, maximizeTag);
	}

	void executeWithIgnoredTagChanges(Runnable runnable) {
		ignoreTagChanges = true;
		try {
			runnable.run();
		} finally {
			ignoreTagChanges = false;
		}
	}

	void maximize(final MUIElement element) {
		if (!element.isToBeRendered()) {
			return;
		}

		List<MUIElement> elementsToMinimize = getElementsToMinimize(element);
		MWindow win = MinMaxAddonUtil.getWindowFor(element);

		// Restore any currently maximized element
		restoreMaximizedElement(element, win);

		for (MUIElement toMinimize : elementsToMinimize) {
			toMinimize.getTags().add(MINIMIZED);
			toMinimize.getTags().add(MINIMIZED_BY_ZOOM);
		}

		adjustCTFButtons(element);

		MinMaxAddonUtil.maximizeMinMaxChildrenArea(this, element);
	}

	/**
	 * @param element
	 * @return The list of elements that need to be minimized during a maximize
	 */
	private List<MUIElement> getElementsToMinimize(MUIElement element) {
		MWindow win = MinMaxAddonUtil.getWindowFor(element);
		MPerspective persp;
		if (win == null) {
			persp = null;
		} else {
			persp = modelService.getActivePerspective(win);
		}

		List<MUIElement> elementsToMinimize = new ArrayList<>();
		int loc = modelService.getElementLocation(element);
		if ((loc & EModelService.OUTSIDE_PERSPECTIVE) != 0) {
			// Minimize all other global stacks
			List<MPartStack> partStacksToMinimize = findValidElementsToMinimize(element, win, win,
					null, MPartStack.class, EModelService.OUTSIDE_PERSPECTIVE, false);
			elementsToMinimize.addAll(partStacksToMinimize);

			// Minimize the Perspective Stack
			MUIElement perspStack = null;
			if (persp == null) {
				// special case for windows with no perspectives (eg bug 372614:
				// intro part with no perspectives). We know we're outside
				// of the perspective stack, so find it top-down
				List<MPerspectiveStack> pStacks = modelService.findElements(win, null, MPerspectiveStack.class);
				perspStack = (pStacks.size() > 0) ? pStacks.get(0) : null;
			} else {
				perspStack = persp.getParent();
			}
			if (perspStack != null) {
				if (perspStack.getElementId() == null || perspStack.getElementId().length() == 0)
				{
					perspStack.setElementId("PerspectiveStack"); //$NON-NLS-1$
				}

				elementsToMinimize.add(perspStack);
			}

			// Find all editor 'area' outside the perspective
			List<MPlaceholder> placeholderToMinimize = findValidElementsToMinimize(element, win,
					win, ID_EDITOR_AREA, MPlaceholder.class, EModelService.OUTSIDE_PERSPECTIVE,
					true);
			elementsToMinimize.addAll(placeholderToMinimize);

		} else {
			List<MPartStack> partStacksToMinimize = findValidElementsToMinimize(element, win,
					persp == null ? win : persp, null, MPartStack.class,
					EModelService.PRESENTATION, false);
			elementsToMinimize.addAll(partStacksToMinimize);
			// Find any 'standalone' views *not* in a stack
			List<String> standaloneTag = new ArrayList<>();
			standaloneTag.add(IPresentationEngine.STANDALONE);
			List<MPlaceholder> standaloneViews = modelService.findElements(persp == null ? win
					: persp, null, MPlaceholder.class, standaloneTag, EModelService.PRESENTATION);
			for (MPlaceholder part : standaloneViews) {
				if (!part.isToBeRendered()) {
					continue;
				}
				elementsToMinimize.add(part);
			}

			// Find the editor 'area'
			List<MPlaceholder> placeholderToMinimize = findValidElementsToMinimize(element, win,
					win, ID_EDITOR_AREA, MPlaceholder.class, EModelService.PRESENTATION, true);
			elementsToMinimize.addAll(placeholderToMinimize);
		}

		MinMaxAddonUtil.handleMinimizeOfMinMaxChildrenArea(modelService, element, win, persp, elementsToMinimize);

		return elementsToMinimize;
	}

	/**
	 * Find all elements based on
	 * {@link EModelService#findElements(MUIElement, String, Class, List, int)} and filter them for
	 * correct window and visibility.
	 *
	 * First all possible elements based on the parameters of EModelService#findElements(MUIElement,
	 * String, Class, List, int)} are retrieved. Then they are checked to be in the correct window.
	 * Then a check for the correct location is made and in the end the elements are checked to be
	 * valid (visible, not yet minimized and have a widget).
	 *
	 * @param elementToMaximize
	 *            the {@link MUIElement} being maximized
	 * @param currentWindow
	 *            the window of the elementToMaximize
	 * @param searchRoot
	 *            the searchRoot for possible elements
	 * @param id
	 *            the id of the element to search
	 * @param clazz
	 *            the Class of the elements to find
	 * @param searchFlag
	 *            the search flags as defined in {@link EModelService}
	 * @param allowSharedArea
	 *            whether the found element is allowed to be in a shared area
	 * @return the list of elements which should be minimized
	 */
	private <T extends MUIElement> List<T> findValidElementsToMinimize(
			MUIElement elementToMaximize, MWindow currentWindow, MUIElement searchRoot, String id,
			Class<T> clazz, int searchFlag, boolean allowSharedArea) {
		List<T> elementsToMinimize = new ArrayList<>();
		List<T> elements = modelService.findElements(searchRoot, id, clazz, null, searchFlag);
		for (T element : elements) {
			if (element == elementToMaximize || !element.isToBeRendered()) {
				continue;
			}

			// Exclude stacks in DW's
			if (MinMaxAddonUtil.getWindowFor(element) != currentWindow) {
				continue;
			}

			int loc = modelService.getElementLocation(element);
			boolean inSharedArea = loc == EModelService.IN_SHARED_AREA;
			boolean validLocation = allowSharedArea || !inSharedArea;
			if (validLocation && element.getWidget() != null && element.isVisible()
					&& !element.getTags().contains(MINIMIZED)) {
				elementsToMinimize.add(element);
			}
		}
		return elementsToMinimize;
	}

	/**
	 * Restore any currently maximized element (except the one we're in the process of maximizing
	 *
	 * @param element
	 * @param win
	 */
	private void restoreMaximizedElement(final MUIElement element, MWindow win) {
		MPerspective elePersp = modelService.getPerspectiveFor(element);
		if (elePersp == null) {
			return;
		}
		List<MUIElement> curMax = modelService.findElements(win, null, MUIElement.class, singletonList(MAXIMIZED));
		if (curMax.size() > 0) {
			for (MUIElement maxElement : curMax) {
				// Only unmax elements in this window
				if (MinMaxAddonUtil.getWindowFor(maxElement) != win) {
					continue;
				}

				MPerspective maxPersp = modelService.getPerspectiveFor(maxElement);
				if (maxPersp != elePersp) {
					continue;
				}
				if (maxElement == element) {
					continue;
				}
				if (MinMaxAddonUtil.isPartOfMinMaxChildrenArea(maxElement)) {
					continue;
				}
				ignoreTagChanges = true;
				try {
					maxElement.getTags().remove(MAXIMIZED);
				} finally {
					ignoreTagChanges = false;
				}
			}
		}
	}



	void unzoom(final MUIElement element) {
		MWindow win = MinMaxAddonUtil.getWindowFor(element);

		List<MUIElement> elementsToRestore = getElementsToRestore(element);
		for (MUIElement toRestore : elementsToRestore) {
			toRestore.getTags().remove(IPresentationEngine.MINIMIZED_BY_ZOOM);
			toRestore.getTags().remove(IPresentationEngine.MINIMIZED);
		}

		adjustCTFButtons(element);

		MinMaxAddonUtil.unzoomStackOfMinMaxChildrenArea(this, element);

		// There are more views available to be active...
		EPartService partService = win.getContext().get(EPartService.class);
		partService.requestActivation();
	}


	/**
	 * @param element
	 * @return The list of elements that need to be restored by an unzoom
	 */
	private List<MUIElement> getElementsToRestore(MUIElement element) {
		MWindow win = MinMaxAddonUtil.getWindowFor(element);
		MPerspective persp = modelService.getActivePerspective(win);

		List<MUIElement> elementsToRestore = new ArrayList<>();

		List<String> minTag = new ArrayList<>();
		minTag.add(IPresentationEngine.MINIMIZED_BY_ZOOM);

		// Restore any minimized stacks
		boolean outsidePerspectives = (modelService.getElementLocation(element) & EModelService.OUTSIDE_PERSPECTIVE) != 0;
		List<MPartStack> stacks = modelService.findElements(win, null, MPartStack.class, minTag,
				EModelService.PRESENTATION);
		for (MPartStack theStack : stacks) {
			if (theStack.getWidget() != null) {
				// Make sure we don't restore perspective-based stacks if we're
				// unzoooming an element outside the perspectives
				if (outsidePerspectives) {
					int stackLoc = modelService.getElementLocation(theStack);
					if ((stackLoc & EModelService.OUTSIDE_PERSPECTIVE) == 0) {
						continue;
					}
				}

				// Make sure we're only working on *our* window
				if (MinMaxAddonUtil.getWindowFor(theStack) == win) {
					elementsToRestore.add(theStack);
				}
			}
		}

		// Restore any minimized standalone views
		List<MPlaceholder> views = modelService.findElements(win, null, MPlaceholder.class, minTag,
				EModelService.PRESENTATION);
		for (MPlaceholder ph : views) {
			if (ph.getWidget() != null && MinMaxAddonUtil.getWindowFor(ph) == win) {
				elementsToRestore.add(ph);
			}
		}

		// Find the editor 'area'
		MPlaceholder eaPlaceholder = (MPlaceholder) modelService.find(ID_EDITOR_AREA,
				persp == null ? win : persp);
		if (element != eaPlaceholder && eaPlaceholder != null
				&& eaPlaceholder.getTags().contains(MINIMIZED_BY_ZOOM)) {
			elementsToRestore.add(eaPlaceholder);
		}

		// Find the Perspective Stack
		int loc = modelService.getElementLocation(element);
		if ((loc & EModelService.OUTSIDE_PERSPECTIVE) != 0) {
			List<MPerspectiveStack> psList = modelService.findElements(win, null, MPerspectiveStack.class);
			if (psList.size() == 1) {
				MPerspectiveStack perspStack = psList.get(0);
				if (element != perspStack && perspStack != null
						&& perspStack.getTags().contains(MINIMIZED_BY_ZOOM)) {
					elementsToRestore.add(perspStack);
				}
			}
		}

		MinMaxAddonUtil.addChildrenOfMinMaxChildrenAreaToRestoreList(modelService, element, win, persp, elementsToRestore);

		return elementsToRestore;
	}

	private void createTrim(MUIElement element) {
		MWindow win = MinMaxAddonUtil.getWindowFor(element);
		if (!(win instanceof MTrimmedWindow)) {
			return;
		}

		MTrimmedWindow window = (MTrimmedWindow) win;
		Shell winShell = (Shell) window.getWidget();

		// Is there already a TrimControl there ?
		String trimId = getTrimId(element, window);
		MToolControl trimStack = (MToolControl) modelService.find(trimId, window);

		if (trimStack == null) {
			trimStack = MenuFactoryImpl.eINSTANCE.createToolControl();
			trimStack.setElementId(trimId);
			trimStack.setContributionURI(TrimStack.CONTRIBUTION_URI);
			trimStack.getTags().add("TrimStack"); //$NON-NLS-1$

			// Check if we have a cached location
			MTrimBar bar = getBarForElement(element, window);
			int index = getCachedIndex(element);
			if (index == -1 || index >= bar.getChildren().size()) {
				bar.getChildren().add(trimStack);
			} else {
				bar.getChildren().add(index, trimStack);
			}

			bar.setVisible(true);

			// get the parent trim bar, see bug 320756
			if (bar.getWidget() == null) {
				// ask it to be rendered
				bar.setToBeRendered(true);

				// create the widget
				context.get(IPresentationEngine.class)
						.createGui(bar, winShell, window.getContext());
			}
		} else {
			// get the parent trim bar, see bug 320756
			MUIElement parent = trimStack.getParent();
			parent.setVisible(true);
			if (parent.getWidget() == null) {
				// ask it to be rendered
				parent.setToBeRendered(true);
				// create the widget
				context.get(IPresentationEngine.class).createGui(parent, winShell,
						window.getContext());
			}
			trimStack.setToBeRendered(true);
		}
	}

	private String getTrimId(MUIElement element, MWindow window) {
		String trimId;
		if (MinMaxAddonUtil.isPartOfMinMaxChildrenArea(element)) {
			trimId = TrimStackIdHelper.createTrimStackId(element, null, window);
		} else {
			trimId = TrimStackIdHelper.createTrimStackId(element, modelService.getPerspectiveFor(element), window);
		}
		return trimId;
	}



	private String getCachedInfo(MUIElement element) {
		String cacheId = GLOBAL_CACHE_ID;
		MPerspective persp = modelService.getPerspectiveFor(element);
		if (persp != null) {
			cacheId = persp.getElementId();
		}
		return minMaxAddon.getPersistedState().get(cacheId);
	}

	private int getCachedIndex(MUIElement element) {
		String cache = getCachedInfo(element);
		if (cache == null) {
			return -1;
		}

		String[] stacks = cache.split("#"); //$NON-NLS-1$
		for (String stackInfo : stacks) {
			String[] vals = stackInfo.split(" "); //$NON-NLS-1$
			if (vals[0].equals(element.getElementId())) {
				return Integer.parseInt(vals[2]);
			}
		}
		return -1;
	}

	private SideValue getCachedBar(MUIElement element) {
		String cache = getCachedInfo(element);
		if (cache == null) {
			return null;
		}

		String[] stacks = cache.split("#"); //$NON-NLS-1$
		for (String stackInfo : stacks) {
			String[] vals = stackInfo.split(" "); //$NON-NLS-1$
			if (vals[0].equals(element.getElementId())) {
				int sideVal = Integer.parseInt(vals[1]);
				return SideValue.get(sideVal);
			}
		}
		return null;
	}

	private MTrimBar getBarForElement(MUIElement element, MTrimmedWindow window) {
		SideValue side = getCachedBar(element);
		if (side == null) {
			Shell winShell = (Shell) window.getWidget();
			Rectangle winBounds = winShell.getBounds();
			int winCenterX = winBounds.width / 2;
			Control stackCtrl = (Control) element.getWidget();
			Rectangle stackBounds = stackCtrl.getBounds();
			stackBounds = winShell.getDisplay().map(stackCtrl, winShell, stackBounds);
			int stackCenterX = stackBounds.x + (stackBounds.width / 2);
			side = stackCenterX < winCenterX ? SideValue.LEFT : SideValue.RIGHT;
		}
		return modelService.getTrim(window, side);
	}
}