/*******************************************************************************
 * Copyright (c) 2016, 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:
 *     Eugen Neufeld (eneufeld@eclipsesource.com) - initial API and implementation
 ******************************************************************************/

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

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
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.MPlaceholder;
import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainer;
import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainerElement;
import org.eclipse.e4.ui.model.application.ui.basic.MPartStack;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.modeling.EModelService;

/**
 * An utility class which provides methods to handle the cases for the special
 * area.
 */
// MinMaximizeableChildrenArea
public class MinMaxAddonUtil {

	private static final String ID_EDITOR_AREA = "org.eclipse.ui.editorss"; //$NON-NLS-1$
	private static String MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG = IPresentationEngine.MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG;

	/**
	 * Check whether the passed {@link MUIElement} is an MArea with a
	 * MinMaximizeableChildrenArea tag and multiple children that are visible
	 * and not minimized.
	 *
	 * @param element
	 *            The element to check.
	 * @return true if the element is a MArea with an
	 *         MinMaximizeableChildrenArea tag and multiple visible not
	 *         minimized children, false otherwise
	 */
	public static boolean isMinMaxChildrenAreaWithMultipleVisibleChildren(MUIElement element) {
		if (!element.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
			return false;
		}
		if (!(element instanceof MArea)) {
			return false;
		}
		MArea area = (MArea) element;
		if (area.getChildren().isEmpty()) {
			return false;
		}
		if (!hasMoreThenOneVisibleRenderableChild(area.getChildren().get(0))) {
			return false;
		}
		return true;
	}

	private static boolean hasMoreThenOneVisibleRenderableChild(MPartSashContainerElement elementToCheck) {
		if (elementToCheck instanceof MPartSashContainer) {
			int partsToRender = 0;
			for (MPartSashContainerElement part : ((MPartSashContainer) elementToCheck).getChildren()) {

				boolean hasMinimizeableChild = hasMoreThenOneVisibleRenderableChild(part);

				if (hasMinimizeableChild) {
					return true;
				}
				if (isVisible(part)) {
					partsToRender++;
				}
			}
			if (partsToRender > 1) {
				return true;
			}
		}
		return false;
	}

	private static boolean isVisible(MUIElement part) {
		boolean visible = part.isToBeRendered() && part.isVisible();
		if (part instanceof MElementContainer && visible) {
			visible = false;
			for (Object element : ((MElementContainer<?>) part).getChildren()) {
				MUIElement innerElement = (MUIElement) element;
				visible |= isVisible(innerElement);
			}
		}
		return visible;
	}

	/**
	 * Remove all partstacks that are inside a special area if the element that
	 * is maximized is a special area.
	 *
	 * @param modelService
	 *            The {@link EModelService} to use
	 * @param element
	 *            The {@link MUIElement} getting maximized
	 * @param curMax
	 *            The list of elements to restore
	 */
	// TODO needed, doesn't this exclude itself with getElementsToRestore?
	public static void ignoreChildrenOfMinMaxChildrenArea(EModelService modelService, MUIElement element,
			List<MUIElement> curMax) {
		if (element instanceof MPlaceholder
				&& ((MPlaceholder) element).getRef().getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
			Set<MUIElement> toRemove = new LinkedHashSet<>();
			for (MUIElement maxElement : curMax) {
				if (modelService.find(maxElement.getElementId(), element) != null) {
					toRemove.add(maxElement);
				}
			}
			curMax.removeAll(toRemove);
		}
	}

	/**
	 * Check for {@link MPartStack MPartStacks} that are inside a special area
	 * and exclude them from being restored as they have to be handled
	 * specially.
	 *
	 * @param modelService
	 *            The {@link EModelService} to use
	 * @param element
	 *            The {@link MUIElement} being restored
	 * @param win
	 *            The {@link MWindow} the {@link MUIElement} is part of
	 * @param persp
	 *            The {@link MPerspective} the {@link MUIElement} is part of
	 * @param elementsToRestore
	 *            The list of elements to restore
	 */
	public static void addChildrenOfMinMaxChildrenAreaToRestoreList(EModelService modelService, MUIElement element,
			MWindow win, MPerspective persp, List<MUIElement> elementsToRestore) {
		List<MPlaceholder> areas = modelService.findElements(persp == null ? win : persp, ID_EDITOR_AREA,
				MPlaceholder.class, null, EModelService.PRESENTATION);

		for (MPlaceholder placeholder : areas) {
			if (placeholder == element) {
				continue;
			}
			if (win != getWindowFor(placeholder)) {
				continue;
			}
			if (!placeholder.getRef().getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				continue;
			}
			List<MPartStack> partStacks = modelService.findElements(placeholder, null, MPartStack.class);
			if (partStacks.contains(element)) {
				continue;
			}
			for (MPartStack partStack : partStacks) {
				elementsToRestore.remove(partStack);
			}
		}
	}

	/**
	 * Restore the inner stack of an area and adjust the buttons of the restored
	 * stack.
	 *
	 * @param minMaxAddon
	 *            The MinMaxAddon calling this
	 * @param element
	 *            The MUIElement being restored.
	 * @param maximizeTag
	 *            The List of tags to search for
	 */
	public static void restoreStacksOfMinMaxChildrenArea(final MinMaxAddon minMaxAddon, MUIElement element,
			List<String> maximizeTag) {
		if (element instanceof MPartStack) {
			MArea area = getAreaFor((MPartStack) element);
			if (area != null && area.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				final List<MPartStack> maximizedAreaChildren = minMaxAddon.modelService.findElements(area, null,
						MPartStack.class,
						maximizeTag);
				minMaxAddon.executeWithIgnoredTagChanges(() -> {
					for (MPartStack partStack : maximizedAreaChildren) {
						partStack.getTags().remove(IPresentationEngine.MAXIMIZED);
						minMaxAddon.adjustCTFButtons(partStack);
					}
				});
			}
		}
	}

	/**
	 * Maximize the area if the inner part stack is maximized and adjust the
	 * buttons of the maximized area.
	 *
	 * @param minMaxAddon
	 *            The MinMaxAddon calling this
	 * @param element
	 *            The MUIElement being restored.
	 */
	public static void maximizeMinMaxChildrenArea(final MinMaxAddon minMaxAddon, final MUIElement element) {
		if (element instanceof MPartStack) {
			MArea area = getAreaFor((MPartStack) element);
			if (area != null && area.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				final MPlaceholder placeholder = area.getCurSharedRef();
				minMaxAddon
						.executeWithIgnoredTagChanges(() -> placeholder.getTags().add(IPresentationEngine.MAXIMIZED));
				minMaxAddon.adjustCTFButtons(placeholder);
			}
		}
	}

	/**
	 * Check whether a special table is one of the minimized elements. If so do
	 * not minimized any contents of such a table.
	 *
	 * @param modelService
	 *            The {@link EModelService} to use
	 * @param element
	 *            The {@link MUIElement} being minimized
	 * @param win
	 *            The {@link MWindow} the minimized element is part of
	 * @param persp
	 *            The {@link MPerspective} the minimized element is part of
	 * @param elementsToMinimize
	 *            The list of elements to minimize
	 */
	public static void handleMinimizeOfMinMaxChildrenArea(EModelService modelService, MUIElement element, MWindow win,
			MPerspective persp, List<MUIElement> elementsToMinimize) {
		// if an area is minimized, exclude the children from being minimized
		List<MPlaceholder> areas = modelService.findElements(persp == null ? win : persp, ID_EDITOR_AREA,
				MPlaceholder.class, null, EModelService.ANYWHERE);
		boolean foundRelevantArea = false;
		for (MPlaceholder placeholder : areas) {
			if (placeholder == element) {
				continue;
			}
			if (win != getWindowFor(placeholder)) {
				continue;
			}
			if (modelService.find(element.getElementId(), placeholder) == null) {
				continue;
			}
			if (placeholder.getRef().getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				foundRelevantArea = true;
			}
			List<MPartStack> partStacks = modelService.findElements(placeholder, null, MPartStack.class);
			for (MPartStack partStack : partStacks) {
				if (partStack == element) {
					continue;
				}
				elementsToMinimize.add(partStack);
			}
		}
		if (foundRelevantArea) {
			List<MUIElement> elementsToRemove = new ArrayList<>();
			for (MUIElement elementMUI : elementsToMinimize) {
				List<Object> findElements = modelService.findElements(elementMUI, element.getElementId(),
						null, null);
				if (findElements != null && !findElements.isEmpty()) {
					elementsToRemove.add(elementMUI);
				}
			}
			elementsToMinimize.removeAll(elementsToRemove);
		}
		// allow Area Children to be maximizable
	}

	/**
	 * Unzoom the special area if a child of it is unzoomed.
	 *
	 * @param minMaxAddon
	 *            The {@link MinMaxAddon} calling
	 * @param element
	 *            The {@link MUIElement} being unzoomed
	 */
	public static void unzoomStackOfMinMaxChildrenArea(final MinMaxAddon minMaxAddon, final MUIElement element) {
		if (element instanceof MPartStack) {
			MArea area = getAreaFor((MPartStack) element);
			if (area != null && area.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				final MPlaceholder placeholder = area.getCurSharedRef();
				minMaxAddon.executeWithIgnoredTagChanges(
						() -> placeholder.getTags().remove(IPresentationEngine.MAXIMIZED));
				minMaxAddon.adjustCTFButtons(placeholder);
			}
		}
	}

	/**
	 * Check whether a {@link MUIElement} is a child of a special area.
	 *
	 * @param element
	 *            The {@link MUIElement} to check
	 * @return true if the element is part of a special area, false otherwise
	 */
	public static boolean isPartOfMinMaxChildrenArea(MUIElement element) {
		if (element instanceof MPartStack) {
			MArea area = getAreaFor((MPartStack) element);
			if (area != null && area.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Return the first {@link MArea} that is the parent of the given
	 * {@link MPartStack}.
	 *
	 * @param stack
	 *            The {@link MPartStack} to find the container {@link MArea} for
	 * @return The container MArea or null if this stack is not part of an area
	 */
	public static MArea getAreaFor(MPartStack stack) {
		MUIElement parent = stack.getParent();
		while (parent != null) {
			if (parent instanceof MArea) {
				return (MArea) parent;
			}
			parent = parent.getParent();
		}
		return null;
	}

	/**
	 * Return the MWindow containing this element (if any). This may either be a
	 * 'top level' window -or- a detached window. This allows the min.max code
	 * to only affect elements in the window containing the element.
	 *
	 * @param element
	 *            The element to check
	 *
	 * @return the window containing the element.
	 */
	public static MWindow getWindowFor(MUIElement element) {
		MUIElement parent = element.getParent();

		// We rely here on the fact that a DW's 'getParent' will return
		// null since it's not in the 'children' hierarchy
		while (parent != null && !(parent instanceof MWindow)) {
			if (parent.getTags().contains(MIN_MAXIMIZEABLE_CHILDREN_AREA_TAG) && parent instanceof MArea) {
				parent = ((MArea) parent).getCurSharedRef();
			} else {
				parent = parent.getParent();
			}
		}

		// A detached window will end up with getParent() == null
		return (MWindow) parent;
	}
}
