/*******************************************************************************
 * Copyright (c) 2009, 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.ui.internal.statushandlers;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.progress.FinishedJobs;
import org.eclipse.ui.internal.progress.FinishedJobs.KeptJobsListener;
import org.eclipse.ui.internal.progress.JobInfo;
import org.eclipse.ui.internal.progress.JobTreeElement;
import org.eclipse.ui.internal.progress.StatusAdapterHelper;
import org.eclipse.ui.progress.IProgressConstants;
import org.eclipse.ui.statushandlers.IStatusAdapterConstants;
import org.eclipse.ui.statushandlers.StatusAdapter;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.statushandlers.StatusManager.INotificationTypes;

/**
 * <p>
 * This class is the actual dialog manager. Status dialog is a very bad thing to
 * manage, because it can switch its modality. As you know this is not possible,
 * so dialog is closed and then reopened. This approach makes impossible to keep
 * dialog data inside dialog class, because the dialog will be disposed.
 * </p>
 * <p>
 * To overcome this issue, a {@link Map} dialogState is introduced, which holds
 * the actual state (and configuration) of the dialog. This map is passed to all
 * dialog subcomponents.
 * </p>
 * <p>
 * Dialog state variables are defined in {@link IStatusDialogConstants}.
 * </p>
 *
 * @since 3.6
 * @noextend This class is not intended to be subclassed by clients.
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public class WorkbenchStatusDialogManagerImpl implements KeptJobsListener {

	static final QualifiedName HINT = new QualifiedName(IStatusAdapterConstants.PROPERTY_PREFIX, "hint"); //$NON-NLS-1$

	private final class StatusDialogDisposeListener implements DisposeListener {

		@Override
		public void widgetDisposed(org.eclipse.swt.events.DisposeEvent e) {
			cleanUp();
		}
	}

	private DisposeListener disposeListener = new StatusDialogDisposeListener();

	/**
	 * This field stores the real dialog that appears to the user.
	 */
	private InternalDialog dialog;

	/**
	 * This variable holds the real state of the dialog.
	 */
	private Map<Object, Object> dialogState = new HashMap<>();

	/**
	 * Returns whether the given StatusAdapter object should be displayed.
	 *
	 * @param statusAdapter a status object
	 * @return <code>true</code> if the given status should be displayed, and
	 *         <code>false</code> otherwise
	 * @see org.eclipse.core.runtime.IStatus#matches(int)
	 */
	public boolean shouldAccept(StatusAdapter statusAdapter) {
		IStatus status = statusAdapter.getStatus();
		IStatus[] children = status.getChildren();
		int mask = ((Integer) dialogState.get(IStatusDialogConstants.MASK)).intValue();
		boolean handleOKStatuses = ((Boolean) dialogState.get(IStatusDialogConstants.HANDLE_OK_STATUSES))
				.booleanValue();
		if (children == null || children.length == 0) {
			return status.matches(mask) || (handleOKStatuses && status.isOK());
		}
		for (IStatus child : children) {
			if (child.matches(mask)) {
				return true;
			}
		}
		if (handleOKStatuses && status.isOK()) {
			return true;
		}
		return false;
	}

	/**
	 * Creates workbench status dialog.
	 *
	 * @param displayMask the mask used to filter the handled
	 *                    <code>StatusAdapter</code> objects, the mask is a logical
	 *                    sum of status severities
	 * @param dialogTitle the title of the dialog. If null, than default will be
	 *                    used.
	 */
	public WorkbenchStatusDialogManagerImpl(int displayMask, String dialogTitle) {

		Assert.isNotNull(Display.getCurrent(), "WorkbenchStatusDialogManager must be instantiated in UI thread"); //$NON-NLS-1$

		dialogState = initDialogState(dialogState, displayMask, dialogTitle);
		FinishedJobs.getInstance().addListener(this);
	}

	/**
	 * This method creates the initial state of the dialog.
	 *
	 * @param dialogState - the map to fill in.
	 * @param displayMask - the mask suggesting which statuses should be displayed
	 * @param dialogTitle - the dialog title.
	 * @return populated dialogState
	 */
	public Map<Object, Object> initDialogState(Map<Object, Object> dialogState, int displayMask, String dialogTitle) {
		dialogState.put(IStatusDialogConstants.MASK, Integer.valueOf(displayMask));
		dialogState.put(IStatusDialogConstants.TITLE,
				dialogTitle == null ? JFaceResources.getString("Problem_Occurred") : //$NON-NLS-1$
						dialogTitle);
		dialogState.put(IStatusDialogConstants.HANDLE_OK_STATUSES, Boolean.FALSE);

		dialogState.put(IStatusDialogConstants.SHOW_SUPPORT, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.ENABLE_DEFAULT_SUPPORT_AREA, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.DETAILS_OPENED, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.TRAY_OPENED, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.HIDE_SUPPORT_BUTTON, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.STATUS_ADAPTERS, Collections.synchronizedSet(new LinkedHashSet<>()));
		dialogState.put(IStatusDialogConstants.STATUS_MODALS, new HashMap<>());
		dialogState.put(IStatusDialogConstants.LABEL_PROVIDER, new LabelProviderWrapper(dialogState));
		dialogState.put(IStatusDialogConstants.MODALITY_SWITCH, Boolean.FALSE);
		return dialogState;
	}

	/**
	 * <p>
	 * Adds a new {@link StatusAdapter} to the status adapters list in the dialog.
	 * </p>
	 * <p>
	 * If the dialog is already visible, the status adapter will be shown
	 * immediately. Otherwise, the dialog with the added status adapter will show
	 * up, if all conditions below are false.
	 * </p>
	 * <ul>
	 * <li>the status adapter has
	 * {@link IProgressConstants#NO_IMMEDIATE_ERROR_PROMPT_PROPERTY} set to
	 * true</li>
	 * </ul>
	 * <p>
	 * All not shown status adapters will be displayed as soon as the dialog shows
	 * up.
	 * </p>
	 *
	 * @param modal         <code>true</code> if the dialog should be modal,
	 *                      <code>false</code> otherwise
	 * @param statusAdapter the status adapter
	 */
	public void addStatusAdapter(final StatusAdapter statusAdapter, final boolean modal) {
		if (ErrorDialog.AUTOMATED_MODE == true) {
			return;
		}
		try {
			doAddStatusAdapter(statusAdapter, modal);
		} catch (Exception e) {
			// if dialog is open, dispose it (and all child controls)
			if (!isDialogClosed()) {
				dialog.getShell().dispose();
			}
			// reset the state
			cleanUp();
			// log original problem
			// TODO: check if is it possible to discover duplicates
			WorkbenchPlugin.log(statusAdapter.getStatus());
			// log the problem with status handling
			WorkbenchPlugin.log(e);
		}
	}

	private boolean isDialogClosed() {
		return dialog == null || dialog.getShell() == null || dialog.getShell().isDisposed();
	}

	private void cleanUp() {
		dialog = null;
		getErrors().clear();
		getModals().clear();
		dialogState.put(IStatusDialogConstants.DETAILS_OPENED, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.TRAY_OPENED, Boolean.FALSE);
		dialogState.put(IStatusDialogConstants.MODALITY_SWITCH, Boolean.FALSE);
		dialogState.remove(IStatusDialogConstants.CURRENT_STATUS_ADAPTER);
	}

	private void doAddStatusAdapter(final StatusAdapter statusAdapter, final boolean modal) {

		if (!PlatformUI.isWorkbenchRunning()) {
			// we are shutting down, so just log
			WorkbenchPlugin.log(statusAdapter.getStatus());
			return;
		}

		// if statusAdapter does not match the mask, ignore it
		if (!shouldAccept(statusAdapter)) {
			return;
		}

		// Add the error in the UI thread to ensure thread safety in the
		// dialog
		if (isDialogClosed()) {

			getErrors().add(statusAdapter);
			getModals().put(statusAdapter, Boolean.valueOf(modal));
			// Delay prompting if the status adapter property is set
			if (shouldPrompt(statusAdapter)) {
				// notify all interested parties that status adapters will be
				// handled
				StatusManager.getManager().fireNotification(INotificationTypes.HANDLED,
						getErrors().toArray(new StatusAdapter[] {}));

				if (dialog == null) {
					setSelectedStatusAdapter(statusAdapter);
					dialog = new InternalDialog(dialogState, shouldBeModal());
					dialog.create();
					dialog.getShell().addDisposeListener(disposeListener);
					boolean showSupport = ((Boolean) dialogState.get(IStatusDialogConstants.SHOW_SUPPORT))
							.booleanValue();
					if (showSupport) {
						dialog.openTray();
						dialog.getShell().setLocation(dialog.getInitialLocation(dialog.getShell().getSize()));
					}
					dialog.open();
				}
				dialog.refresh();
				dialog.refreshDialogSize();
			}

		} else {
			StatusManager.getManager().fireNotification(INotificationTypes.HANDLED,
					new StatusAdapter[] { statusAdapter });
			if (statusAdapter.getProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY) != null) {
				statusAdapter.setProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY, Boolean.FALSE);
			}
			openStatusDialog(modal, statusAdapter);
		}
	}

	/**
	 * Gets a collection of status adapters that were passed to the dialog.
	 *
	 * @return collection of {@link StatusAdapter} objects
	 */
	public Collection<StatusAdapter> getStatusAdapters() {
		return Collections.unmodifiableCollection(getErrors());
	}

	/**
	 * Opens status dialog with particular statusAdapter selected.
	 *
	 * @param modal         decides if window is modal or not.
	 * @param statusAdapter status adapter to be selected.
	 */
	private void openStatusDialog(final boolean modal, final StatusAdapter statusAdapter) {
		getErrors().add(statusAdapter);
		getModals().put(statusAdapter, Boolean.valueOf(modal));
		boolean shouldBeModal = shouldBeModal();
		if (shouldBeModal ^ dialog.isModal()) {
			dialog.getShell().removeDisposeListener(disposeListener);
			dialogState.put(IStatusDialogConstants.MODALITY_SWITCH, Boolean.TRUE);
			dialog.close();
			dialog = new InternalDialog(dialogState, modal);
			dialog.open();
			dialog.getShell().addDisposeListener(disposeListener);
			dialogState.put(IStatusDialogConstants.MODALITY_SWITCH, Boolean.FALSE);
		}
		dialog.refresh();
	}

	/**
	 * Sets current status adapter.
	 *
	 * @param statusAdapter The statusAdapter to set.
	 */
	public void setSelectedStatusAdapter(StatusAdapter statusAdapter) {
		dialogState.put(IStatusDialogConstants.CURRENT_STATUS_ADAPTER, statusAdapter);
	}

	/**
	 * Sets new label provider for the status list. This label provider is used also
	 * to display the second message on the dialog if only one status is available.
	 *
	 * <p>
	 * This method is no longer recommended to use as it is impossible to achieve
	 * consistent behavior after changing only one label provider.
	 * </p>
	 *
	 * @deprecated As of 3.5 {@link #setMessageDecorator} is recommended.
	 *
	 * @param labelProvider a label provider to be used when displaying status
	 *                      adapters.
	 */
	@Deprecated
	public void setStatusListLabelProvider(ITableLabelProvider labelProvider) {
		Assert.isLegal(labelProvider != null, "Label Provider cannot be null"); //$NON-NLS-1$
		dialogState.put(IStatusDialogConstants.CUSTOM_LABEL_PROVIDER, labelProvider);
	}

	/**
	 * Decides if dialog should be modal. Dialog will be modal if any of the
	 * statuses contained by StatusAdapters had been reported with
	 * {@link StatusManager#BLOCK} flag.
	 *
	 * @return true if any StatusHandler should be displayed in modal window
	 */
	public boolean shouldBeModal() {
		Map<?, ?> modals = (Map<?, ?>) dialogState.get(IStatusDialogConstants.STATUS_MODALS);
		for (Object value : modals.values()) {
			if (value instanceof Boolean) {
				Boolean b = (Boolean) value;
				if (b.booleanValue()) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Checks if the user should be prompted immediately about {@link StatusAdapter}
	 *
	 * @param statusAdapter to be checked.
	 * @return true if the statusAdapter should be prompted, false otherwise.
	 */
	public boolean shouldPrompt(final StatusAdapter statusAdapter) {
		Object noPromptProperty = statusAdapter.getProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY);

		boolean prompt = true;
		if (noPromptProperty instanceof Boolean) {
			prompt = !((Boolean) noPromptProperty).booleanValue();
		}
		return prompt;
	}

	/**
	 * Gets the shell of the managed dialog.
	 *
	 * @return Shell or null
	 *
	 */
	public Shell getShell() {
		if (this.dialog == null)
			return null;
		return this.dialog.getShell();
	}

	/**
	 * <p>
	 * This methods sets up the decorator, which is used to modify displayed strings
	 * extracted from StatusAdapter. The decorator should be used to remove
	 * technical codes from the dialog, f.e. following message "<i>ERR2008 Invalid
	 * password</i>" can be translated into "<i>Invalid password</i>".
	 * </p>
	 * <p>
	 * The decorator will be applied only to messages extracted from StatusAdapter
	 * (predefined messages like "This status has children statuses. See 'Details'
	 * for more information." are not affected.
	 * </p>
	 * <p>
	 * This method should not be used together with
	 * {@link #setStatusListLabelProvider(ITableLabelProvider)}.
	 * </p>
	 *
	 * @param decorator - the decorator to be set. Only
	 *                  {@link ILabelDecorator#decorateText(String, Object)} method
	 *                  will be used. This method should return <code>null</code> if
	 *                  and only if the first argument is null. StatusAdapter is
	 *                  passed as second parameter. Other methods should have
	 *                  default behavior as they may be used in future versions of
	 *                  the dialog.
	 * @since 3.5
	 */
	public void setMessageDecorator(ILabelDecorator decorator) {
		dialogState.put(IStatusDialogConstants.DECORATOR, decorator);
	}

	/**
	 * This method sets various properties on the manager.
	 *
	 * @param key   a key of the property to be set.
	 * @param value a value of the property to be set. The value must be of type
	 *              specified by the property key. <code>null</code> should never be
	 *              passed unless the property key javadoc allows for that.
	 * @since 3.5
	 */
	public void setProperty(Object key, Object value) {
		dialogState.put(key, value);
	}

	/**
	 * This method gets various dialog properties.
	 *
	 * @param key a key of the property to be get.
	 * @return a value of the property. The value will be of type specified by the
	 *         property key. <code>null</code> can be returned.
	 * @since 3.5
	 */
	public Object getProperty(Object key) {
		if (key == IStatusDialogConstants.SHELL) {
			return getShell();
		}
		if (key == IStatusDialogConstants.MANAGER_IMPL) {
			return this;
		}
		return dialogState.get(key);
	}

	/**
	 * This method makes the dialog to be similar to the JFace ErrorDialog. The
	 * dialog handles {@link StatusAdapter}s wrapping {@link IStatus} with severity
	 * {@link IStatus#OK}, does not display the link to the error log, does not
	 * display the link to the support area but always opens it.
	 *
	 * @see ErrorDialog
	 * @since 3.6
	 */
	public void enableErrorDialogCompatibility() {
		setProperty(IStatusDialogConstants.ERRORLOG_LINK, Boolean.FALSE);
		setProperty(IStatusDialogConstants.HANDLE_OK_STATUSES, Boolean.TRUE);
		setProperty(IStatusDialogConstants.SHOW_SUPPORT, Boolean.TRUE);
		setProperty(IStatusDialogConstants.HIDE_SUPPORT_BUTTON, Boolean.TRUE);
	}

	/**
	 * This method is public for testing purposes only.
	 *
	 * @return Returns the dialog.
	 */
	public InternalDialog getDialog() {
		return dialog;
	}

	/**
	 * This method is public for testing purposes only.
	 *
	 * @param dialog The dialog to set.
	 */
	public void setDialog(InternalDialog dialog) {
		this.dialog = dialog;
	}

	/**
	 * This method is public for testing purposes only.
	 *
	 * @return dialog state.
	 */
	public Map getDialogState() {
		return dialogState;
	}

	/**
	 * Utility method to access StatusAdapters
	 *
	 * @return Collection of StatusAdapters
	 */
	@SuppressWarnings("unchecked")
	private Collection<StatusAdapter> getErrors() {
		return (Collection<StatusAdapter>) dialogState.get(IStatusDialogConstants.STATUS_ADAPTERS);
	}

	/**
	 * Utility method to access StatusAdapter modal flag.
	 *
	 * @return Collection of StatusAdapter modal flag.
	 */
	private Map<Object, Object> getModals() {
		return (Map<Object, Object>) dialogState.get(IStatusDialogConstants.STATUS_MODALS);
	}

	@Override
	public void finished(JobTreeElement jte) {
		// no-op
	}

	@Override
	public void removed(JobTreeElement jte) {
		if (jte instanceof JobInfo) {
			JobInfo jobInfo = (JobInfo) jte;
			StatusAdapter statusAdapter = StatusAdapterHelper.getInstance().getStatusAdapter(jobInfo);
			if (statusAdapter != null) {
				getErrors().remove(statusAdapter);
			}
		} else if (jte == null) {
			StatusAdapterHelper.getInstance().clear();
			getErrors().clear();
		}

	}
}
