/*******************************************************************************
 * Copyright (c) 2010, 2011 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.ui.viewmodel.properties;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.DsfMultiStatus;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.core.runtime.IStatus;

/**
 * Status object for use with the IPropertiesUpdate.  This status class
 * allows setting a different status result for each property.  This allows
 * for better interpretation of status by the client of the update.
 * <p>
 * This status class derives from MultiStatus class such that the status
 * objects for each property can be accessed through the standard
 * {@link #getChildren()} method.  Also, multiple properties can reference
 * the same status object, meaning that the number of properties returned
 * by {@link #getProperties()} may be greater than the status objects
 * returned by <code>getChildren()</code>.
 * <p>
 * The properties status object does not have its own message, severity,
 * error status or exception.  All these attributes are calculated from
 * the child status objects.  If the status has more than one status child,
 * the String returned by {@link #getMessage()} is: "Multiple errors reported".
 *
 * @since 2.2
 */
public class PropertiesUpdateStatus extends DsfMultiStatus {

	final private Map<String, IStatus> fPropertiesStatus = new HashMap<>(1);
	private boolean fFirstStatusSet;

	public PropertiesUpdateStatus() {
		super(DsfUIPlugin.PLUGIN_ID, 0, "", null); //$NON-NLS-1$
	}

	/**
	 * Returns set of properties that have an additional status specified.
	 */
	public Set<String> getProperties() {
		return fPropertiesStatus.keySet();
	}

	/**
	 * Returns an additional status for the given property in a property
	 * update.  Returned value may be <code>null</code> if no additional
	 * status is given.
	 */
	public IStatus getStatus(String property) {
		return fPropertiesStatus.get(property);
	}

	/**
	 * Sets the given status for the given property.
	 */
	public void setStatus(String property, IStatus status) {
		IStatus child = findEquivalentChild(status);
		if (child != null) {
			status = child;
		} else {
			add(status);
		}

		fPropertiesStatus.put(property, status);
	}

	/**
	 * Sets the given status for the properties array.
	 */
	public void setStatus(String[] properties, IStatus status) {
		IStatus child = findEquivalentChild(status);
		if (child != null) {
			status = child;
		} else {
			add(status);
		}

		for (String property : properties) {
			fPropertiesStatus.put(property, status);
		}
	}

	/**
	 * Merges data in the new status into the base status data, and returns the
	 * resulting status. Only properties specified in the given set are merged.
	 * <p>
	 * The new status is considered to be more up to date than the base
	 * status and its data overrides the base status .  If the base status
	 * holds an error for a given property, which is found in the
	 * given set, and the new status does not, then the base error status is
	 * removed.
	 *
	 * @param baseStatus Properties into which the new status properties will
	 * be merged.
	 * @param newStatus Properties status to merge.
	 * @param properties The properties to consider in the new status.
	 * @return Resulting merged status object.
	 */
	public static PropertiesUpdateStatus mergePropertiesStatus(PropertiesUpdateStatus baseStatus,
			PropertiesUpdateStatus newStatus, Set<String> properties) {
		PropertiesUpdateStatus mergedStatus = new PropertiesUpdateStatus();
		// Copy the property status map from the base status.
		mergedStatus.fPropertiesStatus.putAll(baseStatus.fPropertiesStatus);

		// Add in the property statuses from the new status, but only for the
		// specified properties.
		for (String property : properties) {
			IStatus propertyStatus = newStatus.getStatus(property);
			if (propertyStatus != null) {
				mergedStatus.fPropertiesStatus.put(property, propertyStatus);
			} else {
				mergedStatus.fPropertiesStatus.remove(property);
			}
		}

		// Children of merged status should contain all statuses that are found in the fPropertiesStatus map, but
		// without duplicates.
		Set<IStatus> children = new HashSet<>(
				(baseStatus.getChildren().length + newStatus.getChildren().length) * 4 / 3);
		children.addAll(mergedStatus.fPropertiesStatus.values());
		for (IStatus child : children) {
			mergedStatus.add(child);
		}

		// Merged status should contain all children statuses that were added without a corresponding property to the
		// base status and to the new status.
		Collection<IStatus> baseStatusPropertyChildren = baseStatus.fPropertiesStatus.values();
		for (IStatus baseStatusChild : baseStatus.getChildren()) {
			if (!baseStatusPropertyChildren.contains(baseStatusChild)) {
				mergedStatus.add(baseStatusChild);
			}
		}
		Collection<IStatus> newStatusPropertyChildren = newStatus.fPropertiesStatus.values();
		for (IStatus newStatusChild : newStatus.getChildren()) {
			if (!newStatusPropertyChildren.contains(newStatusChild)) {
				mergedStatus.add(newStatusChild);
			}
		}

		return mergedStatus;
	}

	/**
	 * Adds the given status object as a child of this status.  If there's an
	 * equivalent child status already, the new status is ignored.
	 */
	@Override
	public void add(IStatus status) {
		if (findEquivalentChild(status) != null) {
			return;
		}

		super.add(status);

		boolean firstSet;
		synchronized (this) {
			firstSet = fFirstStatusSet;
			fFirstStatusSet = true;
		}

		if (!firstSet) {
			setMessage(status.getMessage());
		} else {
			setMessage(MessagesForProperties.PropertiesUpdateStatus_message);
		}
	}

	/**
	 * Finds a child status that is equivalent to the given status.
	 */
	private IStatus findEquivalentChild(IStatus status) {
		if (getChildren().length != 0) {
			for (IStatus child : getChildren()) {
				if (areEquivalent(child, status)) {
					return child;
				}
			}
		}
		return null;
	}

	/**
	 * Compares two status objects to determine if they are equivalent.
	 */
	private boolean areEquivalent(IStatus s1, IStatus s2) {
		if ((s1 == null && s2 != null) || (s1 != null && s2 == null)) {
			return false;
		}
		if (s1 == null) {
			return true;
		}
		if ((s1.getSeverity() != s2.getSeverity()) || !s1.getPlugin().equals(s2.getPlugin())
				|| (s1.getCode() != s2.getCode())) {
			return false;
		}
		if ((s1.getException() == null && s1.getException() != null)
				|| (s1.getException() != null && s1.getException() == null)
				|| (s1.getException() != null && !s1.getException().equals(s2.getException()))) {
			return false;
		}
		return s1.getMessage().equals(s2.getMessage());
	}

	/**
	 * Convenience method that returns and optionally creates a properties
	 * update status object for the given update.
	 */
	public static PropertiesUpdateStatus getPropertiesStatus(IPropertiesUpdate update) {
		IStatus updateStatus = update.getStatus();
		if (updateStatus instanceof PropertiesUpdateStatus) {
			return (PropertiesUpdateStatus) updateStatus;
		} else {
			PropertiesUpdateStatus propertiesStatus = new PropertiesUpdateStatus();
			update.setStatus(propertiesStatus);
			if (!updateStatus.isOK()) {
				propertiesStatus.add(updateStatus);
			}
			return propertiesStatus;
		}
	}

	/**
	 * Convenience method that returns and optionally creates a properties
	 * update status object for the given update.
	 */
	public static PropertiesUpdateStatus makePropertiesStatus(IStatus updateStatus) {
		if (updateStatus instanceof PropertiesUpdateStatus) {
			return (PropertiesUpdateStatus) updateStatus;
		} else {
			PropertiesUpdateStatus propertiesStatus = new PropertiesUpdateStatus();
			if (!updateStatus.isOK()) {
				propertiesStatus.add(updateStatus);
			}
			return propertiesStatus;
		}
	}
}
