/*
 * 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}を生成するクラスです。
 * <br/>適用しているデザインパターン：Builder。
 * 
 * @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 final Object lock = new Object();
	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)}で識別子を検索する際のタイムアウト時間。
	 */
	public RunnableGate(final String startName, final String endName,
			final ExecutorService executor, final long timeout) {
		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}コマンドを実行するオブジェクト。
	 */
	public RunnableGate(final ExecutorService executor) {
		this(START, END, executor, TIMEOUT);
	}

	/**
	 * 別のスレッドでコマンドを実行します。
	 * 
	 * @param runnable コマンド。
	 * @return コマンドと関連付けられた{@link Context}。
	 * @throws NullPointerException 引数が null の場合。
	 * @throws IllegalStateException ({@link InterruptedException})時間内にスレッドを認識することに失敗した場合。
	 */
	public Context execute(final Runnable runnable) {
		synchronized (lock) {
			final long mark = System.currentTimeMillis();
			lastExecuted = null;
			executor.execute(getGate(startName, runnable, endName));
			try {
				while (lastExecuted == null) {
					final long t = timeout - (System.currentTimeMillis() - mark);
					if (t <= 0) {
						throw new InterruptedException();
					}
					lock.wait(t);
				}
				return lastExecuted;
			} catch (final InterruptedException e) {
				Thread.currentThread().interrupt();
				throw new IllegalStateException(e);
			} finally {
				lastExecuted = null;
			}
		}
	}

	private synchronized void put(final String name) {
		synchronized (lock) {
			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());
			}
			lock.notifyAll();
		}
	}

	/**
	 * 委譲先を実行する前後に識別子を記録するコマンドを返します。
	 * 
	 * @param startName 委譲先の開始前を意味する識別子。
	 * @param runnable 委譲先。
	 * @param endName 委譲先の終了後を意味する識別子。
	 * @return 委譲先の実行前後に識別子を{@link Context#getEndList()}に記録するコマンド。
	 */
	public Runnable getGate(
			final String startName,
			final Runnable runnable,
			final String endName) {
		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 IllegalStateException(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 IllegalStateException
		 *             ({@link InterruptedException})時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitFor(final String name) {
			synchronized (RunnableGate.this.lock) {
				final long mark = System.currentTimeMillis();
				while (true) {
					if (list.contains(name)) {
						return true;
					}
					final long t = timeout - (System.currentTimeMillis() - mark);
					if (t <= 0) {
						return false;
					}
					try {
						RunnableGate.this.lock.wait(t);
					} catch (final InterruptedException e) {
						Thread.currentThread().interrupt();
						throw new IllegalStateException(e);
					}
				}
			}
		}

		/**
		 * コマンド開始の識別子が出現するまで一定時間待機します。
		 * 
		 * @return 一定時間内にコマンド開始の識別子が出現した場合は true。
		 * @throws IllegalStateException
		 *             ({@link InterruptedException})時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitForStart() {
			return waitFor(startName);
		}

		/**
		 * コマンド終了の識別子が出現するまで一定時間待機します。
		 * 
		 * @return 一定時間内にコマンド終了の識別子が出現した場合は true。
		 * @throws IllegalStateException
		 *             ({@link InterruptedException})時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitForEnd() {
			return waitFor(endName);
		}

		/**
		 * 実行順に記録された識別子の一覧を返します。
		 * 
		 * @return 実行順に記録された識別子の一覧。
		 */
		public List<String> getList() {
			return unmodifiableList;
		}

		/**
		 * コマンド終了の識別子が出現するまで一定時間待機し、 コマンド開始識別子とコマンド終了識別子を除いた、
		 * 実行順に記録された識別子の一覧を返す。
		 * 
		 * @return コマンド開始識別子とコマンド終了識別子を除いた、実行順に記録された識別子の一覧。
		 * @throws IllegalStateException
		 *             ({@link InterruptedException})時間内にスレッドを認識することに失敗した場合。
		 */
		public List<String> getEndList() {
			if (!waitForEnd()) {
				throw new IllegalStateException("not finished.");
			}
			return unmodifiableList.subList(1, unmodifiableList.size() - 1);
		}

		/**
		 * 本クラスのインスタンスに対応付けられているスレッドを返します。
		 * 
		 * @return 本クラスのインスタンスに対応付けられているスレッド。
		 */
		public Thread getThread() {
			return thread;
		}
	}
}
