/*******************************************************************************
 * Copyright (c) 2010, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 ******************************************************************************/

package org.eclipse.e4.ui.internal.workbench;

import static java.util.Collections.singletonList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.ui.model.application.MApplication;
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.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.MCompositePart;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.e4.ui.workbench.modeling.EPartService;
import org.eclipse.emf.ecore.EObject;

class PartActivationHistory {

	private PartServiceImpl partService;

	private EModelService modelService;

	private LinkedList<MPart> generalActivationHistory = new LinkedList<>();

	PartActivationHistory(PartServiceImpl partService, EModelService modelService) {
		this.partService = partService;
		this.modelService = modelService;
	}

	public void clear() {
		generalActivationHistory.clear();
	}

	void activate(MPart part, boolean activateBranch) {
		IEclipseContext context = part.getContext();
		if (context != null) {
			if (activateBranch) {
				context.activateBranch();
			} else {
				IEclipseContext parent = context.getParent();
				do {
					context.activate();
					context = parent;
					parent = parent.getParent();
				} while (parent.get(MWindow.class) != null);
			}
		}
		prepend(part);
	}

	/**
	 * Places the specified part at the end of the activation history if it is not already in the
	 * list. If it is already in the activation history, then its position will not change.
	 *
	 * @param part
	 *            the part to possibly add to the end of the activation history
	 */
	void append(MPart part) {
		if (!generalActivationHistory.contains(part)) {
			generalActivationHistory.addLast(part);
		}
	}

	/**
	 * Adds the specified part to the front of the activation history.
	 *
	 * @param part
	 *            the part to insert into the front of the activation history
	 */
	void prepend(MPart part) {
		generalActivationHistory.remove(part);
		generalActivationHistory.addFirst(part);
	}

	/**
	 * Checks to see if this element and its parents are actually being rendered.
	 */
	boolean isValid(MUIElement element) {
		if (element == null || !element.isToBeRendered() || !element.isVisible()) {
			return false;
		}

		if (element instanceof MApplication) {
			return true;
		}

		MUIElement parent = element.getParent();
		if (parent == null && element instanceof MWindow) {
			// might be a detached window
			parent = (MUIElement) ((EObject) element).eContainer();
		}

		if (parent == null) {
			return isValid(partService.getLocalPlaceholder(element));
		}

		return isValid(parent);
	}

	/**
	 * Checks to see if this element and its parents are actually being rendered.
	 */
	boolean isValid(MPerspective perspective, MUIElement element) {
		if (element instanceof MApplication) {
			return true;
		} else if (element == null || !element.isToBeRendered() || !element.isVisible()) {
			return false;
		}

		MElementContainer<?> parent = element.getParent();
		if (parent == null) {
			for (MPlaceholder placeholder : modelService.findElements(perspective, null,
					MPlaceholder.class, null)) {
				if (placeholder.getRef() == element) {
					return isValid(perspective, placeholder);
				}
			}

			if (element instanceof MWindow) {
				// might be a detached window
				return isValid(perspective, (MUIElement) ((EObject) element).eContainer());
			}
			return false;
		} else if (parent instanceof MGenericStack && parent.getSelectedElement() != element) {
			// if in a stack, then only valid if we're the selected element
			return false;
		}

		return isValid(perspective, parent);
	}

	/**
	 * Determines whether this element is contained within an MArea, and to return the area if it
	 * is.
	 */
	private MArea isInArea(MUIElement element) {
		if (element == null) {
			return null;
		}
		MPlaceholder placeholder = element.getCurSharedRef();
		if (placeholder == null) {
			MUIElement parent = element.getParent();
			if (parent == null) {
				// may be null for detached windows
				parent = (MUIElement) ((EObject) element).eContainer();
			}
			return parent instanceof MApplication ? null : parent instanceof MArea ? (MArea) parent
					: isInArea(parent);
		}

		MUIElement parent = placeholder.getParent();
		if (parent == null) {
			// may be null for detached windows
			parent = (MUIElement) ((EObject) placeholder).eContainer();
		}
		return parent instanceof MApplication ? null : parent instanceof MArea ? (MArea) parent
				: isInArea(parent);
	}

	private MPart getActivationCandidate(MPart part) {
		// get all the possible parts that we can possibly activate
		Collection<MPart> candidates = part.getContext().get(EPartService.class).getParts();
		return findActivationCandidate(candidates, part);
	}

	/**
	 * Finds and returns a part that is a valid candidate to be granted activation.
	 */
	private MPart findActivationCandidate(Collection<MPart> candidates, MPart currentlyActivePart) {
		candidates.remove(currentlyActivePart);

		// If there is a composite part in the candidates, remove the child Parts
		List<MPart> compositeParts = candidates.stream().filter(MCompositePart.class::isInstance).toList();
		for (MPart compositePart : compositeParts) {

			List<MPart> childParts = modelService.findElements(compositePart, null, MPart.class);
			childParts.remove(compositePart);
			candidates.removeAll(childParts);
		}

		MPlaceholder activePlaceholder = partService.getLocalPlaceholder(currentlyActivePart);
		for (MPart candidate : candidates) {
			// make sure it's rendered and visible
			if (isValid(candidate)) {
				MPlaceholder placeholder = partService.getLocalPlaceholder(candidate);
				MElementContainer<MUIElement> parent = placeholder == null ? candidate.getParent()
						: placeholder.getParent();
				// stacks require special considerations because we don't want to activate something
				// that's not the selected element if possible get the selected element
				if (parent instanceof MGenericStack) {
					MUIElement selection = parent.getSelectedElement();
					// if the selected element is the currently active part, the candidate is valid
					if (selection == activePlaceholder || selection == currentlyActivePart) {
						return candidate;
					}

					// if the selected element is the current candidate, the candidate is valid
					if (selection == candidate || selection == placeholder) {
						return candidate;
					}
				} else {
					// not in a stack, just return the candidate then
					return candidate;
				}
			}
		}
		return null;
	}

	MPart getActivationCandidate(Collection<MPart> validParts) {
		// check activation history, since the history is global, we need to filter it down first
		Collection<MPart> validCandidates = new ArrayList<>();
		for (MPart validPart : generalActivationHistory) {
			if (validParts.contains(validPart)) {
				validCandidates.add(validPart);
			}
		}

		MPart candidate = findActivationCandidate(validCandidates);
		if (candidate == null) {
			validParts.removeAll(validCandidates);
			return findActivationCandidate(validParts);
		}
		return candidate;
	}

	private MPart findActivationCandidate(Collection<MPart> candidates) {
		for (MPart candidate : candidates) {
			// make sure it's rendered and visible
			if (isValid(candidate)) {
				MPlaceholder placeholder = partService.getLocalPlaceholder(candidate);
				MElementContainer<MUIElement> parent = placeholder == null ? candidate.getParent()
						: placeholder.getParent();
				// stacks require special considerations because we don't want to activate something
				// that's not the selected element if possible get the selected element
				if (parent instanceof MGenericStack) {
					MUIElement selection = parent.getSelectedElement();
					// if the selected element is the current candidate, the candidate is valid
					if (selection == candidate || selection == placeholder) {
						return candidate;
					}
				} else {
					// not in a stack, just return the candidate then
					return candidate;
				}
			}
		}
		return null;
	}

	MPart getNextActivationCandidate(Collection<MPart> validParts, MPart part) {
		MArea area = isInArea(part);
		if (area != null) {
			// focus should stay in the area if possible
			MPart candidate = getSiblingActivationCandidate(part);
			if (candidate != null) {
				return candidate;
			}

			// no sibling candidate, find another part in the area to activate
			candidate = findActivationCandidate(modelService.findElements(area, null, MPart.class), part);
			if (candidate != null) {
				return candidate;
			}
		}

		// check activation history, since the history is global, we need to filter it down first
		Collection<MPart> validCandidates = new ArrayList<>();
		for (MPart validPart : generalActivationHistory) {
			if (validParts.contains(validPart)) {
				validCandidates.add(validPart);
			}
		}

		MPart candidate = findActivationCandidate(validCandidates, part);
		return candidate == null ? getActivationCandidate(part) : candidate;
	}

	void forget(MWindow window, MPart part, boolean full) {
		if (full) {
			generalActivationHistory.remove(part);
		} else {
			for (MPlaceholder placeholder : modelService.findElements(window, null, MPlaceholder.class)) {
				// if there is at least one placeholder around, we should keep this
				if (placeholder.getRef() == part && placeholder.isToBeRendered()) {
					return;
				}
			}

			generalActivationHistory.remove(part);
		}
	}

	MPart getActivationCandidate(MPerspective perspective) {
		for (MPart candidate : generalActivationHistory) {
			if (partService.isInContainer(perspective, candidate)
					&& isValid(perspective, candidate)) {
				return candidate;
			}
		}

		List<MPart> activeCandidates = modelService.findElements(perspective, null, MPart.class,
				singletonList(EPartService.ACTIVE_ON_CLOSE_TAG));
		if (activeCandidates.size() > 0) {
			activeCandidates.get(0).getTags().remove(EPartService.ACTIVE_ON_CLOSE_TAG);
			MPart candidate = activeCandidates.get(0);
			if (partService.isInContainer(perspective, candidate)
					&& isValid(perspective, candidate)) {
				return candidate;
			}
		}

		Collection<MPart> candidates = perspective.getContext().get(EPartService.class).getParts();
		for (MPart candidate : candidates) {
			if (isValid(perspective, candidate)) {
				return candidate;
			}
		}
		return null;
	}

	private MPart getSiblingActivationCandidate(MPart part) {
		MPlaceholder placeholder = part.getCurSharedRef();
		MUIElement candidate = getSiblingSelectionCandidate(part, placeholder == null ? part
				: placeholder);
		return (MPart) (candidate instanceof MPlaceholder ? ((MPlaceholder) candidate).getRef()
				: candidate);
	}

	private MUIElement getSiblingSelectionCandidate(MPart part, MUIElement element) {
		List<MUIElement> siblings = element.getParent().getChildren();
		for (MPart previouslyActivatedPart : generalActivationHistory) {
			if (previouslyActivatedPart != part && isValid(previouslyActivatedPart)) {
				if (siblings.contains(previouslyActivatedPart)) {
					return previouslyActivatedPart;
				}

				MPlaceholder placeholder = partService.getLocalPlaceholder(previouslyActivatedPart);
				if (placeholder != null && placeholder.isToBeRendered()
						&& siblings.contains(placeholder)) {
					return placeholder;
				}
			}
		}
		return null;
	}

	MUIElement getSiblingSelectionCandidate(MPart part) {
		MPlaceholder placeholder = part.getCurSharedRef();
		return getSiblingSelectionCandidate(part, placeholder == null ? part : placeholder);
	}
}
