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

import java.util.*;
import java.io.*;
import java.net.*;
import paraselene.*;
import paraselene.supervisor.*;

/**
 * HTML汎用タグ。
 * スレッドセーフです。
 */
public class Tag extends HTMLPart {
	private static final Page	null_page = new NullPage(){
		public Forward input( RequestParameter req, Forward fw ) throws PageException {return null;}
	};
	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 boolean dirty_f = true;

	private static String	VALUE = "value";

	private Page	embed_page = null_page;

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

	/**
	 * 更新された事を通知する。
	 */
	public void setModify() {
		dirty_f = true;
		if ( getAttribute( "id" ) == null ) {
			getOuterTag().setModify();
		}
	}

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

	private synchronized 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$";

	private void makeID() {
		if ( getAttribute( "id" ) != null )	return;
		long	now = 0;
		synchronized( VALUE ){
			now = tag_id;
			tag_id++;
		}
		long	time = (now << 56) | (new Date().getTime() & 0x00ffffffffffffffL);
		StringBuilder	buf = new StringBuilder( PARASELENE_ID );
		buf = buf.append( Long.toString( time, Character.MAX_RADIX ) );
		setAttribute( new Attribute( "id", buf.toString() ) );
	}

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

	/**
	 * 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>();
		synchronized( this ) {
			for ( String k : attr.keySet() ) {
				list.add( attr.get( k ) );
			}
		}
		return list.toArray( new Attribute[0] );
	}

	/**
	 * 複製の作成。<br>
	 * 複製したものは、name属性値も引き継がれるので
	 * addHTMLPart()する時に配列化にされる場合があります。
	 * @return 複製。
	 */
	public synchronized HTMLPart getReplica() {
		Tag	tag = newReplica();
		tag.visible_f = this.visible_f;
		for ( String k : attr.keySet() ) {
			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 );
	}

	/**
	 * タグ名の取得。小文字で返す。
	 * @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;
				}
			}
		}
		synchronized( this ) {
			attr.put( at.getName(), at );
		}
		at.setAssignedTag( this );
		if ( "name".equals( at_name ) ) {
			Tag	out = getOuterTag();
			if ( out != null )	out.makeID();
		}
	}

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

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

	/**
	 * 属性の取得。無ければnullを返す。
	 * @param at_name 属性名。
	 * @return 属性。
	 */
	public synchronized 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();
	}

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

	/**
	 * HTML文書の追加。name属性値が衝突すると、それを変更します。
	 * @param idx 追加位置。
	 * @param d 文書。
	 */
	public synchronized void addHTMLPart( int idx, HTMLPart d ) {
		setModify();
		doc.add( idx, d );
		if ( d instanceof Tag ) {
			Tag tag = (Tag)d;
			getAssignedPage().nameTag( tag );
			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 );
			if ( tag.getAttribute( "name" ) != null )	makeID();
		}
		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>
	 * タグで挟まれた部分全てを取り込む。そのタグ自体は取り込まない。
	 * @param tag 取り込むタグ。
	 */
	public synchronized void include( Tag tag ) {
		if ( tag == null )	return;
		int	cnt = tag.getHTMLPartCount();
		for ( int i = 0; i < cnt; i++ ) {
			addHTMLPart( tag.getHTMLPart( i ) );
		}
	}

	/**
	 * HTMLのインクルード。<br>
	 * bodyタグの内側部分全てを取り込む。
	 * @param page 取り込むページ。
	 */
	public synchronized void include( Page page ) {
		Tag	tag = page.getFirstTagByType( "body" );
		if ( tag == null )	return;
		include( tag );
	}

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

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

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

	/**
	 * HTML文書の削除。最初に登場した指定インスタンスを削除します。
	 * @param part 削除対象。
	 */
	public synchronized 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 synchronized void removeHTMLPart() {
		int	cnt = doc.size();
		for ( cnt--; cnt >= 0; cnt-- ) {
			removeHTMLPart( cnt );
		}
	}

	/**
	 * タグの除外。
	 * removeHTMLPartと異なり、除去対象タグの内部のデータが残存します。
	 * @param tag 除去対象。
	 */
	public synchronized 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 synchronized int indexOf( HTMLPart part ) {
		if ( part == null ) return -1;
		return doc.indexOf( part );
	}

	/**
	 * 開始タグの文字列。
	 * @return 開始タグ。
	 */
	protected synchronized String getFirstTag() {
		boolean	xml = false;
		xml = getAssignedPage().isXML();
		StringBuilder	buf = new StringBuilder( "<" );
		buf = buf.append( getName() );
		for ( String k : attr.keySet() ) {
			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 synchronized 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 ) {
		dirty_f = false;
		if ( !visible_f )	return;
		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 ) {
			synchronized( this ) {
				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 ) {
			synchronized( this ) {
				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 synchronized 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 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 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();
		}
		synchronized( this ) {
			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 synchronized 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;
	}
}

