/*
 * Paraselene
 * Copyright (c) 2009, 2010  Akira Terasaki
 * このファイルは同梱されているLicense.txtに定めた条件に同意できる場合にのみ
 * 利用可能です。
 */
package paraselene.supervisor;


import java.util.*;
import java.io.*;
import paraselene.*;
import paraselene.supervisor.*;
import paraselene.tag.*;
import paraselene.ui.*;
import paraselene.ajax.*;
import paraselene.ajax.data.*;

class Child extends Thread implements DaemonChannel {
	static volatile HashMap<String, Child>	name_space = new HashMap<String, Child>();

	Date	start_dt;

	volatile Page main_page = null;
	volatile boolean call_input;
	volatile boolean refresh_f;
	volatile RequestParameter request = null;
	volatile Forward forward = null;
	volatile Page param_page = null;
	volatile Throwable th = null;
	volatile long	thread_id = -1;

	// for Daemon
	volatile Daemon	daemon = null;
	volatile String	daemon_name = null;
	volatile ArrayList<History>	hist = null;
	volatile Date	limit_dt = null;
	volatile KilledByTimeoutException	kbt = null;

	Child() {
		setDaemon( true );
		start();
	}

	void addHistory( History h ) {
		synchronized( hist ) {
			if ( hist.contains( h ) )	return;
			hist.add( h );
		}
	}

	void removeHistory( History h ) {
		try {
			h.lock();
			synchronized( hist ) {
				hist.remove( h );
			}
		}
		finally {
			h.unlock();
		}
	}

	public String getDaemonName() {
		return daemon_name;
	}

	public void startDaemon( String name, Daemon daemon ) {
		SandBox.startDaemon( name, daemon );
	}

	public void depriveDaemonOfPage() {
		SandBox.depriveDaemonOfPage();
	}

	public Object postDaemon( String name, Object obj ) {
		return SandBox.postDaemon( name, obj );
	}

	public boolean tryLockPageForUpdate() {
		if ( hist == null )	return true;
		synchronized( hist ) {
			int cnt = hist.size();
			boolean	ret = true;
			for ( int i = 0; i < cnt; i++ ) {
				History	h = hist.get( i );
				ret = h.tryLock();
				if ( !ret )	break;
			}
			if ( ret )	return ret;
			for ( int i = 0; i < cnt; i++ ) {
				History	h = hist.get( i );
				h.unlock();
			}
			return ret;
		}
	}

	public Page[] getPageForUpdate() {
		if ( hist == null ) {
			return request.getHistory().getBrowsingPage();
		}
		synchronized( hist ) {
			ArrayList<Page>	list = new ArrayList<Page>();
			int	cnt = hist.size();
			ArrayList<History>	drop = new ArrayList<History>();
			for ( int i = 0; i < cnt; i++ ) {
				History	h = hist.get( i );
				if ( h.drop_f ) {
					drop.add( h );
					continue;
				}
				Page[]	p = h.getBrowsingPage();
				for ( int j = 0; j < p.length; j++ ) {
					list.add( p[j] );
				}
			}
			cnt = drop.size();
			for ( int i = 0; i < cnt; i++ )	hist.remove( drop.get( i ) );
			return list.toArray( new Page[0] );
		}
	}

	public Page[] getPageForUpdate( PageID ... id ) {
		Page[]	page = getPageForUpdate();
		ArrayList<Page>	ret = new ArrayList<Page>();
		for ( int i = 0; i < page.length; i++ ) {
			PageID	p = page[i].getID();
			for ( int j = 0; j < id.length; j++ ) {
				if ( id[j] == p ) {
					ret.add( page[i] );
					break;
				}
			}
		}
		return ret.toArray( new Page[0] );
	}

	public void commitPage( DaemonForward ... fw ) {
		if ( hist == null )	return;
		synchronized( hist ) {
			int	cnt = hist.size();
			for ( int i = 0; i < cnt; i++ ) {
				History	h = hist.get( i );
				Page[]	p = h.getBrowsingPage();
				for ( int j = 0; j < p.length; j++ ) {
					for ( int fw_i = 0; fw_i < fw.length; fw_i++ ) {
						if ( fw[fw_i].page == p[j] ) {
							CometHolder	ch = new CometHolder( h, fw[fw_i].forward );
							try {
								Ajax.addComet( h.session_id, ch.getPostBack() );
							}
							catch( Exception e ) {
								e.printStackTrace();
							}
						}
					}
				}
				h.unlock();
			}
		}
	}

	public void delayTimeout() {
		Date	now = new Date();
		limit_dt = new Date( now.getTime() + SandBox.DAEMON_TIMEOUT );
	}

	private static Forward input( Page page, RequestParameter req, Forward fw ) throws Exception {
		if ( page == null )	return fw;
		PageHooker[]	hook = page.getPageHooker();
		for ( int i = 0; i < hook.length; i++ ) {
			Forward	old = fw;
			fw = hook[i].beforeInput( page, req, fw );
			if ( fw == null )	fw = old;
		}
		return page.input( req, fw );
	}

	private static Page output( Page page, Page from, RequestParameter req, boolean new_f ) throws Exception {
		if ( page == null )	return null;
		Page	old = page;
		page.beginning( req );
		page = page.output( from, req );
		if ( page == null )	page = old;
		Tag	body = page.getBodyTag();
		if ( body != null ) {
			if ( body.isModified() ) {
				new_f = true;
			}
		}

		PageBypassedDiv[]	div = page.getPageBypassedDiv();
		for ( int i = 0; i < div.length; i++ ) {
			boolean	div_new = div[i].isModified();
			div[i].setView( output( div[i].getBypassPage(), from, req, div_new ) );
		}

		PageHooker[]	hook = page.getPageHooker();
		if ( page.isAjax() && !new_f ) {
			Tag[]	t = page.getModifiedTag();
			ArrayList<PageHooker>	list = new ArrayList<PageHooker>();
			for ( int i = 0; i < t.length; i++ ) {

				PageHooker[]	tmp = t[i].getInnerHTMLPart( hook );
				for ( int j = 0; j < tmp.length; j++ ) {
					list.add( tmp[j] );
				}
			}
			for ( int i = 0; i < hook.length; i++ ) {
				if ( hook[i] instanceof PageBypassedDiv ) {
					list.add( hook[i] );
				}
			}
			hook = list.toArray( new PageHooker[0] );
		}
		for ( int i = 0; i < hook.length; i++ ) {
			hook[i].afterOutput( from, old, req );
		}
		if ( old != page ) {
			old.resetOnLoadScript();
		}
		return page;
	}

	private void userFunction() {
		History	hist = null;
		if ( main_page == null )	return;
		try {
			try {
				hist = request.getHistory();
				if ( hist != null ) {
					hist.lock();
				}
			}
			catch( Exception e ) {
			}

			if ( main_page.getAjaxSupport() == Page.AjaxSupport.SERVER_PUSH ) {
				Ajax.entryComet( Ajax.COMET );
			}
			if ( call_input ) {
				forward = input( main_page, request, forward );
			}
			else {
				param_page = output( main_page, param_page, request, refresh_f );

			}
		}
		catch( Throwable e ) {
			th = e;
		}
		finally {
			if ( hist != null )	hist.unlock();
		}
	}

	private void daemonFunction() {
		if ( daemon == null )	return;
		try {
			daemon.run( this );
		}
		catch( Throwable e ) {
			th = e;
		}
		commitPage();
		if ( kbt != null )	th = kbt;
		try {
			if ( th != null )	th.printStackTrace();
			daemon.onDestroy( th );
		}
		finally {
			if ( daemon_name != null ) {
				synchronized( name_space ) {
					name_space.remove( daemon_name );
				}
			}
			if ( hist != null ) {
				synchronized( hist ) {
					int	cnt = hist.size();
					for ( int  i = 0; i < cnt; i++ ) {
						hist.get( i ).unlock();
					}
				}
			}
			SandBox.removeChild( this );
		}
	}

	public void run() {
		thread_id = Thread.currentThread().getId();
		try {
			synchronized( this ) {
				wait();
			}
		}
		catch( Exception e ){}
		if ( main_page != null )	userFunction();
		else	daemonFunction();
	}
}

/**
 * ページ処理実行環境。<BR>
 * 各Pageインスタンスのinput()とoutput()は個別スレッドにて実行し、
 * サーブレットコンテナ提供のスレッドから切り離して実行されます。<BR>
 * 実際のリクエストに先行し、このスレッドは開始直前の状態のままで複数のスレッドが
 * プーリングされ、リクエスト発生に備えています。<BR>
 * このスレッド群は同一サーブレットコンテナ内で実行される
 * Paraseleneを使用する全てのコンテキストで共用されています。<BR>
 * スレッドプーリング数は、リクエスト処理数をモニターしながら、随時調整されています。
 * 瞬間リクエスト数が増加すると、プーリング数も増加します。
 * ただし、Java仮想マシンが使用できるメモリ容量の93.75%が使用されていると、
 * プーリングを見送ります。
 * また逆にリクエスト数が低下すると、プーリング中のスレッドを破棄します。<BR>
 * 実行されるPageインスタンスは、実行時間を監視され、規定値(デフォルト3分)を
 * 上回ると中断され、GateクラスのonErrorに{@link KilledByTimeoutException}を
 * 通知します。
 */
public class SandBox extends Thread {
	static volatile long	wait_ms = 3 * 60 * 1000;
	private static LinkedList<Child>	waiting = new LinkedList<Child>();
	private static ArrayList<Child>	running = new ArrayList<Child>();
	private static SandBox sand_box = null;

	static final long	DAEMON_TIMEOUT = 1000 * 60 * 30;

	private static Child getMyself() {
		long	my_id = Thread.currentThread().getId();
		synchronized( running ) {
			int	cnt = running.size();
			for ( int i = 0; i < cnt; i++ ) {
				Child	child = running.get( i );
				if ( child.thread_id == my_id )	return child;
			}
		}
		return null;
	}

	/**
	 * 実行中ページの取得。
	 * 各ページの、input または output が呼ばれている時、自身のページを
	 * いつでも参照できます。<br>
	 * input や output 経由で呼ばれているのであれば、どのクラスメソッドからでも
	 * 参照可能です。
	 * @return 実行中のページ。
	 */
	public static Page getCurrentPage() {
		Child	child = getMyself();
		if ( child == null )	return null;
		return child.main_page;
	}

	/**
	 * リクエストパラメータの取得。
	 * 各ページの、input または output が呼ばれている時、現在のリクエストパラメータを
	 * いつでも参照できます。<br>
	 * input や output 経由で呼ばれているのであれば、どのクラスメソッドからでも
	 * 参照可能です。
	 * @return 現在のリクエストパラメータ。
	 */
	public static RequestParameter getCurrentRequestParameter() {
		Child	child = getMyself();
		if ( child == null )	return null;
		return child.request;
	}

	/**
	 * Input処理中であるか。
	 * 各ページの、input または output が呼ばれている時、現在どちらを
	 * 処理しているか返します。<br>
	 * input や output 経由で呼ばれているのであれば、どのクラスメソッドからでも
	 * 参照可能です。
	 * @return true:input、false:input以外 output や firstOutput。
	 */
	public static boolean isCurrentInput() {
		Child	child = getMyself();
		if ( child == null )	return false;
		return child.call_input;
	}

	/**
	 * このスレッドがデーモンであるか判定します。
	 * @return true:デーモン、false:デーモンではない。
	 */
	public static boolean isCurrentDaemon() {
		Child	child = getMyself();
		if ( child == null )	return false;
		return child.daemon != null;
	}

	private SandBox(){}

	/**
	 * 待機スレッド数の取得。
	 * @return 待機スレッド数。
	 */
	public static int getWaitChild() {
		synchronized( waiting ) {
			return waiting.size();
		}
	}

	/**
	 * 実行スレッド数。
	 * @return 実行スレッド数。
	 */
	public static int getRunningChild() {
		synchronized( running ) {
			return running.size();
		}
	}

	/**
	 * メモリ使用率の確認。
	 * 残りメモリが逼迫している場合は、ガベージコレクトをJVMに要求します。
	 * @return true:良好、false:使用可能メモリの93.75%を使っている。
	 */
	public static boolean isSafeFreeMemory() {
		MemoryInformation	mem = new MemoryInformation();
		mem.doGC();
		return mem.isSafeFreeMemory();
	}

	/**
	 * SandBoxが監視するタイムアウト分数の取得。
	 * @return タイムアウト分数。
	 */
	public static long getTimeoutMinute() {
		return wait_ms / (60 * 1000);
	}

	/**
	 * SandBoxが監視するタイムアウト分数の設定。
	 * 同一サーブレットコンテナ上で実行されるParaselene使用コンテキストの全てに
	 * 影響します。デフォルトは3分です。
	 * @param min タイムアウト分数。
	 */
	public static void setTimeoutMinute( long min ) {
		wait_ms = min * 60 * 1000;
	}

	static void doStart() {
		synchronized( waiting ) {
			if ( sand_box != null )	return;
			ChildKiller.init( running );
			sand_box = new SandBox();
			sand_box.setDaemon( true );
			sand_box.setPriority( MIN_PRIORITY );
			sand_box.setDaemon( true );
			sand_box.start();
		}
	}

	public void run() {
		int	low_cnt = 20;
		int	running_last_cnt = low_cnt;
		int include_cnt = low_cnt >> 2;
		long	max_interval = 1000 * 15;
		long	interval = 500;
//		long	last_tm = new Date().getTime();
		long	last_tm = 0;

		while( true ) {
			if ( waiting == null || running == null )	return;
			try {
				synchronized( waiting ) {
					int cnt = waiting.size();
					if ( cnt > 0 && !Option.isReleaseMode() ) {
						while ( true ) {
							cnt = waiting.size();
							if ( cnt <= 0 )	break;
							Child	child = waiting.poll();
							synchronized( child ) {
								child.notifyAll();
							}
						}
					}
					if ( isSafeFreeMemory() && cnt < running_last_cnt && Option.isReleaseMode() ) {
						for ( int i = 0; i < include_cnt; i++ ) {
							Child	child = new Child();
							waiting.wait( 10 );
							waiting.offer( child );
						}
					}
					else if ( cnt > (running_last_cnt + include_cnt) ) {
						Child	child = waiting.poll();
						synchronized( child ) {
							child.notifyAll();
						}
					}
				}
				synchronized( this ) {
					last_tm = new Date().getTime();
					wait( interval, 1 );
				}
				long	time_sub = new Date().getTime() - last_tm;
				int	cnt = 0;
				if ( running == null )	break;
				synchronized( running ) {
					cnt = running.size() - running_last_cnt;
				}
				if ( cnt > running_last_cnt ) {
					running_last_cnt = cnt;
				}
				else if ( low_cnt < running_last_cnt ) {
					running_last_cnt--;
				}
				include_cnt = running_last_cnt >> 2;
				if ( time_sub < interval ) {
					interval >>= 1;
				}
				else if ( interval < max_interval ) {
					interval += 100;
				}
			}
			catch( Exception e ) {
				try {
					e.printStackTrace();
					Thread.sleep( 1000 );
				}
				catch( Exception ee ) {}
			}
		}
	}

	static Child getChild() {
		Child	child = null;
		synchronized( waiting ) {
			int cnt = waiting.size();
			if ( cnt > 0 )	child = waiting.poll();
		}
		if ( child == null )	child = new Child();
		while( child.getState() != Thread.State.WAITING ) {
			try {
				Thread.sleep( 100 );
			}
			catch( Exception e ) {}
		}
		return child;
	}

	private static void startChild( Child child ) {
		synchronized( running ) {
			running.add( child );
		}
		synchronized( child ) {
			child.start_dt = new Date();
			child.limit_dt = new Date( child.start_dt.getTime() + SandBox.DAEMON_TIMEOUT );
			child.notifyAll();
		}
		synchronized( sand_box ) {
			sand_box.notify();
		}
	}

	private static History[] getCurrentHistory() {
		Child	myself = getMyself();
		if ( myself == null )	return null;
		History[]	hist = null;
		if ( myself.daemon != null ) {
			synchronized( myself.hist ) {
				return myself.hist.toArray( new History[0] );
			}
		}
		return new History[] { getCurrentRequestParameter().getHistory() };
	}

	/**
	 * バックグラウンド処理の実行。<br>
	 * 既に同名処理が実行されていれば、そのスレッドの管理ページに、このメソッドを
	 * 呼び出したスレッドが管理しているページを追加します。<br>
	 * 以前に同名処理へそのスレッドの管理ページの管理を任せていれば何もしません。
	 * そのため、何度呼び出しても構いません。
	 * @param name 処理名。
	 * null を指定すると無名処理になり、各アクセスメソッドが利用できません。
	 * @param daemon 処理インスタンス。
	 * 既に同名処理があれば、このインスタンスは破棄されます。
	 */
	public static void startDaemon( String name, Daemon daemon ) {
		History[]	hist = getCurrentHistory();
		if ( hist == null )	return;
		Child	child = null;
		synchronized( Child.name_space ) {
			child = Child.name_space.get( name );
			if ( child == null ) {
				child = getChild();
				child.daemon = daemon;
				child.hist = new ArrayList<History>();
				if ( name != null ) {
					child.daemon_name = name;
					Child.name_space.put( name, child );
				}
				for ( int i = 0; i < hist.length; i++ )	child.addHistory( hist[i] );
				startChild( child );
				return;
			}
		}
		for ( int i = 0; i < hist.length; i++ )	child.addHistory( hist[i] );
	}

	/**
	 * 全バックグラウンド処理からの管理ページ剥奪。
	 * このメソッドを呼び出したスレッドが管理しているページにあたるものを、
	 * 全てのバックグラウンド処理の管理下から外します。
	 */
	public static void depriveDaemonOfPage() {
		History[]	hist = getCurrentHistory();
		if ( hist == null )	return;
		if ( hist.length == 0 )	return;
		ArrayList<Child>	list = new ArrayList<Child>();
		synchronized( running ) {
			int	cnt = running.size();
			for ( int  i = 0; i < cnt; i++ ) {
				Child	child = running.get( i );
				list.add( child );
			}
		}
		int	cnt = list.size();
		for ( int i = 0; i < cnt; i++ ) {
			Child	child = list.get( i );
			if ( child.hist == null )	continue;
			for ( int j = 0; j < hist.length; j++ )	child.removeHistory( hist[j] );
		}
	}

	/**
	 * バックグラウンド処理へ通信を行う。
	 * バックグラウンド処理へ obj を post します。
	 * 指定スレッドが存在しない時、null を返します。
	 * @param name スレッド名。
	 * @param obj 送信インスタンス。
	 * @return post応答。
	 */
	public static Object postDaemon( String name, Object obj ) {
		Child	child = null;
		synchronized( Child.name_space ) {
			child = Child.name_space.get( name );
		}
		if ( child == null )	return null;
		return child.daemon.post( obj );
	}

	/**
	 * バックグラウンド処理が存在しているか確認します。
	 * @param name スレッド名。
	 * @return true:存在している、false:存在しない。
	 */
	public static boolean isAliveDaemon( String name ) {
		synchronized( Child.name_space ) {
			return Child.name_space.get( name ) != null;
		}
	}

	static void removeChild( Child child ) {
		synchronized( running ) {
			running.remove( child );
		}
	}

	@SuppressWarnings("deprecation")
	private static void kick( Child child ) throws Throwable {
		startChild( child );
		child.join( wait_ms );
		if ( !child.isAlive() ) {
			removeChild( child );
			if ( child.th != null )	throw child.th;
			return;
		}
		throw ChildKiller.kill( child );
	}

	static Forward input( Page action, RequestParameter req, Forward fw )
	throws Throwable {
		Child	child = getChild();
		child.main_page = action;
		child.request = req;
		child.forward = fw;
		child.call_input = true;
		kick( child );
		return child.forward;
	}

	static Page output( Page action, Page page, RequestParameter req, boolean refresh_f )
	throws Throwable {
		Child	child = getChild();
		child.main_page = action;
		child.param_page = page;
		child.request = req;
		child.call_input = false;
		child.refresh_f = refresh_f;
		kick( child );
		return child.param_page;
	}
}

