/*******************************************************************************
 * Copyright (c) 2016 vogella GmbH 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:
 *     Simon Scholz <simon.scholz@vogella.com> - initial API and implementation
 ******************************************************************************/

package org.eclipse.core.databinding.observable.sideeffect;

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

import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.runtime.Assert;

/**
 * Represents an {@link ISideEffect} that is composed of a bunch of component
 * {@link ISideEffect}s. It has the following properties:
 *
 * <ul>
 * <li>Disposing the composite will dispose all of the children.</li>
 * <li>If the composite is paused, all of the children will be paused as well.
 * </li>
 * </ul>
 *
 * Note that resuming a composite does not guarantee that all children will
 * resume. Children may also be paused externally, in which case the child must
 * be resumed both by the composite and by the external source(s) before it will
 * execute.
 * <p>
 * Children may belong to multiple composites. When this occurs, all of its
 * parent composites must be resumed in order for the child to execute and the
 * child will be disposed the first time any of its parents are disposed.
 * <p>
 * Children may be removed from a composite. When this occurs, the child may be
 * resumed immediately if the composite was paused and disposing the composite
 * will no longer have any effect on the removed child.
 * <p>
 * Disposing a child will automatically remove it from its parent composite(s).
 * <p>
 * The main use of this class is to manage a group of side-effects that share
 * the same life-cycle. For example, all side-effects used to populate widgets
 * within a workbench part would likely be paused and resumed when the part is
 * made visible or invisible, and would all be disposed together when the part
 * is closed.
 *
 * @since 1.6
 */
public final class CompositeSideEffect implements ISideEffect {

	private final List<ISideEffect> sideEffects;
	private int pauseDepth;
	private boolean isDisposed;
	private final Realm realm;

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

	private Consumer<ISideEffect> removalConsumer = this::remove;

	/**
	 * Default constructor of an CompositeSideEffect.
	 */
	public CompositeSideEffect() {
		realm = Realm.getDefault();
		sideEffects = new ArrayList<>();
	}

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

	@Override
	public void dispose() {
		checkRealm();
		if (isDisposed) {
			return;
		}
		sideEffects.forEach(s -> s.removeDisposeListener(removalConsumer));
		sideEffects.forEach(ISideEffect::dispose);
		sideEffects.clear();
		isDisposed = true;
		if (disposeListeners != null) {
			List<Consumer<ISideEffect>> listeners = disposeListeners;
			disposeListeners = null;
			listeners.forEach(dc -> dc.accept(CompositeSideEffect.this));
		}
	}

	@Override
	public boolean isDisposed() {
		checkRealm();
		return this.isDisposed;
	}

	@Override
	public void addDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		checkRealm();
		if (isDisposed()) {
			return;
		}
		if (this.disposeListeners == null) {
			this.disposeListeners = new ArrayList<>();
		}
		this.disposeListeners.add(disposalConsumer);
	}

	@Override
	public void removeDisposeListener(Consumer<ISideEffect> disposalConsumer) {
		checkRealm();
		if (this.disposeListeners == null) {
			return;
		}
		this.disposeListeners.remove(disposalConsumer);
	}

	@Override
	public void pause() {
		checkRealm();
		pauseDepth++;
		if (pauseDepth == 1) {
			sideEffects.forEach(ISideEffect::pause);
		}
	}

	@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 (pauseDepth == 0) {
			sideEffects.forEach(ISideEffect::resume);
		}
	}

	@Override
	public void resumeAndRunIfDirty() {
		checkRealm();
		pauseDepth--;
		if (pauseDepth == 0) {
			sideEffects.forEach(ISideEffect::resumeAndRunIfDirty);
		}
	}

	@Override
	public void runIfDirty() {
		checkRealm();
		if (pauseDepth <= 0) {
			sideEffects.forEach(ISideEffect::runIfDirty);
		}
	}

	/**
	 * Adds the given {@link ISideEffect} instance from the composite.
	 *
	 * @param sideEffect
	 *            {@link ISideEffect}
	 */
	public void add(ISideEffect sideEffect) {
		checkRealm();
		if (!sideEffect.isDisposed()) {
			sideEffects.add(sideEffect);
			if (pauseDepth > 0) {
				sideEffect.pause();
			}
			sideEffect.addDisposeListener(removalConsumer);
		}
	}

	/**
	 * Removes the given {@link ISideEffect} instance from the composite. This
	 * has no effect if the given side-effect is not part of the composite.
	 *
	 * @param sideEffect
	 *            {@link ISideEffect}
	 */
	public void remove(ISideEffect sideEffect) {
		checkRealm();
		sideEffects.remove(sideEffect);
		if (!sideEffect.isDisposed()) {
			if (pauseDepth > 0) {
				sideEffect.resume();
			}
			sideEffect.removeDisposeListener(removalConsumer);
		}
	}
}
