/*******************************************************************************
 * Copyright (c) 2006, 2015 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.Collections;
import java.util.HashMap;
import java.util.Random;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.examples.dsf.DsfExamplesPlugin;

/**
 * Thread-based implementation of the data generator.
 * <p>
 * This generator is based around a queue of client requests and a thread which
 * reads the requests from the queue and processes them. The distinguishing
 * feature of this generator is that it uses a a blocking queue as the main
 * synchronization object.  However, fListeners, fShutdown,  and fChangedIndexes
 * fields also need to be thread-safe and so they implement their own
 * synchronization.
 * </p>
 */
public class DataGeneratorWithThread extends Thread implements IDataGenerator {

	// Request objects are used to serialize the interface calls into objects
	// which can then be pushed into a queue.
	abstract class Request {
		final RequestMonitor fRequestMonitor;

		Request(RequestMonitor rm) {
			fRequestMonitor = rm;
		}
	}

	class CountRequest extends Request {
		CountRequest(DataRequestMonitor<Integer> rm) {
			super(rm);
		}
	}

	class ItemRequest extends Request {
		final int fIndex;

		ItemRequest(int index, DataRequestMonitor<Integer> rm) {
			super(rm);
			fIndex = index;
		}
	}

	class ShutdownRequest extends Request {
		ShutdownRequest(RequestMonitor rm) {
			super(rm);
		}
	}

	// Main request queue of the data generator.  The getValue(), getCount(),
	// and shutdown() methods write into the queue, while the run() method
	// reads from it.
	private final BlockingQueue<Request> fQueue = new LinkedBlockingQueue<Request>();

	// ListenerList class provides thread safety.
	private ListenerList<Listener> fListeners = new ListenerList<>();

	// Current number of elements in this generator.
	private int fCount = MIN_COUNT;

	// Counter used to determine when to reset the element count.
	private int fCountResetTrigger = 0;

	// Elements which were modified since the last reset.
	private Map<Integer, Integer> fChangedValues = Collections.synchronizedMap(new HashMap<Integer, Integer>());

	// Used to determine when to make changes in data.
	private long fLastChangeTime = System.currentTimeMillis();

	// Flag indicating when the generator has been shut down.
	private AtomicBoolean fShutdown = new AtomicBoolean(false);

	public DataGeneratorWithThread() {
		// Immediately kick off the request processing thread.
		start();
	}

	@Override
	public void shutdown(RequestMonitor rm) {
		// Mark the generator as shut down.  After the fShutdown flag is set,
		// all new requests should be shut down.
		if (!fShutdown.getAndSet(true)) {
			fQueue.add(new ShutdownRequest(rm));
		} else {
			//
			rm.setStatus(new Status(IStatus.ERROR, DsfExamplesPlugin.PLUGIN_ID, "Supplier shut down"));
			rm.done();
		}
	}

	@Override
	public void getCount(DataRequestMonitor<Integer> rm) {
		if (!fShutdown.get()) {
			fQueue.add(new CountRequest(rm));
		} else {
			rm.setStatus(new Status(IStatus.ERROR, DsfExamplesPlugin.PLUGIN_ID, "Supplier shut down"));
			rm.done();
		}
	}

	@Override
	public void getValue(int index, DataRequestMonitor<Integer> rm) {
		if (!fShutdown.get()) {
			fQueue.add(new ItemRequest(index, rm));
		} else {
			rm.setStatus(new Status(IStatus.ERROR, DsfExamplesPlugin.PLUGIN_ID, "Supplier shut down"));
			rm.done();
		}
	}

	@Override
	public void addListener(Listener listener) {
		fListeners.add(listener);
	}

	@Override
	public void removeListener(Listener listener) {
		fListeners.remove(listener);
	}

	@Override
	public void run() {
		try {
			while (true) {
				// Get the next request from the queue.  The time-out
				// ensures that that the random changes get processed.
				final Request request = fQueue.poll(100, TimeUnit.MILLISECONDS);

				// If a request was dequeued, process it.
				if (request != null) {
					// Simulate a processing delay.

					if (request instanceof CountRequest) {
						processCountRequest((CountRequest) request);
					} else if (request instanceof ItemRequest) {
						processItemRequest((ItemRequest) request);
					} else if (request instanceof ShutdownRequest) {
						// If shutting down, just break out of the while(true)
						// loop and thread will exit.
						request.fRequestMonitor.done();
						break;
					}
				} else {
					Thread.sleep(PROCESSING_DELAY);
				}

				// Simulate data changes.
				randomChanges();
			}
		} catch (InterruptedException x) {
		}
	}

	private void processCountRequest(CountRequest request) {
		@SuppressWarnings("unchecked") // Suppress warning about lost type info.
		DataRequestMonitor<Integer> rm = (DataRequestMonitor<Integer>) request.fRequestMonitor;

		rm.setData(fCount);
		rm.done();
	}

	private void processItemRequest(ItemRequest request) {
		@SuppressWarnings("unchecked") // Suppress warning about lost type info.
		DataRequestMonitor<Integer> rm = (DataRequestMonitor<Integer>) request.fRequestMonitor;

		if (fChangedValues.containsKey(request.fIndex)) {
			rm.setData(fChangedValues.get(request.fIndex));
		} else {
			rm.setData(request.fIndex);
		}
		rm.done();
	}

	private void randomChanges() {
		// Check if enough time is elapsed.
		if (System.currentTimeMillis() > fLastChangeTime + RANDOM_CHANGE_INTERVAL) {
			fLastChangeTime = System.currentTimeMillis();

			// Once every number of changes, reset the count, the rest of the
			// times just change certain values.
			if (++fCountResetTrigger % RANDOM_COUNT_CHANGE_INTERVALS == 0) {
				randomCountReset();
			} else {
				randomDataChange();
			}
		}
	}

	private void randomCountReset() {
		// Calculate the new count.
		Random random = new java.util.Random();
		fCount = MIN_COUNT + random.nextInt(MAX_COUNT - MIN_COUNT);

		// Reset the changed values.
		fChangedValues.clear();

		// Notify listeners
		for (Object listener : fListeners.getListeners()) {
			((Listener) listener).countChanged();
		}
	}

	private void randomDataChange() {
		// Calculate the indexes to change.
		Random random = new java.util.Random();
		Map<Integer, Integer> changed = new HashMap<Integer, Integer>();
		for (int i = 0; i < fCount * RANDOM_CHANGE_SET_PERCENTAGE / 100; i++) {
			int randomIndex = random.nextInt(fCount);
			int randomValue = random.nextInt(fCount);
			changed.put(randomIndex, randomValue);
		}

		// Add the indexes to an overall set of changed indexes.
		fChangedValues.putAll(changed);

		// Notify listeners
		for (Object listener : fListeners.getListeners()) {
			((Listener) listener).valuesChanged(changed.keySet());
		}
	}
}
