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


import java.util.*;
import java.util.concurrent.locks.*;
import java.io.*;
import paraselene.*;
import paraselene.ajax.data.*;
import paraselene.ajax.*;
import paraselene.ui.*;

/**
 * 画面遷移履歴。<br>
 * 現在のポップアップ表示中ページも保持します。
 * ポップアップページは履歴には入りません。
 * <br>スレッドセーフです。
 */
public class History implements Serializable {
	private static final long serialVersionUID = 2L;
	private ArrayList<Page>	hist = new ArrayList<Page>();
	private int	histry_no;
	private ReentrantLock	lock = new ReentrantLock();
	private ArrayList<Page>	popup = new ArrayList<Page>();
	volatile boolean drop_f = false;
	volatile String	session_id;

	private void comet_stop( Page p ) {
		StringBuilder	buf = new StringBuilder( "paraselene.comet_stop('" );
		buf = buf.append( p.getUniqueKey() );
		buf = buf.append( "');" );
		Ajax.addComet( session_id, new PostBack( new Exec( buf.toString() ) ) );
	}

	boolean tryLock() {
		if ( lock.isHeldByCurrentThread() )	return true;
		Option.trace( "call tryLock " );
		boolean	ret = lock.tryLock();
		Option.trace( "tryLock %s ", ret  );
		return ret;
	}

	void lock() {
		if ( lock.isHeldByCurrentThread() )	return;
		Option.trace( "call lock " );
		lock.lock();
		Option.trace( "locked " );
	}

	void unlock() {
		if ( !lock.isHeldByCurrentThread() )	return;
		Option.trace( "call unlock " );
		lock.unlock();
		Option.trace( "unlocked " );
	}

	/**
	 * コンストラクタ。
	 */
	private History() {}
	History( int n, String s ) {
		histry_no = n;
		session_id = s;
	}

	/**
	 * サテライトページ追加。
	 * @param page 追加ページ。
	 */
	public void addSatellitePage( Page page ) {
		if ( page == null )	return;
		synchronized( popup ) {
			removePopup( page );
			setInfo( page );
			popup.add( page );
		}
		Option.trace( "addSatellitePage %d %s", hashCode(), page.getUniqueKey() );
	}

	private void initSatellitePage( Page parent ) {
		if ( parent == null )	return;
		PageBypassedDiv[]	div = parent.getPageBypassedDiv();
		for ( int i = 0; i < div.length; i++ ) {
			initSatellitePage( div[i].getBypassPage() );
		}
		if ( parent != parent.getParentPage() ) {
			addSatellitePage( parent );
		}
	}

	/**
	 * サテライトページ削除。
	 * @param page 削除ページ。
	 */
	public void removeSatellitePage( Page page ) {
		if ( page == null )	return;
		if ( page.getAjaxSupport() == Page.AjaxSupport.SERVER_PUSH ) {
			comet_stop( page );
		}
		removePopup( page );
	}

	private boolean addPopup( Page page ) {
		if ( page.getPopupType() == null )	return false;
		addSatellitePage( page );
		return true;
	}

	void removePopup( Page page ) {
		String	key = page.getUniqueKey();
		synchronized( popup ) {
			int	cnt = popup.size();
			for ( int i = 0; i < cnt; i++ ) {
				if ( popup.get( i ).getUniqueKey().equals( key ) ) {
					popup.remove( i );
					Option.trace( "removePopup %s", key );
					return;
				}
			}
		}
		Option.trace( "donot remove, not found %s", key );
	}

	private void clearPopup() {
		synchronized( popup ) {
			popup.clear();
			Option.traceWithStack( "popup clear" );
		}
	}

	/**
	 * 追加。<br>
	 * ポップアップページは履歴に追加されません。
	 * 新しく画面遷移が発生すると、ポップアップ情報はクリアされます。
	 * @param page 追加するページ。
	 */
	public void add( Page page ) {
		if ( page == null )	return;
		page = page.getParentPage();
		if ( addPopup( page ) )	return;
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			Page	old = null;
			if ( cnt > 0 ) {
				old = getPage();
			}
			if ( page.isHistoryClear() ) {
				for ( int i = 0; i < cnt; i++ ) {
					Page	h = hist.get( i );
					if ( page.equals( h ) ) {
						for ( int j = cnt -1; j >= i; j-- ) {
							hist.remove( j ).setHistoryKey( HistorySet.ROOT );
						}
						break;
					}
				}
			}
			if ( page.isAllowHistoryAdd() ) {
				setInfo( page );
				hist.add( page );
				page.setHistoryKey( histry_no );
			}
			cnt = getHistoryCount();
			if ( cnt == 0 || old != getPage() ) {
				if ( cnt > 0 && old != null ) {
					Option.trace(
						String.format( "old=%s(%s) / new=%s(%s)",
							old.getID().toString(), old.getUniqueKey(),
							getPage().getID().toString(),
							getPage().getUniqueKey()
						)
					);
				}
				clearPopup();
				initSatellitePage( getPage() );
			}
		}
	}

	private void setInfo( Page page ) {
		if ( page.getPageServerInformation() != null )	return;
		Page	old = getPage();
		if ( old == null )	return;
		page.setPageServerInformation( old.getPageServerInformation() );
	}

	/**
	 * 履歴の件数を取得。
	 * @return 履歴件数。
	 */
	public int getHistoryCount() {
		return hist.size();
	}

	/**
	 * 履歴を取得。指定ページと同じものを履歴から取得します。
	 * @param id 指定ページ。
	 * @return 指定ページと同じページの配列。無ければ0個の配列。
	 */
	public Page[] getAllPage( PageID id ) {
		if ( id == null ) return new Page[0];
		ArrayList<Page>	list = new ArrayList<Page>();
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = 0; i < cnt; i++ ) {
				Page	h = hist.get( i );
				if ( h.equals( id ) ) {
					int	hld = list.size();
					boolean	flag = true;
					for ( int j = 0; j < hld; j++ ) {
						if ( list.get( j ).getUniqueKey() == h.getUniqueKey() ) {
							flag = false;
							break;
						}
					}
					if ( flag )	list.add( h );
				}
			}
		}
		return list.toArray( new Page[0] );
	}

	/**
	 * 履歴を取得。指定ページと同じものを履歴から取得します。
	 * @param id 指定ページ。
	 * @return 指定ページと同じページ。無ければnullを返す。
	 */
	public Page getPage( PageID id ) {
		Page[]	p = getAllPage( id );
		if ( p.length == 0 )	return null;
		return p[p.length - 1];
	}

	/**
	 * 現在カレントのブラウザで表示されているページ一覧を返します。
	 * @return ポップアップを含めた表示ページ。
	 */
	public Page[] getBrowsingPage() {
		ArrayList<Page>	list = new ArrayList<Page>();
		lock();
		synchronized( popup ) {
			int	cnt = popup.size();
			for ( int i = 0; i < cnt; i++ ){
				list.add( popup.get( i ) );
			}
		}
		list.add( getPage() );
		return list.toArray( new Page[0] );
	}

	/**
	 * 現在カレントのブラウザで表示されているページから、ユニークキー
	 * ({@link paraselene.Page#getUniqueKey()})が一致するものを返します。
	 * ポップアップを含めて探します。
	 * @param uni ユニークキー。
	 * @return 一致したページ。
	 */
	public Page getBrowsingPage( String uni ) {
		Option.trace( "getBrowsingPage  %d >> %s", hashCode(), uni );
		Page	page = null;
		synchronized( popup ) {
			int cnt = popup.size();
			for ( int i = 0; i < cnt; i++ ){
				page = popup.get( i );
				Option.trace( "getBrowsingPage popup %d %s", hashCode(), page.getUniqueKey() );
				if ( page.getUniqueKey().equals( uni ) )	return page;
			}
		}
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = 0; i < cnt; i++ ) {
				page = hist.get( i );
				Page	hit = page.getBypassPage( uni );
				if ( hit != null ) {
					initSatellitePage( page );
					return hit;
				}
			}
		}
		return null;
	}

	/**
	 * 履歴を削除。直前のページを削除すると、ポップアップ情報もクリアされます。
	 * @param last_no
	 * 0:直前のページ ～ getHistoryCount()-1:最古のページ。
	 */
	public void removePage( int last_no ) {
		if ( last_no < 0 )	return;
		if ( last_no == 0 )	clearPopup();
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = cnt - 1; i >= 0; i-- ) {
				if ( last_no == 0 )	{
					hist.remove( i ).setHistoryKey( HistorySet.ROOT );
					return;
				}
				last_no--;
			}
		}
		if ( last_no == 0 )	initSatellitePage( getPage() );
	}

	/**
	 * 履歴を削除。指定ページIDなら、全て削除する。
	 * 指定ページIDが直前のページなら、ポップアップ情報もクリアされます。
	 * @param page_id ページID。
	 */
	public void removePage( PageID page_id ) {
		boolean	init_f = false;
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = cnt - 1; i >= 0; i-- ) {
				Page	page = hist.get( i );
				if ( page.getID() == page_id )	{
					hist.remove( i ).setHistoryKey( HistorySet.ROOT );
					if ( i == (cnt - 1) ) {
						clearPopup();
						init_f = true;
					}
				}
			}
		}
		if ( init_f )	initSatellitePage( getPage() );
	}

	/**
	 * 履歴を全て削除。
	 * ポップアップ情報もクリアされます。
	 */
	public void removePage() {
		clearPopup();
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = 0; i < cnt; i++ ) {
				hist.get( i ).setHistoryKey( HistorySet.ROOT );
			}
			hist.clear();
		}
	}

	/**
	 * 履歴を取得。
	 * @param last_no
	 * 0:直前のページ ～ getHistoryCount()-1:最古のページ。<br>
	 * 範囲外であるとnullを返す。
	 * @return 指定ページの内容。
	 */
	public Page getPage( int last_no ) {
		if ( last_no < 0 )	return null;
		synchronized( hist ) {
			int	cnt = getHistoryCount();
			for ( int i = cnt - 1; i >= 0; i-- ) {
				if ( last_no == 0 )	{
					return hist.get( i );
				}
				last_no--;
			}
		}
		return null;
	}

	/**
	 * 直前のページを取得。getPage(0)と等価。
	 * @return 遷移元ページ。
	 */
	public Page getPage() {
		return getPage( 0 );
	}
}

