/*
 * 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;

/**
 * 並列性制御(セマフォとロック)をテストするための、{@link Runnable}や{@link Context}を生成するクラス。
 * 適用しているパターン：Hook Operation。
 * @author nakamura
 *
 */
public class RunnableGate {
	/**
	 * {@link #execute(Runnable)}で実行したスレッドを検索する際や{@link Context#waitFor(String)}で識別子を検索する際のタイムアウト時間のデフォルト値。
	 */
	public static final long TIMEOUT = 500;
	
	/**
	 * {@link #execute(Runnable)}を用いて別のスレッドでコマンドを実行する際の、コマンド開始を意味する識別子のデフォルト値。
	 */
	public static final String START = "start";
	
	/**
	 * {@link #execute(Runnable)}を用いて別のスレッドでコマンドを実行する際の、コマンド終了を意味する識別子のデフォルト値。
	 */
	public static final String END = "end";

	private final String startName;
	private final String endName;
	private final ExecutorService executor;
	private final long timeout;
	private final Map<Thread,Context> map;
	
	private Context lastExecuted;
	
	/**
	 * コンストラクタ。
	 * @param startName {@link #execute(Runnable)}を用いて別のスレッドでコマンドを実行する際の、コマンド開始を意味する識別子。
	 * @param endName {@link #execute(Runnable)}を用いて別のスレッドでコマンドを実行する際の、コマンド終了を意味する識別子。
	 * @param executor {@link Runnable}コマンドを実行するオブジェクト。
	 * @param timeout {@link #execute(Runnable)}で実行したスレッドを検索する際や{@link Context#waitFor(String)}で識別子を検索する際のタイムアウト時間。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException startName または endName が空の場合。
	 */
	public RunnableGate(final String startName, final String endName, final ExecutorService executor, final long timeout){
		startName.charAt(0);
		endName.charAt(0);
		executor.getClass();
		if(timeout <= 0){
			throw new IllegalArgumentException("timeout must be positive value.");
		}
		
		this.startName = startName;
		this.endName = endName;
		this.executor = executor;
		this.timeout = timeout;
		
		map = new HashMap<Thread,Context>();
	}
	
	/**
	 * コンストラクタ。
	 * @param executor {@link Runnable}コマンドを実行するオブジェクト。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public RunnableGate(final ExecutorService executor){
		this(START, END, executor, TIMEOUT);
	}
	
	/**
	 * 別のスレッドでコマンドを実行する。
	 * @param runnable コマンド。
	 * @return コマンドと関連付けられた{@link Context}。
	 * @throws NullPointerException 引数が null の場合。
	 * @throws RunnableException (InterruptedException)時間内にスレッドを認識することに失敗した場合。
	 */
	public synchronized Context execute(final Runnable runnable){
		final long mark = System.currentTimeMillis();
		lastExecuted = null;
		executor.execute(getGate(startName, runnable, endName));
		try{
			while(lastExecuted == null){
				long t = timeout - (System.currentTimeMillis() - mark);
				if(t <= 0){
					throw new InterruptedException();
				}
				this.wait(t);
			}
			return lastExecuted;
		} catch (final InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new RuntimeException(e);
		}finally{
			lastExecuted = null;
		}
	}
	
	private synchronized void put(final String name){
		if(name.equals(startName)){
			final Context context = new Context(Thread.currentThread());
			map.put(Thread.currentThread(), context);
			lastExecuted = context;
		}
		map.get(Thread.currentThread()).add(name);
		if(name.equals(endName)){
			map.remove(Thread.currentThread());
		}
		notifyAll();
	}
	
	/**
	 * 委譲先を実行する前後に識別子を記録するコマンドを返す。
	 * @param startName 委譲先の開始前を意味する識別子。
	 * @param runnable 委譲先。
	 * @param endName 委譲先の終了後を意味する識別子。
	 * @return 委譲先の実行前後に識別子を{@link Context#getEndList()}に記録するコマンド。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException startName または endName が空の場合。
	 */
	public Runnable getGate(final String startName, final Runnable runnable, final String endName){
		startName.charAt(0);
		runnable.getClass();
		endName.charAt(0);
		return new Runnable(){
			public void run() {
				put(startName);
				try{
					runnable.run();
				}finally{
					put(endName);
				}
			}
		};
	}
	
	/**
	 * 実行時に識別子を記録するコマンドを返す。
	 * @param name 識別子。
	 * @return 実行時に識別子を{@link Context#getEndList()}に記録するコマンド。
	 * @throws NullPointerException 引数が null の場合。
	 * @throws StringIndexOutOfBoundsException 引数が空の場合。
	 */
	public Runnable getGate(final String name){
		return new Runnable(){
			public void run() {
				put(name);
			}
		};
	}
	
	private Runnable getPause(){
		return new Runnable(){
			public void run() {
				final Semaphore semaphore = new Semaphore(0, true);
				map.get(Thread.currentThread()).setSemaphore(semaphore);
				try {
					semaphore.acquire();
				} catch (final InterruptedException e) {
					Thread.currentThread().interrupt();
					throw new RuntimeException(e);
				}finally{
					map.get(Thread.currentThread()).setSemaphore(null);
				}
			}
		};
	}
	
	/**
	 * 一時停止の実行前後に識別子を記録するコマンドを返す。
	 * @param startName 一時停止の開始前を意味する識別子。
	 * @param endName 一時停止の終了後を意味する識別子。
	 * @return 一時停止の実行前後に識別子を{@link Context#getEndList()}に記録するコマンド。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException startName または endName が空の場合。
	 */
	public Runnable getPause(final String startName, final String endName){
		return getGate(startName, getPause(), endName);
	}
	
	/**
	 * {@link RunnableGate#execute(Runnable)}実行時に返却される、ひとつのコマンドの実行状況を保持し制御するクラス。
	 * @author nakamura
	 *
	 */
	public class Context {
		private final Thread thread;
		private final List<String> list;
		private final List<String> unmodifiableList;
		
		private Semaphore semaphore;
		
		Context(final Thread thread){
			this.thread = thread;
			list = new ArrayList<String>();
			unmodifiableList = Collections.unmodifiableList(list);
		}
		
		void setSemaphore(final Semaphore semaphore){
			this.semaphore = semaphore;
		}
		
		void add(final String s){
			list.add(s);
		}

		/**
		 * {@link RunnableGate#getPause(String, String)}で一時停止中のスレッドを再開する。
		 *
		 */
		public void resume(){
			semaphore.release();
		}
		
		/**
		 * 指定した識別子が出現するまで一定時間待機する。
		 * @param name 識別子。
		 * @return 一定時間内に指定した識別子が出現した場合は true。
		 * @throws NullPointerException 引数が null の場合。
		 * @throws StringIndexOutOfBoundsException 引数が空の場合。
		 * @throws RunnableException (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitFor(final String name){
			name.charAt(0);
			synchronized(RunnableGate.this){
				final long mark = System.currentTimeMillis();
				while(true){
					if(list.contains(name)){
						return true;
					}
					long t = timeout - (System.currentTimeMillis() - mark);
					if(t <= 0){
						return false;
					}
					try {
						RunnableGate.this.wait(t);
					} catch (final InterruptedException e) {
						Thread.currentThread().interrupt();
						throw new RuntimeException(e);
					}
				}
			}
		}
		
		/**
		 * コマンド開始の識別子が出現するまで一定時間待機する。
		 * @return 一定時間内にコマンド開始の識別子が出現した場合は true。
		 * @throws RunnableException (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitForStart(){
			return waitFor(startName);
		}
		
		/**
		 * コマンド終了の識別子が出現するまで一定時間待機する。
		 * @return 一定時間内にコマンド終了の識別子が出現した場合は true。
		 * @throws RunnableException (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitForEnd(){
			return waitFor(endName);
		}
		
		/**
		 * 実行順に記録された識別子の一覧を返す。
		 * @return 実行順に記録された識別子の一覧。
		 */
		public List<String> getList(){
			return unmodifiableList;
		}
		
		/**
		 * コマンド終了の識別子が出現するまで一定時間待機し、
		 * コマンド開始識別子とコマンド終了識別子を除いた、
		 * 実行順に記録された識別子の一覧を返す。
		 * @return コマンド開始識別子とコマンド終了識別子を除いた、実行順に記録された識別子の一覧。
		 * @throws RunnableException (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public List<String> getEndList(){
			if(!waitForEnd()){
				throw new RuntimeException("not finished.");
			}
			return unmodifiableList.subList(1, unmodifiableList.size()-1);
		}
		
		/**
		 * 本クラスのインスタンスに対応付けられているスレッドを返す。
		 * @return 本クラスのインスタンスに対応付けられているスレッド。
		 */
		public Thread getThread(){
			return thread;
		}
	}
}
