package edu.cmu.cs.vbc.prog.prevayler.demos.scalability;

import org.prevayler.foundation.Cool;
import org.prevayler.foundation.StopWatch;

import java.text.DecimalFormat;
import java.util.LinkedList;
import java.util.List;

/** Represents a single run of a scalability test. To understand the implementation of this class, you must be familiar with Prevayler's Scalability Test (run org.prevayler.test.scalability.ScalabilityTest).
*/
abstract class ScalabilityTestRun {

	static private final long ROUND_DURATION_MILLIS = 1000 * 20;

	private final ScalabilityTestSubject subject;
	protected final int numberOfObjects;

	private double bestRoundOperationsPerSecond;
	private int bestRoundThreads;

	private final List connectionCache = new LinkedList();

	private long operationCount = 0;
	private long lastOperation = 0;
	private boolean isRoundFinished;
	private int activeRoundThreads = 0;


	/** @return Example: "123.12 operations/second (12 threads)".
	*/
	public String getResult() {
		return toResultString(bestRoundOperationsPerSecond, bestRoundThreads);
	}


	public double getOperationsPerSecond() {
		return bestRoundOperationsPerSecond;
	}


	protected ScalabilityTestRun(ScalabilityTestSubject subject, int numberOfObjects, int minThreads, int maxThreads) {
		if (minThreads > maxThreads) throw new IllegalArgumentException("The minimum number of threads cannot be greater than the maximum number.");
		if (minThreads < 1) throw new IllegalArgumentException("The minimum number of threads cannot be smaller than one.");

		this.subject = subject;
		this.numberOfObjects = numberOfObjects;

		out("\n\n========= Running " + name() + " (" + (maxThreads - minThreads + 1) + " rounds). Subject: " + subject.name() + "...");
		prepare();

		out("Each round will take approx. " + ROUND_DURATION_MILLIS / 1000 + " seconds to run...");
		performTest(minThreads, maxThreads);
		out("\n----------- BEST ROUND: " + getResult());
	}

	protected void prepare() {
		subject.replaceAllRecords(numberOfObjects);
		System.gc();
	}


	/** @return The name of the test to be executed. Example: "Prevayler Query Test".
	*/
	protected abstract String name();


	private void performTest(int minThreads, int maxThreads) {

		int threads = minThreads;
		while (threads <= maxThreads) {
			double operationsPerSecond = performRound(threads);

			if (operationsPerSecond > bestRoundOperationsPerSecond) {
				bestRoundOperationsPerSecond = operationsPerSecond;
				bestRoundThreads = threads;
			}

			threads++;
		}
	}


	/** @return The number of operations the test managed to execute per second with the given number of threads.
	*/
	private double performRound(int threads) {
		long initialOperationCount = operationCount;
		StopWatch stopWatch = StopWatch.start();

		startThreads(threads);
		sleep();
		stopThreads();

		double secondsEllapsed = stopWatch.secondsEllapsed();
		double operationsPerSecond = (operationCount - initialOperationCount) / secondsEllapsed;

		out("\nMemory used: " + Runtime.getRuntime().totalMemory());
		subject.reportResourcesUsed(System.out);
		out("Seconds ellapsed: " + secondsEllapsed);
		out("--------- Round Result: " + toResultString(operationsPerSecond, threads));

		return operationsPerSecond;
	}


	private void startThreads(int threads) {
		isRoundFinished = false;

		int i = 1;
		while(i <= threads) {
			startThread(lastOperation + i, threads);
			i++;
		}
	}


	private void startThread(final long startingOperation, final int operationIncrement) {
		(new Thread() {
			public void run() {
				try {
					Object connection = acquireConnection();

					long operation = startingOperation;
					while (!isRoundFinished) {
						executeOperation(connection, operation);
						operation += operationIncrement;
					}

					synchronized (connectionCache) {
						connectionCache.add(connection);
						operationCount += (operation - startingOperation) / operationIncrement;
						if (lastOperation < operation) lastOperation = operation;
						activeRoundThreads--;
					}

				} catch (OutOfMemoryError err) {
					outOfMemory();
				}
			}
		}).start();

		activeRoundThreads++;
	}


	protected abstract void executeOperation(Object connection, long operation);


	private Object acquireConnection() {
		synchronized (connectionCache) {
			return connectionCache.isEmpty()
				? subject.createTestConnection()
				: connectionCache.remove(0);
		}
	}


	private void stopThreads() {
		isRoundFinished = true;
		while (activeRoundThreads != 0) {
			Thread.yield();
		}
	}


	static private String toResultString(double operationsPerSecond, int threads) {
		String operations = new DecimalFormat("0.00").format(operationsPerSecond);
		return "" + operations + " operations/second (" + threads + " threads)";
	}

	static void outOfMemory() {
		System.gc();
		out(
			"\n\nOutOfMemoryError.\n" +
			"===========================================================\n" +
			"The VM must be started with a sufficient maximum heap size.\n" +
			"Example for Linux and Windows:  java -Xmx512000000 ...\n\n"
		);
	}

	static private void sleep() {
		Cool.sleep(ROUND_DURATION_MILLIS);
	}

	
	static private void out(Object obj) {
		System.out.println(obj);
	}
}
