/*******************************************************************************
 * Copyright (c) 2006, 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
 *     Brad Reynolds - bug 168153
 *     Boris Bokowski - bug 245647
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 *******************************************************************************/

package org.eclipse.core.databinding.observable;

import java.util.Timer;
import java.util.TimerTask;

import org.eclipse.core.databinding.util.Policy;
import org.eclipse.core.internal.databinding.observable.Queue;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

/**
 * A realm defines a context from which objects implementing {@link IObservable}
 * must be accessed, and on which these objects will notify their listeners. To
 * bridge between observables from different realms, subclasses of
 * <code>Binding</code> can be used.
 * <p>
 * A block of code is said to be executing within a realm if calling
 * {@link #isCurrent()} from that block returns true. Code reached by calling
 * methods from that block will execute within the same realm, with the
 * exception of methods on this class that can be used to execute code within a
 * specific realm. Clients can use {@link #syncExec(Runnable)},
 * {@link #asyncExec(Runnable)}, or {@link #exec(Runnable)} to execute a
 * runnable within this realm. Note that using {@link #syncExec(Runnable)} can
 * lead to deadlocks and should be avoided if the current thread holds any
 * locks.
 * </p>
 * <p>
 * It is instructive to think about possible implementations of Realm: It can be
 * based on executing on a designated thread such as a UI thread, or based on
 * holding a lock. In the former case, calling syncExec on a realm that is not
 * the current realm will execute the given runnable on a different thread (the
 * designated thread). In the latter case, calling syncExec may execute the
 * given runnable on the calling thread, but calling
 * {@link #asyncExec(Runnable)} will execute the given runnable on a different
 * thread. Therefore, no assumptions can be made about the thread that will
 * execute arguments to {@link #asyncExec(Runnable)},
 * {@link #syncExec(Runnable)}, or {@link #exec(Runnable)}.
 * </p>
 * <p>
 * It is possible that a block of code is executing within more than one realm.
 * This can happen for implementations of Realm that are based on holding a lock
 * but don't use a separate thread to run runnables given to
 * {@link #syncExec(Runnable)}. Realm implementations of this kind should be
 * appropriately documented because it increases the opportunity for deadlock.
 * </p>
 * <p>
 * Some implementations of {@link IObservable} provide constructors which do not
 * take a Realm argument and are specified to create the observable instance
 * with the current default realm. The default realm can be set for the
 * currently executing thread by using {@link #runWithDefault(Realm, Runnable)}.
 * Note that the default realm does not have to be the current realm.
 * </p>
 * <p>
 * Subclasses must override at least one of asyncExec()/syncExec(). For realms
 * based on a designated thread, it may be easier to implement asyncExec and
 * keep the default implementation of syncExec. For realms based on holding a
 * lock, it may be easier to implement syncExec and keep the default
 * implementation of asyncExec.
 * </p>
 *
 * @since 1.0
 *
 * @see IObservable
 */
public abstract class Realm {

	private static ThreadLocal<Realm> defaultRealm = new ThreadLocal<>();

	/**
	 * Returns the default realm for the calling thread, or <code>null</code> if
	 * no default realm has been set.
	 *
	 * @return the default realm, or <code>null</code>
	 */
	public static Realm getDefault() {
		return defaultRealm.get();
	}

	/**
	 * Sets the default realm for the calling thread, returning the current
	 * default thread. This method is inherently unsafe, it is recommended to
	 * use {@link #runWithDefault(Realm, Runnable)} instead. This method is
	 * exposed to subclasses to facilitate testing.
	 *
	 * @param realm
	 *            the new default realm, or <code>null</code>
	 * @return the previous default realm, or <code>null</code>
	 */
	protected static Realm setDefault(Realm realm) {
		Realm oldValue = getDefault();
		defaultRealm.set(realm);
		return oldValue;
	}

	/**
	 * @return true if the caller is executing in this realm. This method must
	 *         not have side-effects (such as, for example, implicitly placing
	 *         the caller in this realm).
	 */
	abstract public boolean isCurrent();

	private Thread workerThread;

	private volatile Timer timer;

	Queue workQueue = new Queue();

	/**
	 * Runs the given runnable. If an exception occurs within the runnable, it
	 * is logged and not re-thrown. If the runnable implements
	 * {@link ISafeRunnable}, the exception is passed to its
	 * <code>handleException<code> method.
	 *
	 * @param runnable
	 */
	protected static void safeRun(final Runnable runnable) {
		ISafeRunnable safeRunnable;
		if (runnable instanceof ISafeRunnable) {
			safeRunnable = (ISafeRunnable) runnable;
		} else {
			safeRunnable = new ISafeRunnable() {
				@Override
				public void handleException(Throwable exception) {
					Policy.getLog()
							.log(new Status(
									IStatus.ERROR,
									Policy.JFACE_DATABINDING,
									IStatus.OK,
									"Unhandled exception: " + exception.getMessage(), exception)); //$NON-NLS-1$
				}
				@Override
				public void run() throws Exception {
					runnable.run();
				}
			};
		}
		SafeRunner.run(safeRunnable);
	}

	/**
	 * Causes the <code>run()</code> method of the runnable to be invoked from
	 * within this realm. If the caller is executing in this realm, the
	 * runnable's run method is invoked directly, otherwise it is run at the
	 * next reasonable opportunity using asyncExec.
	 * <p>
	 * If the given runnable is an instance of {@link ISafeRunnable}, its
	 * exception handler method will be called if any exceptions occur while
	 * running it. Otherwise, the exception will be logged.
	 * </p>
	 *
	 * @param runnable
	 */
	public void exec(Runnable runnable) {
		if (isCurrent()) {
			safeRun(runnable);
		} else {
			asyncExec(runnable);
		}
	}

	/**
	 * Causes the <code>run()</code> method of the runnable to be invoked from
	 * within this realm at the next reasonable opportunity. The caller of this
	 * method continues to run in parallel, and is not notified when the
	 * runnable has completed.
	 * <p>
	 * If the given runnable is an instance of {@link ISafeRunnable}, its
	 * exception handler method will be called if any exceptions occur while
	 * running it. Otherwise, the exception will be logged.
	 * </p>
	 * <p>
	 * Subclasses should use {@link #safeRun(Runnable)} to run the runnable.
	 * </p>
	 *
	 * @param runnable
	 */
	public void asyncExec(Runnable runnable) {
		synchronized (workQueue) {
			ensureWorkerThreadIsRunning();
			workQueue.enqueue(runnable);
			workQueue.notifyAll();
		}
	}

	/**
	 * Causes the <code>run()</code> method of the runnable to be invoked from
	 * within this realm after the specified number of milliseconds have
	 * elapsed. If milliseconds is less than zero, the runnable is not executed.
	 * The caller of this method continues to run in parallel, and is not
	 * notified when the runnable has completed.
	 * <p>
	 * If the given runnable is an instance of {@link ISafeRunnable}, its
	 * exception handler method will be called if any exceptions occur while
	 * running it. Otherwise, the exception will be logged.
	 * </p>
	 * <p>
	 * Subclasses should use {@link #safeRun(Runnable)} to run the runnable.
	 * </p>
	 *
	 * @param milliseconds
	 * @param runnable
	 * @since 1.2
	 */
	public void timerExec(int milliseconds, final Runnable runnable) {
		if (milliseconds < 0) {
			return;
		} else if (milliseconds == 0) {
			asyncExec(runnable);
		} else {
			synchronized (workQueue) {
				if (timer == null) {
					timer = new Timer(true);
				}
				timer.schedule(new TimerTask() {
					@Override
					public void run() {
						asyncExec(runnable);
					}
				}, milliseconds);
			}
		}

	}

	/**
	 *
	 */
	private void ensureWorkerThreadIsRunning() {
		if (workerThread == null) {
			workerThread = new Thread() {
				@Override
				public void run() {
					try {
						while (true) {
							Runnable work = null;
							synchronized (workQueue) {
								while (workQueue.isEmpty()) {
									workQueue.wait();
								}
								work = (Runnable) workQueue.dequeue();
							}
							syncExec(work);
						}
					} catch (InterruptedException e) {
						// exit
					}
				}
			};
			workerThread.start();
		}
	}

	/**
	 * Causes the <code>run()</code> method of the runnable to be invoked from
	 * within this realm at the next reasonable opportunity. This method is
	 * blocking the caller until the runnable completes.
	 * <p>
	 * If the given runnable is an instance of {@link ISafeRunnable}, its
	 * exception handler method will be called if any exceptions occur while
	 * running it. Otherwise, the exception will be logged.
	 * </p>
	 * <p>
	 * Subclasses should use {@link #safeRun(Runnable)} to run the runnable.
	 * </p>
	 * <p>
	 * Note: This class is not meant to be called by clients and therefore has
	 * only protected access.
	 * </p>
	 *
	 * @param runnable
	 */
	protected void syncExec(Runnable runnable) {
		SyncRunnable syncRunnable = new SyncRunnable(runnable);
		asyncExec(syncRunnable);
		synchronized (syncRunnable) {
			while (!syncRunnable.hasRun) {
				try {
					syncRunnable.wait();
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
			}
		}
	}

	static class SyncRunnable implements Runnable {
		boolean hasRun = false;

		private Runnable runnable;

		SyncRunnable(Runnable runnable) {
			this.runnable = runnable;
		}

		@Override
		public void run() {
			try {
				safeRun(runnable);
			} finally {
				synchronized (this) {
					hasRun = true;
					this.notifyAll();
				}
			}
		}
	}

	/**
	 * Sets the provided <code>realm</code> as the default for the duration of
	 * {@link Runnable#run()} and resets the previous realm after completion.
	 * Note that this will not set the given realm as the current realm.
	 *
	 * @param realm
	 * @param runnable
	 */
	public static void runWithDefault(Realm realm, Runnable runnable) {
		Realm oldRealm = Realm.getDefault();
		try {
			defaultRealm.set(realm);
			runnable.run();
		} finally {
			defaultRealm.set(oldRealm);
		}
	}
}
