/*******************************************************************************
 * Copyright (c) 2008, 2011 Wind River Systems 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.concurrent;

import java.util.Arrays;

import org.eclipse.core.runtime.CoreException;

/**
 * @since 2.2
 */
public abstract class Transaction<V> {

	/**
	 * The exception we throw when the client transaction logic asks us to
	 * validate a cache object that is stale (or has never obtained a value from
	 * the source)
	 */
	private static final InvalidCacheException INVALID_CACHE_EXCEPTION = new InvalidCacheException();

	/** The request object we've been given to set the transaction results in */
	private DataRequestMonitor<V> fRm;

	public static class InvalidCacheException extends Exception {
		private static final long serialVersionUID = 1L;
	}

	/**
	 * Kicks off the transaction. We'll either complete the request monitor
	 * immediately if all the data points the transaction needs are cached and
	 * valid, or we'll end up asynchronously completing the monitor if and when
	 * either (a) all the data points are available and up-to-date, or (b)
	 * obtaining them from the source encountered an error. Note that there is
	 * potential in (b) for us to never complete the monitor. If one or more
	 * data points are perpetually becoming stale, then we'll indefinitely wait
	 * for them to stabilize. The caller should cancel its request monitor in
	 * order to get us to stop waiting.
	 *
	 * @param rm Request completion monitor.
	 */
	public void request(DataRequestMonitor<V> rm) {
		if (fRm != null) {
			assert fRm.isCanceled();
			fRm.done();
		}
		fRm = rm;
		assert fRm != null;
		execute();
	}

	/**
	 * The transaction logic--code that tries to synchronously make use of,
	 * usually, multiple data points that are normally obtained asynchronously.
	 * Each data point is represented by a cache object. The transaction logic
	 * must check the validity of each cache object just prior to using it
	 * (calling its getData()). It should do that check by calling one of our
	 * validate() methods. Those methods will throw InvalidCacheException if the
	 * cached data is invalid (stale, e.g.,) or CoreException if an error was
	 * encountered the last time it got data form the source. The exception will
	 * abort the transaction, but in the case of InvalidCacheException, we
	 * schedule an asynchronous call that will re-invoke the transaction
	 * logic once the cache object has been updated from the source.
	 *
	 * @return the cached data if it's valid, otherwise an exception is thrown
	 * @throws Transaction.InvalidCacheException Exception indicating that a
	 * cache is not valid and transaction will need to be rescheduled.
	 * @throws CoreException Exception indicating that one of the caches is
	 * in error state and transaction cannot be processed.
	 */
	abstract protected V process() throws InvalidCacheException, CoreException;

	/**
	 * Method which invokes the transaction logic and handles any exception that
	 * may result. If that logic encounters a stale/unset cache object, then we
	 * simply do nothing. This method will be called again once the cache
	 * objects tell us it has obtained an updated value form the source.
	 */
	private void execute() {
		if (fRm.isCanceled()) {
			fRm.done();
			fRm = null;
			return;
		}

		try {
			// Execute the transaction logic
			V data = process();

			// No exception means all cache objects used by the transaction
			// were valid and up to date. Complete the request
			fRm.setData(data);
			fRm.done();
			fRm = null;
		} catch (CoreException e) {
			// At least one of the cache objects encountered a failure obtaining
			// the data from the source. Complete the request.
			fRm.setStatus(e.getStatus());
			fRm.done();
			fRm = null;
		} catch (InvalidCacheException e) {
			// At least one of the cache objects was stale/unset. Keep the
			// request monitor in the incomplete state, thus leaving our client
			// "waiting" (asynchronously). We'll get called again once the cache
			// objects are updated, thus re-starting the whole transaction
			// attempt.
		}
	}

	/**
	 * Clients must call one of our validate methods prior to using (calling
	 * getData()) on data cache object.
	 *
	 * @param cache
	 *            the object being validated
	 * @throws InvalidCacheException
	 *             if the data is stale/unset
	 * @throws CoreException
	 *             if an error was encountered getting the data from the source
	 */
	public void validate(ICache<?> cache) throws InvalidCacheException, CoreException {
		if (cache.isValid()) {
			if (!cache.getStatus().isOK()) {
				throw new CoreException(cache.getStatus());
			}
		} else {
			// Throw the invalid cache exception, but first ask the cache to
			// update itself from its source, and schedule a re-attempt of the
			// transaction logic to occur when the stale/unset cache has been
			// updated
			cache.update(new RequestMonitor(ImmediateExecutor.getInstance(), fRm) {
				@Override
				protected void handleCompleted() {
					execute();
				}
			});
			throw INVALID_CACHE_EXCEPTION;
		}
	}

	/**
	 * See {@link #validate(RequestCache)}. This variant simply validates
	 * multiple cache objects.
	 */
	public <T> void validate(ICache<?>... caches) throws InvalidCacheException, CoreException {
		validate(Arrays.asList(caches));
	}

	/**
	 * See {@link #validate(RequestCache)}. This variant simply validates
	 * multiple cache objects.
	 */
	public void validate(@SuppressWarnings("rawtypes") Iterable caches) throws InvalidCacheException, CoreException {
		// Check if any of the caches have errors:
		boolean allValid = true;

		for (Object cacheObj : caches) {
			ICache<?> cache = (ICache<?>) cacheObj;
			if (cache.isValid()) {
				if (!cache.getStatus().isOK()) {
					throw new CoreException(cache.getStatus());
				}
			} else {
				allValid = false;
			}
		}
		if (!allValid) {
			// Throw the invalid cache exception, but first schedule a
			// re-attempt of the transaction logic, to occur when the
			// stale/unset cache objects have been updated
			CountingRequestMonitor countringRm = new CountingRequestMonitor(ImmediateExecutor.getInstance(), fRm) {
				@Override
				protected void handleCompleted() {
					execute();
				}
			};
			int count = 0;
			for (Object cacheObj : caches) {
				ICache<?> cache = (ICache<?>) cacheObj;
				if (!cache.isValid()) {
					cache.update(countringRm);
					count++;
				}
			}
			countringRm.setDoneCount(count);
			throw INVALID_CACHE_EXCEPTION;
		}
	}

}
