/*******************************************************************************
 * Copyright (c) 2016, 2020 Lars Vogel 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:
 *     Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
 *     Axel Richard <axel.richard@obeo.fr> - Bug 486644, Bug 492438
 *     Mikael Barbero <mikael@eclipse.org> - Bug 486644
 *******************************************************************************/
package org.eclipse.ui.internal.ide.addons;

import java.util.concurrent.TimeUnit;

import javax.annotation.PreDestroy;
import javax.inject.Inject;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.di.extensions.Preference;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.workbench.UIEvents;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.IPreferenceConstants;
import org.eclipse.ui.progress.WorkbenchJob;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

/**
 * Model add-on for automatic save of dirty editors.
 *
 * @since 3.12
 */
public class SaveAllDirtyPartsAddon {

	private final class DirtyEventHandler implements EventHandler {
		@Override
		public void handleEvent(Event event) {
			if (isAutoSaveActive) {
				Object isDirty = event.getProperty(UIEvents.EventTags.NEW_VALUE);
				if (isDirty instanceof Boolean && (Boolean) isDirty) {
					autoSaveJob.schedule(autoSaveInterval);
					addIdleListenerToWorkbenchDisplay();
				} else if (noDirtyEditor(PlatformUI.getWorkbench())) {
					removeIdleListenerFromWorkbenchDisplay();
					autoSaveJob.cancel();
				}
			}
		}

		private boolean noDirtyEditor(IWorkbench workbench) {
			IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
			for (IWorkbenchWindow window : windows) {
				IWorkbenchPage p = window.getActivePage();
				if (p != null) {
					for (IEditorReference editorRef : p.getEditorReferences()) {
						if (editorRef.isDirty()) {
							return false;
						}
					}
				}
			}
			return true;
		}
	}

	private final class IdleListener implements Listener {
		@Override
		public void handleEvent(org.eclipse.swt.widgets.Event event) {
			// a menu has been hidden, re-schedule the job.
			if (event.type == SWT.Hide && event.widget instanceof Menu) {
				autoSaveJob.cancel();
				autoSaveJob.schedule(autoSaveInterval);
			} else if (autoSaveJob.getState() == Job.SLEEPING) {
				// a menu has been shown, cancel the job. The job will be
				// re-schedule when the menu will be hidden.
				if (event.type == SWT.Show && event.widget instanceof Menu) {
					autoSaveJob.cancel();
				} else {
					// the user has pressed a key or has clicked somewhere
					// (see #addIdleListenerToWorkbenchDisplay for exact list of
					// listened events), re-schedule the job if it the previous
					// delay has not expired yet.
					autoSaveJob.cancel();
					autoSaveJob.schedule(autoSaveInterval);
				}
			}
		}
	}

	private final class AutoSaveJob extends WorkbenchJob {

		private AutoSaveJob(String name) {
			super(name);
		}

		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			if (isAutoSaveActive) {
				IWorkbench workbench = PlatformUI.getWorkbench();
				if (workbench != null) {
					IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
					for (IWorkbenchWindow window : windows) {
						IWorkbenchPage p = window.getActivePage();
						// We do not want to save dirty editors when a sub
						// shell is visible or active (e.g. content assist,
						// javadoc hover...)
						if (p != null && !hasVisibleSubShell(getWorkbenchDisplay())) {
							p.saveAllEditors(false);
						} else {
							// reschedule the job. No need to wait for the full
							// interval time as we already have waited for this
							// amount of time.
							this.schedule(autoSaveInterval / 2);
						}
					}
				}
			}
			return Status.OK_STATUS;
		}
	}

	@Inject
	IEventBroker eventBroker;

	private final WorkbenchJob autoSaveJob;

	private final EventHandler dirtyHandler;

	private final Listener idleListener;

	private boolean isAutoSaveActive;

	private long autoSaveInterval;

	/**
	 * @param autoSave
	 */
	@Inject
	@Optional
	public void setAutoSave(
			@Preference(value = IPreferenceConstants.SAVE_AUTOMATICALLY, nodePath = "org.eclipse.ui.workbench") boolean autoSave) {
		isAutoSaveActive = autoSave;
		if (isAutoSaveActive) {
			eventBroker.subscribe(UIEvents.Dirtyable.TOPIC_DIRTY, dirtyHandler);
			autoSaveJob.schedule();
		} else {
			eventBroker.unsubscribe(dirtyHandler);
		}
	}

	/**
	 * @param newInterval
	 */
	@Inject
	@Optional
	public void autoSaveIntervalChanged(
			@Preference(value = IPreferenceConstants.SAVE_AUTOMATICALLY_INTERVAL, nodePath = "org.eclipse.ui.workbench") int newInterval) {
		autoSaveInterval = TimeUnit.SECONDS.toMillis(newInterval);
	}

	/**
	 * Default constructor
	 */
	public SaveAllDirtyPartsAddon() {
		autoSaveJob = new AutoSaveJob("Auto save all editors"); //$NON-NLS-1$
		// auto-save job should not be displayed in the progress view.
		autoSaveJob.setSystem(true);
		idleListener = new IdleListener();
		dirtyHandler = new DirtyEventHandler();
	}

	private void addIdleListenerToWorkbenchDisplay() {
		Display display = getWorkbenchDisplay();
		if (display != null && !display.isDisposed()) {
			display.addFilter(SWT.KeyUp, idleListener);
			display.addFilter(SWT.MouseUp, idleListener);
			display.addFilter(SWT.Show, idleListener);
			display.addFilter(SWT.Hide, idleListener);
		}
	}

	private void removeIdleListenerFromWorkbenchDisplay() {
		Display display = getWorkbenchDisplay();
		if (display != null && !display.isDisposed()) {
			display.removeFilter(SWT.MouseUp, idleListener);
			display.removeFilter(SWT.KeyUp, idleListener);
			display.removeFilter(SWT.Show, idleListener);
			display.removeFilter(SWT.Hide, idleListener);
		}
	}

	@PreDestroy
	private void shutdown() {
		eventBroker.unsubscribe(dirtyHandler);
		autoSaveJob.cancel();

		final Display display = getWorkbenchDisplay();
		if (display != null && !display.isDisposed()) {
			try {
				display.asyncExec(() -> {
					removeIdleListenerFromWorkbenchDisplay();
					// save jov could have been rescheduled by idleListener
					// before it has been removed
					autoSaveJob.cancel();
				});
			} catch (SWTException ex) {
				// ignore
			}
		}
	}

	/**
	 * Checks whether the current active shell has at least one visible sub
	 * shell. This is especially the case when the content assist popup is
	 * visible or the javadoc on mouse hover is enabled.
	 *
	 * @param display
	 *            the display from which the active shell should be retrieved
	 * @return true if the active shell has at least one sub shell visible,
	 *         false otherwise.
	 */
	private static boolean hasVisibleSubShell(final Display display) {
		if (display != null && !display.isDisposed()) {
			Shell shell = display.getActiveShell();
			if (shell != null) {
				for (Shell subShell : shell.getShells()) {
					if (subShell.isVisible()) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * Returns the current workbench display, null otherwise.
	 *
	 * @return the current workbench display, null otherwise.
	 */
	private static Display getWorkbenchDisplay() {
		final IWorkbench workbench = PlatformUI.getWorkbench();
		if (workbench != null) {
			return workbench.getDisplay();
		}
		return null;
	}
}
