/*******************************************************************************
 * 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 *     Simon Scholz <simon.scholz@vogella.com> - Bug 484427
 ******************************************************************************/

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionInfo;
import org.eclipse.core.expressions.OrExpression;
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.MCoreExpression;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.MExpression;
import org.eclipse.e4.ui.model.application.ui.impl.UiFactoryImpl;
import org.eclipse.e4.ui.model.application.ui.menu.MMenu;
import org.eclipse.e4.ui.model.application.ui.menu.MMenuContribution;
import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement;
import org.eclipse.e4.ui.model.application.ui.menu.MMenuSeparator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.action.MenuManager;

public class ContributionRecord {
	public static final String FACTORY = "ContributionFactory"; //$NON-NLS-1$
	static final String STATIC_CONTEXT = "ContributionFactoryContext"; //$NON-NLS-1$

	MMenu menuModel;
	private MMenuContribution menuContribution;
	private ArrayList<MMenuElement> generatedElements = new ArrayList<>();
	private HashSet<MMenuElement> sharedElements = new HashSet<>();
	private MenuManagerRenderer renderer;
	boolean isVisible = true;
	private IEclipseContext infoContext;
	private Runnable factoryDispose;

	public ContributionRecord(MMenu menuModel, MMenuContribution contribution,
			MenuManagerRenderer renderer) {
		this.menuModel = menuModel;
		this.menuContribution = contribution;
		this.renderer = renderer;
	}

	public MenuManager getManagerForModel() {
		return renderer.getManager(menuModel);
	}

	public MMenuContribution getMenuContribution() {
		return menuContribution;
	}

	/**
	 * Access to analyze for tests. For Looking, not touching!
	 *
	 * @return the shared elements collection
	 */
	public Collection<MMenuElement> getSharedElements() {
		return sharedElements;
	}

	/**
	 * Access to analyze for tests. For Looking, not touching!
	 *
	 * @return the generated elements collection
	 */
	public Collection<MMenuElement> getGeneratedElements() {
		return generatedElements;
	}

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

		if (changed) {
			MenuManager manager = getManagerForModel();
			if (manager != null) {
				manager.markDirty();
			}
		}
	}

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

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

	public boolean computeVisibility(
			HashSet<ContributionRecord> recentlyUpdated, MMenuElement item,
			ExpressionContext exprContext) {
		boolean currentVisibility = isVisible;
		if (item instanceof MMenu || item instanceof MMenuSeparator) {
			ArrayList<ContributionRecord> list = renderer.getList(item);
			if (list != null) {
				Iterator<ContributionRecord> cr = list.iterator();
				while (!currentVisibility && cr.hasNext()) {
					ContributionRecord 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;
	}

	private Expression getExpression(MExpression expression) {
		if (expression instanceof MCoreExpression) {
			Object coreExpression = ((MCoreExpression) expression)
					.getCoreExpression();
			return coreExpression instanceof Expression ? (Expression) coreExpression
					: null;
		}
		return null;
	}

	private MExpression merge(MExpression expressionA, MExpression expressionB) {
		Expression coreExpressionA = getExpression(expressionA);
		Expression coreExpressionB = getExpression(expressionB);
		if (coreExpressionA == null || coreExpressionB == null) {
			// implied to always be visible
			return null;
		}
		if (coreExpressionA.equals(coreExpressionB)) {
			return expressionA;
		}

		// combine the two expressions
		OrExpression expression = new OrExpression();
		expression.add(coreExpressionA);
		expression.add(coreExpressionB);

		MCoreExpression exp = UiFactoryImpl.eINSTANCE.createCoreExpression();
		exp.setCoreExpressionId("programmatic.value"); //$NON-NLS-1$
		exp.setCoreExpression(expression);
		return exp;
	}

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

		final List<MMenuElement> copyElements;
		if (menuContribution.getTransientData().get(FACTORY) != null) {
			copyElements = mergeFactoryIntoModel();
		} else {
			copyElements = new ArrayList<>();
			for (MMenuElement item : menuContribution.getChildren()) {
				MMenuElement copy = (MMenuElement) EcoreUtil
						.copy((EObject) item);
				copyElements.add(copy);
			}
		}

		for (MMenuElement copy : copyElements) {
			if (copy instanceof MMenu) {
				MMenu shared = findExistingMenu(copy.getElementId());
				if (shared == null) {
					shared = (MMenu) copy;
					renderer.linkElementToContributionRecord(copy, this);
					menuModel.getChildren().add(idx++, copy);
				} else {
					shared.setVisibleWhen(merge(copy.getVisibleWhen(),
							shared.getVisibleWhen()));
					copy = shared;
				}
				sharedElements.add(shared);
			} else if (copy instanceof MMenuSeparator) {
				MMenuSeparator shared = findExistingSeparator(copy
						.getElementId());
				if (shared == null) {
					shared = (MMenuSeparator) copy;
					renderer.linkElementToContributionRecord(copy, this);
					menuModel.getChildren().add(idx++, copy);
				} else {
					copy = shared;
				}
				sharedElements.add(shared);
			} else {
				generatedElements.add(copy);
				renderer.linkElementToContributionRecord(copy, this);
				menuModel.getChildren().add(idx++, copy);
			}
			if (copy instanceof MMenu || copy instanceof MMenuSeparator) {
				renderer.addRecord(copy, this);
			}
		}
		return true;
	}

	/**
	 * @return
	 */
	private List<MMenuElement> mergeFactoryIntoModel() {
		Object obj = menuContribution.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(menuModel);
			if (parentContext != null) {
				infoContext = parentContext.createChild(STATIC_CONTEXT);
			} else {
				infoContext = EclipseContextFactory.create(STATIC_CONTEXT);
			}
			ContributionsAnalyzer.populateModelInterfaces(menuModel,
					infoContext, menuModel.getClass().getInterfaces());
			infoContext.set(MenuManagerRenderer.class, renderer);
		}
		return infoContext;
	}

	MMenu findExistingMenu(String id) {
		if (id == null) {
			return null;
		}
		for (MMenuElement item : menuModel.getChildren()) {
			if (item instanceof MMenu && id.equals(item.getElementId())) {
				return (MMenu) item;
			}
		}
		return null;
	}

	MMenuSeparator findExistingSeparator(String id) {
		if (id == null) {
			return null;
		}
		for (MMenuElement item : menuModel.getChildren()) {
			if (item instanceof MMenuSeparator
					&& id.equals(item.getElementId())) {
				return (MMenuSeparator) item;
			}
		}
		return null;
	}

	public void dispose() {
		for (MMenuElement copy : generatedElements) {
			menuModel.getChildren().remove(copy);
		}
		for (MMenuElement shared : sharedElements) {
			renderer.removeRecord(shared, this);
			ArrayList<ContributionRecord> array = renderer.getList(shared);
			if (array.isEmpty()) {
				menuModel.getChildren().remove(shared);
			}
		}
		if (factoryDispose != null) {
			factoryDispose.run();
			factoryDispose = null;
		}
	}

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

		int idx = 0;
		int size = menuModel.getChildren().size();
		while (idx < size) {
			if (id.equals(menuModel.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
							&& !(menuModel.getChildren().get(idx) instanceof MMenuSeparator && menuModel
									.getChildren().get(idx).getElementId() != null)) {
						idx++;
					}
				}
				return idx;
			}
			idx++;
		}
		return id.equals("additions") ? menuModel.getChildren().size() : -1; //$NON-NLS-1$
	}
}