/*
 * 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;

/**
 * 並列性制御(セマフォとロック)をテストするための、{@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(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 RuntimeException("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(){
		System.out.print(label);
		System.out.print(": ");
		
		if(count == 0){
			System.out.print("count:0");
		}else{
			System.out.print("thread");
			print(threadArray);
			System.out.print(", ");
			
			System.out.print("mean:");
			System.out.print(culcMean(threadArray));
			System.out.print(", ");
			
			System.out.print("concurrent");
			print(concurrentArray);
			System.out.print(", ");

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

		System.out.println();
	}
	
	/**
	 * {@link #execute()}の実行状況を検証する。
	 * 検証結果が偽の場合は実行状況を標準出力する。
	 * @return {@link #execute()}の実行状況を検証した結果。
	 */
	public boolean assertValid(){
		final boolean result = minValidFlag && maxValidFlag && assertThreshold(threadArray);
		if(!result){
			System.out.println("minValid:" + minValidFlag);
			System.out.println("maxValid:" + maxValidFlag);
			System.out.println("threshold:" + assertThreshold(threadArray));
			print();
		}
		return result;
	}
	
	private void print(final int[] array){
		for(int i=0; i<array.length; i++){
			System.out.print("-");
			System.out.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;
	}
}
