/*******************************************************************************
 * Copyright (c) 2005, 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
 *     Matthew Hall - bugs 210115, 146397, 249526, 262269, 251424
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 *******************************************************************************/
package org.eclipse.core.databinding.observable;

import java.util.Set;

import org.eclipse.core.databinding.util.Policy;
import org.eclipse.core.internal.databinding.identity.IdentitySet;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

/**
 * This class makes it possible to monitor whenever an IObservable is read from.
 * This can be used to automatically attach and remove listeners. How to use it:
 *
 * <p>
 * If you are implementing an IObservable, invoke getterCalled(this) whenever a
 * getter is called - that is, whenever your observable is read from. You only
 * need to do this once per method call. If one getter delegates to another, the
 * outer getter doesn't need to call the method since the inner one will.
 * </p>
 *
 * <p>
 * If you want to determine what observables were used in a particular block of
 * code, call runAndMonitor(Runnable). This will execute the given runnable and
 * return the set of observables that were read from.
 * </p>
 *
 * <p>
 * This can be used to automatically attach listeners. For example, imagine you
 * have a block of code that updates some widget by reading from a bunch of
 * observables. Whenever one of those observables changes, you want to re-run
 * the code and cause the widget to be refreshed. You could do this in the
 * traditional manner by attaching one listener to each observable and
 * re-running your widget update code whenever one of them changes, but this
 * code is repetitive and requires updating the listener code whenever you
 * refactor the widget updating code.
 * </p>
 *
 * <p>
 * Alternatively, you could use a utility class that runs the code in a
 * runAndMonitor block and automatically attach listeners to any observable used
 * in updating the widget. The advantage of the latter approach is that it,
 * eliminates the code for attaching and detaching listeners and will always
 * stay in synch with changes to the widget update logic.
 * </p>
 *
 * @since 1.0
 */
public class ObservableTracker {

	/**
	 * Threadlocal storage pointing to the current Set of IObservables, or null
	 * if none. Note that this is actually the top of a stack. Whenever a method
	 * changes the current value, it remembers the old value as a local variable
	 * and restores the old value when the method exits.
	 */
	private static ThreadLocal<IChangeListener> currentChangeListener = new ThreadLocal<>();

	private static ThreadLocal<IStaleListener> currentStaleListener = new ThreadLocal<>();

	private static ThreadLocal<Set<IObservable>> currentGetterCalledSet = new ThreadLocal<>();

	private static ThreadLocal<Set<IObservable>> currentObservableCreatedSet = new ThreadLocal<>();

	private static ThreadLocal<Integer> currentIgnoreCount = new ThreadLocal<>();

	/**
	 * Invokes the given runnable, and returns the set of IObservables that were
	 * read by the runnable. If the runnable calls this method recursively, the
	 * result will not contain IObservables that were used within the inner
	 * runnable.
	 *
	 * @param runnable
	 *            runnable to execute
	 * @param changeListener
	 *            listener to register with all accessed observables, or
	 *            <code>null</code> if no change listener is to be registered
	 * @param staleListener
	 *            listener to register with all accessed observables, or
	 *            <code>null</code> if no stale listener is to be registered
	 * @return an array of unique observable objects
	 */
	public static IObservable[] runAndMonitor(Runnable runnable,
			IChangeListener changeListener, IStaleListener staleListener) {
		// Remember the previous value in the listener stack
		Set<IObservable> lastObservableSet = currentGetterCalledSet.get();
		IChangeListener lastChangeListener = currentChangeListener.get();
		IStaleListener lastStaleListener = currentStaleListener.get();
		Integer lastIgnore = currentIgnoreCount.get();

		Set<IObservable> observableSet = new IdentitySet<>();
		// Push the new listeners to the top of the stack
		currentGetterCalledSet.set(observableSet);
		currentChangeListener.set(changeListener);
		currentStaleListener.set(staleListener);
		currentIgnoreCount.remove();
		try {
			runnable.run();
		} finally {
			// Pop the new listener off the top of the stack (by restoring the
			// previous listener)
			currentGetterCalledSet.set(lastObservableSet);
			currentChangeListener.set(lastChangeListener);
			currentStaleListener.set(lastStaleListener);
			checkUnmatchedIgnore(runnable);
			currentIgnoreCount.set(lastIgnore);
		}

		return observableSet.toArray(new IObservable[observableSet.size()]);
	}

	/**
	 * Invokes the given runnable, and returns the set of IObservables that were
	 * created by the runnable. If the runnable calls this method recursively,
	 * the result will not contain IObservables that were created within the
	 * inner runnable.
	 * <p>
	 * <em>NOTE: As of 1.2 (Eclipse 3.5), there are unresolved problems with this API, see
	 * <a href="https://bugs.eclipse.org/278550">bug 278550</a>. If we cannot
	 * find a way to make this API work, it will be deprecated as of 3.6.</em>
	 * </p>
	 *
	 * @param runnable
	 *            runnable to execute
	 * @return an array of unique observable objects
	 * @since 1.2
	 */
	public static IObservable[] runAndCollect(Runnable runnable) {
		Set<IObservable> lastObservableCreatedSet = currentObservableCreatedSet.get();
		Integer lastIgnore = currentIgnoreCount.get();

		Set<IObservable> observableSet = new IdentitySet<>();
		// Push the new listeners to the top of the stack
		currentObservableCreatedSet.set(observableSet);
		currentIgnoreCount.remove();
		try {
			runnable.run();
		} finally {
			// Pop the new listener off the top of the stack (by restoring the
			// previous listener)
			currentObservableCreatedSet.set(lastObservableCreatedSet);
			checkUnmatchedIgnore(runnable);
			currentIgnoreCount.set(lastIgnore);
		}

		return observableSet.toArray(new IObservable[observableSet.size()]);
	}

	private static void checkUnmatchedIgnore(Runnable runnable) {
		if (isIgnore()) {
			Policy.getLog()
					.log(new Status(
							IStatus.ERROR,
							Policy.JFACE_DATABINDING,
							"There were " //$NON-NLS-1$
									+ currentIgnoreCount.get()
									+ " unmatched setIgnore(true) invocations in runnable " //$NON-NLS-1$
									+ runnable));
		}
	}

	/**
	 * If the argument is <code>true</code>, causes subsequent calls to
	 * {@link #getterCalled(IObservable)} and
	 * {@link #observableCreated(IObservable)} to be ignored on the current thread.
	 * When the flag is set to <code>false</code>, calls to
	 * {@link #getterCalled(IObservable)} and
	 * {@link #observableCreated(IObservable)} will resume gathering observables.
	 * Nested calls to this method are stacked.
	 *
	 * @param ignore the new ignore state
	 *
	 * @exception IllegalStateException if <code>ignore</code> is false and the
	 *                                  ignore count is already zero.
	 *
	 * @see #getterCalled(IObservable)
	 * @see #observableCreated(IObservable)
	 * @since 1.3
	 */
	public static void setIgnore(boolean ignore) {
		Integer lastCount = currentIgnoreCount.get();

		int newCount = (lastCount == null ? 0 : lastCount.intValue())
				+ (ignore ? 1 : -1);

		if (newCount < 0)
			throw new IllegalStateException("Ignore count is already zero"); //$NON-NLS-1$

		if (newCount == 0) {
			currentIgnoreCount.remove();
		} else {
			currentIgnoreCount.set(Integer.valueOf(newCount));
		}
	}

	/**
	 * Runs the given runnable without tracking dependencies.
	 *
	 * @param runnable the runnable to execute
	 *
	 * @since 1.1
	 * @see #setIgnore(boolean)
	 */
	public static void runAndIgnore(Runnable runnable) {
		setIgnore(true);
		try {
			runnable.run();
		} finally {
			setIgnore(false);
		}
	}

	/*
	 * Returns the same string as the default Object.toString() implementation.
	 * getterCalled() uses this method IObservable.toString() to avoid infinite
	 * recursion and stack overflow.
	 */
	private static String toString(IObservable observable) {
		return observable.getClass().getName() + "@" //$NON-NLS-1$
				+ Integer.toHexString(System.identityHashCode(observable));
	}

	private static boolean isIgnore() {
		return currentIgnoreCount.get() != null;
	}

	/**
	 * Notifies the ObservableTracker that an observable was read from. The
	 * JavaDoc for methods that invoke this method should include the following
	 * tag: "@TrackedGetter This method will notify ObservableTracker that the
	 * receiver has been read from". This lets callers know that they can rely
	 * on automatic updates from the object without explicitly attaching a
	 * listener.
	 *
	 * @param observable the observable whose getter was called
	 */
	public static void getterCalled(IObservable observable) {
		if (observable.isDisposed())
			Assert.isTrue(false, "Getter called on disposed observable " //$NON-NLS-1$
					+ toString(observable));
		Realm realm = observable.getRealm();
		if (!realm.isCurrent())
			Assert.isTrue(false, "Getter called outside realm of observable " //$NON-NLS-1$
					+ toString(observable));

		if (isIgnore())
			return;

		Set<IObservable> getterCalledSet = currentGetterCalledSet.get();
		if (getterCalledSet != null && getterCalledSet.add(observable)) {
			// If anyone is listening for observable usage...
			IChangeListener changeListener = currentChangeListener.get();
			if (changeListener != null)
				observable.addChangeListener(changeListener);
			IStaleListener staleListener = currentStaleListener.get();
			if (staleListener != null)
				observable.addStaleListener(staleListener);
		}
	}

	/**
	 * Notifies the ObservableTracker that an observable was created.
	 *
	 * @param observable
	 *            the observable that was created
	 * @since 1.2
	 */
	public static void observableCreated(IObservable observable) {
		if (isIgnore())
			return;
		Set<IObservable> observableCreatedSet = currentObservableCreatedSet.get();
		if (observableCreatedSet != null) {
			observableCreatedSet.add(observable);
		}
	}
}
