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


import java.util.*;
import paraselene.*;


class AliasData {
	Page	page = null;
	Integer	to_histry = null;
}

/**
 * ページファクトリー。<br>
 * スレッドを生成し、バックグラウンドでPageインスタンスを事前準備して
 * メインルーチンに提供します。<br>
 * スレッドセーフです。
 */
public abstract class PageFactory extends Thread {
	/**
	 * ページロード情報。
	 */
	public class PageInformation {
		/**
		 * ページID。
		 */
		public final String	page_id;
		/**
		 * クラス名。
		 */
		public final String class_name;
		/**
		 * 待機インスタンス数。
		 */
		public final int	loaded;
		/**
		 * 最終アクセス時間。
		 */
		public final Date	access_date;
		PageInformation( PageID id ) {
			page_id = id.toString();
			synchronized( class_def ) {
				class_name = class_def.get( id.getID() ).getName();
				loaded = pre_load.get( id.getID() ).size();
				access_date = last_access.get( id.getID() );
			}
		}
	}
	/**
	 * ページ生成情報。
	 */
	public class Information {
		/**
		 * クラス名。
		 */
		public final String name;
		/**
		 * 補充開始ページ数。
		 */
		public final int page_cnt;
		/**
		 * 秒おきに補充する数。
		 */
		public final int include_cnt;
		/**
		 * ページ待機情報。
		 */
		public final PageInformation[]	page;
		Information( PageFactory pf ) {
			name = pf.getClass().getName();
			page_cnt = pf.page_cnt;
			include_cnt = pf.include_cnt;
			ArrayList<PageInformation>	info = new ArrayList<PageInformation>();
			int	cnt = pf.page_list.size();
			for ( int i = 0; i < cnt; i++ ) {
				info.add( new PageInformation( pf.page_list.get( i ) ) );
			}
			page = info.toArray( new PageInformation[0] );
		}
	}

	protected volatile boolean	teapot_f = false;

	int	page_cnt;
	int include_cnt;

	private HashMap<Integer, Class<? extends Page>>	class_def = new HashMap<Integer, Class<? extends Page>>();
	private HashMap<Integer, LinkedList<Page>>	pre_load = new HashMap<Integer, LinkedList<Page>>();
	private HashMap<Integer, Date>	last_access = new HashMap<Integer, Date>();
	private ArrayList<String>	alias_list = new ArrayList<String>();
	private ArrayList<Boolean>	full_match = new ArrayList<Boolean>();
	private HashMap<String, Integer>	alias = new HashMap<String, Integer>();
	ArrayList<PageID>	page_list = new ArrayList<PageID>();
	volatile boolean	running_f = true;

	private HashMap<Integer, PageID>	subpage = new HashMap<Integer, PageID>();

	/**
	 * Ajax 代理ページの登録。<br>
	 * クライアントから Ajax 通信の痕跡が無い場合に代わりに output するページを
	 * 指定します。<br>
	 * Forward / Popup は PageID を指定されると、状況によりこの代理ページを
	 * 遷移先に切り替えます。
	 * @param real 本来のページ。
	 * @param surrogate Ajax 通信が無い場合に使用するページ。
	 */
	public void addAjaxSurrogatePage( PageID real, PageID surrogate ) {
		synchronized( subpage ) {
			subpage.put( real.getID(), surrogate );
		}
	}

	PageID getAjaxSurrogatePage( PageID real ) {
		RequestParameter	req = SandBox.getCurrentRequestParameter();
		if ( req == null )	return real;
		if ( req.wasUsedAjax() )	return real;
		synchronized( subpage ) {
			PageID	id = subpage.get( real.getID() );
			if ( id != null )	return id;
		}
		return real;
	}

	/**
	 * ページ生成情報の取得。
	 */
	public Information getInformation() {
		return new Information( this );
	}

	/**
	 * コンストラクタ。<br>
	 * 10秒おきにチェックを行い、各Pageインスタンスを事前に準備します。<br>
	 * 各インスタンス数がstock_cntを下回ると、inc_cnt数追加します。
	 * @param stock_cnt 補充を開始する数。
	 * @param inc_cnt	10秒おきに補充する数。
	 */
	public PageFactory( int stock_cnt, int inc_cnt ) {
		Option.entry( this );
		page_cnt = stock_cnt;
		include_cnt = inc_cnt;
		setDaemon( true );
		setPriority( MIN_PRIORITY );
	}

	/**
	 * ページ定義の登録。
	 * @param id ページID。
	 * @param cls クラス定義。
	 */
	public void addDefine( PageID id, Class<? extends Page> cls )
	throws Exception {
		Page	page = null;
		LinkedList<Page>	que = new LinkedList<Page>();
		synchronized( class_def ) {
			page_list.add( id );
			class_def.put( id.getID(), cls );
			last_access.put( id.getID(), new Date() );
			page = cls.newInstance();
			if ( Option.isReleaseMode() )	que.offer( page );
			pre_load.put( id.getID(), que );
			String	uri = page.getAliasURI();
			if ( uri == null )	return;
			if ( uri.length() < 4 ) {
				throw new Exception( uri + "(alias of " + id.toString() + ") are less than 4 characters." );
			}
			if ( alias.put( uri, id.getID() ) != null ) {
				throw new Exception( uri + "(alias of " + id.toString() + ") is duplicate." );
			}
			boolean	flag = TransactionSequencer.EXTENSION.equals( uri.substring( uri.length() - 3 ) );
			full_match.add( flag );
			if ( flag ) {
				alias_list.add( uri );
				return;
			}
			int	size = alias_list.size();
			for ( int i = 0; i < size; i++ ) {
				if ( full_match.get( i ) )	continue;
				if ( alias_list.get( i ).indexOf( uri ) == 0 ||
				uri.indexOf( alias_list.get( i ) ) == 0 ) {
					throw new Exception( uri + "(alias of " + id.toString() + ") conflict with " + alias_list.get( i ) + "." );
				}
			}
			alias_list.add( uri );
		}
	}

	/**
	 * nullページ(遷移元が無い時のページ)の取得。
	 * @return nullページ。
	 */
	public abstract NullPage getNullPage();

	/**
	 * ページの取得。
	 * @param id ページID。
	 * @return 取得ページ。
	 */
	public Page getPage( PageID id ) {
		return getPage( id.getID() );
	}

	Page getPage( int id ) {
		Class<? extends Page>	cls = null;
		synchronized( class_def ) {
			LinkedList<Page>	que = pre_load.get( id );
			last_access.put( id, new Date() );
			int	cnt = 0;
			if ( que != null ) {
				cnt = que.size();
			}
			if ( cnt > 0 ) {
				class_def.notifyAll();
				Page	pg = que.poll();
				pg.setPageServerInformation( Supervisor.getPageServerInformation() );
				return pg;
			}
			cls = class_def.get( id );
		}
		try {
			if ( !SandBox.isSafeFreeMemory() ) {
				Thread.sleep( 1200 );
			}
			Page	pg = cls.newInstance();
			pg.setPageServerInformation( Supervisor.getPageServerInformation() );
			return pg;
		}
		catch( Exception e ) {
			e.printStackTrace();
			return null;
		}
	}

	private String parseAlias( String uri, AliasData data ) {
		String[]	part = uri.split( "\\." );
		if ( part.length < 4 )	return uri;
		if ( TransactionSequencer.SELENE[5].equals( part[0] ) && TransactionSequencer.SELENE[6].equals( part[2] ) ) {
			data.to_histry = Integer.parseInt( part[1], Character.MAX_RADIX );
			StringBuilder	buf = new StringBuilder( part[3] );
			for ( int i = 4; i < part.length; i++ ) {
				buf = buf.append( "." );
				buf = buf.append( part[i] );
			}
			uri = buf.toString();
		}
		return uri;
	}


	AliasData getPage( String uri ) {
		if ( !Supervisor.isMyHost( uri ) )	return null;
		AliasData	alias_data = new AliasData();
		String[]	path = uri.split( "#" );
		if ( path.length == 0 )	path = new String[] { uri };
		path = path[0].split( "\\?" );
		if ( path.length == 0 )	path = new String[] { uri };
		path = path[0].split( ";" );
		if ( path.length == 0 )	path = new String[] { uri };
		path = path[0].split( "/" );
		int	list_cnt = alias_list.size();
		for ( int i = 0; i < path.length; i++ ) {
			if ( path[i].length() < 4 )	continue;
			if ( !TransactionSequencer.EXTENSION.equals( path[i].substring( path[i].length() - 3 ) ) )	continue;
			path[i] = parseAlias( path[i], alias_data );
			Option.trace( "alias URI = %s", uri );
			String	hit = null;
			for ( int j = 0; j < list_cnt; j++ ) {
				String	name = alias_list.get( j );
				boolean full = full_match.get( j );
				if ( full ) {
					if ( !name.equals( path[i] ) )	continue;
				}
				else {
					if ( name.length() >= path[i].length() )	continue;
					if ( !name.equals( path[i].substring( 0, name.length() ) ) ) continue;
				}
				hit = name;
			}
			if ( hit == null )	continue;
			Integer	id = alias.get( hit );
			Option.trace( "alias id = %s", id );
			if ( id != null ) {
				alias_data.page = getPage( id.intValue() );
				return alias_data;
			}
		}
		return null;
	}

	/**
	 * エイリアスURIであるか？
	 * 指定 URI が何れかのページのエイリアスであれば true を返します。
	 * @param uri URI。
	 * @return true:エイリアス、false:エイリアスではない。
	 */
	public boolean isAlias( String uri ) {
		AliasData	ret = getPage( uri );
		if ( ret == null )	return false;
		returnPage( ret.page );
		return true;
	}

	/**
	 * インスタンスの返却。未使用のページをファクトリーに戻します。
	 * @param page 返却インスタンス。
	 */
	public void returnPage( Page page ) {
		page.setPageServerInformation( null );
		if ( page instanceof NullPage )	return;
		if ( !page.isInitialized() )	return;
		synchronized( class_def ) {
			LinkedList<Page> que = pre_load.get( page.getID().getID() );
			if ( que == null )	return;
			if ( que.indexOf( page ) >= 0 )	return;
			if ( Option.isReleaseMode() )	que.offerFirst( page );
		}
	}

	public void run() {
		while( running_f ) {
			synchronized( class_def ) {
				Date	now = new Date();
				try {
					for ( Integer key: pre_load.keySet() ) {
						LinkedList<Page>	que = pre_load.get( key );
						Date	lac = last_access.get( key );
						int	cnt = que.size();
						if ( cnt > 0 && !Option.isReleaseMode() ) {
							que.clear();
							continue;
						}
						if ( (now.getTime() - lac.getTime()) > (1000 * 60 * 5) ||
						!SandBox.isSafeFreeMemory() ) {
							if ( cnt > 1 ) {
								que.poll();
							}
							continue;
						}
						if ( que.size() < page_cnt && Option.isReleaseMode() ) {
							Class<? extends Page>	cls = class_def.get( key );
							for ( int j = 0; j < include_cnt; j++ ) {
								Page	pg = cls.newInstance();
								que.offer( pg );
								class_def.wait( 1 );
							}
						}
						class_def.wait( 10 );
					}
					class_def.wait( 1000 * 60 );
				}
				catch( Exception e ) {
					try {
						class_def.wait( 1000 );
					}
					catch( Exception e2 ) {}
				}
			}
		}
	}
}

