/*******************************************************************************
 * Copyright (c) 2005, 2018 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472784, 474273
 *******************************************************************************/
package org.eclipse.ui.internal.ide.application;

import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobFunction;
import org.eclipse.core.runtime.jobs.Job;
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.ui.PlatformUI;
import org.eclipse.ui.application.IWorkbenchConfigurer;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.internal.ide.Policy;

/**
 * The idle helper detects when the system is idle in order to perform garbage
 * collection in a way that minimizes impact on responsiveness of the UI.
 * The algorithm for determining when to perform a garbage collection
 * is as follows:
 *
 *  - Never gc if there is a test harness present
 *  - Don't gc if background jobs are running
 *  - Don't gc if the keyboard or mouse have been active within IDLE_INTERVAL
 *  - Don't gc if there has been a GC within the minimum gc interval (system property PROP_GC_INTERVAL)
 *  - After a gc, don't gc again until (duration * GC_DELAY_MULTIPLIER) has elapsed.
 *    For example, if a GC takes 100ms and the multiplier is 60, don't gc for at least five seconds
 *  - Never gc again if any single gc takes longer than system property PROP_GC_MAX
 */
class IDEIdleHelper {

	/**
	 * The default minimum time between garbage collections.
	 */
	private static final int DEFAULT_GC_INTERVAL = 60000;

	/**
	 * The default maximum duration for a garbage collection, beyond which
	 * the explicit gc mechanism is automatically disabled.
	 */
	private static final int DEFAULT_GC_MAX = 8000;

	/**
	 * The multiple of the last gc duration before we will consider doing
	 * another one.
	 */
	private static final int GC_DELAY_MULTIPLIER = 60;

	/**
	 * The time interval of no keyboard or mouse events after which the system
	 * is considered idle.
	 */
	private static final int IDLE_INTERVAL = 5000;

	/**
	 * The name of the boolean system property that specifies whether explicit
	 * garbage collection is enabled.
	 */
	private static final String PROP_GC = "ide.gc"; //$NON-NLS-1$

	/**
	 * The name of the integer system property that specifies the minimum time
	 * interval in milliseconds between garbage collections.
	 */
	private static final String PROP_GC_INTERVAL = "ide.gc.interval"; //$NON-NLS-1$

	/**
	 * The name of the integer system property that specifies the maximum
	 * duration for a garbage collection. If this duration is ever exceeded, the
	 * explicit gc mechanism is disabled for the remainder of the session.
	 */
	private static final String PROP_GC_MAX = "ide.gc.max"; //$NON-NLS-1$

	protected IWorkbenchConfigurer configurer;

	private Listener idleListener;

	/**
	 * The last time we garbage collected.
	 */
	private long lastGC = System.currentTimeMillis();

	/**
	 * The maximum gc duration. If this value is exceeded, the
	 * entire explicit gc mechanism is disabled.
	 */
	private int maxGC = DEFAULT_GC_MAX;
	/**
	 * The minimum time interval until the next garbage collection
	 */
	private int minGCInterval = DEFAULT_GC_INTERVAL;

	/**
	 * The time interval until the next garbage collection
	 */
	private int nextGCInterval = DEFAULT_GC_INTERVAL;

	private Job gcJob;

	private Runnable handler;

	/**
	 * Creates and initializes the idle handler
	 * @param aConfigurer The workbench configurer.
	 */
	IDEIdleHelper(IWorkbenchConfigurer aConfigurer) {
		this.configurer = aConfigurer;
		//don't gc while running tests because performance tests are sensitive to timing (see bug 121562)
		if (PlatformUI.getTestableObject().getTestHarness() != null) {
			return;
		}
		String enabled = System.getProperty(PROP_GC);
		// since 4.6 gc is turned off by default if property is missing
		if (enabled == null || enabled.equalsIgnoreCase(Boolean.FALSE.toString())) {
			return;
		}
		//init gc interval
		Integer prop = Integer.getInteger(PROP_GC_INTERVAL);
		if (prop != null && prop.intValue() >= 0) {
			minGCInterval = nextGCInterval = prop.intValue();
		}

		//init max gc interval
		prop = Integer.getInteger(PROP_GC_MAX);
		if (prop != null) {
			maxGC = prop.intValue();
		}

		createGarbageCollectionJob();

		//hook idle handler
		final Display display = configurer.getWorkbench().getDisplay();
		handler = new Runnable() {
					@Override
					public void run() {
						if (!display.isDisposed() && !configurer.getWorkbench().isClosing()) {
							int nextInterval;
							final long start = System.currentTimeMillis();
							//don't garbage collect if background jobs are running
							if (!Job.getJobManager().isIdle()) {
								nextInterval = IDLE_INTERVAL;
							} else if ((start - lastGC) < nextGCInterval) {
								//don't garbage collect if we have collected within the specific interval
								nextInterval = nextGCInterval - (int) (start - lastGC);
							} else {
								gcJob.schedule();
								nextInterval = minGCInterval;
							}
							display.timerExec(nextInterval, this);
						}
					}
				};
		idleListener = event -> display.timerExec(IDLE_INTERVAL, handler);
		display.addFilter(SWT.KeyUp, idleListener);
		display.addFilter(SWT.MouseUp, idleListener);
	}

	/**
	 * Creates the job that performs garbage collection
	 */
	private void createGarbageCollectionJob() {
		gcJob = Job.create(IDEWorkbenchMessages.IDEIdleHelper_backgroundGC, (IJobFunction) monitor -> {
			final Display display = configurer.getWorkbench().getDisplay();
			if (display != null && !display.isDisposed()) {
				final long start = System.currentTimeMillis();
				System.gc();
				System.runFinalization();
				lastGC = start;
				final int duration = (int) (System.currentTimeMillis() - start);
				if (Policy.DEBUG_GC) {
					System.out.println("Explicit GC took: " + duration); //$NON-NLS-1$
				}
				if (duration > maxGC) {
					if (Policy.DEBUG_GC) {
						System.out.println("Further explicit GCs disabled due to long GC"); //$NON-NLS-1$
					}
					shutdown();
				} else {
					// if the gc took a long time, ensure the next gc doesn't happen for awhile
					nextGCInterval = Math.max(minGCInterval, GC_DELAY_MULTIPLIER * duration);
					if (Policy.DEBUG_GC) {
						System.out.println("Next GC to run in: " + nextGCInterval); //$NON-NLS-1$
					}
				}
			}
			return Status.OK_STATUS;
		});
		gcJob.setSystem(true);
	}

	/**
	 * Shuts down the idle helper, removing any installed listeners, etc.
	 */
	void shutdown() {
		if (idleListener == null) {
			return;
		}
		final Display display = configurer.getWorkbench().getDisplay();
		if (display != null && !display.isDisposed()) {
			try {
				display.asyncExec(() -> {
					display.timerExec(-1, handler);
					display.removeFilter(SWT.KeyUp, idleListener);
					display.removeFilter(SWT.MouseUp, idleListener);
				});
			} catch (SWTException ex) {
				// ignore (display might be disposed)
			}
		}
	}
}
