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

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


/**
 * URIを示すデータ。
 * URIを示す文字列は、url(xxx) 形式の文字列を渡すと、内部の xxx だけ取り出します。
 * スレッドセーフです。
 */
public class URIValue extends PlainText implements AttributeValuable, CSSValuable {
	private static final long serialVersionUID = 2L;
	private static final String DOWNLOAD_DIR = "rabbit/";
	/**
	 * デフォルトのエンコード(UTF-8)。
	 */
	public static final String DEFAULT_ENC = "UTF-8";

	private URIValue() {}

	private String out_enc = null;
	private String getCharset() {
		if ( out_enc != null )	return out_enc;
		String	ret = null;
		ret = getAssignedTag().getAssignedPage().getCharset();
		if ( ret != null )	return ret;
		return DEFAULT_ENC;
	}

	/**
	 * CSSのURL表現、url(xxx)形式の記述から、xxx部分を取り出して返します。
	 * @param org_url 取り出し対象文字列。
	 * @return 取り出したURL。フォーマットが合わない場合、org_urlをそのまま返します。
	 */
	public static String CSSURLStrip( String org_url ) {
		if ( org_url == null )	return null;
		org_url = org_url.trim();
		if ( org_url.length() < 6 )	return org_url;
		if ( !"url(".equalsIgnoreCase( org_url.substring( 0, 4 ) ) )	return org_url;
		String	url = org_url.substring( 4 ).trim();
		int	len = url.length();
		if ( len < 1 )	return org_url;
		if ( url.charAt( len - 1 ) != ')' )	return org_url;
		url = url.substring( 1, len );
		len = url.length();
		if ( len == 0 )	return "";
		char[]	ch = new char[] {
			url.charAt( 0 ), url.charAt( len - 1 )
		};
		if ( ch[0] == ch[1] && (ch[0] == '\'' || ch[0] == '"') ) {
			url = url.substring( 1, len - 1 );
		}
		return url;
	}

	/**
	 * CSSのURL表現であるか？
	 * @return true:URLである、false:URLではない。
	 */
	public static boolean isCSSURL( String url ) {
		if ( url == null )	return false;
		url = url.trim();
		return !url.equals( CSSURLStrip( url ) );
	}

	/**
	 * 複製の作成。
	 * @return 複製。
	 */
	public HTMLPart getReplica() {
		try {
			return new URIValue( getURI(), DEFAULT_ENC, out_enc );
		}
		catch ( Exception e ) {
			Option.debug( e );
		}
		return null;
	}

	/**
	 * URIのエンコード。
	 * @param uri URI。
	 * @param enc 文字コード。
	 * @return エンコード後の文字列。
	 */
	public static String encode( String uri, String enc ) throws UnsupportedEncodingException {
		uri = CSSURLStrip( uri );
		if ( enc == null )	enc = DEFAULT_ENC;
		String[]	frag = uri.split( "#" );
		if ( frag.length == 0 )	return uri;
		String[]	query = frag[0].split( "\\?" );
		if ( query.length == 0 )	return uri;
		String[]	test = query[0].split( ":" );
		String	scheme = null;
		String	main_path = test[test.length - 1];
		if ( test.length > 1 ) {
			StringBuilder	sc = new StringBuilder( test[0] );
			if ( main_path.indexOf( "//" ) == 0 ) {
				main_path = main_path.substring( 2 );
				sc = sc.append( "://" );
			}
			else {
				sc = sc.append( ":" );
			}
			scheme = sc.toString();
		}
		else {
			int	len = query[0].length();
			if ( len > 0 ) {
				if ( query[0].charAt( len - 1 ) == ':' ) {
					scheme = query[0];
					main_path = "";
				}
			}
		}
		StringBuilder	buf = new StringBuilder( " " );
		buf = buf.append( main_path );
		buf = buf.append( " " );
		String[]	path = buf.toString().split( "/" );
		buf = new StringBuilder( URLEncoder.encode( path[0], enc ) );
		for ( int i = 1; i < path.length; i++ ) {
			buf = buf.append( "/" );
			buf = buf.append( URLEncoder.encode( path[i], enc ) );
		}
		String	tmp = buf.toString();
		buf = new StringBuilder( tmp.substring( 1, tmp.length() -1 ) );
		if ( query.length > 1 ) {
			buf = buf.append( "?" );
			String[]	param = query[1].split( "&" );
			for ( int i = 0 ;i < param.length; i++ ) {
				if ( i > 0 )	buf = buf.append( "&" );
				String[]	data = param[i].split( "=" );
				buf = buf.append( URLEncoder.encode( data[0], enc ) );
				buf = buf.append( "=" );
				if ( data.length > 1 ) {
					buf = buf.append( URLEncoder.encode( data[1], enc ) );
				}
			}
		}
		if ( frag.length > 1 ) {
			 buf = buf.append( "#" );
			 buf = buf.append( URLEncoder.encode( frag[1], enc ) );
		}
		if ( scheme != null ) {
			StringBuilder	full = new StringBuilder( scheme );
			buf = full.append( buf );
		}
		return buf.toString();
	}

	/**
	 * コンストラクタ。エスケープされていないURIを格納します。
	 * @param u URI。
	 * @param enc setEncode()参照。
	 */
	public URIValue( String u, String enc ) throws UnsupportedEncodingException, URISyntaxException {
		u = CSSURLStrip( u );
		setEncode( enc );
		setURI( makeURI( u, enc ), enc );
	}

	/**
	 * コンストラクタ。エスケープされていないURIを格納します。<br>
	 * CSSのURL形式文字列を渡す事も可能ですが、そのパスがエスケープされていると
	 * 正しく動作しません。
	 * @param u URI。
	 */
	public URIValue( String u ) throws UnsupportedEncodingException, URISyntaxException {
		this( CSSURLStrip( u ), null );
	}

	/**
	 * コンストラクタ。URIはdecに従いデコードします。
	 * @param u URI。
	 * @param dec デコード文字コード。
	 * @param enc setEncode()参照。
	 */
	public URIValue( URI u, String dec, String enc ) throws UnsupportedEncodingException, URISyntaxException {
		this( URLDecoder.decode( u.toString(), dec ), enc );
	}

	/**
	 * URIの設定。エスケープがあればdecに従いデコードします。
	 * @param u URI。
	 * @param dec デコード文字コード。
	 */
	public void setURI( URI u, String dec ) throws UnsupportedEncodingException {
		if ( u == null ) {
			setText( null );
		}
		else {
			if ( dec == null )	dec = DEFAULT_ENC;
			setText( URLDecoder.decode( u.toString(), dec ) );
		}
	}

	private static URI makeURI( String uri ) {
		return makeURI( uri, DEFAULT_ENC );
	}

	private static URI makeURI( String uri, String enc ) {
		uri = CSSURLStrip( uri );
		if ( uri == null )	return null;
		try {
			return new URI( encode( uri, enc ) );
		}
		catch( Exception e ) {
			Option.debug( e );
		}
		return null;
	}

	/**
	 * URIの取得。
	 * return 属性値。
	 */
	public URI getURI() {
		return makeURI( super.toString( StringMode.PLAIN ) );
	}

	/**
	 * クエリー部分の取得。<br>
	 * http://xxx.com/yyy.cgi?a=1&b=2 ならば、<br>
	 * [0] &quot;a=1&quot;<br>
	 * [1] &quot;b=2&quot;<br>
	 * を返します。エスケープされていない状態で返します。
	 * @return クエリー部分。クエリー部が無ければ0個の配列。
	 */
	public QueryItem[] getQuery() {
		String	q = super.toString();
		if ( q == null ) {
			return new QueryItem[0];
		}
		String[]	str = q.split( "&" );
		ArrayList<QueryItem>	list = new ArrayList<QueryItem>();
		for ( int i = 0; i < str.length; i++ ) {
			String[]	part = str[i].split( "=" );
			switch( part.length ) {
			case 1:
				list.add( new QueryItem( part[0] ) );
				break;
			case 2:
				list.add( new QueryItem( part[0], part[1] ) );
				break;
			}
		}
		return list.toArray( new QueryItem[0] );
	}

	/**
	 * URIのクエリ設定。元のURIにフラグメントを含んでいる場合、それを引き継ぎます。
	 * @param query エスケープしないで下さい。
	 * null を渡すと、0個の配列と同じ意味となります。
	 */
	public void setQuery( QueryItem ... query ) {
		try {
			setText( setQuery( getURI(), query ) );
		}
		catch( Exception e ){
			Option.debug( e );
		}
	}


	/**
	 * 自サイト内ページへのURI生成。
	 * @param pid ページID。
	 * @param fragment フラグメント。#は含めないで下さい。
	 * 不要な場合はnullを指定して下さい。
	 * @param query エスケープしないで下さい。
	 * @return URI。相対パスです。
	 */
	public static String pageToURI( PageID pid, String fragment, QueryItem ... query ) {
		StringBuilder	buf = new StringBuilder( Supervisor.LINK_DEF[0] );
		buf = buf.append( pid.getID() );
		if ( fragment != null ) {
			buf = buf.append( "#" );
			buf = buf.append( fragment );
		}
		try {
			return setQuery( makeURI( buf.toString(), DEFAULT_ENC ), query );
		}
		catch( Exception e ){
			Option.debug( e );
			return null;
		}
	}

	/**
	 * 自サイト内ページへのURI生成。ダウンロード用。
	 * @param pid ページID。
	 * @param filename ブラウザに認識させるファイル名。
	 * @param query エスケープしないで下さい。
	 * @return URI。相対パスです。
	 */
	public static String pageToDownloadURI( PageID pid, String filename, QueryItem ... query ) {
		StringBuilder	buf = new StringBuilder( DOWNLOAD_DIR );
		buf = buf.append( pageToURI( pid, null ) );
		buf = buf.append( "/" );
		buf = buf.append( filename );
		try {
			return setQuery( makeURI( buf.toString(), DEFAULT_ENC ), query );
		}
		catch( Exception e ){
			Option.debug( e );
			return null;
		}
	}

	/**
	 * スキーマ。
	 */
	public enum Scheme {
		HTTP( Supervisor.LINK_DEF[1], "http", 80 ),
		HTTPS( Supervisor.LINK_DEF[2], "https", 443 );
		private static final long serialVersionUID = 1L;
		String	str;
		String	prot;
		int	port;
		private Scheme( String s, String p, int pt ) {
			str = s;
			prot = p;
			port = pt;
		}
		/**
		 * インスタンスの比較。
		 * 渡されたものがStringならば、プロトコル名とみなし比較します。
		 * @param o 比較対象。
		 * @return true:一致、false:不一致。
		 */
		private boolean equalsString( String o ) {
			return prot.equals( o );
		}
		/**
		 * URIのスキーマを返します。
		 * @param uri URI。
		 * @return スキーマ。
		 */
		public static Scheme getScheme( URI uri ) {
			Scheme[]	s = values();
			String	p = uri.getScheme();
			for ( int i = 0; i < s.length; i++ ) {
				if ( s[i].equalsString( p ) )	return s[i];
			}
			return null;
		}
	}
	/**
	 * プロトコル名を伴う絶対パスの生成。
	 * サーブレットコンテナに問い合わせ、ホスト名、コンテキストパスを取得し
	 * 絶対パスを生成します。<br>
	 * 例えば、makeAbsolutePath( URIValue.Scheme.HTTP, 0, "abc.html" )は<br>
	 * http://ホスト名/コンテキストパス/abc.html<br>
	 * を生成します。<br>
	 * makeAbsolutePath( URIValue.Scheme.HTTPS, 0, pageToURI( pid, null ) )<br>
	 * のように、プロトコルをHTTPSに切り替えたい場合に有用です。
	 * @param prt スキーマ。
	 * @param port ポート。0以下ならばHTTPなら80のように標準的なポートを使用します。
	 * 特殊なポートを指定したい場合に値を設定して下さい。
	 * @param path 相対パス。スキーマやホスト名を含んだ文字列を指定しないで下さい。
	 */
	public static String makeAbsolutePath( Scheme prt, int port, String path ) {
		StringBuilder	buf = new StringBuilder( prt.str );
		if ( port == prt.port )	port = 0;
		buf = buf.append( port );
		if ( path == null )	path = "/";
		if ( path.charAt( 0 ) != '/' ) {
			buf = buf.append( "/" );
		}
		buf = buf.append( path );
		return buf.toString();
	}

	private static String setQuery( URI uri, QueryItem ... q ) {
		if ( q == null )	q = new QueryItem[0];
		StringBuilder	buf = new StringBuilder();
		String	tmp;
		tmp = uri.getScheme();
		if ( tmp != null ) {
			buf = buf.append( tmp );
			buf = buf.append( "://" );
		}
		tmp = uri.getUserInfo();
		if ( tmp != null ) {
			buf = buf.append( tmp );
			buf = buf.append( "@" );
		}
		tmp = uri.getHost();
		if ( tmp != null ) {
			buf = buf.append( tmp );
		}
		int	port = uri.getPort();
		if ( port != -1 ) {
			buf = buf.append( ":" );
			buf = buf.append( port );
		}
		buf = buf.append( uri.getPath() );
		for ( int i = 0; i < q.length; i++ ) {
			if ( i == 0 ) {
				buf = buf.append( "?" );
			}
			else {
				buf = buf.append( "&" );
			}
			buf = buf.append( q[i].toString() );
		}
		tmp = uri.getFragment();
		if ( tmp != null ) {
			buf = buf.append( "#" );
			buf = buf.append( tmp );
		}
		try {
			return URLDecoder.decode( buf.toString(), DEFAULT_ENC );
		}
		catch( Exception e ) {
			Option.debug( e );
		}
		return null;
	}

	/**
	 * 出力文字コード指定。
	 * 出力時にエスケープする際の文字コードを設定します。
	 * @param enc 例:&quot;UTF-8&quot;、
	 * &quot;Windows-31J&quot;、&quot;EUC-JP&quot;
	 * @exception UnsupportedEncodingException サポートされない文字コード。
	 */
	public void setEncode( String enc ) throws UnsupportedEncodingException {
		if ( enc != null ) {
			URLEncoder.encode( "テスト", enc );
		}
		out_enc = enc;
	}

	public String toString( StringMode mode ) {
		String	ret = null;
		try {
			if ( paraselene.mockup.Linker.flag ) {
				ret = URIValue.encode( super.toString( StringMode.PLAIN ), getCharset() );
			}
			else {
				ret = Supervisor.makeWithSessionURI( super.toString( StringMode.PLAIN ), getCharset(), getAssignedTag() );
			}
		}
		catch( Exception e ){
			Option.debug( e );
			ret = super.toString( StringMode.PLAIN );
		}
		if ( ret != null && mode == StringMode.CSS ) {
			StringBuilder	buf = new StringBuilder( "url(" );
			buf = buf.append( ret );
			buf = buf.append( ")" );
			ret = buf.toString();
		}
		return ret;
	}

}


