/*******************************************************************************
 * Copyright (c) 2010, 2017 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
 *     Philipp Bumann <bumannp@gmail.com> - Bug 477602
 ******************************************************************************/

package org.eclipse.e4.ui.progress.internal;

import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.inject.Inject;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.ui.di.UISynchronize;
import org.eclipse.e4.ui.progress.IProgressConstants;
import org.eclipse.e4.ui.progress.IProgressService;
import org.eclipse.e4.ui.progress.UIJob;
import org.eclipse.e4.ui.progress.internal.legacy.EventLoopProgressMonitor;
import org.eclipse.e4.ui.progress.internal.legacy.PlatformUI;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class ProgressServiceImpl implements IProgressService {

	private static final String IMAGE_KEY = "org.eclipse.ui.progress.images"; //$NON-NLS-1$

	private Hashtable<Object, String> imageKeyTable = new Hashtable<>();

	@Inject
	@Optional
	ProgressManager progressManager;

	@Inject
	@Optional
	FinishedJobs finishedJobs;

	@Inject
	@Optional
	ContentProviderFactory contentProviderFactory;

	@Inject
	@Optional
	UISynchronize uiSynchronize;

	@Override
	public int getLongOperationTime() {
		return 800;
	}

	@Override
	public void registerIconForFamily(ImageDescriptor icon, Object family) {
		String key = IMAGE_KEY + imageKeyTable.size();
		imageKeyTable.put(family, key);
		ImageRegistry registry = JFaceResources.getImageRegistry();

		// Avoid registering twice
		if (registry.getDescriptor(key) == null) {
			registry.put(key, icon);
		}
	}

	@Override
	public void runInUI(IRunnableContext context,
			IRunnableWithProgress runnable, ISchedulingRule rule)
			throws InvocationTargetException, InterruptedException {
		final RunnableWithStatus runnableWithStatus = new RunnableWithStatus(
				context,
				runnable, rule);
		uiSynchronize.syncExec(() -> BusyIndicator.showWhile(getDisplay(), runnableWithStatus));

		IStatus status = runnableWithStatus.getStatus();
		if (!status.isOK()) {
			Throwable exception = status.getException();
			if (exception instanceof InvocationTargetException)
				throw (InvocationTargetException) exception;
			else if (exception instanceof InterruptedException)
				throw (InterruptedException) exception;
			else // should be OperationCanceledException
				throw new InterruptedException(exception.getMessage());
		}
	}

	@Override
	public Image getIconFor(Job job) {
		Enumeration<Object> families = imageKeyTable.keys();
		while (families.hasMoreElements()) {
			Object next = families.nextElement();
			if (job.belongsTo(next)) {
				return JFaceResources.getImageRegistry().get(imageKeyTable.get(next));
			}
		}
		return null;
	}

	@Override
	public void busyCursorWhile(final IRunnableWithProgress runnable)
			throws InvocationTargetException, InterruptedException {
		final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog(
				ProgressManagerUtil.getDefaultParent(), this, progressManager,
				contentProviderFactory, finishedJobs);
		dialog.setOpenOnRun(false);
		final InvocationTargetException[] invokes = new InvocationTargetException[1];
		final InterruptedException[] interrupt = new InterruptedException[1];
		// show a busy cursor until the dialog opens
		Runnable dialogWaitRunnable = () -> {
			try {
				dialog.setOpenOnRun(false);
				setUserInterfaceActive(false);
				dialog.run(true, true, runnable);
			} catch (InvocationTargetException e1) {
				invokes[0] = e1;
			} catch (InterruptedException e2) {
				interrupt[0] = e2;
			} finally {
				setUserInterfaceActive(true);
			}
		};
		busyCursorWhile(dialogWaitRunnable, dialog);
		if (invokes[0] != null) {
			throw invokes[0];
		}
		if (interrupt[0] != null) {
			throw interrupt[0];
		}
	}

	@Override
	public void run(boolean fork, boolean cancelable,
			IRunnableWithProgress runnable) throws InvocationTargetException,
			InterruptedException {
		if (fork == false || cancelable == false) {
			// backward compatible code
			final ProgressMonitorJobsDialog dialog = new ProgressMonitorJobsDialog(
					null, this, progressManager, contentProviderFactory,
					finishedJobs);
			dialog.run(fork, cancelable, runnable);
			return;
		}

		busyCursorWhile(runnable);
	}


	@Override
	public void showInDialog(Shell shell, Job job) {
		if (shouldRunInBackground()) {
			return;
		}

		final ProgressMonitorFocusJobDialog dialog = new ProgressMonitorFocusJobDialog(
				shell, this, progressManager, contentProviderFactory,
				finishedJobs);
		dialog.show(job, shell);
	}

	/**
	 * Return whether or not dialogs should be run in the background
	 *
	 * @return <code>true</code> if the dialog should not be shown.
	 */
	protected boolean shouldRunInBackground() {
		return Preferences.getBoolean(IProgressConstants.RUN_IN_BACKGROUND);
	}

	private static class RunnableWithStatus implements Runnable {

		IStatus status = Status.OK_STATUS;
		private final IRunnableContext context;
		private final IRunnableWithProgress runnable;
		private final ISchedulingRule rule;

		public RunnableWithStatus(IRunnableContext context,
				IRunnableWithProgress runnable, ISchedulingRule rule) {
			this.context = context;
			this.runnable = runnable;
			this.rule = rule;
		}

		@Override
		public void run() {
			IJobManager manager = Job.getJobManager();
			try {
				manager.beginRule(rule, getEventLoopMonitor());
				context.run(false, false, runnable);
			} catch (InvocationTargetException | InterruptedException | OperationCanceledException e) {
				status = new Status(IStatus.ERROR, IProgressConstants.PLUGIN_ID, e
						.getMessage(), e);
			} finally {
				manager.endRule(rule);
			}
		}

		/**
		 * Get a progress monitor that forwards to an event loop monitor.
		 * Override #setBlocked() so that we always open the blocked dialog.
		 *
		 * @return the monitor on the event loop
		 */
		private IProgressMonitor getEventLoopMonitor() {

			if (PlatformUI.isWorkbenchStarting())
				return new NullProgressMonitor();

			return new EventLoopProgressMonitor(new NullProgressMonitor()) {

				@Override
				public void setBlocked(IStatus reason) {

					// Set a shell to open with as we want to create
					// this
					// even if there is a modal shell.
					Dialog.getBlockedHandler().showBlocked(
							ProgressManagerUtil.getDefaultParent(), this,
							reason, getTaskName());
				}
			};
		}

		public IStatus getStatus() {
			return status;
		}

	}

	/**
	 * Show the busy cursor while the runnable is running. Schedule a job to
	 * replace it with a progress dialog.
	 *
	 * @param dialogWaitRunnable
	 * @param dialog
	 */
	private void busyCursorWhile(Runnable dialogWaitRunnable,
			ProgressMonitorJobsDialog dialog) {
		// create the job that will open the dialog after a delay
		scheduleProgressMonitorJob(dialog);
		final Display display = getDisplay();
		if (display == null) {
			return;
		}
		// show a busy cursor until the dialog opens
		BusyIndicator.showWhile(display, dialogWaitRunnable);
	}

	/**
	 * Schedule the job that will open the progress monitor dialog
	 *
	 * @param dialog
	 *            the dialog to open
	 */
	private void scheduleProgressMonitorJob(
			final ProgressMonitorJobsDialog dialog) {

		Job updateJob = UIJob.create(ProgressMessages.ProgressManager_openJobName, monitor -> {
			setUserInterfaceActive(true);
			if (ProgressManagerUtil.safeToOpen(dialog, null)) {
				dialog.open();
			}
		});
		updateJob.setSystem(true);
		updateJob.schedule(getLongOperationTime());

	}

	/**
	 * Iterate through all of the windows and set them to be disabled or enabled
	 * as appropriate.'
	 *
	 * @param active
	 *            The set the windows will be set to.
	 */
	private void setUserInterfaceActive(boolean active) {
		Shell[] shells = getDisplay().getShells();
		if (active) {
			for (Shell shell : shells) {
				if (!shell.isDisposed()) {
					shell.setEnabled(active);
				}
			}
		} else {
			// Deactive shells in reverse order
			for (int i = shells.length - 1; i >= 0; i--) {
				if (!shells[i].isDisposed()) {
					shells[i].setEnabled(active);
				}
			}
		}
	}

	protected Display getDisplay() {
		return Services.getInstance().getDisplay();
	}

}
