/*******************************************************************************
 * Copyright (c) 2008, 2017 Matthew Hall 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:
 *     Matthew Hall - initial API and implementation (bug 218269)
 *     Boris Bokowski - bug 218269
 *     Matthew Hall - bug 237884, 240590, 251003, 251424, 278550, 332504
 *     Ovidio Mallo - bug 238909, 235859
 *     Stefan R�ck - bug 332504
 ******************************************************************************/

package org.eclipse.core.databinding.validation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.databinding.ValidationStatusProvider;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.IStaleListener;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.Observables;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.StaleEvent;
import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListDiffVisitor;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.value.AbstractObservableValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.internal.databinding.Util;
import org.eclipse.core.internal.databinding.validation.ValidatedObservableList;
import org.eclipse.core.internal.databinding.validation.ValidatedObservableMap;
import org.eclipse.core.internal.databinding.validation.ValidatedObservableSet;
import org.eclipse.core.internal.databinding.validation.ValidatedObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;

/**
 * A validator for cross-constraints between observables.
 *
 * <p>
 * Some practical examples of cross-constraints:
 * <ul>
 * <li>A start date cannot be later than an end date
 * <li>A list of percentages should add up to 100%
 * </ul>
 * <p>
 * Example: require two integer fields to contain either both even or both odd
 * numbers.
 *
 * <pre>
 * DataBindingContext dbc = new DataBindingContext();
 *
 * IObservableValue target0 = SWTObservables.observeText(text0, SWT.Modify);
 * IObservableValue target1 = SWTObservables.observeText(text1, SWT.Modify);
 *
 * // Binding in two stages (from target to middle, then from middle to model)
 * // simplifies the validation logic.  Using the middle observables saves
 * // the trouble of converting the target values (Strings) to the model type
 * // (integers) manually during validation.
 * final IObservableValue middle0 = new WritableValue(null, Integer.TYPE);
 * final IObservableValue middle1 = new WritableValue(null, Integer.TYPE);
 * dbc.bind(target0, middle0, null, null);
 * dbc.bind(target1, middle1, null, null);
 *
 * // Create the multi-validator
 * MultiValidator validator = new MultiValidator() {
 * 	protected IStatus validate() {
 * 		// Calculate the validation status
 * 		Integer value0 = (Integer) middle0.getValue();
 * 		Integer value1 = (Integer) middle1.getValue();
 * 		if (Math.abs(value0.intValue()) % 2 != Math.abs(value1.intValue()) % 2)
 * 			return ValidationStatus
 * 					.error(&quot;Values must be both even or both odd&quot;);
 * 		return ValidationStatus.ok();
 * 	}
 * };
 * dbc.addValidationStatusProvider(validator);
 *
 * // Bind the middle observables to the model observables.
 * IObservableValue model0 = new WritableValue(Integer.valueOf(2), Integer.TYPE);
 * IObservableValue model1 = new WritableValue(Integer.valueOf(4), Integer.TYPE);
 * dbc.bind(middle0, model0, null, null);
 * dbc.bind(middle1, model1, null, null);
 * </pre>
 *
 * <p>
 * MultiValidator can also prevent invalid data from being copied to model. This
 * is done by wrapping each target observable in a validated observable, and
 * then binding the validated observable to the model.
 *
 * <pre>
 *
 * ...
 *
 * // Validated observables do not change value until the validator passes.
 * IObservableValue validated0 = validator.observeValidatedValue(middle0);
 * IObservableValue validated1 = validator.observeValidatedValue(middle1);
 * IObservableValue model0 = new WritableValue(Integer.valueOf(2), Integer.TYPE);
 * IObservableValue model1 = new WritableValue(Integer.valueOf(4), Integer.TYPE);
 * // Bind to the validated value, not the middle/target
 * dbc.bind(validated0, model0, null, null);
 * dbc.bind(validated1, model1, null, null);
 * </pre>
 *
 * Note: No guarantee is made as to the order of updates when multiple validated
 * observables change value at once (i.e. multiple updates pending when the
 * status becomes valid). Therefore the model may be in an invalid state after
 * the first but before the last pending update.
 *
 * @since 1.1
 */
public abstract class MultiValidator extends ValidationStatusProvider {
	private Realm realm;
	private ValidationStatusObservableValue validationStatus;
	private IObservableValue<IStatus> unmodifiableValidationStatus;
	private WritableList<IObservable> targets;
	private IObservableList<IObservable> unmodifiableTargets;
	private IObservableList<IObservable> models;

	IListChangeListener<IObservable> targetsListener = event -> {
		event.diff.accept(new ListDiffVisitor<IObservable>() {
			@Override
			public void handleAdd(int index, IObservable dependency) {
				dependency.addChangeListener(dependencyListener);
				dependency.addStaleListener(dependencyListener);
			}

			@Override
			public void handleRemove(int index, IObservable dependency) {
				dependency.removeChangeListener(dependencyListener);
				dependency.removeStaleListener(dependencyListener);
			}
		});
	};

	private class DependencyListener implements IChangeListener, IStaleListener {
		@Override
		public void handleChange(ChangeEvent event) {
			revalidate();
		}

		@Override
		public void handleStale(StaleEvent staleEvent) {
			validationStatus.makeStale();
		}
	}

	private DependencyListener dependencyListener = new DependencyListener();

	/**
	 * Constructs a MultiValidator on the default realm.
	 */
	public MultiValidator() {
		this(Realm.getDefault());
	}

	/**
	 * Constructs a MultiValidator on the given realm.
	 *
	 * @param realm
	 *            the realm on which validation takes place.
	 */
	public MultiValidator(Realm realm) {
		Assert.isNotNull(realm, "Realm cannot be null"); //$NON-NLS-1$
		this.realm = realm;

		ObservableTracker.setIgnore(true);
		try {
			validationStatus = new ValidationStatusObservableValue(realm);

			targets = new WritableList<>(realm, new ArrayList<IObservable>(), IObservable.class);
			targets.addListChangeListener(targetsListener);
			unmodifiableTargets = Observables.unmodifiableObservableList(targets);

			models = Observables.emptyObservableList(realm);
		} finally {
			ObservableTracker.setIgnore(false);
		}
	}

	private void checkObservable(IObservable target) {
		Assert.isNotNull(target, "Target observable cannot be null"); //$NON-NLS-1$
		Assert.isTrue(realm.equals(target.getRealm()),
				"Target observable must be in the same realm as MultiValidator"); //$NON-NLS-1$
	}

	/**
	 * Returns an {@link IObservableValue} whose value is always the current
	 * validation status of this MultiValidator. The returned observable is in
	 * the same realm as this MultiValidator.
	 *
	 * @return an {@link IObservableValue} whose value is always the current
	 *         validation status of this MultiValidator.
	 */
	@Override
	public IObservableValue<IStatus> getValidationStatus() {
		if (unmodifiableValidationStatus == null) {
			ObservableTracker.setIgnore(true);
			try {
				unmodifiableValidationStatus = Observables
						.unmodifiableObservableValue(validationStatus);
			} finally {
				ObservableTracker.setIgnore(false);
			}
			revalidate();
		}
		return unmodifiableValidationStatus;
	}

	/**
	 * Signals that a re-evaluation of the current validation status is
	 * necessary.
	 * <p>
	 * Clients may invoke this method whenever the validation status needs to be
	 * updated due to some state change which cannot be automatically tracked by
	 * the MultiValidator as it is not captured by any {@link IObservable}
	 * instance.
	 * <p>
	 * Note: There is no guarantee as of whether the MultiValidator will
	 * immediately re-evaluate the validation status by calling
	 * {@link #validate} when becoming dirty. Instead, it may decide to perform
	 * the re-evaluation lazily.
	 *
	 * @see #validate()
	 * @since 1.2
	 */
	protected final void revalidate() {
		class ValidationRunnable implements Runnable {
			IStatus validationResult;

			@Override
			public void run() {
				try {
					validationResult = validate();
					if (validationResult == null)
						validationResult = ValidationStatus.ok();
				} catch (RuntimeException e) {
					// Usually an NPE as dependencies are init'ed
					validationResult = ValidationStatus
							.error(e.getMessage(), e);
				}
			}
		}

		ValidationRunnable validationRunnable = new ValidationRunnable();
		final IObservable[] dependencies = ObservableTracker.runAndMonitor(
				validationRunnable, null, null);

		ObservableTracker.setIgnore(true);
		try {
			List<IObservable> newTargets = new ArrayList<>(Arrays.asList(dependencies));

			// Internal observables should not be dependencies
			// (prevent dependency loop)
			for (Iterator<IObservable> itNew = newTargets.iterator(); itNew.hasNext();) {
				IObservable newDependency = itNew.next();
				if (newDependency == validationStatus
						|| newDependency == unmodifiableValidationStatus
						|| newDependency == targets
						|| newDependency == unmodifiableTargets
						|| newDependency == models) {
					itNew.remove();
				}
			}

			// This loop is roughly equivalent to:
			// targets.retainAll(newTargets);
			// newTargets.removeAll(targets);
			// Except that dependencies are compared by identity instead of
			// equality
			outer: for (int i = targets.size() - 1; i >= 0; i--) {
				IObservable oldDependency = targets.get(i);
				for (Iterator<IObservable> itNew = newTargets.iterator(); itNew.hasNext();) {
					Object newDependency = itNew.next();
					if (oldDependency == newDependency) {
						// Dependency is already known--remove from list of
						// new dependencies
						itNew.remove();
						continue outer;
					}
				}
				// Old dependency is no longer a dependency--remove from
				// targets
				targets.remove(i);
			}

			targets.addAll(newTargets);
		} finally {
			ObservableTracker.setIgnore(false);
		}

		// Once the dependencies are up-to-date, we set the new status.
		validationStatus.setValue(validationRunnable.validationResult);
	}

	/**
	 * Returns the current validation status.
	 * <p>
	 * Note: To ensure that the validation status is kept current automatically,
	 * all dependencies used to calculate status should be accessed through
	 * {@link IObservable} instances. Each dependency observable must be in the
	 * same realm as the MultiValidator. Other dependencies not captured by the
	 * state of those observables may be accounted for by having clients
	 * <i>explicitly</i> call {@link #revalidate()} whenever the validation
	 * status needs to be re-evaluated due to some arbitrary change in the
	 * application state.
	 *
	 * @return the current validation status.
	 *
	 * @see #revalidate()
	 */
	protected abstract IStatus validate();

	/**
	 * Returns a wrapper {@link IObservableValue} which stays in sync with the
	 * given target observable only when the validation status is valid.
	 * Statuses of {@link IStatus#OK OK}, {@link IStatus#INFO INFO} or
	 * {@link IStatus#WARNING WARNING} severity are considered valid.
	 * <p>
	 * The wrapper behaves as follows with respect to the validation status:
	 * <ul>
	 * <li>While valid, the wrapper stays in sync with its target observable.
	 * <li>While invalid, the wrapper's value is the target observable's last
	 * valid value. If the target changes value, a stale event is fired
	 * signaling that a change is pending.
	 * <li>When status changes from invalid to valid, the wrapper takes the
	 * value of the target observable, and synchronization resumes.
	 * </ul>
	 *
	 * @param target
	 *            the target observable being wrapped. Must be in the same realm
	 *            as the MultiValidator.
	 * @return an IObservableValue which stays in sync with the given target
	 *         observable only with the validation status is valid.
	 */
	public <T> IObservableValue<T> observeValidatedValue(IObservableValue<T> target) {
		checkObservable(target);
		return new ValidatedObservableValue<>(target, getValidationStatus());
	}

	/**
	 * Returns a wrapper {@link IObservableList} which stays in sync with the
	 * given target observable only when the validation status is valid.
	 * Statuses of {@link IStatus#OK OK}, {@link IStatus#INFO INFO} or
	 * {@link IStatus#WARNING WARNING} severity are considered valid.
	 * <p>
	 * The wrapper behaves as follows with respect to the validation status:
	 * <ul>
	 * <li>While valid, the wrapper stays in sync with its target observable.
	 * <li>While invalid, the wrapper's elements are the target observable's
	 * last valid elements. If the target changes elements, a stale event is
	 * fired signaling that a change is pending.
	 * <li>When status changes from invalid to valid, the wrapper takes the
	 * elements of the target observable, and synchronization resumes.
	 * </ul>
	 *
	 * @param target
	 *            the target observable being wrapped. Must be in the same realm
	 *            as the MultiValidator.
	 * @return an IObservableValue which stays in sync with the given target
	 *         observable only with the validation status is valid.
	 */
	public <E> IObservableList<E> observeValidatedList(IObservableList<E> target) {
		checkObservable(target);
		return new ValidatedObservableList<>(target, getValidationStatus());
	}

	/**
	 * Returns a wrapper {@link IObservableSet} which stays in sync with the
	 * given target observable only when the validation status is valid.
	 * Statuses of {@link IStatus#OK OK}, {@link IStatus#INFO INFO} or
	 * {@link IStatus#WARNING WARNING} severity are considered valid.
	 * <p>
	 * The wrapper behaves as follows with respect to the validation status:
	 * <ul>
	 * <li>While valid, the wrapper stays in sync with its target observable.
	 * <li>While invalid, the wrapper's elements are the target observable's
	 * last valid elements. If the target changes elements, a stale event is
	 * fired signaling that a change is pending.
	 * <li>When status changes from invalid to valid, the wrapper takes the
	 * elements of the target observable, and synchronization resumes.
	 * </ul>
	 *
	 * @param target
	 *            the target observable being wrapped. Must be in the same realm
	 *            as the MultiValidator.
	 * @return an IObservableValue which stays in sync with the given target
	 *         observable only with the validation status is valid.
	 */
	public <E> IObservableSet<E> observeValidatedSet(IObservableSet<E> target) {
		checkObservable(target);
		return new ValidatedObservableSet<>(target, getValidationStatus());
	}

	/**
	 * Returns a wrapper {@link IObservableMap} which stays in sync with the
	 * given target observable only when the validation status is valid.
	 * Statuses of {@link IStatus#OK OK}, {@link IStatus#INFO INFO} or
	 * {@link IStatus#WARNING WARNING} severity are considered valid.
	 * <p>
	 * The wrapper behaves as follows with respect to the validation status:
	 * <ul>
	 * <li>While valid, the wrapper stays in sync with its target observable.
	 * <li>While invalid, the wrapper's entries are the target observable's last
	 * valid entries. If the target changes entries, a stale event is fired
	 * signaling that a change is pending.
	 * <li>When status changes from invalid to valid, the wrapper takes the
	 * entries of the target observable, and synchronization resumes.
	 * </ul>
	 *
	 * @param target
	 *            the target observable being wrapped. Must be in the same realm
	 *            as the MultiValidator.
	 * @return an IObservableValue which stays in sync with the given target
	 *         observable only with the validation status is valid.
	 */
	public <K, V> IObservableMap<K, V> observeValidatedMap(IObservableMap<K, V> target) {
		checkObservable(target);
		return new ValidatedObservableMap<>(target, getValidationStatus());
	}

	@Override
	public IObservableList<IObservable> getTargets() {
		return unmodifiableTargets;
	}

	@Override
	public IObservableList<IObservable> getModels() {
		return models;
	}

	@Override
	public void dispose() {
		if (targets != null) {
			targets.clear(); // Remove listeners from dependencies
		}

		if (unmodifiableValidationStatus != null) {
			unmodifiableValidationStatus.dispose();
			unmodifiableValidationStatus = null;
		}

		if (validationStatus != null) {
			validationStatus.dispose();
			validationStatus = null;
		}

		if (unmodifiableTargets != null) {
			unmodifiableTargets.dispose();
			unmodifiableTargets = null;
		}

		if (targets != null) {
			targets.dispose();
			targets = null;
		}

		if (models != null) {
			models.dispose();
			models = null;
		}

		realm = null;

		super.dispose();
	}

	private class ValidationStatusObservableValue extends AbstractObservableValue<IStatus> {
		private IStatus value = ValidationStatus.ok();

		private boolean stale = false;

		public ValidationStatusObservableValue(Realm realm) {
			super(realm);
		}

		@Override
		protected IStatus doGetValue() {
			return value;
		}

		@Override
		protected void doSetValue(IStatus value) {
			boolean oldStale = stale;

			// Update the staleness state by checking whether any of the current
			// dependencies is stale.
			stale = false;
			for (Iterator<IObservable> iter = targets.iterator(); iter.hasNext();) {
				IObservable dependency = iter.next();
				if (dependency.isStale()) {
					stale = true;
					break;
				}
			}

			IStatus oldValue = this.value;
			this.value = value;

			// If either becoming non-stale or setting a new value, we must fire
			// a value change event.
			if ((oldStale && !stale) || !Util.equals(oldValue, value)) {
				fireValueChange(Diffs.createValueDiff(oldValue, value));
			} else if (!oldStale && stale) {
				fireStale();
			}
		}

		void makeStale() {
			if (!stale) {
				stale = true;
				fireStale();
			}
		}

		@Override
		public boolean isStale() {
			ObservableTracker.getterCalled(this);
			return stale;
		}

		@Override
		public Object getValueType() {
			return IStatus.class;
		}
	}
}
