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

import java.util.*;
import java.io.*;
import java.net.*;
import java.math.*;
import javax.servlet.http.*;
import paraselene.*;
import paraselene.ui.*;
import paraselene.tag.form.*;
import paraselene.supervisor.*;
import paraselene.ajax.data.*;

/**
 * HTML汎用タグ。
 */
public class Tag extends HTMLPart {
	static final Page	null_page = new NullPage(){
		public Forward input( RequestParameter req, Forward fw ) throws PageException {return null;}
	};

	private static HashMap<String, String>	name_ok_map = new HashMap<String, String>();
	static {
		String[] name_ok = {
			"applet", "area", "embed", "frame", "frameset", "iframe", "ilayer",
			"img", "layer", "map", "meta", "object", "param", 
		};
		for ( int  i = 0; i < name_ok.length; i++ ) {
			name_ok_map.put( name_ok[i], name_ok[i] );
		}
	}

	private String name;
	protected HashMap<String,Attribute>	attr = new HashMap<String,Attribute>();
	private ArrayList<HTMLPart>		doc = new ArrayList<HTMLPart>();
	private ArrayList<Tag>		tag_only = new ArrayList<Tag>();
	private boolean nest_f = false;
	private boolean visible_f = true;
	private volatile boolean dirty_f = true;
	private short[]	location_no = new short[]{ 0 };

	private static String	VALUE = "value";

	private Page	embed_page = null_page;
	private boolean	ajax_f = true;


	/**
	 * Ajax有効設定。初期値は true になっています。<br>
	 * false を設定すると、Page が Ajax化されていても該当タグからは
	 * Ajax 通信を行いません。<br>
	 * この設定で効果があるのは、以下のクラスのみです。<ul>
	 * <li>Anchor
	 * <li>Button
	 * <li>Form
	 * </ul>
	 * @param flag true:Ajax有効、false:Ajax無効。
	 */
	public void setAjaxEnable( boolean flag ) {
		ajax_f = flag;
		setModify();
	}

	/**
	 * Ajax有効設定の取得。
	 * @return true:Ajax有効、false:Ajax無効。
	 */
	public boolean isAjaxEnable() {
		return ajax_f;
	}

	/**
	 * 所属先ページの取得。
	 * @return 所属先。
	 */
	public Page getAssignedPage() {
		if ( embed_page == null ) return null_page;
		return embed_page;
	}

	private boolean modify_enable = true;

	private static final String[] ERR_IE = new String[] {
		"table", "thead", "tbody", "tfoot", "tr"
	};

	private boolean isTable() {
		String	name = getName();
		for ( int i = 0; i < ERR_IE.length; i++ ) {
			if ( ERR_IE[i].equals( name ) )	return true;
		}
		return false;
	}

	/**
	 * フレームワークが使用します。
	 */
	protected void setModifyMyselef() {
		if ( getAttribute( "id" ) == null || isTable() ) {
			Tag	out = getOuterTag();
			if ( out != null ) {
				out.setModifyMyselef();
				return;
			}
		}
		dirty_f = true;
	}

	/**
	 * フレームワークが使用します。
	 */
	public void setModify() {
		if ( !modify_enable )	return;
		Tag	out = getOuterTag();
		if ( out != null ) {
			out.setModifyMyselef();
			return;
		}
		setModifyMyselef();
	}

	/**
	 * フレームワークが使用します。
	 */
	public boolean isModified() {
		return dirty_f;
	}

	/**
	 * フレームワークが使用します。
	 */
	public void resetModify() {
		dirty_f = false;
	}

	/**
	 * フレームワークが使用します。
	 */
	public void setModifyEnable( boolean flag ) {
		modify_enable = flag;
	}

	/**
	 * フレームワークが使用します。
	 */
	protected void getModifiedTag( ArrayList<Tag> list ) {
		if ( dirty_f ) {
			list.add( this );
			return;
		}
		Tag[]	tag = getTagArray();
		for ( int i = 0; i < tag.length; i++ ) {
			tag[i].getModifiedTag( list );
		}
	}

	public Tag[] getModifiedTag() {
		ArrayList<Tag>	list = new ArrayList<Tag>();
		getModifiedTag( list );
		return list.toArray( new Tag[0] );
	}

	/**
	 * 外側のタグを返す。
	 * @return 外側のタグ。自身が最外であるならnull。
	 */
	public Tag getOuterTag() {
		return getAssignedTag();
	}

	private Tag[] getOverUnder() {
		Tag[]	ret = new Tag[]{ null, null };
		int	cnt = getAssignedTag().tag_only.size();
		for ( int i = 0; i < cnt; i++ ) {
			if ( getAssignedTag().tag_only.get( i ) == this ) {
				if ( i > 0 ) {
					ret[0] = getAssignedTag().tag_only.get( i - 1 );
				}
				if ( i < cnt - 1 ) {
					ret[1] = getAssignedTag().tag_only.get( i + 1 );
				}
				break;
			}
		}
		return ret;
	}

	/**
	 * 同階層の真上のタグを返す。
	 * @return 近接する真上のタグを返す。
	 */
	public Tag getOverTag() {
		return getOverUnder()[0];
	}

	/**
	 * 同階層の真下のタグを返す。
	 * @return 近接する真下のタグを返す。
	 */
	public Tag getUnderTag() {
		return getOverUnder()[1];
	}

	protected Tag() {
		embed_page = null_page;
	}

	private static final String[]	plain_tag = new String[] {
		"code",
		"comment",
		"plaintext",
		"xmp",
		"script",
		"style",
		"listing",
	};
	private static HashMap<String,String>	plain_map = new HashMap<String,String>();

	static {
		for ( int i = 0; i < plain_tag.length; i++ ) {
			plain_map.put( plain_tag[i], plain_tag[i] );
		}
	}

	/**
	 * エスケープしないタグであるか？
	 * @param name タグ名。
	 * @return true:エスケープしない、false:エスケープする。
	 */
	public static boolean isPlainTag( String name ) {
		return plain_map.get( name.toLowerCase( Locale.ENGLISH ) ) != null;
	}

	/**
	 * 閉じタグの有無。
	 * @return true:閉じタグなし、false:閉じタグあり。
	 */
	public boolean isSimpleTag() {
		return !nest_f;
	}

	/**
	 * 可視であるか？
	 * @return true:可視、false:不可視。
	 */
	public boolean isVisible() {
		return visible_f;
	}

	/**
	 * 可視設定。
	 * @param flag true:可視、false:不可視。
	 */
	public void setVisible( boolean flag ) {
		setModify();
		visible_f = flag;
	}

	private static volatile short tag_id;
	private static final String PARASELENE_ID = "paraselene$id$";

	/**
	 * ユニークIDの設定。
	 * このタグに重複しないID属性を設定します。<br>
	 * ただし、既にID属性も持っている場合は何もしません。
	 * @return ID属性値。ID属性を変更しなかった場合は、元の値を返します。
	 */
	public String makeID() {
		Attribute	id = getAttribute( "id" );
		if ( id != null )	return id.getString();
		if ( "head".equals( getName() ) )	return null;
		long	now = 0;
		synchronized( PARASELENE_ID ) {
			now = tag_id;
			tag_id++;
		}
		long	time = (now << 56) ^ (new Date().getTime() & 0x00ffffffffffffffL);
		time &= 0x7fffffffffffffffL;
		StringBuilder	buf = new StringBuilder( PARASELENE_ID );
		buf = buf.append( Long.toString( time, Character.MAX_RADIX ) );
		String	nid = buf.toString();
		setAttribute( new Attribute( "id", nid ) );
		return nid;
	}

	/**
	 * id属性の取得。
	 * @return id属性に設定されているID。
	 */
	public String getIDAttribute() {
		Attribute	attr = getAttribute( "id" );
		if ( attr == null )	return null;
		return attr.getString();
	}

	/**
	 * コンストラクタ。
	 * @param n タグ名。
	 * @param simple_f true:閉じタグ無し、false:閉じタグ有り。
	 */
	public Tag( String n, boolean simple_f ) {
		this();
		name = n.toLowerCase( Locale.ENGLISH );
		if ( "body".equals( name ) )	makeID();
		nest_f = !simple_f;
	}

	/**
	 * name属性の取得。
	 * @return name属性に設定されている名前。
	 */
	public String getNameAttribute() {
		try {
			Attribute	n = getAttribute( "name" );
			if ( n == null )	return null;
			AttributeValuable	t = n.get();
			if ( t == null )	return null;
			return t.toString();
		}
		catch( Exception e ) {
			return null;
		}
	}

	/**
	 * 属性の全取得。
	 * @return 属性配列。属性が無い場合、0個の配列を返します。
	 */
	public Attribute[] getAllAttribute() {
		ArrayList<Attribute>	list = new ArrayList<Attribute>();
		for ( String k : attr.keySet() ) {
			list.add( attr.get( k ) );
		}
		return list.toArray( new Attribute[0] );
	}

	/**
	 * 複製の作成。<br>
	 * 複製したものは、name属性値も引き継がれるので
	 * addHTMLPart()する時に配列化にされる場合があります。
	 * @return 複製。
	 */
	public HTMLPart getReplica() {
		Tag	tag = newReplica();
		tag.visible_f = this.visible_f;
		for ( String k : attr.keySet() ) {
			if ( "id".equals( k ) )	continue;
			tag.setAttribute( attr.get( k ).getReplica() );
		}
		int	size = doc.size();
		for ( int i = 0; i < size; i++ ) {
			tag.addHTMLPart( doc.get( i ).getReplica() );
		}
		return tag;
	}

	/**
	 * 複製用インスタンスの生成。
	 * @return 自身と同等のインスタンス。
	 */
	protected Tag newReplica() {
		return new Tag( name, !nest_f );
	}

	/**
	 * タグ名の取得。<br>
	 * a や div のようなタグ名を小文字で返します。
	 * @return タグ名。
	 */
	public String getName() {
		return name;
	}

	/**
	 * 属性の追加/置換。
	 * @param at 属性。
	 */
	public void setAttribute( Attribute at ) {
		setModify();
		String	at_name = at.getName();
		if ( "id".equals( at_name ) ) {
			if ( getAttribute( "id" ) != null ) {
				String	val = at.getString();
				if ( val != null ) {
					if ( val.indexOf( PARASELENE_ID ) == 0 )	return;
				}
			}
		}
		attr.put( at.getName(), at );
		at.setAssignedTag( this );
		if ( "name".equals( at_name ) ) {
			Tag	out = getOuterTag();
			while ( out != null ) {
				if ( out.isTable() ) {
					out = out.getOuterTag();
					continue;
				}
				break;
			}
			if ( out != null ) {
				out.makeID();
			}
		}
	}

	/**
	 * 複数の属性の追加/置換。
	 * @param at 属性。
	 */
	public void setAttribute( Attribute ... at ) {
		for ( int i = 0; i < at.length; i++ ) {
			setAttribute( at[i] );
		}
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n ) ) と等価です。
	 * @param n 属性名。
	 */
	public void setAttribute( String n ) {
		setAttribute( new Attribute( n ) );
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n, v ) ) と等価です。
	 * @param n 属性名。
	 * @param v 属性値。
	 */
	public void setAttribute( String n, BigDecimal v ) {
		setAttribute( new Attribute( n, v ) );
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n, v ) ) と等価です。
	 * @param n 属性名。
	 * @param v 属性値。
	 */
	public void setAttribute( String n, double v ) {
		setAttribute( new Attribute( n, v ) );
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n, v ) ) と等価です。
	 * @param n 属性名。
	 * @param v 属性値。
	 */
	public void setAttribute( String n, int v ) {
		setAttribute( new Attribute( n, v ) );
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n, v ) ) と等価です。
	 * @param n 属性名。
	 * @param v 属性値。
	 */
	public void setAttribute( String n, String v ) {
		setAttribute( new Attribute( n, v ) );
	}

	/**
	 * 属性の追加/置換。<br>
	 * setAttribute( new Attribute( n, v ) ) と等価です。
	 * @param n 属性名。
	 * @param v 属性値。
	 */
	public void setAttribute( String n, Valuable ... v ) {
		setAttribute( new Attribute( n, v ) );
	}

	/**
	 * 属性の削除。
	 * @param at_name 属性名。
	 */
	public void removeAttribute( String at_name ) {
		setModify();
		attr.remove( at_name.toLowerCase( Locale.ENGLISH ) );
	}

	/**
	 * 属性の取得。無ければnullを返す。
	 * @param at_name 属性名。
	 * @return 属性。
	 */
	public Attribute getAttribute( String at_name ) {
		return attr.get( at_name.toLowerCase( Locale.ENGLISH ) );
	}

	/**
	 * 属性値を文字列にして取得。無ければnullを返す。
	 * @param at_name 属性名。
	 * @return 属性値の文字列。
	 */
	public String getAttributeToString( String at_name ) {
		Attribute	attr = getAttribute( at_name );
		if ( attr == null )	return null;
		return attr.getString();
	}

	/**
	 * 属性値を数値にして取得。数値化に失敗するとnullを返す。<br>
	 * 属性値がNumberValueの場合にのみ成功します。
	 * @param at_name 属性名。
	 * @return 属性値の数値表現。
	 */
	public Integer getAttributeToInteger( String at_name ) {
		NumberValue	num = getAttributeToNumberValue( at_name );
		if ( num == null )	return null;
		return num.toInteger();
	}

	/**
	 * 属性値を数値にして取得。数値化に失敗するとnullを返す。<br>
	 * 属性値がNumberValueの場合にのみ成功します。
	 * @param at_name 属性名。
	 * @return 属性値の数値表現。
	 */
	public Double getAttributeToDouble( String at_name ) {
		NumberValue	num = getAttributeToNumberValue( at_name );
		if ( num == null )	return null;
		return num.toDouble();
	}

	/**
	 * 属性値を数値にして取得。数値化に失敗するとnullを返す。<br>
	 * 属性値がNumberValueの場合にのみ成功します。
	 * @param at_name 属性名。
	 * @return 属性値の数値表現。
	 */
	public BigDecimal getAttributeToBigDecimal( String at_name ) {
		NumberValue	num = getAttributeToNumberValue( at_name );
		if ( num == null )	return null;
		return num.toBigDecimal();
	}

	private NumberValue getAttributeToNumberValue( String at_name ) {
		Attribute	attr = getAttribute( at_name );
		if ( attr == null )	return null;
		AttributeValuable	av = attr.get();
		if ( !(av instanceof NumberValue) )	return null;
		return (NumberValue)av;
	}

	/**
	 * 属性値をURIにして取得。URIValue でない場合、null を返す。<br>
	 * @param at_name 属性名。
	 * @return 属性値のURI表現。
	 */
	public URI getAttributeToURI( String at_name ) {
		Attribute	attr = getAttribute( at_name );
		if ( attr == null )	return null;
		AttributeValuable	av = attr.get();
		if ( !(av instanceof URIValue) )	return null;
		URIValue	u = (URIValue)av;
		return u.getURI();
	}

	/**
	 * HTML文書の追加。name属性値が衝突すると、それを変更します。
	 * 末尾に追加されます。
	 * @param d 文書。
	 */
	public void addHTMLPart( HTMLPart d ) {
		addHTMLPart( doc.size(), d );
	}

	/**
	 * タグのソート。タグをHTML先頭からの登場順にソートします。
	 * @param array ソート対象。この配列インスタンス自体がソートされます。
	 * @return array を返します。
	 */
	public static Tag[] sort( Tag[] array ) {
		if ( array == null )	return null;
		if ( array.length <= 1 )	return array;
		int	h = sortDiv( array.length );
		while( true ) {
			if ( h == 9 || h == 10 )	h = 11;
			boolean	done = false;
			for ( int i = 0; (i + h) < array.length; i++ ) {
				boolean	swap_f = false;
				if ( array[i + h].location_no.length < array[i].location_no.length ) {
					swap_f = true;
				}
				else if ( array[i + h].location_no.length == array[i].location_no.length ) {
					for ( int j = 0; j < array[i].location_no.length; j++ ) {
						if ( array[i + h].location_no[j] < array[i].location_no[j] ) {
							swap_f = true;
							break;
						}
						if ( array[i + h].location_no[j] > array[i].location_no[j] ) {
							break;
						}
					}
				}
				if ( swap_f ) {
					Tag	tmp = array[i + h];
					array[i + h] = array[i];
					array[i] = tmp;
					done = true;
				}
			}
			if ( h <= 1 && !done ) return array;
			h = sortDiv( h );
		}
	}

	private static final BigInteger BI13 = new BigInteger( "13" );

	private static int sortDiv( int org ) {
		BigInteger	bd = new BigInteger( Integer.toString( org ) );
		bd = bd.multiply( BigInteger.TEN );
		bd = bd.divide( BI13 );
		int	ret = bd.intValue();
		if ( ret < 1 )	ret = 1;
		return ret;
	}

	private void setLocation() {
		Tag[]	list = getTagArray();
		short	last_no = -1;
		for ( int i = 0; i < list.length; i++ ) {
			if ( list[i].location_no.length == (this.location_no.length + 1) ) {
				boolean	flag = true;
				for ( int j = 0; j < this.location_no.length; j++ ) {
					if ( list[i].location_no[j] != this.location_no[j] ) {
						flag = false;
						break;
					}
				}
				if ( flag && list[i].location_no[this.location_no.length] > last_no ) {
					last_no = list[i].location_no[this.location_no.length];
					continue;
				}
			}
			list[i].location_no = new short[this.location_no.length + 1];
			for ( int j = 0; j < this.location_no.length; j++ ) {
				list[i].location_no[j] = this.location_no[j];
			}
			last_no++;
			if ( last_no < 0 ) {
				Option.debug( "tag overflow!" );
				last_no = 0;
			}
			list[i].location_no[this.location_no.length] = last_no;
			list[i].setLocation();
		}
	}

	/**
	 * HTML文書の追加。name属性値が衝突すると、それを変更します。
	 * @param idx 追加位置。
	 * @param d 文書。
	 */
	public void addHTMLPart( int idx, HTMLPart d ) {
		if ( this instanceof VirtualTag || d instanceof VirtualTag )	return;
		setModify();
		doc.add( idx, d );
		if ( d instanceof Tag ) {
			Tag tag = (Tag)d;
			int	cnt = doc.size();
			idx = 0;
			for ( int i = 0; i < cnt; i++ ) {
				HTMLPart	p = doc.get( i );
				if ( p == d )	break;
				if ( p instanceof Tag ) {
					idx++;
				}
			}
			tag_only.add( idx, tag );
			getAssignedPage().nameTag( tag );
			if ( tag.getAttribute( "name" ) != null )	makeID();
			setLocation();
		}
		d.setAssignedTag( this );
	}

	/**
	 * 複数のHTML文書の追加。name属性値が衝突すると、それを変更します。
	 * 末尾に追加されます。
	 * @param d 文書。
	 */
	public void addHTMLPart( HTMLPart ... d ) {
		for ( int i = 0; i < d.length; i++ ) {
			addHTMLPart( d[i] );
		}
	}

	/**
	 * 複数のHTML文書の追加。name属性値が衝突すると、それを変更します。
	 * 指定位置から連続挿入されます。
	 * @param idx 追加位置。
	 * @param d 文書。
	 */
	public void addHTMLPart( int idx, HTMLPart ... d ) {
		for ( int i = 0; i < d.length; i++ ) {
			addHTMLPart( idx + i, d[i] );
		}
	}

	/**
	 * タグのインクルード。<br>
	 * include( tag, false ) と等価です。
	 * @param tag 取り込むタグ。
	 */
	public void include( Tag tag ) {
		if ( tag == null )	return;
		int	cnt = tag.getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			addHTMLPart( tag.getHTMLPart( i ).getReplica() );
		}
	}

	/**
	 * タグのインクルード。<br>
	 * タグで挟まれた部分全てを取り込む。そのタグ自体は取り込まない。<br>
	 * move_f が true の時、引数のタグの内側のインスタンスが移動します。<br>
	 * move_f が false の時、引数のタグの内側はそのままで、そのgetReplica()
	 * したものを取り込みます。
	 * @param tag 取り込むタグ。
	 * @param move_f true:移動、false:コピー。
	 */
	public void include( Tag tag, boolean move_f ) {
		if ( !move_f ) {
			include( tag );
			return;
		}
		if ( tag == null )	return;
		int cnt = tag.getHTMLPartCount();
		HTMLPart[]	data = new HTMLPart[cnt];
		for ( int i = 0; i < cnt; i++ ) {
			data[i] = tag.getHTMLPart( i );
		}
		tag.removeHTMLPart();
		for ( int i = 0; i < cnt; i++ ) {
			addHTMLPart( data[i] );
		}
	}

	/**
	 * HTMLのインクルード。<br>
	 * include( page, false ) と等価です。
	 * @param page 取り込むページ。
	 */
	public void include( Page page ) {
		include( page, false );
	}

	/**
	 * HTMLのインクルード。<br>
	 * page の bodyタグの内側部分全てを取り込む。<br>
	 * move_f が true の時、引数のタグの内側のインスタンスが移動します。<br>
	 * move_f が false の時、引数のタグの内側はそのままで、そのgetReplica()
	 * したものを取り込みます。
	 * @param page 取り込むページ。
	 * @param move_f true:移動、false:コピー。
	 */
	public void include( Page page, boolean move_f ) {
		if ( page == null )	return;
		include( page.getBodyTag(), move_f );
	}

	/**
	 * HTML文書の取得。
	 * @param idx 取得位置。
	 */
	public HTMLPart getHTMLPart( int idx ) {
		return doc.get( idx );
	}

	/**
	 * HTML文書件数の取得。
	 * @return 件数。
	 */
	public int getHTMLPartCount() {
		return doc.size();
	}

	/**
	 * HTML文書の削除。
	 * @param idx 削除位置。
	 */
	public void removeHTMLPart( int idx ) {
		HTMLPart	p = doc.remove( idx );
		if ( p == null )	return;
		removeHTMLPart( p );
	}

	/**
	 * HTML文書の削除。最初に登場した指定インスタンスを削除します。
	 * @param part 削除対象。
	 */
	public void removeHTMLPart( HTMLPart part ) {
		if ( part == null )	return;
		setModify();
		if ( part instanceof Tag ) {
			Tag	tag = (Tag)part;
			tag_only.remove( tag );
			getAssignedPage().removeNameEntry( tag );
		}
		part.setAssignedTag( null );
		doc.remove( part );
	}

	/**
	 * HTML文書の全削除。
	 */
	public void removeHTMLPart() {
		int	cnt = doc.size();
		for ( cnt--; cnt >= 0; cnt-- ) {
			removeHTMLPart( cnt );
		}
	}

	/**
	 * タグの除外。
	 * removeHTMLPartと異なり、除去対象タグの内部のデータが残存します。
	 * @param tag 除去対象。
	 */
	public  void exclude( Tag tag ) {
		int	idx = indexOf( tag );
		removeHTMLPart( tag );
		int	cnt = tag.getHTMLPartCount();
		for ( int i = cnt - 1; i >= 0; i-- ) {
			addHTMLPart( idx, tag.getHTMLPart( i ) );
		}
	}

	/**
	 * 位置を返す。最初に登場した指定インスタンスの位置を返します。
	 * @param part 検出対象。無ければ-1を返す。
	 */
	public int indexOf( HTMLPart part ) {
		if ( part == null ) return -1;
		return doc.indexOf( part );
	}

	/**
	 * 開始タグの文字列。
	 * @return 開始タグ。
	 */
	protected String getFirstTag() {
		boolean	xml = false;
		xml = getAssignedPage().isXML();
		StringBuilder	buf = new StringBuilder( "<" );
		boolean	name_f = ( this instanceof Anchor || this instanceof Form || this instanceof Control );
		String	myname = getName();
		if ( !name_f ) {
			name_f = ( name_ok_map.get( myname ) != null );
		}
		buf = buf.append( myname );
		for ( String k : attr.keySet() ) {
			if ( "name".equals( k ) && !name_f )	continue;
			buf = buf.append( " " );
			buf = buf.append( attr.get( k ).toHtmlString( xml ) );
		}
		if ( xml && isSimpleTag() ) {
			buf = buf.append( " /" );
		}
		buf = buf.append( ">" );
		return buf.toString();
	}

	/**
	 * 終了タグの文字列。
	 * @return 終了タグ。
	 */
	protected String getLastTag() {
		StringBuilder	buf = new StringBuilder( "</" );
		buf = buf.append( name );
		buf = buf.append( ">" );
		return buf.toString();
	}

	/**
	 * モードの決定。
	 * @param tag_name タグ名。
	 * @return モード。
	 */
	public static StringMode selectMode( String tag_name ) {
		tag_name = tag_name.toLowerCase( Locale.ENGLISH );
		StringMode	mode = StringMode.BODY;

		if ( "textarea".equals( tag_name ) || "pre".equals( tag_name ) ) {
			mode = StringMode.TEXTAREA;
		}
		else if ( isPlainTag( tag_name ) ) {
			mode = StringMode.PLAIN;
		}
		return mode;
	}

	/**
	 * モードの決定。
	 * @retrun モード。
	 */
	private StringMode selectMode() {
		return selectMode( name );
	}

	/**
	 * 文書の出力。
	 * @retrun 文書の文字列化。
	 */
	private String getHTMLParts( StringMode mode ) {

		int	cnt = getHTMLPartCount();
		StringBuilder	buf = new StringBuilder();
		if ( mode != StringMode.TEXTAREA )	mode = selectMode();
		for ( int i = 0; i < cnt; i++ ) {
			buf = buf.append( getHTMLPart( i ).toString( mode ) );
		}
		return buf.toString();
	}

	/**
	 * 文字列化。
	 * @param mode 文字列種別。
	 * @return 文字列。
	 */
	public String toString( StringMode mode ) {
		if ( !visible_f )	return "";
		StringBuilder	buf = new StringBuilder( "\n" );
		buf = buf.append( getFirstTag() );
		buf = buf.append( getHTMLParts( mode ) );
		if ( nest_f ) {
			buf = buf.append( getLastTag() );
		}
		return buf.toString();
	}

	/**
	 * 出力。
	 * @param w ライター。
	 * @param mode 文字列種別。
	 */
	public void write( PrintWriter w, StringMode mode ) {
		if ( !visible_f )	return;
		dirty_f = false;
		w.print( "\r\n" );
		w.print( getFirstTag() );
		int	cnt = getHTMLPartCount();
		if ( mode != StringMode.TEXTAREA )	mode = selectMode();
		for ( int i = 0; i < cnt; i++ ) {
			getHTMLPart( i ).write( w, mode );
		}
		if ( nest_f ) {
			w.print( getLastTag() );
		}
	}

	/**
	 * value属性値の取得。
	 * @return 文字列。
	 */
	protected String getValueAttribute() {
		Attribute	attr = getAttribute( VALUE );
		if ( attr == null )	return null;
		AttributeValuable	text = attr.get();
		if ( text == null )	return null;
		return text.toString( StringMode.PLAIN );
	}

	/**
	 * 値の取得。<br><br>
	 * 閉じタグを持たないものであれば、value属性値を返します。<br>
	 * ただし、value属性値がなければnullを返します。<br><br>
	 * 閉じタグを持つものであれば、タグで括られた最初の文字列を返します。<br>
	 * タグで括られた内部にText要素が無ければnullを返します。
	 * @return 文字列。
	 */
	public String getValueString() {
		if  ( nest_f ) {
			int	cnt = getHTMLPartCount();
			for ( int i = 0; i < cnt; i++ ) {
				HTMLPart	pt = getHTMLPart( i );
				if ( pt instanceof Text ) {
					return ((Text)pt).toString( StringMode.PLAIN );
				}
			}
			return null;
		}
		return getValueAttribute();
	}

	/**
	 * value属性への値設定。
	 * @param v 設定値。
	 */
	protected void setValueAttribute( String v ) {
		Attribute	attr = getAttribute( VALUE );
		if ( attr == null ) {
			try {
				setAttribute( new Attribute( VALUE, v ) );
			}
			catch( Exception e ) {}
			return;
		}
		attr.set( v );
	}

	/**
	 * 値の設定。<br>
	 * 閉じタグを持たないものであれば、value属性値に設定します。<br>
	 * 閉じタグを持つものであれば、タグ内部に持たせます。
	 * @param v 設定値。
	 */
	public void setValueString( String v ) {
		if ( nest_f ) {
			int	cnt = getHTMLPartCount();
			for ( int i = 0; i < cnt; i++ ) {
				HTMLPart	pt = getHTMLPart( i );
				if ( pt instanceof Text ) {
					((Text)pt).setText( v, StringMode.PLAIN );
					return;
				}
			}
			addHTMLPart( new Text( v ) );
			return;
		}
		setValueAttribute( v );
	}

	/**
	 * 値を持っているか？
	 * @return true:空文字列かnull値である、false:値を持っている。
	 */
	public boolean isEmptyValue() {
		String	str = getValueString();
		if ( str == null )	return true;
		return str.isEmpty();
	}

	/**
	 * 内包タグの列挙。得られるタグはthisが直接保持しているものだけです。
	 * 再帰的に全てのタグを探すわけではありません。
	 * @return タグ。無ければ0個の配列を返す。
	 */
	public Tag[] getTagArray() {
		return tag_only.toArray( new Tag[0] );
	}

	/**
	 * 内包タグの列挙。
	 * 指定クラスと同一、または指定クラスから派生したクラスを取り出し
	 * 配列にして返します。再帰的に探します。
	 * @param cls 探すクラス。Tagの派生クラスを指定します。
	 * @return タグ。無ければ0個の配列を返す。登場順にソートされてます。
	 * ただし、clsがnullなら、nullを返す。
	 */
	public Tag[] getAllTagByClass( Class<?> cls ) {
		if ( cls == null )	return null;
		ArrayList<Tag>	list = new ArrayList<Tag>();
		Tag[]	crt = getTagArray();
		for ( int i = 0; i < crt.length; i++ ) {
			Tag[]	sub = crt[i].getAllTagByClass( cls );
			for ( int j = 0; j < sub.length; j++ ) {
				list.add( sub[j] );
			}
			if ( cls.isAssignableFrom( crt[i].getClass() ) ) {
				list.add( crt[i] );
			}
		}
		return sort( list.toArray( new Tag[0] ) );
	}

	/**
	 * タグの取得。AやTABLEのような、タグの種別で探します。再帰的に探します。
	 * @param name タグの種別。
	 * @return タグの配列。無ければ0個の配列を返す。登場順にソートされてます。
	 * ただし、nameがnullなら、nullを返す。
	 */
	public Tag[] getAllTagByType( String name ) {
		if ( name == null )	return null;
		name = name.toLowerCase( Locale.ENGLISH );
		ArrayList<Tag>	list = new ArrayList<Tag>();
		Tag[]	crt = getTagArray();
		for ( int i = 0; i < crt.length; i++ ) {
			Tag[]	sub = crt[i].getAllTagByType( name );
			for ( int j = 0; j < sub.length; j++ ) {
				list.add( sub[j] );
			}
			if ( name.equals( crt[i].getName() ) ) {
				list.add( crt[i] );
			}
		}
		return sort( list.toArray( new Tag[0] ) );
	}

	/**
	 * タグの取得。AやTABLEのような、タグの種別で探します。
	 * 再帰的に探し、最初に見つかったインスタンスを返します。
	 * @param name タグの種別。
	 * @return タグ。見つからなければnull。
	 */
	public Tag getFirstTagByType( String name ) {
		if ( name == null )	return null;
		name = name.toLowerCase( Locale.ENGLISH );
		Tag[]	crt = getTagArray();
		for ( int i = 0; i < crt.length; i++ ) {
			if ( name.equals( crt[i].getName() ) )	return crt[i];
			Tag	sub = crt[i].getFirstTagByType( name );
			if ( sub != null )	return sub;
		}
		return null;
	}

	/**
	 * インスタンスを内包しているか？
	 * @param target 検索対象。
	 * @return true:持っている、false:持っていない。
	 */
	public boolean isInner( HTMLPart target ) {
		return getDirectInnerTag( target ) != null;
	}

	/**
	 * 指定インスタンスを直接保持しているタグを返す。
	 * @param target 検索対象。
	 * @return 直接保持しているタグ。無ければnull。
	 */
	public Tag getDirectInnerTag( HTMLPart target ) {
		if ( this == target )	return null;
		if ( target instanceof Tag ) {
			return ((Tag)target).getOuterTag();
		}
		int	cnt = doc.size();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	part = doc.get( i );
			if ( part == target )	return this;
			if ( part instanceof Tag ) {
				Tag	tag = ((Tag)part).getDirectInnerTag( target );
				if ( tag != null )	return tag;
			}
		}	
		return null;
	}

	/**
	 * 配列の中にthisを含むか？
	 * @param tag 検索する配列。
	 * @return true:含む、false:含まない。
	 */
	public boolean isContain( Tag[] tag ) {
		if ( tag == null )	return false;
		for ( int i = 0; i < tag.length; i++ ) {
			if ( this == tag[i] )	return true;
		}
		return false;
	}

	/**
	 * テキストの生成。
	 */
	protected Text makeText( String str ) {
		return new Text( str );
	}

	/**
	 * BRタグを改行コードに置換する。
	 */
	public void replaceBR() {
		int	cnt = getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	p = getHTMLPart( i );
			if ( !(p instanceof Tag) )	continue;
			Tag	tag = (Tag)p;
			if ( !"br".equals( tag.getName() ) )	continue;
			if ( tag.getAllAttribute().length > 0 )	continue;
			removeHTMLPart( i );
			addHTMLPart( i, makeText( "\n" ) );
		}

		for ( int i = cnt - 1; i > 0; i-- ) {
			HTMLPart	p = getHTMLPart( i );
			if ( !(p instanceof Text) )	continue;
			HTMLPart	p2 = getHTMLPart( i - 1 );
			if ( !(p2 instanceof Text) )	continue;
			Text	next = (Text)p, pre = (Text)p2;
			pre.append( next );
			removeHTMLPart( i );
		}
	}

	/**
	 * 所属先ページ設定。
	 * @param p 所属先。
	 */
	public void setAssignedPage( Page p ) {
		if ( p == null )	p = null_page;
		embed_page = p;
	}

	private ArrayList<HTMLPart> getInnerHTMLPart( ArrayList<HTMLPart> list, ArrayList<HTMLPart> d ) {
		int	tgt_cnt = d.size();
		if ( tgt_cnt <= 0 )	return list;
		int	cnt = getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	data = getHTMLPart( i );
			for ( int j = tgt_cnt - 1; j >= 0; j-- ) {
				if ( data == d.get( j ) ) {
					list.add( data );
					d.remove( data );
					tgt_cnt--;
					if ( tgt_cnt <= 0 )	return list;
				}
			}
			if ( data instanceof Tag ) {
				Tag	tag = (Tag)data;
				list = tag.getInnerHTMLPart( list, d );
				tgt_cnt = d.size();
				if ( tgt_cnt <= 0 )	return list;
			}
		}
		return list;
	}

	/**
	 * 内包物の検索。
	 * 引数で指定されたインスタンス群のうち、this が内包しているもの
	 * のみを抽出して返します。<br>
	 * 直接内包ではなく、再帰的にネストされた要素を検索します。
	 * @param d 検索対象の集合。
	 * @return 存在した要素の集合。無ければ空の配列を返します。
	 */
	public HTMLPart[] getInnerHTMLPart( HTMLPart[] d ) {
		if ( d == null )	return new HTMLPart[0];
		ArrayList<HTMLPart>	list = new ArrayList<HTMLPart>();
		ArrayList<HTMLPart>	tgt = new ArrayList<HTMLPart>();
		for ( int i = 0; i < d.length; i++ ) {
			tgt.add( d[i] );
		}
		return getInnerHTMLPart( list, tgt ).toArray( new HTMLPart[0] );
	}

	/**
	 * 内包物の検索。
	 * 引数で指定されたインスタンス群のうち、this が内包しているもの
	 * のみを抽出して返します。<br>
	 * 直接内包ではなく、再帰的にネストされた要素を検索します。
	 * @param d 検索対象の集合。
	 * @return 存在した要素の集合。無ければ空の配列を返します。
	 */
	public PageHooker[] getInnerHTMLPart( PageHooker[] d ) {
		if ( d == null )	return new PageHooker[0];
		HTMLPart[]	ret = new HTMLPart[d.length];
		for ( int i = 0; i < ret.length; i++ ){
			ret[i] = (HTMLPart)d[i];
		}
		ret = getInnerHTMLPart( ret );
		d = new PageHooker[ret.length];
		for ( int i = 0; i < ret.length; i++ ){
			d[i] = (PageHooker)ret[i];
		}
		return d;
	}

	/**
	 * 内包物の検索。
	 * 引数で指定されたインスタンス群のうち、this が内包しているもので
	 * 最初に見つかったものを返します。<br>
	 * 直接内包ではなく、再帰的にネストされた要素を検索します。
	 * @param d 検索対象の集合。
	 * @return 存在した要素。無ければnullを返します。
	 */
	public HTMLPart getFirstInnerHTMLPart( HTMLPart[] d ) {
		if ( d == null )	return null;
		if ( d.length == 0 )	return null;
		int	cnt = getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			HTMLPart	data = getHTMLPart( i );
			for ( int j = 0; j < d.length; j++ ) {
				if ( data == d[j] ) return data;
			}
			if ( data instanceof Tag ) {
				Tag	tag = (Tag)data;
				HTMLPart	ret = tag.getFirstInnerHTMLPart( d );
				if ( ret != null )	return ret;
			}
		}
		return null;
	}

	/**
	 * イベントハンドラの登録。
	 * このタグが存在するページの、{@link Page#addOnLoadScript}を
	 * 呼び出します。そのため、ページに追加されたタグでなければなりません。<br>
	 * また、上と同じ理由で、ページ出力されると内容がクリアされます。
	 * @param triger &quot;click&quot;や&quot;submit&quot;等。
	 * &quot;on&quot;を付けないで下さい。
	 * @param function JavaScript関数名。()や;を付けないで下さい。
	 */
	public void setEventHandler( String triger, String function ) {
		Page	page = getAssignedPage();
		String	id = makeID();
		StringBuilder	buf = new StringBuilder();
		buf = buf.append( "paraselene.safe_add_event('" );
		buf = buf.append( id );
		buf = buf.append( "','" );
		buf = buf.append( triger );
		buf = buf.append( "'," );
		buf = buf.append( function );
		buf = buf.append( ");" );
		page.addOnLoadScript( buf.toString() );
	}

	/**
	 * イベントハンドラの登録。
	 * {@link #setEventHandler}と異なり、無名関数を登録します。
	 * @param triger &quot;click&quot;や&quot;submit&quot;等。
	 * &quot;on&quot;を付けないで下さい。
	 * @param function JavaScript無名関数。例えば、<pre>
	 * &quot;function(event){&quot;,
	 * &quot;  alert( 'CLICK' );&quot;,
	 * &quot;}&quot;
	 * </pre>のような文字列群です。各文字列には改行が追加されます。
	 */
	public void setAnonymousEventHandler( String triger, String ... function ) {
		Page	page = getAssignedPage();
		String	id = makeID();
		StringBuilder	script = new StringBuilder();
		for ( int i = 0; i < function.length; i++ ) {
			if ( i > 0 )	script = script.append( "\r\n" );
			script = script.append( function[i] );
		}
		String	ano = script.toString();
		StringBuilder	buf = new StringBuilder( "paraselene_handler_" );
		buf = buf.append( id );
		int	hc = ano.hashCode();
		if ( hc < 0 )	hc *= -1;
		buf = buf.append( Integer.toString( hc, Character.MAX_RADIX ) );
		ano = buf.toString();
		buf = new StringBuilder( "var " );
		buf = buf.append( ano );
		buf = buf.append( "=" );
		buf = buf.append( script );
		buf = buf.append( ";" );
		page.addOnLoadScript( buf.toString() );
		setEventHandler( triger, ano );
	}

	/**
	 * class属性の取得。空白文字で分割したものを返します。
	 * @return 無ければ 0 個の配列。
	 */
	public String[]	getClassAttribute() {
		Attribute	cls = getAttribute( "class" );
		if ( cls == null )	return new String[0];
		String	str = cls.getString();
		if ( str == null )	return new String[0];
		return str.split( "\\p{Space}" );
	}

	/**
	 * JavaScript を安全に記述するための script タグを生成します。<br>
	 * このタグの内容を変更しても、Feedback として通知されないので注意して下さい。
	 * @param script JavaScript。各引数には改行が挟まれて出力されます。
	 * @return script タグ。
	 */
	public static Tag makeScript( String ... script ) {
		Tag	tag = new Tag( "script", false ) {
			public void setModify() {return;}
		};
		tag.setAttribute( new Attribute( "type", "text/javascript" ) );
		tag.addHTMLPart( new paraselene.ajax.data.JavaScript( script ) );
		return tag;
	}

	private String makeCookieName() throws IOException {
		String	name = getNameAttribute();
		if ( name == null )	throw new IOException( "no name attribute." );
		Page	page = getAssignedPage();
		if ( page == null )	throw new IOException( "no page assigned." );
		String	pid = page.getID().toString();
		StringBuilder	buf = new StringBuilder( "paraselene" );
		buf = buf.append( "-" );
		buf = buf.append( pid );
		buf = buf.append( "-" );
		buf = buf.append( name );
		return buf.toString();
	}

	private Object getCookieValue( String str ) throws IOException, ClassNotFoundException {
		ByteArrayInputStream	bais = new ByteArrayInputStream(  new BigInteger( str, Character.MAX_RADIX ).toByteArray() );
		ObjectInputStream	ois = new ObjectInputStream( bais );
		return ois.readObject();
	}

	private String makeCookieValue( Object obj ) throws IOException {
		ByteArrayOutputStream	baos = new ByteArrayOutputStream();
		ObjectOutputStream	oos = new ObjectOutputStream( baos );
		oos.writeObject( obj );
		return new BigInteger( baos.toByteArray() ).toString( Character.MAX_RADIX );
	}

	/**
	 * このタグのクッキー設定値を返します。
	 * @param req リクエスト内容。
	 * @return クッキー設定値。クッキーが存在しない場合、null。
	 */
	protected Object getCookieValue( RequestParameter req ) throws IOException, ClassNotFoundException {
		Cookie	c = req.getCookie( makeCookieName() );
		if ( c == null )	return null;
		return getCookieValue( c.getValue() );
	}

	/**
	 * このタグの設定値を保存するためのクッキーを生成。
	 * @param req リクエスト内容。
	 * @param val 設定値。
	 * @param expiry クッキー存続時間(秒)。
	 * @param ssl_f true:HTTPSの場合のみクッキーを返すようにする、false:全てのプロトコルでクッキーを返すようにする。
	 * @return クッキー。
	 */
	protected Cookie makeCookieValue( RequestParameter req, Object val, int expiry, boolean ssl_f ) throws IOException {
		Cookie	c = new Cookie( makeCookieName(), makeCookieValue( val ) );
		c.setMaxAge( expiry );
		c.setPath( req.getContextPath() );
		c.setSecure( ssl_f );
		return c;
	}

	/**
	 * クッキーからの値を復元。<br>
	 * 自身のタグに対するクッキーから、以前の値を setValueString() します。<br>
	 * ただし、isEmptyValue() が false なら処理しません。
	 * @param req リクエスト内容。
	 */
	public void importFromCookie( RequestParameter req ) {
		if ( !isEmptyValue() )	return;
		try {
			String	val = (String)getCookieValue( req );
			if ( val != null )	setValueString( val );
		}
		catch( Exception e ){}
	}

	/**
	 * クッキーへ値の保存。<br>
	 * クッキーへ getValueString() したものを設定します。<br>
	 * ただし、isEmptyValue() が true なら、クッキーへ保存しません。<br>
	 * また、<ul>
	 * <li>ページに属していないタグ
	 * <li>name 属性を持たないタグ
	 * </ul>もクッキーへ保存できません。
	 * @param req リクエスト内容。
	 * @param fw 設定先インスタンス。
	 * @param expiry クッキー存続時間(秒)。
	 * @param ssl_f true:HTTPSの場合のみクッキーを返すようにする、
	 * false:全てのプロトコルでクッキーを返すようにする。
	 * @return fw をそのまま返します。
	 */
	public Forward exportToCookie( RequestParameter req, Forward fw, int expiry, boolean ssl_f ) {
		if ( isEmptyValue() )	return fw;
		try {
			fw.addCookie( makeCookieValue( req, getValueString(), expiry, ssl_f ) );
		}
		catch( Exception e ){}
		return fw;
	}

	/**
	 * クッキーへ値の保存。<br>
	 * クッキーへ getValueString() したものを設定します。<br>
	 * ただし、isEmptyValue() が true なら、クッキーへ保存しません。<br>
	 * また、<ul>
	 * <li>ページに属していないタグ
	 * <li>name 属性を持たないタグ
	 * </ul>もクッキーへ保存できません。
	 * @param req リクエスト内容。
	 * @param page 設定先インスタンス。
	 * @param expiry クッキー存続時間(秒)。
	 * @param ssl_f true:HTTPSの場合のみクッキーを返すようにする、
	 * false:全てのプロトコルでクッキーを返すようにする。
	 * @return page をそのまま返します。
	 */
	public Page exportToCookie( RequestParameter req, Page page, int expiry, boolean ssl_f ) {
		if ( isEmptyValue() )	return page;
		try {
			page.addCookie( makeCookieValue( req, getValueString(), expiry, ssl_f ) );
		}
		catch( Exception e ){}
		return page;
	}

	/**
	 * クッキーへ値の保存。
	 * このタグが所属するページにクッキーを設定します<br>
	 * クッキーへ getValueString() したものを設定します。<br>
	 * ただし、isEmptyValue() が true なら、クッキーへ保存しません。<br>
	 * また、<ul>
	 * <li>ページに属していないタグ
	 * <li>name 属性を持たないタグ
	 * </ul>もクッキーへ保存できません。
	 * @param req リクエスト内容。
	 * @param expiry クッキー存続時間(秒)。
	 * @param ssl_f true:HTTPSの場合のみクッキーを返すようにする、
	 * false:全てのプロトコルでクッキーを返すようにする。
	 * @return このタグが所属するページ(クッキー設定先)を返します。
	 */
	public Page exportToCookie( RequestParameter req, int expiry, boolean ssl_f ) {
		return exportToCookie( req, getAssignedPage(), expiry, ssl_f );
	}
}

