/*******************************************************************************
 * Copyright (c) 2015 Google, Inc. 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:
 *     Stefan Xenos (Google) - initial API and implementation
 ******************************************************************************/
package org.eclipse.core.internal.databinding.observable.sideeffect;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.sideeffect.ISideEffect;
import org.eclipse.core.runtime.Assert;

/**
 * Concrete implementation of the {@link ISideEffect} interface.
 *
 * @since 1.6
 */
public final class SideEffect implements ISideEffect {

	/**
	 * Holds a singleton side-effect which does nothing.
	 */
	public static final ISideEffect NULL_SIDE_EFFECT = new ISideEffect() {
		@Override
		public void dispose() {
		}

		@Override
		public void pause() {
		}

		@Override
		public void resume() {
		}

		@Override
		public void resumeAndRunIfDirty() {
		}

		@Override
		public void runIfDirty() {
		}

		@Override
		public boolean isDisposed() {
			// This side-effect never executes nor retains any references and
			// callers may rely upon this, so we should return true.
			return true;
		}

		@Override
		public void addDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		}

		@Override
		public void removeDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		}
	};

	/**
	 * True if we've been dirtied since the last time we executed
	 * {@link #runnable}. A side-effect becomes dirtied if:
	 * <ul>
	 * <li>{@link #markDirty()} is called
	 * <li>one of its dependencies changes
	 * <li>it was newly created without executing the runnable
	 * </ul>
	 */
	private boolean dirty;
	/**
	 * True if PrivateInterface is currently enqueued in a call to
	 * realm.asyncExec
	 */
	private boolean asyncScheduled;
	private int pauseDepth;
	private Runnable runnable;
	/**
	 * Dependencies which we are currently listening for change events from
	 */
	private IObservable[] dependencies;
	private Realm realm;

	private PrivateInterface privateInterface = new PrivateInterface();

	/**
	 * List of dispose listeners. Null if empty
	 */
	private List<Consumer<ISideEffect>> disposeListeners;

	/**
	 * Creates a SideEffect in the paused state that wraps the given runnable on
	 * the default Realm.
	 *
	 * @param runnable
	 *            the runnable to execute.
	 */
	public SideEffect(Runnable runnable) {
		this(Realm.getDefault(), runnable);
	}

	/**
	 * Creates a SideEffect in the given realm that wraps the given runnable.
	 *
	 * @param realm
	 *            the realm to use for this SideEffect.
	 * @param runnable
	 *            the runnable to execute.
	 */
	public SideEffect(Realm realm, Runnable runnable) {
		this.runnable = runnable;
		this.realm = realm;
		this.dirty = true;
		this.pauseDepth = 1;
	}

	/**
	 * Creates a SideEffect with the given initial set of dependencies in the
	 * default realm that wraps the given runnable.
	 *
	 * @param runnable
	 *            the runnable to wrap
	 * @param dependencies
	 *            the initial set of dependencies
	 */
	public SideEffect(Runnable runnable, IObservable... dependencies) {
		this.dependencies = dependencies;
		this.runnable = runnable;
		this.dirty = false;
		this.pauseDepth = 0;
		this.realm = Realm.getDefault();

		for (IObservable next : dependencies) {
			next.addChangeListener(privateInterface);
		}
	}

	@Override
	public void resume() {
		checkRealm();
		pauseDepth--;
		if (pauseDepth < 0) {
			throw new IllegalStateException(
					"The resume() method was called more times than pause()."); //$NON-NLS-1$
		} else if (dirty && pauseDepth == 0) {
			scheduleUpdate();
		}
	}

	@Override
	public void pause() {
		checkRealm();
		pauseDepth++;
		if (dirty && pauseDepth == 1) {
			// No need to continue listening if we're already dirtied, since
			// we'll just end up running again after we're resumed
			stopListening();
			dependencies = null;
		}
	}

	@Override
	public void resumeAndRunIfDirty() {
		checkRealm();
		pauseDepth--;
		update();
	}

	private void update() {
		if (dirty && pauseDepth <= 0) {
			dirty = false;
			// Hold a reference to the old dependencies to prevent them from
			// being garbage collected until we've computed the new set. In the
			// event that a dependency is lazily created, this prevents it from
			// being destroyed and immediately recreated.

			// Stop listening for dependency changes.
			stopListening();

			// This line will do the following:
			// - Run the calculate method
			// - While doing so, add any observable that is touched to the
			// dependencies list
			IObservable[] newDependencies = ObservableTracker.runAndMonitor(runnable, null, null);

			// If the side-effect disposed itself, exit without attaching any
			// listeners.
			if (isDisposed()) {
				return;
			}

			for (IObservable next : newDependencies) {
				next.addChangeListener(privateInterface);
			}

			dependencies = newDependencies;
		}
	}

	@Override
	public void dispose() {
		checkRealm();
		if (isDisposed()) {
			return;
		}
		pauseDepth = 0;
		stopListening();
		dependencies = null;
		runnable = null;
		if (disposeListeners != null) {
			List<Consumer<ISideEffect>> oldListeners = disposeListeners;
			disposeListeners = null;
			oldListeners.forEach(dc -> dc.accept(SideEffect.this));
		}
	}

	@Override
	public boolean isDisposed() {
		return runnable == null;
	}

	/**
	 * Add an disposal consumer for this {@link ISideEffect} instance.
	 *
	 * @param disposalConsumer
	 *            a consumer which will be notified once this
	 *            {@link ISideEffect} is disposed.
	 */
	@Override
	public void addDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		checkRealm();
		if (isDisposed()) {
			return;
		}
		if (this.disposeListeners == null) {
			this.disposeListeners = new ArrayList<>();
		}
		this.disposeListeners.add(disposalConsumer);
	}

	/**
	 * Remove an disposal consumer for this {@link ISideEffect} instance.
	 *
	 * @param disposalConsumer
	 *            a consumer which is supposed to be removed from the dispose
	 *            listener list.
	 */
	@Override
	public void removeDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		checkRealm();
		if (this.disposeListeners == null) {
			return;
		}
		this.disposeListeners.remove(disposalConsumer);
	}

	@Override
	public void runIfDirty() {
		checkRealm();
		update();
	}

	private void stopListening() {
		if (dependencies != null) {
			for (IObservable observable : dependencies) {
				observable.removeChangeListener(privateInterface);
			}
		}
	}

	private void markDirtyInternal() {
		if (!dirty) {
			dirty = true;

			if (pauseDepth <= 0) {
				scheduleUpdate();
			} else {
				stopListening();
				dependencies = null;
			}
		}
	}

	private void scheduleUpdate() {
		if (this.asyncScheduled) {
			return;
		}

		this.asyncScheduled = true;
		realm.asyncExec(privateInterface);
	}

	private void checkRealm() {
		Assert.isTrue(realm.isCurrent(), "This operation must be run within the observable's realm"); //$NON-NLS-1$
	}

	/**
	 * Creates a runnable which will execute the given supplier and pass the
	 * result to the given consumer while suppressing all tracked getters from
	 * the consumer.
	 *
	 * @param supplier
	 *            supplier to execute
	 * @param consumer
	 *            a consumer that will receive the value and in which tracked
	 *            getters will be suppressed.
	 * @return a newly constructed runnable
	 */
	public static <T> Runnable makeRunnable(Supplier<T> supplier, Consumer<T> consumer) {
		return () -> {
			T value = supplier.get();

			ObservableTracker.setIgnore(true);
			try {
				consumer.accept(value);
			} finally {
				ObservableTracker.setIgnore(false);
			}
		};
	}

	private class PrivateInterface implements IChangeListener, Runnable {
		@Override
		public void handleChange(ChangeEvent event) {
			markDirtyInternal();
		}

		@Override
		public void run() {
			if (isDisposed()) {
				return;
			}
			asyncScheduled = false;
			update();
		}
	}
}
