/*******************************************************************************
 * Copyright (c) 2011, 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
 *     Maxime Porhel <maxime.porhel@obeo.fr> Obeo - Bug 410426
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 ******************************************************************************/

package org.eclipse.e4.ui.workbench.renderers.swt;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Stream;
import org.eclipse.core.expressions.ExpressionInfo;
import org.eclipse.e4.core.commands.ExpressionContext;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IContextFunction;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.ui.internal.workbench.ContributionsAnalyzer;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.model.application.ui.menu.MToolBar;
import org.eclipse.e4.ui.model.application.ui.menu.MToolBarContribution;
import org.eclipse.e4.ui.model.application.ui.menu.MToolBarElement;
import org.eclipse.e4.ui.model.application.ui.menu.MToolBarSeparator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.swt.widgets.Shell;

public class ToolBarContributionRecord {
	public static final String FACTORY = "ToolBarContributionFactory"; //$NON-NLS-1$
	static final String STATIC_CONTEXT = "ToolBarContributionFactoryContext"; //$NON-NLS-1$

	MToolBar toolbarModel;
	MToolBarContribution toolbarContribution;
	List<MToolBarElement> generatedElements = new CopyOnWriteArrayList<>();
	Set<MToolBarElement> sharedElements = new CopyOnWriteArraySet<>();
	ToolBarManagerRenderer renderer;
	boolean isVisible = true;
	private IEclipseContext infoContext;
	private Runnable factoryDispose;

	public ToolBarContributionRecord(MToolBar model,
			MToolBarContribution contribution, ToolBarManagerRenderer renderer) {
		this.toolbarModel = model;
		this.toolbarContribution = contribution;
		this.renderer = renderer;
	}

	public ToolBarManager getManagerForModel() {
		return renderer.getManager(toolbarModel);
	}

	public void updateVisibility(IEclipseContext context) {
		ExpressionContext exprContext = new ExpressionContext(context);
		updateIsVisible(exprContext);
		HashSet<ToolBarContributionRecord> recentlyUpdated = new HashSet<>();
		recentlyUpdated.add(this);
		boolean changed = false;
		for (MToolBarElement item : generatedElements) {
			boolean currentVisibility = computeVisibility(recentlyUpdated, item, exprContext);
			if (item.isVisible() != currentVisibility) {
				item.setVisible(currentVisibility);
				changed = true;
			}
		}
		for (MToolBarElement item : sharedElements) {
			boolean currentVisibility = computeVisibility(recentlyUpdated, item, exprContext);
			if (item.isVisible() != currentVisibility) {
				item.setVisible(currentVisibility);
				changed = true;
			}
		}

		// if nothing has changed, we have nothing to update
		if (!changed) {
			return;
		}
		ToolBarManager managerForModel = getManagerForModel();
		managerForModel.markDirty();

		// if not visible no need to request the layout of the toolbar
		if (!isVisible) {
			return;
		}
		// Make sure the MToolBar model is visible because
		// TrimBarLayout.hideManagedTB hides and IPresentationEngine moves
		// it to the Limbo-Shell
		boolean anyMatch = Stream.of(managerForModel.getItems()).anyMatch(IContributionItem::isVisible);
		if (anyMatch) {
			MWindow window = getWindow();
			if (window != null) {
				Object widget = window.getWidget();
				if (widget instanceof Shell) {
					((Shell) widget).requestLayout();
				}
			}
		}
	}


	private MWindow getWindow() {
		EObject n = (EObject) toolbarModel;
		while (n.eContainer() != null) {
			n = n.eContainer();
			if (n instanceof MWindow) {
				return (MWindow) n;
			}
		}
		return null;
	}

	public void updateIsVisible(ExpressionContext exprContext) {
		isVisible = ContributionsAnalyzer.isVisible(toolbarContribution,
				exprContext);
	}

	public boolean computeVisibility(
			HashSet<ToolBarContributionRecord> recentlyUpdated,
			MToolBarElement item, ExpressionContext exprContext) {
		boolean currentVisibility = isVisible;
		if (item instanceof MToolBarSeparator) {
			ArrayList<ToolBarContributionRecord> list = renderer.getList(item);
			if (list != null) {
				Iterator<ToolBarContributionRecord> cr = list.iterator();
				while (!currentVisibility && cr.hasNext()) {
					ToolBarContributionRecord rec = cr.next();
					if (!recentlyUpdated.contains(rec)) {
						rec.updateIsVisible(exprContext);
						recentlyUpdated.add(rec);
					}
					currentVisibility |= rec.isVisible;
				}
			}
		}
		if (currentVisibility
				&& item.getPersistedState().get(
						MenuManagerRenderer.VISIBILITY_IDENTIFIER) != null) {
			String identifier = item.getPersistedState().get(
					MenuManagerRenderer.VISIBILITY_IDENTIFIER);
			Object rc = exprContext.eclipseContext.get(identifier);
			if (rc instanceof Boolean) {
				currentVisibility = ((Boolean) rc).booleanValue();
			}
		}
		if (currentVisibility && item.getVisibleWhen() != null) {
			boolean val = ContributionsAnalyzer.isVisible(item.getVisibleWhen(), exprContext);
			currentVisibility = val;
		}
		return currentVisibility;
	}

	public void collectInfo(ExpressionInfo info) {
		ContributionsAnalyzer.collectInfo(info,
				toolbarContribution.getVisibleWhen());
		for (MToolBarElement item : generatedElements) {
			ContributionsAnalyzer.collectInfo(info, item.getVisibleWhen());
		}
		for (MToolBarElement item : sharedElements) {
			ContributionsAnalyzer.collectInfo(info, item.getVisibleWhen());
		}
	}

	public boolean anyVisibleWhen() {
		if (toolbarContribution.getVisibleWhen() != null) {
			return true;
		}

		List<MToolBarElement> childrenToInspect;
		if (toolbarContribution.getTransientData().get(FACTORY) != null) {
			// See mergeIntoModel
			childrenToInspect = this.generatedElements;
		} else {
			childrenToInspect = toolbarContribution.getChildren();
		}

		for (MToolBarElement child : childrenToInspect) {
			if (requiresVisibilityCheck(child)) {
				return true;
			}
		}
		return false;
	}

	private boolean requiresVisibilityCheck(MToolBarElement toolBarElement) {
		return toolBarElement.getVisibleWhen() != null
				|| toolBarElement.getPersistedState().get(MenuManagerRenderer.VISIBILITY_IDENTIFIER) != null;
	}

	public boolean mergeIntoModel() {
		int idx = getIndex(toolbarModel,
				toolbarContribution.getPositionInParent());
		if (idx == -1) {
			return false;
		}

		final List<MToolBarElement> copyElements;
		if (toolbarContribution.getTransientData().get(FACTORY) != null) {
			copyElements = mergeFactoryIntoModel();
		} else {
			copyElements = new ArrayList<>();
			for (MToolBarElement item : toolbarContribution.getChildren()) {
				MToolBarElement copy = (MToolBarElement) EcoreUtil
						.copy((EObject) item);
				copyElements.add(copy);
			}
		}
		for (MToolBarElement copy : copyElements) {
			// if a visibleWhen clause is defined, the item should not be
			// visible until the clause has been evaluated and returned 'true'
			copy.setVisible(!requiresVisibilityCheck(copy));
			if (copy instanceof MToolBarSeparator) {
				MToolBarSeparator shared = findExistingSeparator(copy
						.getElementId());
				if (shared == null) {
					shared = (MToolBarSeparator) copy;
					renderer.linkElementToContributionRecord(copy, this);
					toolbarModel.getChildren().add(idx++, copy);
				} else {
					copy = shared;
				}
				sharedElements.add(shared);
			} else {
				generatedElements.add(copy);
				renderer.linkElementToContributionRecord(copy, this);
				toolbarModel.getChildren().add(idx++, copy);
			}
			if (copy instanceof MToolBarSeparator) {
				ArrayList<ToolBarContributionRecord> array = renderer
						.getList(copy);
				array.add(this);
			}
		}
		return true;
	}

	/**
	 * @return
	 */
	private List<MToolBarElement> mergeFactoryIntoModel() {
		Object obj = toolbarContribution.getTransientData().get(FACTORY);
		if (!(obj instanceof IContextFunction)) {
			return Collections.emptyList();
		}
		IEclipseContext staticContext = getStaticContext();
		staticContext.remove(List.class);
		factoryDispose = (Runnable) ((IContextFunction) obj).compute(
				staticContext, null);
		return staticContext.get(List.class);
	}

	private IEclipseContext getStaticContext() {
		if (infoContext == null) {
			IEclipseContext parentContext = renderer.getContext(toolbarModel);
			if (parentContext != null) {
				infoContext = parentContext.createChild(STATIC_CONTEXT);
			} else {
				infoContext = EclipseContextFactory.create(STATIC_CONTEXT);
			}
			ContributionsAnalyzer.populateModelInterfaces(toolbarModel,
					infoContext, toolbarModel.getClass().getInterfaces());
			infoContext.set(ToolBarManagerRenderer.class, renderer);
		}
		return infoContext;
	}

	MToolBarSeparator findExistingSeparator(String id) {
		if (id == null) {
			return null;
		}
		for (MToolBarElement item : toolbarModel.getChildren()) {
			if (item instanceof MToolBarSeparator
					&& id.equals(item.getElementId())) {
				return (MToolBarSeparator) item;
			}
		}
		return null;
	}

	public void dispose() {
		for (MToolBarElement copy : generatedElements) {
			toolbarModel.getChildren().remove(copy);
		}
		for (MToolBarElement shared : sharedElements) {
			ArrayList<ToolBarContributionRecord> array = renderer
					.getList(shared);
			array.remove(this);
			if (array.isEmpty()) {
				toolbarModel.getChildren().remove(shared);
			}
		}
		if (factoryDispose != null) {
			factoryDispose.run();
			factoryDispose = null;
		}
	}

	private static int getIndex(MElementContainer<?> model,
			String positionInParent) {
		String id = null;
		String modifier = null;
		if (positionInParent != null && positionInParent.length() > 0) {
			String[] array = positionInParent.split("="); //$NON-NLS-1$
			modifier = array[0];
			id = array[1];
		}
		if (id == null) {
			return model.getChildren().size();
		}

		int idx = 0;
		int size = model.getChildren().size();
		while (idx < size) {
			if (id.equals(model.getChildren().get(idx).getElementId())) {
				if ("after".equals(modifier)) { //$NON-NLS-1$
					idx++;
				} else if ("endof".equals(modifier)) { //$NON-NLS-1$
					// Skip current menu item
					idx++;

					// Skip all menu items until next MenuSeparator is found
					while (idx < size
							&& !(model.getChildren().get(idx) instanceof MToolBarSeparator && model
									.getChildren().get(idx).getElementId() != null)) {
						idx++;
					}
				}
				return idx;
			}
			idx++;
		}
		return id.equals("additions") ? model.getChildren().size() : -1; //$NON-NLS-1$
	}
}