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

import paraselene.*;
import paraselene.tag.*;
import java.io.*;
import java.util.*;

/**
 * スタイルシートセレクタ。
 */
public class Selector implements Serializable {
	private static final long serialVersionUID = 2L;

	private	String tag_name;
	private String id_name;
	private String class_name;
	private ArrayList<SelectorAttribute>	attr = new ArrayList<SelectorAttribute>();

	private Selector	parent, child, up, down, define_prev, define_next;
	private boolean		parent_near_f = false, child_near_f = false;

	private Selector getStart() {
		if ( parent != null )	return parent.getStart();
		if ( up != null )	return up.getStart();
		return this;
	}

	private Selector getEnd() {
		if ( child != null )	return child.getEnd();
		if ( down != null )	return down.getEnd();
		return this;
	}

	private Selector getDefineStart() {
		if ( define_prev != null )	return define_prev.getStart();
		return this;
	}

	private Selector getDefineEnd() {
		if ( define_next != null )	return define_next.getEnd();
		return this;
	}

	/**
	 * 子孫設定。
	 * @param parent 上位セレクタ。
	 * このセレクタに下位セレクタが存在すれば、最下位セレクタの下に接続します。
	 * @param child 下位セレクタ。
	 * このセレクタに上位セレクタが存在すれば、最上位セレクタに接続します。
	 * @param direct true:直下、false:直下とは限らない。
	 * @return 最下位セレクタを返します。
	 * parentかchildがnullなら、childをそのまま返します。
	 */
	public static Selector joinChild( Selector parent, Selector child, boolean direct ) {
		Selector	ret = child;
		if ( parent == null || child == null )	return ret;
		synchronized( ret ) {
			parent = parent.getEnd();
			child = child.getStart();
			parent.child = child;
			parent.child_near_f = direct;
			child.parent = parent;
			child.parent_near_f = direct;
			return ret.getEnd();
		}
	}

	/**
	 * 隣接設定。
	 * @param up 上位セレクタ。
	 * このセレクタに下位セレクタが存在すれば、最下位セレクタの下に接続します。
	 * @param down 下位セレクタ。
	 * このセレクタに上位セレクタが存在すれば、最上位セレクタに接続します。
	 * @return 最下位セレクタを返します。
	 * upかdownがnullなら、downをそのまま返します。
	 */
	public static Selector joinNext( Selector up, Selector down ) {
		Selector	ret = down;
		if ( up == null || down == null )	return ret;
		synchronized( ret ) {
			up = up.getEnd();
			down = down.getStart();
			up.down = down;
			down.up = up;
			return ret.getEnd();
		}
	}

	/**
	 * 疑似クラス設定。
	 * @param main 疑似クラス設定先。
	 * @param cls 疑似クラス。
	 * @return mainをそのまま返します。
	 */
	public static Selector joinClass( Selector main, Selector cls ) {
		Selector	ret = main;
		if ( main == null || cls == null )	return ret;
		synchronized( ret ) {
			main = main.getDefineEnd();
			cls = cls.getDefineStart();
			main.define_next = cls;
			cls.define_prev = main;
			return ret.getDefineStart();
		}
	}

	public Selector getReplica() {
		synchronized( this ) {
			Selector	myself = new Selector( tag_name, id_name, class_name, attr.toArray( new SelectorAttribute[0] ) );
			if ( define_next != null ) {
				joinClass( myself, define_next.getReplica() );
			}
			if ( parent != null ) {
				joinChild( parent.getReplica(), myself, parent_near_f );
			}
			if ( up != null ) {
				joinNext( up.getReplica(), myself );
			}
			return myself;
		}
	}

	private StringBuilder toStringBuilder( StringBuilder buf ) {
		if ( parent != null ) {
			buf = parent.toStringBuilder( buf );
			buf = buf.append( " " );
			if ( child_near_f ) {
				buf = buf.append( "> " );
			}
		}
		if ( up != null ) {
			buf = up.toStringBuilder( buf );
			buf = buf.append( " + " );
		}

		if ( tag_name != null ) {
			buf = buf.append( CSSParser.escape( tag_name ) );
		}
		if ( id_name != null ) {
			buf = buf.append( "#" );
			buf = buf.append( CSSParser.escape( id_name ) );
		}
		if ( class_name != null ) {
			buf = buf.append( "." );
			buf = buf.append( CSSParser.escape( class_name ) );
		}
		if ( define_next != null ) {
			buf = buf.append( ":" );
			buf = define_next.toStringBuilder( buf );
		}
		int	cnt = attr.size();
		for ( int i = 0; i < cnt; i++ ) {
			buf = buf.append( attr.get( i ).toString() );
		}

		return buf;
	}

	public String toString() {
		return toStringBuilder( new StringBuilder() ).toString();
	}

	/**
	 * タグが条件に合うか？
	 * @param tag 検査するタグ。
	 */
	public boolean isHit( Tag tag ) {
		return isHit( tag, false );
	}

	private boolean isHit( Tag tag, boolean pa_f ) {
		if ( tag == null )	return false;
		if ( tag_name != null ) {
			if ( !"*".equals( tag_name ) ) {
				if ( !tag_name.equals( tag.getName() ) )	return pa_f?	isHit( tag.getOuterTag(), true ):	false;
			}
		}
		int	cnt = attr.size();
		for ( int i = 0; i < cnt; i++ ) {
			SelectorAttribute	sa = attr.get( i );
			Attribute	a = tag.getAttribute( sa.name );
			if ( a == null )	return pa_f?	isHit( tag.getOuterTag(), true ):	false;
			String	str = a.getString();
			if ( str == null )	return pa_f?	isHit( tag.getOuterTag(), true ):	false;
			boolean	flag = false;
			if ( sa.include_f || sa.part_f ) {
				String[]	p = CSSParser.split( str, sa.include_f?	' ': '-' );
				for ( int j = 0; j < p.length; j++ ) {
					flag = p[j].equals( sa.value );
					if ( flag )	break;
				}
			}
			else {
				flag = str.equals( sa.value );
			}
			if ( !flag )	return pa_f?	isHit( tag.getOuterTag(), true ):	false;
		}
		if ( parent != null ) {
			return isHit( tag.getOuterTag(), !parent_near_f );
		}
		if ( up != null ) {
			return isHit( tag.getOverTag(), false );
		}
		return true;
	}

	private Selector(){}
	/**
	 * コンストラクタ。
	 * @param t タグ名または疑似クラス名。不要な場合、null。
	 * @param id ID。不要な場合、null。
	 * @param cls クラス。不要な場合、null。
	 * @param a 属性指定。
	 */
	public Selector( String t, String id, String cls, SelectorAttribute ... a ) {
		tag_name = t.toLowerCase( Locale.ENGLISH ).trim();
		id_name = id.trim();
		class_name = cls.trim();
		for ( int i = 0; i < a.length; i++ ) {
			attr.add( a[i] );
		}
	}

	private static void set( Selector s, char m, StringBuilder b ) {
		String	str = b.toString().trim();
		switch( m ) {
		case '.':
			s.class_name = str;
			s.attr.add( new SelectorAttribute( "class", str, true, false ) );
			break;
		case '#':
			s.id_name = str;
			s.attr.add( new SelectorAttribute( "id", str, false, false ) );
			break;
		default:
			s.tag_name = str.toLowerCase( Locale.ENGLISH );
			break;
		}
	}

	private void makeAttribute( String str ) {
		if ( str == null )	return;
		String[]	part = CSSParser.split( str, '[', ']' );
		for ( int i = 0; i < part.length; i++ ) {
			if ( part[i].isEmpty() )	continue;
			String[]	next = CSSParser.split( part[i], '=' );
			int	len = next[0].length();
			char	ch = next[0].charAt( len - 1 );
			if ( ch == '~' || ch == '|' ) {
				next[0] = next[0].substring( 0, len - 1 );
			}
			String	v = null;
			if ( next.length > 1 ) {
				v = next[1];
			}
			SelectorAttribute	s = new SelectorAttribute( next[0], v, ch == '~', ch == '|' );
			attr.add( s );
		}
	}

	private static Selector make( String str ) {
		if ( str.isEmpty() )	return null;
		Selector	ret = new Selector();
		StringBuilder	buf = new StringBuilder();
		char[]	ch = str.toCharArray();
		char	marker = ' ';
		for ( int i = 0; i < ch.length; i++ ) {
			if ( ch[i] == ':' || ch[i] == '.' || ch[i] == '#' || ch[i] == '[' ) {
				set( ret, marker, buf );
				if ( ch[i] == '[' ) {
					ret.makeAttribute( new String( ch, i, ch.length - i ) );
					return ret;
				}
				marker = ch[i];
				buf = new StringBuilder();
				continue;
			}
			buf = buf.append( ch[i] );
		}
		set( ret, marker, buf );
		return ret;
	}

	private static Selector nest( String str ) {
		String[]	part = CSSParser.split( str, ' ', '\t', '\n', '\r', '\f' );
		ArrayList<Selector>	list = new ArrayList<Selector>();
		Selector	last = null;
		boolean	pc_f = false, br_f = false;
		for ( int i = 0; i < part.length; i++ ) {
			if ( part[i].isEmpty() )	continue;
			if ( part[i].equals( ">" ) ) {
				pc_f = true;
				continue;
			}
			if ( part[i].equals( "+" ) ) {
				br_f = true;
				continue;
			}
			ArrayList<Selector>	define = new ArrayList<Selector>();
			String[]	def = CSSParser.split( part[i], ':' );
			for ( int j = 1; j < def.length; j++ ) {
				def[j] = def[j].trim();
				if ( def[j].isEmpty() )	continue;
				define.add( make( def[j] ) );
			}
			Selector	sel = make( def[0] );
			int	cnt = define.size();
			Selector	loop = sel;
			for ( int j = 0; j < cnt; j++ ) {
				Selector	d = define.get( j );
				loop.define_next = d;
				d.define_prev = loop;
				loop = d;
			}
			if ( br_f && last != null ) {
				last.down = sel;
				sel.up = last;
			}
			else if ( last != null ) {
				last.child = sel;
				sel.parent = last;
				if ( pc_f ) {
					last.child_near_f = true;
					sel.parent_near_f = true;
				}
			}
			last = sel;
			list.add( sel );
		}
		return last;
	}

	/**
	 * 文字列からセレクタの生成。
	 * @param str セレクタ文字列。
	 */
	public static Selector[] create( String str ) {
		String[]	pare = CSSParser.split( str.trim(), ',' );
		ArrayList<Selector>	list = new ArrayList<Selector>();
		for ( int i = 0; i < pare.length; i++ ) {
			pare[i] = pare[i].trim();
			if ( pare[i].isEmpty() )	continue;
			Selector	sel = nest( pare[i] );
			sel = sel.getEnd();
			if ( sel != null )	list.add( sel );
		}
		return list.toArray( new Selector[0] );
	}

}

