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


import java.util.*;
import java.io.*;
import javax.servlet.http.*;
import paraselene.*;
import paraselene.ajax.*;

class DebugEcho implements HttpSessionBindingListener {
	private static final String	SESSION_KEY = "paraselene$DebugEcho$";
	private String	key;
	private String	session_id;
	private String	map_key;
	private HashMap<String, DebugEcho>	map;
	static final String MYSELF = "paraselene.supervisor.DebugEcho";

	static void removeSession( HttpSession session, String k ) {
		session.removeAttribute( SESSION_KEY + k );
	}

	DebugEcho( HttpSession session, String k, HashMap<String, DebugEcho> m ) {
		removeSession( session, k );
		session_id = session.getId();
		key = k;
		map = m;
		map_key = session_id + "$" + key;
		if ( map.get( map_key ) != null )	return;
		map.put( map_key, this );
		session.setAttribute( SESSION_KEY + key, this );
		Ajax.add( session, key, null );
	}

	boolean isSame( HttpSession session, String k ) {
		if ( !session_id.equals( session.getId() ) )	return false;
		if ( !key.equals( k ) )	return false;
		return true;
	}

	public void valueBound( HttpSessionBindingEvent ev ) {}
	public void valueUnbound( HttpSessionBindingEvent ev ) {
		synchronized( map ) {
			map.remove( map_key );
		}
	}

	void echo( String mes, StackTraceElement[] ste ) {
		Ajax.add( session_id, key, new Option( mes, ste ) );
	}
}

/**
 * Paraselene 動作オプション。
 */
public class Option implements Serializable {
	/**
	 * 現在日時。
	 */
	public String	now = new Text( new Date(), "yyyy/MM/dd HH:mm:ss" ).toString();
	/**
	 * メッセージ。
	 */
	public String[]	message;
	/**
	 * スタックトレース。
	 */
	public String[]	stack;
	Option( String mes, StackTraceElement[] ste ){
		message = mes.split( "[\n\r]" );
		ArrayList<String>	list = new ArrayList<String>();
		int	start = 0;
		boolean	flag = false;
		for ( ; start < ste.length; start++ ) {
			String	name = ste[start].getClassName();
			if ( MYSELF.equals( name ) || DebugEcho.MYSELF.equals( name ) ) {
				flag = true;
				break;
			}
		}
		if ( !flag )	start = 0;
		for ( ; start < ste.length; start++ ) {
			String	name = ste[start].getClassName();
			if ( !MYSELF.equals( name ) && !DebugEcho.MYSELF.equals( name ) )
				break;
		}
		for ( ; start < ste.length; start++ ) {
			list.add( ste[start].toString() );
		}
		stack = list.toArray( new String[0] );
	}

	private static final String MYSELF = "paraselene.supervisor.Option";
	private static volatile boolean debug_f = false;
	private static ArrayList<PageFactory>	factory =
		new ArrayList<PageFactory>();
	private static HashMap<String, HashMap<String, DebugEcho>>	echo =
		new HashMap<String, HashMap<String, DebugEcho>>();

	/**
	 * デバッグモードにする。デバッグモードにすると以下の動作が変更されます。
	 * <ul>
	 * <li>リクエスト処理スレッドのプーリング動作が停止します。
	 * リクエストの度に処理スレッドが新規作成されます。
	 * <li>ページインスタンスのプーリング動作が停止します。
	 * 新規リクエストの度にページインスタンスが新規作成されます。
	 * </ul>
	 * これを指定すると、同一サーブレットコンテナ内の、Paraselene構築サイトの
	 * 全てががデバッグモードになります。
	 * デバッグモードにすると、事前new動作を行わなくなります。
	 * これにより、サーブレットコンテナのクラス更新時リロード処理が発生した時に
	 * 変更したクラスと事前newしたインスタンスが
	 * アンマッチで例外が発生するのを防ぎます。開発時に有用です。<br>
	 * ただし、デバッグモード中でも履歴に残ったページにアクセスしてアンマッチを
	 * 起こす可能性があります。
	 */
	public static void setDebugMode() {
		debug_f = true;
	}

	/**
	 * リリースモードにする。デバッグモードを解除し、リリースモードにします。
	 * デバッグモードの動作は、setDebugMode() を参照して下さい。
	 */
	public static void setReleaseMode() {
		debug_f = false;
	}

	/**
	 * リリースモードであるか？
	 * @return true:リリースモード、false:デバッグモード。
	 */
	public static boolean isReleaseMode() { return !debug_f; }

	static void entry( PageFactory pf ) {
		String	cls = pf.getClass().getName();
		int	cnt = factory.size();
		for ( int i = 0; i < cnt; i++ ) {
			if ( factory.get( i ).getClass().getName().equals( cls ) ) {
				factory.remove( i ).running_f = false;
				break;
			}
		}
		factory.add( pf );
	}

	/**
	 * ページ生成情報の取得。
	 * @return 全サイトのページ生成情報。
	 */
	public static PageFactory.Information[] getPageFactoryInformation() {
		int	cnt = factory.size();
		PageFactory.Information[]	info = new PageFactory.Information[cnt];
		for ( int i = 0; i < cnt; i++ ) {
			info[i] = factory.get( i ).getInformation();
		}
		return info;
	}

	/**
	 * デバッグ出力取得エントリー。
	 * @param name クラス名、パッケージ名。
	 * @param session Ajax処理するセッション。
	 * @param key Ajaxのキー。
	 */
	public static void entryDebug( String name, HttpSession session, String key ) {
		synchronized( echo ) {
			HashMap<String, DebugEcho>	map = echo.get( name );
			if ( map == null ) {
				map = new HashMap<String, DebugEcho>();
				echo.put( name, map );
			}
			new DebugEcho( session, key, map );
		}
	}

	/**
	 * デバッグ出力取得エントリーの削除。
	 * @param session Ajax処理していたセッション。
	 * @param key 削除するAjaxのキー。
	 */
	public static void removeDebug( HttpSession session, String key ) {
		DebugEcho.removeSession( session, key );
	}

	/**
	 * デバッグ出力。String.formatと同じ書式です。
	 * @param format フォーマット。
	 * @param param パラメーター。
	 */
	public static void debug( String format, Object ... param ) {
		if ( isReleaseMode() )	return;
		debug( String.format( format, param ), Thread.currentThread().getStackTrace() );
	}

	/**
	 * デバッグ出力。
	 * @param e 例外。
	 */
	public static void debug( Throwable e ) {
		e.printStackTrace();
		if ( isReleaseMode() )	return;
		debug( e.toString(), e.getStackTrace() );
	}

	private static void debug( String mes, StackTraceElement[] sta ) {
		synchronized( echo ) {
			for ( String k : echo.keySet() ) {
				boolean	call = false;
				int	k_len = k.length();
				for ( int i = 0; i < sta.length; i++ ) {
					String	cmp = sta[i].getClassName();
					if ( cmp.length() > k_len ) {
						cmp = cmp.substring( 0, k_len );
					}
					if ( k.equals( cmp ) ) {
						call = true;
						break;
					}
				}
				if ( !call )	continue;
				HashMap<String, DebugEcho>	map = echo.get( k );
				if ( map == null )	continue;
				for ( String k2 : map.keySet() ) {
					DebugEcho	de = map.get( k2 );
					de.echo( mes, sta );
				}
			}
		}
	}
}

