/*******************************************************************************
 * Copyright (c) 2000, 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
 *******************************************************************************/
package org.eclipse.ltk.core.refactoring;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;

/**
 * A <code>RefactoringStatus</code> object represents the outcome of a
 * condition checking operation. It manages a list of <code>
 * RefactoringStatusEntry</code> objects. Each <code>RefactoringStatusEntry
 * </code> object describes one particular problem detected during
 * condition checking.
 * <p>
 * Additionally a problem severity is managed. Severities are ordered as follows:
 * <code>OK</code> &lt; <code>INFO</code> &lt; <code>WARNING</code> &lt; <code>
 * ERROR</code> &lt; <code>FATAL</code>. The status's problem severity is the maximum
 * of the severities of all entries. If the status doesn't have any entry the status's
 * severity is <code>OK</code>.
 * </p>
 * <p>
 * Note: this class is not intended to be extended by clients.
 * </p>
 *
 * @see RefactoringStatusEntry
 * @see Refactoring#checkAllConditions(IProgressMonitor)
 *
 * @since 3.0
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class RefactoringStatus {

	/**
	 * Status severity constant (value 0) indicating this status represents the nominal case.
	 */
	public static final int OK= 0;

	/**
	 * Status severity constant (value 1) indicating this status is informational only.
	 */
	public static final int INFO= 1;

	/**
	 * Status severity constant (value 2) indicating this status represents a warning.
	 * <p>
	 * Use this severity if the refactoring can be performed, but you assume that the
	 * user could not be aware of problems or confusions resulting from the execution.
	 * </p>
	 */
	public static final int WARNING= 2;

	/**
	 * Status severity constant (value 3) indicating this status represents an error.
	 * <p>
	 * Use this severity if the refactoring can be performed, but the refactoring will
	 * not be behavior preserving and/or the partial execution will lead to an inconsistent
	 * state (e.g. compile errors).
	 * </p>
	 */
	public static final int ERROR= 3;

	/**
	 * Status severity constant (value 4) indicating this status represents a fatal error.
	 * <p>
	 * Use this severity if the refactoring cannot be performed, and execution would lead
	 * to major problems. Note that this completely blocks the user from performing this refactoring.
	 * It is often preferable to use an {@link #ERROR} status and allow a partial execution
	 * (e.g. if just one reference to a refactored element cannot be updated).
	 * </p>
	 */
	public static final int FATAL= 4;

	/**
	 * List of refactoring status entries.
	 */
	private List<RefactoringStatusEntry> fEntries;

	/**
	 * The status's severity. The following invariant holds for
	 * <code>fSeverity</code>: <code>OK</code> &le; <code>fSeverity</code> &le;
	 * <code>FATAL</code>.
	 */
	private int fSeverity= OK;

	/**
	 * Creates a new refactoring status with an empty list of
	 * status entries and a severity of <code>OK</code>.
	 */
	public RefactoringStatus() {
		fEntries= new ArrayList<>(0);
	}

	/**
	 * Returns the severity.
	 *
	 * @return the severity.
	 */
	public int getSeverity() {
		return fSeverity;
	}

	/**
	 * Returns the list of refactoring status entries.
	 *
	 * @return the list of refactoring status entries. Returns an empty array
	 *  if no entries are managed.
	 */
	public RefactoringStatusEntry[] getEntries() {
		return fEntries.toArray(new RefactoringStatusEntry[fEntries.size()]);
	}

	/**
	 * Returns a list of refactoring status entries which are considered equal
	 * to the specified status entry.
	 *
	 * @param comparator the comparator to determine whether two status entries
	 *               are considered equal
	 * @param entry the refactoring status entry to compare the entries of this
	 *               status with
	 * @return the list of refactoring status entries that are considered equal
	 *               to the specified one, in no particular order. Returns an empty
	 *               array if no entries are managed or none of them matches.
	 *
	 * @since 3.1
	 */
	public RefactoringStatusEntry[] getEntries(IRefactoringStatusEntryComparator comparator, RefactoringStatusEntry entry) {
		final List<RefactoringStatusEntry> matches= new ArrayList<>(fEntries.size());
		for (RefactoringStatusEntry current : fEntries) {
			if (comparator.compare(current, entry) == 0)
				matches.add(current);
		}
		return matches.toArray(new RefactoringStatusEntry[matches.size()]);
	}

	/**
	 * Returns whether the status has entries or not.
	 *
	 * @return <code>true</code> if the status as any entries; otherwise
	 *  <code>false</code> is returned.
	 */
	public boolean hasEntries() {
		return !fEntries.isEmpty();
	}

	/**
	 * Returns the <code>RefactoringStatusEntry</code> at the specified index.
	 *
	 * @param index the index of the entry to return
	 * @return the entry at the specified index
	 *
	 * @throws IndexOutOfBoundsException if the index is out of range
	 */
	public RefactoringStatusEntry getEntryAt(int index) {
		return fEntries.get(index);
	}

	/**
	 * Returns the first entry managed by this refactoring status that
	 * matches the given plug-in identifier and code. If more than one
	 * entry exists that matches the criteria the first one in the list
	 * of entries is returned. Returns <code>null</code> if no entry
	 * matches.
	 *
	 * @param pluginId the entry's plug-in identifier
	 * @param code the entry's code
	 * @return the entry that matches the given plug-in identifier and
	 *  code; <code>null</code> otherwise
	 */
	public RefactoringStatusEntry getEntryMatchingCode(String pluginId, int code) {
		Assert.isTrue(pluginId != null);
		for (RefactoringStatusEntry entry : fEntries) {
			if (pluginId.equals(entry.getPluginId()) && entry.getCode() == code)
				return entry;
		}
		return null;
	}

	/**
	 * Returns the first entry which severity is equal or greater than the
	 * given severity. If more than one entry exists that matches the
	 * criteria the first one is returned. Returns <code>null</code> if no
	 * entry matches.
	 *
	 * @param severity the severity to search for. Must be one of <code>FATAL
	 *  </code>, <code>ERROR</code>, <code>WARNING</code> or <code>INFO</code>
	 * @return the entry that matches the search criteria
	 */
	public RefactoringStatusEntry getEntryMatchingSeverity(int severity) {
		Assert.isTrue(severity >= OK && severity <= FATAL);
		if (severity > fSeverity)
			return null;
		for (RefactoringStatusEntry entry : fEntries) {
			if (entry.getSeverity() >= severity)
				return entry;
		}
		return null;
	}

	/**
	 * Returns the entry with the highest severity. If there is more than one
	 * entry that matches the first one found in the list of entries is returned.
	 *
	 * @return the entry with the highest severity or <code>null</code> if no
	 *  entries are present
	 *
	 * @since 3.1
	 */
	public RefactoringStatusEntry getEntryWithHighestSeverity() {
		if (fEntries == null || fEntries.isEmpty())
			return null;
		RefactoringStatusEntry result= fEntries.get(0);
		for (int i= 1; i < fEntries.size(); i++) {
			RefactoringStatusEntry entry= fEntries.get(i);
			if (result.getSeverity() < entry.getSeverity())
				result= entry;
		}
		return result;
	}

	/**
	 * Returns the first message which severity is equal or greater than the
	 * given severity. If more than one entry exists that matches the criteria
	 * the first one is returned. Returns <code>null</code> if no entry matches.
	 *
	 * @param severity the severity to search for. Must be one of <code>FATAL
	 *  </code>, <code>ERROR</code>, <code>WARNING</code> or <code>INFO</code>
	 * @return the message of the entry that matches the search criteria
	 */
	public String getMessageMatchingSeverity(int severity) {
		RefactoringStatusEntry entry= getEntryMatchingSeverity(severity);
		if (entry == null)
			return null;
		return entry.getMessage();
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one entry filled with the given
	 * arguments.
	 *
	 * @param severity the severity
	 * @param msg the message
	 * @param context the context. Can be <code>null</code>
	 * @param pluginId the plug-in identifier. Can be <code>null</code> if argument <code>
	 *  code</code> equals <code>NO_CODE</code>
	 * @param code the problem code. Must be either <code>NO_CODE</code> or a positive integer
	 * @param data application specific data
	 *
	 * @return the newly created refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createStatus(int severity, String msg, RefactoringStatusContext context, String pluginId, int code, Object data) {
		RefactoringStatus result= new RefactoringStatus();
		result.fEntries.add(new RefactoringStatusEntry(severity, msg, context, pluginId, code, data));
		result.fSeverity= severity;
		return result;
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one <code>INFO</code> entry
	 * filled with the given message.
	 *
	 * @param msg the message of the info entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createInfoStatus(String msg) {
		return createStatus(INFO, msg, null, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one <code>INFO</code> entry
	 * filled with the given message and context.
	 *
	 * @param msg the message of the info entry
	 * @param context the context of the info entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createInfoStatus(String msg, RefactoringStatusContext context) {
		return createStatus(INFO, msg, context, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one <code>WARNING</code> entry
	 * filled with the given message.
	 *
	 * @param msg the message of the warning entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createWarningStatus(String msg) {
		return createStatus(WARNING, msg, null, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a <code>RefactoringStatus</code> with one <code>WARNING</code> entry
	 * fill with the given message and context.
	 *
	 * @param msg the message of the warning entry
	 * @param context the context of the warning entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createWarningStatus(String msg, RefactoringStatusContext context) {
		return createStatus(WARNING, msg, context, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one <code>ERROR</code> entry
	 * filled with the given message.
	 *
	 * @param msg the message of the error entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createErrorStatus(String msg) {
		return createStatus(ERROR, msg, null, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a <code>RefactoringStatus</code> with one <code>ERROR</code> entry
	 * fill with the given message and context.
	 *
	 * @param msg the message of the error entry
	 * @param context the context of the error entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createErrorStatus(String msg, RefactoringStatusContext context) {
		return createStatus(ERROR, msg, context, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> with one <code>FATAL</code> entry
	 * filled with the given message.
	 *
	 * @param msg the message of the fatal entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createFatalErrorStatus(String msg) {
		return createStatus(FATAL, msg, null, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a <code>RefactoringStatus</code> with one <code>FATAL</code> entry
	 * fill with the given message and context.
	 *
	 * @param msg the message of the fatal entry
	 * @param context the context of the fatal entry
	 * @return the refactoring status
	 *
	 * @see RefactoringStatusEntry
	 */
	public static RefactoringStatus createFatalErrorStatus(String msg, RefactoringStatusContext context) {
		return createStatus(FATAL, msg, context, null, RefactoringStatusEntry.NO_CODE, null);
	}

	/**
	 * Creates a new <code>RefactoringStatus</code> from the given <code>IStatus</code>. An
	 * OK status is mapped to an OK refactoring status, an information status is mapped
	 * to a warning refactoring status, a warning status is mapped to an error refactoring
	 * status and an error or cancel status is mapped to a fatal refactoring status. An unknown
	 * status is converted into a fatal error status as well. If the status is a <code>MultiStatus
	 * </code> then the first level of children of the status will be added as refactoring status
	 * entries to the created refactoring status.
	 *
	 * @param status the status to create a refactoring status from
	 * @return the refactoring status
	 *
	 * @see IStatus
	 * @since 3.2
	 */
	public static RefactoringStatus create(IStatus status) {
		if (status.isOK())
			return new RefactoringStatus();

		if (!status.isMultiStatus()) {
			switch (status.getSeverity()) {
				case IStatus.OK :
					return new RefactoringStatus();
				case IStatus.INFO :
					return RefactoringStatus.createWarningStatus(status.getMessage());
				case IStatus.WARNING :
					return RefactoringStatus.createErrorStatus(status.getMessage());
				case IStatus.ERROR :
					return RefactoringStatus.createFatalErrorStatus(status.getMessage());
				case IStatus.CANCEL :
					return RefactoringStatus.createFatalErrorStatus(status.getMessage());
				default :
					return RefactoringStatus.createFatalErrorStatus(status.getMessage());
			}
		} else {
			IStatus[] children= status.getChildren();
			RefactoringStatus result= new RefactoringStatus();
			for (IStatus child : children) {
				result.merge(RefactoringStatus.create(child));
			}
			return result;
		}
	}

	/**
	 * Merges the receiver and the parameter statuses. The resulting list of
	 * entries in the receiver will contain entries from both. The resulting
	 * severity in the receiver will be the more severe of its current severity
	 * and the parameter's severity. Merging with <code>null</code> is
	 * allowed - it has no effect.
	 *
	 * @param other the refactoring status to merge with
	 */
	public void merge(RefactoringStatus other) {
		if (other == null)
			return;
		fEntries.addAll(other.fEntries);
		fSeverity= Math.max(fSeverity, other.getSeverity());
	}

	/**
	 * Adds an <code>INFO</code> entry filled with the given message to this status.
	 * If the current severity is <code>OK</code> it will be changed to <code>INFO
	 * </code>. It will remain unchanged otherwise.
	 *
	 * @param msg the message of the info entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addInfo(String msg) {
		addInfo(msg, null);
	}

	/**
	 * Adds an <code>INFO</code> entry filled with the given message and context to
	 * this status. If the current severity is <code>OK</code> it will be changed to
	 * <code>INFO</code>. It will remain unchanged otherwise.
	 *
	 * @param msg the message of the info entry
	 * @param context the context of the info entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addInfo(String msg, RefactoringStatusContext context) {
		fEntries.add(new RefactoringStatusEntry(RefactoringStatus.INFO, msg, context));
		fSeverity= Math.max(fSeverity, INFO);
	}

	/**
	 * Adds a <code>WARNING</code> entry filled with the given message to this status.
	 * If the current severity is <code>OK</code> or <code>INFO</code> it will be
	 * changed to <code>WARNING</code>. It will remain unchanged otherwise.
	 *
	 * @param msg the message of the warning entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addWarning(String msg) {
		addWarning(msg, null);
	}

	/**
	 * Adds a <code>WARNING</code> entry filled with the given message and context to
	 * this status. If the current severity is <code>OK</code> or <code>INFO</code> it
	 * will be changed to <code>WARNING</code>. It will remain unchanged otherwise.
	 *
	 * @param msg the message of the warning entry
	 * @param context the context of the warning entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addWarning(String msg, RefactoringStatusContext context) {
		fEntries.add(new RefactoringStatusEntry(RefactoringStatus.WARNING, msg, context));
		fSeverity= Math.max(fSeverity, WARNING);
	}

	/**
	 * Adds an <code>ERROR</code> entry filled with the given message to this status.
	 * If the current severity is <code>OK</code>, <code>INFO</code> or <code>WARNING
	 * </code> it will be changed to <code>ERROR</code>. It will remain unchanged
	 * otherwise.
	 *
	 * @param msg the message of the error entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addError(String msg) {
		addError(msg, null);
	}

	/**
	 * Adds an <code>ERROR</code> entry filled with the given message and context to
	 * this status. If the current severity is <code>OK</code>, <code>INFO</code> or
	 * <code>WARNING</code> it will be changed to <code>ERROR</code>. It will remain
	 * unchanged otherwise.
	 *
	 * @param msg the message of the error entry
	 * @param context the context of the error entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addError(String msg, RefactoringStatusContext context) {
		fEntries.add(new RefactoringStatusEntry(RefactoringStatus.ERROR, msg, context));
		fSeverity= Math.max(fSeverity, ERROR);
	}

	/**
	 * Adds a <code>FATAL</code> entry filled with the given message to this status.
	 * The severity of this status will changed to <code>FATAL</code>.
	 *
	 * @param msg the message of the fatal entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addFatalError(String msg) {
		addFatalError(msg, null);
	}

	/**
	 * Adds a <code>FATAL</code> entry filled with the given message and status to
	 * this status. The severity of this status will changed to <code>FATAL</code>.
	 *
	 * @param msg the message of the fatal entry
	 * @param context the context of the fatal entry
	 *
	 * @see RefactoringStatusEntry
	 */
	public void addFatalError(String msg, RefactoringStatusContext context) {
		fEntries.add(new RefactoringStatusEntry(RefactoringStatus.FATAL, msg, context));
		fSeverity= Math.max(fSeverity, FATAL);
	}

	/**
	 * Adds a new entry filled with the given arguments to this status. The severity
	 * of this status is set to the maximum of <code>fSeverity</code> and
	 * <code>severity</code>.
	 *
	 * @param severity the severity of the entry
	 * @param msg the message of the entry
	 * @param context the context of the entry. Can be <code>null</code>
	 * @param pluginId the plug-in identifier of the entry. Can be <code>null</code> if
	 *  argument <code>code</code> equals <code>NO_CODE</code>
	 * @param code the problem code of the entry. Must be either <code>NO_CODE</code>
	 *  or a positive integer
	 */
	public void addEntry(int severity, String msg, RefactoringStatusContext context, String pluginId, int code) {
		fEntries.add(new RefactoringStatusEntry(severity, msg, context, pluginId, code));
		fSeverity= Math.max(fSeverity, severity);
	}

	/**
	 * Adds a new entry filled with the given arguments to this status. The severity
	 * of this status is set to the maximum of <code>fSeverity</code> and
	 * <code>severity</code>.
	 *
	 * @param severity the severity of the entry
	 * @param msg the message of the entry
	 * @param context the context of the entry. Can be <code>null</code>
	 * @param pluginId the plug-in identifier of the entry. Can be <code>null</code> if
	 *  argument <code>code</code> equals <code>NO_CODE</code>
	 * @param code the problem code of the entry. Must be either <code>NO_CODE</code>
	 *  or a positive integer
	 * @param data application specific data of the entry
	 */
	public void addEntry(int severity, String msg, RefactoringStatusContext context, String pluginId, int code, Object data) {
		fEntries.add(new RefactoringStatusEntry(severity, msg, context, pluginId, code, data));
		fSeverity= Math.max(fSeverity, severity);
	}

	/**
	 * Adds the given <code>RefactoringStatusEntry</code>. The severity of this
	 * status is set to the maximum of <code>fSeverity</code> and the severity of
	 * the entry.
	 *
	 * @param entry the <code>RefactoringStatusEntry</code> to be added
	 */
	public void addEntry(RefactoringStatusEntry entry) {
		Assert.isNotNull(entry);
		fEntries.add(entry);
		fSeverity= Math.max(fSeverity, entry.getSeverity());
	}

	/**
	 * Returns whether the status's severity is <code>OK</code> or not.
	 *
	 * @return <code>true</code> if the severity is <code>OK</code>;
	 *  otherwise <code>false</code> is returned
	 */
	public boolean isOK() {
		return fSeverity == OK;
	}

	/**
	 * Returns <code>true</code> if the current severity is <code>
	 * FATAL</code>.
	 *
	 * @return <code>true</code> if the current severity is <code>
	 *  FATAL</code>; otherwise <code>false</code> is returned
	 */
	public boolean hasFatalError() {
		return fSeverity == FATAL;
	}

	/**
	 * Returns <code>true</code> if the current severity is <code>
	 * FATAL</code> or <code>ERROR</code>.
	 *
	 * @return <code>true</code> if the current severity is <code>
	 *  FATAL</code> or <code>ERROR</code>; otherwise <code>false
	 *  </code> is returned
	 */
	public boolean hasError() {
		return fSeverity == FATAL || fSeverity == ERROR;
	}

	/**
	 * Returns <code>true</code> if the current severity is <code>
	 * FATAL</code>, <code>ERROR</code> or <code>WARNING</code>.
	 *
	 * @return <code>true</code> if the current severity is <code>
	 *  FATAL</code>, <code>ERROR</code> or <code>WARNING</code>;
	 *  otherwise <code>false</code> is returned
	 */
	public boolean hasWarning() {
		return fSeverity == FATAL || fSeverity == ERROR || fSeverity == WARNING;
	}

	/**
	 * Returns <code>true</code> if the current severity is <code>
	 * FATAL</code>, <code>ERROR</code>, <code>WARNING</code> or
	 * <code>INFO</code>.
	 *
	 * @return <code>true</code> if the current severity is <code>
	 *  FATAL</code>, <code>ERROR</code>, <code>WARNING</code> or
	 *  <code>INFO</code>; otherwise <code>false</code> is returned
	 */
	public boolean hasInfo() {
		return fSeverity == FATAL || fSeverity == ERROR || fSeverity == WARNING || fSeverity == INFO;
	}

	/*
	 * (non java-doc)
	 * for debugging only
	 */
	@Override
	public String toString() {
		StringBuilder buff= new StringBuilder();
		buff.append("<") //$NON-NLS-1$
			.append(getSeverityString(fSeverity)).append("\n"); //$NON-NLS-1$
		if (!isOK()) {
			for (RefactoringStatusEntry refactoringStatusEntry : fEntries) {
				buff.append("\t") //$NON-NLS-1$
					.append(refactoringStatusEntry).append("\n"); //$NON-NLS-1$
			}
		}
		buff.append(">"); //$NON-NLS-1$
		return buff.toString();
	}

	/*
	 * non java-doc
	 * for debugging only not for nls
	 */
	/* package */static String getSeverityString(int severity) {
		Assert.isTrue(severity >= OK && severity <= FATAL);
		if (severity == RefactoringStatus.OK)
			return "OK"; //$NON-NLS-1$
		if (severity == RefactoringStatus.INFO)
			return "INFO"; //$NON-NLS-1$
		if (severity == RefactoringStatus.WARNING)
			return "WARNING"; //$NON-NLS-1$
		if (severity == RefactoringStatus.ERROR)
			return "ERROR"; //$NON-NLS-1$
		if (severity == RefactoringStatus.FATAL)
			return "FATALERROR"; //$NON-NLS-1$
		return null;
	}
}
