/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package woolpack.test;

import java.io.PrintStream;

/**
 * 並列性制御(セマフォとロック)をテストするための、{@link #execute()}内で実行状況を記録するクラスです。
 * 
 * @author nakamura
 * 
 */
public class TestConcurrent {
	private final String label;
	private final Object lock;
	private final int minConcurrent;
	private final int maxConcurrent;
	private final int threshold;
	private final double failOdds;
	private final int[] concurrentArray;
	private final int[] threadArray;
	private final ThreadLocal<Integer> threadLocal;
	private int currentConcurrent;
	private int count;
	private boolean minValidFlag;
	private boolean maxValidFlag;

	/**
	 * @param label 実行状況を標準出力する際の先頭の文字列。
	 * @param lock 実行状況を記録する際のロックオブジェクト。
	 * @param minConcurrent 期待される同時実行可能な最小のスレッド数。
	 * @param maxConcurrent 期待される同時実行可能な最大のスレッド数。
	 * @param threadCount テストにおいて並行して実行するスレッド数。
	 * @param threshold 実行状況を検証するための許容範囲(単位は件)。
	 */
	public TestConcurrent(final String label, final Object lock,
			final int minConcurrent, final int maxConcurrent,
			final int threadCount, final int threshold) {
		this(label, lock, minConcurrent, maxConcurrent, threadCount, threshold,
				0.5);
	}

	/**
	 * @param label 実行状況を標準出力する際の先頭の文字列。
	 * @param lock 実行状況を記録する際のロックオブジェクト。
	 * @param minConcurrent 期待される同時実行可能な最小のスレッド数。
	 * @param maxConcurrent 期待される同時実行可能な最大のスレッド数。
	 * @param threadCount テストにおいて並行して実行するスレッド数。
	 * @param threshold 実行状況を検証するための許容範囲(単位は件)。
	 * @param failOdds 実行状況を記録した後に{@link RuntimeException}を発生させる確率。
	 */
	public TestConcurrent(final String label, final Object lock,
			final int minConcurrent, final int maxConcurrent,
			final int threadCount, final int threshold, final double failOdds) {
		this.label = label;
		this.lock = lock;
		this.minConcurrent = minConcurrent;
		this.maxConcurrent = maxConcurrent;
		this.threshold = threshold;
		this.failOdds = failOdds;
		concurrentArray = new int[maxConcurrent + 2];
		threadArray = new int[threadCount];
		threadLocal = new ThreadLocal<Integer>();

		currentConcurrent = 0;
		count = 0;
		minValidFlag = true;
		maxValidFlag = true;
	}

	/**
	 * {@link #execute()}が実行された回数を返します。
	 * @return {@link #execute()}が実行された回数。
	 */
	public int getCount() {
		return count;
	}

	/**
	 * 記録するスレッドの識別子(ゼロ開始)をスレッドローカルに設定します。
	 * @param i
	 */
	public void setThreadId(final int i) {
		threadLocal.set(i);
	}

	private void increaseCount() {
		synchronized (lock) {
			count++;
		}
	}

	private void recodeThread() {
		synchronized (lock) {
			threadArray[threadLocal.get()]++;
		}
	}

	private void recodeConcurrent() {
		synchronized (lock) {
			concurrentArray[currentConcurrent - 1]++;
		}
	}

	private void increaseConcurrent() {
		synchronized (lock) {
			currentConcurrent++;
		}
	}

	private void decreaseConcurrent() {
		synchronized (lock) {
			currentConcurrent--;
		}
	}

	private void check() {
		synchronized (lock) {
			minValidFlag &= (minConcurrent <= currentConcurrent);
			maxValidFlag &= (currentConcurrent <= maxConcurrent);
		}
	}

	private void failRandom() {
		if (Math.random() < failOdds) {
			throw new IllegalStateException("random fail.");
		}
	}

	/**
	 * 並行性制御を実装しているクラスから呼び出されます(called)。
	 * 呼び出された回数、スレッド識別子、並行実行数を記録し、
	 * 内容がコンストラクタ引数と合致しているか検証します。
	 * 最後に確率的に{@link RuntimeException}を発生させます。
	 */
	public void execute() {
		Thread.yield();
		increaseCount();
		Thread.yield();
		recodeThread();
		Thread.yield();
		increaseConcurrent();
		Thread.yield();
		recodeConcurrent();
		Thread.yield();
		check();
		Thread.yield();
		decreaseConcurrent();
		Thread.yield();
		failRandom();
		Thread.yield();
	}

	/**
	 * {@link #execute()}の実行状況を標準出力します。
	 */
	public void print() {
		final PrintStream stream = System.out;
		stream.print(label);
		stream.print(" : ");

		if (count == 0) {
			stream.print("count:0");
		} else {
			stream.print("thread");
			print(threadArray);
			stream.print(", ");

			stream.print("mean : ");
			stream.print(culcMean(threadArray));
			stream.print(", ");

			stream.print("concurrent");
			print(concurrentArray);
			stream.print(", ");

			stream.print("min : ");
			stream.print(minValidFlag);
			stream.print(", ");
			stream.print("max : ");
			stream.print(maxValidFlag);
		}

		stream.println();
	}

	/**
	 * {@link #execute()}の実行状況を検証します。
	 * 検証結果が偽の場合は実行状況を標準出力します。
	 * 
	 * @return {@link #execute()}の実行状況を検証した結果。
	 */
	public boolean assertValid() {
		final boolean result = minValidFlag && maxValidFlag
				&& assertThreshold(threadArray);
		if (!result) {
			final PrintStream stream = System.out;
			stream.println("minValid : ");
			stream.println(minValidFlag);
			stream.println("maxValid : ");
			stream.println(maxValidFlag);
			stream.println("threshold : ");
			stream.println(assertThreshold(threadArray));
			print();
		}
		return result;
	}

	private void print(final int[] array) {
		final PrintStream stream = System.out;
		for (int i = 0; i < array.length; i++) {
			stream.print("-");
			stream.print(array[i]);
		}
	}

	private int culcMean(final int[] array) {
		if (array.length == 0) {
			return 0;
		}
		int mean = 0;
		for (int i = 0; i < array.length; i++) {
			mean += array[i];
		}
		mean /= array.length;
		return mean;
	}

	private boolean assertThreshold(final int[] array) {
		final int mean = culcMean(array);
		for (int i = 0; i < array.length; i++) {
			if (Math.abs(mean - array[i]) > threshold) {
				return false;
			}
		}
		return true;
	}
}
