/*******************************************************************************
 * Copyright (c) 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
 *******************************************************************************/
//#ifdef exercises
package org.eclipse.cdt.examples.dsf.dataviewer;
//#else

//#package org.eclipse.cdt.examples.dsf.dataviewer.answers;
//#endif

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.ICache;
import org.eclipse.cdt.dsf.concurrent.ImmediateInDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Transaction;
import org.eclipse.core.runtime.CoreException;

/**
 * A data generator which performs a sum computation on data retrieved from a
 * number of other data generators.  The data retrieval from other generators
 * is performed using ACPM caches and the result is calculated once all caches
 * are valid.
 * <p>
 * Unlike {@link AsyncSumDataGenerator}, this data generator listens to events
 * from the individual the data providers.  Theve events are used to
 * invalidate caches to make sure that they don't return incorrect data.  This
 * generator also sends out events to its clients to notify them to update, or
 * invalidate their caches.
 * </p>
 */
public class ACPMSumDataGenerator implements IDataGenerator, IDataGenerator.Listener {

	/**
	 * DSF executor used to serialize data access within this data generator.
	 */
	final private DsfExecutor fExecutor;

	/**
	 * Data generators to retrieve original data to perform calculations on.
	 * The generators are accessed through the cache manager wrappers.
	 */
	final private DataGeneratorCacheManager[] fDataGeneratorCMs;

	/**
	 * List of listeners for this data generator.
	 */
	final private List<Listener> fListeners = new LinkedList<Listener>();

	public ACPMSumDataGenerator(DsfExecutor executor, IDataGenerator[] generators) {
		fExecutor = executor;

		// Create wrappers for data generators and add ourselves as listener
		// to their events.
		fDataGeneratorCMs = new DataGeneratorCacheManager[generators.length];
		ImmediateInDsfExecutor immediateExecutor = new ImmediateInDsfExecutor(fExecutor);
		for (int i = 0; i < generators.length; i++) {
			fDataGeneratorCMs[i] = new DataGeneratorCacheManager(immediateExecutor, generators[i]);
			generators[i].addListener(this);
		}
	}

	@Override
	public void getCount(final DataRequestMonitor<Integer> rm) {
		// Artificially delay the retrieval of the sum data to simulate
		// real processing time.
		fExecutor.schedule(new Runnable() {
			@Override
			public void run() {
				// Create the transaction here to put all the ugly
				// code in one place.
				new Transaction<Integer>() {
					@Override
					protected Integer process() throws Transaction.InvalidCacheException, CoreException {
						return processCount(this);
					}
				}.request(rm);
			}
		}, PROCESSING_DELAY, TimeUnit.MILLISECONDS);
	}

	/**
	 * Perform the calculation to get the max count for the given transaction.
	 * @param transaction The ACPM transaction to use for calculation.
	 * @return Calculated count.
	 * @throws Transaction.InvalidCacheException {@link Transaction#process}
	 * @throws CoreException See {@link Transaction#process}
	 */
	private Integer processCount(Transaction<Integer> transaction)
			throws Transaction.InvalidCacheException, CoreException {
		// Assemble all needed count caches into a collection.
		List<ICache<Integer>> countCaches = new ArrayList<ICache<Integer>>(fDataGeneratorCMs.length);
		for (DataGeneratorCacheManager dataGeneratorCM : fDataGeneratorCMs) {
			countCaches.add(dataGeneratorCM.getCount());
		}
		// Validate all count caches at once.  This executes needed requests
		// in parallel.
		transaction.validate(countCaches);

		// Calculate the max value and return.
		int maxCount = 0;
		for (ICache<Integer> countCache : countCaches) {
			maxCount = Math.max(maxCount, countCache.getData());
		}
		return maxCount;
	}

	@Override
	public void getValue(final int index, final DataRequestMonitor<Integer> rm) {
		// Add a processing delay.
		fExecutor.schedule(new Runnable() {
			@Override
			public void run() {
				new Transaction<Integer>() {
					@Override
					protected Integer process() throws Transaction.InvalidCacheException, CoreException {
						return processValue(this, index);
					}
				}.request(rm);
			}
		}, PROCESSING_DELAY, TimeUnit.MILLISECONDS);
	}

	/**
	 * Perform the calculation to get the sum of values at given index.
	 * @param transaction The ACPM transaction to use for calculation.
	 * @param index Index of value to calculate.
	 * @return Calculated value.
	 * @throws Transaction.InvalidCacheException {@link Transaction#process}
	 * @throws CoreException See {@link Transaction#process}
	 */
	private Integer processValue(Transaction<Integer> transaction, int index)
			throws Transaction.InvalidCacheException, CoreException {
		List<ICache<Integer>> valueCaches = new ArrayList<ICache<Integer>>(fDataGeneratorCMs.length);
		for (DataGeneratorCacheManager dataGeneratorCM : fDataGeneratorCMs) {
			valueCaches.add(dataGeneratorCM.getValue(index));
		}
		// Validate all value caches at once.  This executes needed requests
		// in parallel.
		transaction.validate(valueCaches);

		int sum = 0;
		for (ICache<Integer> valueCache : valueCaches) {
			sum += valueCache.getData();
		}
		return sum;
	}

	@Override
	public void shutdown(final RequestMonitor rm) {
		for (DataGeneratorCacheManager dataGeneratorCM : fDataGeneratorCMs) {
			dataGeneratorCM.getDataGenerator().removeListener(this);
			dataGeneratorCM.dispose();
			rm.done();
		}
		rm.done();
	}

	@Override
	public void addListener(final Listener listener) {
		// Must access fListeners on executor thread.
		try {
			fExecutor.execute(new DsfRunnable() {
				@Override
				public void run() {
					fListeners.add(listener);
				}
			});
		} catch (RejectedExecutionException e) {
		}
	}

	@Override
	public void removeListener(final Listener listener) {
		// Must access fListeners on executor thread.
		try {
			fExecutor.execute(new DsfRunnable() {
				@Override
				public void run() {
					fListeners.remove(listener);
				}
			});
		} catch (RejectedExecutionException e) {
		}
	}

	@Override
	public void countChanged() {
		// Must access fListeners on executor thread.
		try {
			fExecutor.execute(new DsfRunnable() {
				@Override
				public void run() {
					for (Listener listener : fListeners) {
						listener.countChanged();
					}
				}
			});
		} catch (RejectedExecutionException e) {
		}
	}

	@Override
	public void valuesChanged(final Set<Integer> changed) {
		// Must access fListeners on executor thread.
		try {
			fExecutor.execute(new DsfRunnable() {
				@Override
				public void run() {
					for (Object listener : fListeners) {
						((Listener) listener).valuesChanged(changed);
					}
				}
			});
		} catch (RejectedExecutionException e) {
		}
	}
}
