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

import woolpack.utils.CheckUtils;

/**
 * 並列性制御(セマフォとロック)をテストするための、{@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 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)}で識別子を検索する際のタイムアウト時間。
	 * @throws NullPointerException
	 *             引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException
	 *             startName または endName が空の場合。
	 */
	public RunnableGate(final String startName, final String endName,
			final ExecutorService executor, final long timeout) {
		CheckUtils.checkNotEmpty(startName);
		CheckUtils.checkNotEmpty(endName);
		CheckUtils.checkNotNull(executor);
		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 IllegalStateException
	 *             (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()}に記録するコマンド。
	 * @throws NullPointerException
	 *             引数のいずれかが null の場合。
	 * @throws StringIndexOutOfBoundsException
	 *             startName または endName が空の場合。
	 */
	public Runnable getGate(final String startName, final Runnable runnable,
			final String endName) {
		CheckUtils.checkNotEmpty(startName);
		CheckUtils.checkNotNull(runnable);
		CheckUtils.checkNotEmpty(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 NullPointerException
		 *             引数が null の場合。
		 * @throws StringIndexOutOfBoundsException
		 *             引数が空の場合。
		 * @throws IllegalStateException
		 *             (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitFor(final String name) {
			CheckUtils.checkNotEmpty(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
		 *             (InterruptedException)時間内にスレッドを認識することに失敗した場合。
		 */
		public boolean waitForStart() {
			return waitFor(startName);
		}

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

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

		/**
		 * コマンド終了の識別子が出現するまで一定時間待機し、 コマンド開始識別子とコマンド終了識別子を除いた、
		 * 実行順に記録された識別子の一覧を返す。
		 * 
		 * @return コマンド開始識別子とコマンド終了識別子を除いた、実行順に記録された識別子の一覧。
		 * @throws IllegalStateException
		 *             (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;
		}
	}
}
