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


import java.util.*;
import java.net.*;
import java.io.*;
import java.nio.charset.*;
import paraselene.*;
import paraselene.tag.*;
import paraselene.dyna.*;
import paraselene.dyna.mockup.*;


enum LineSeparator {
	CR( "cr", "\r" ),
	LF( "lf", "\n" ),
	CRLF( "crlf", "\r\n" ),
	LFCR( "lfcr", "\n\r" );

	private static LineSeparator	use;
	private String name;
	private String code;
	private LineSeparator( String n, String c ) {
		name = n;
		code = c;
	}
	static String getOSName() {
		LineSeparator[]	ls = values();
		for ( int i = 0; i < ls.length; i++ ) {
			if ( ls[i].code.equals( System.getProperty("line.separator") ) ) {
				return ls[i].name;
			}
		}
		return CRLF.name;
	}

	static boolean setUse( String name ) {
		LineSeparator[]	ls = values();
		for ( int i = 0; i < ls.length; i++ ) {
			if ( ls[i].name.equals( name ) ) {
				use = ls[i];
				return true;
			}
		}
		return false;
	}

	static String getUse() {
		if ( use == null )	return System.getProperty("line.separator");
		return use.code;
	}
}

class HtmlExt {
	private static String[]	ext = new String[] {
		"htm", "html", "xht", "xhtml"
	};

	static String[] getParam() {
		String[]	ret = new String[ext.length];
		for ( int i = 0; i < ret.length; i++ ) {
			ret[i] = "*." + ext[i];
		}
		return ret;
	}

	static boolean isHtml( String e ) {
		for ( int i = 0; i < ext.length; i++ ) {
			if ( ext[i].equalsIgnoreCase( e ) )	return true;
		}
		return false;
	}
}

public enum Param {
	HTML_ROOT( "-html",
		"読み込むhtmlの配置ディレクトリ。その直下より再帰読み込みします。",
		true, true ),
	OUT_PATH( "-out",
		"出力ディレクトリ。スケルトンソースファイルや定義ファイルの出力先です。\n" +
		"出力ディレクトリが無ければ作成します。\n" +
		"入力htmlと出力ファイルはタイムスタンプを比較し、必要な物のみ出力します。",
		true, true  ),
	OUT_SOURCE_PATH( "-out_source",
		OUT_PATH.key + "で指定したディレクトリの中に作成する\n" +
		"ソースファイル出力ディレクトリ名を指定します。",
		false, true, "src" ),
	PACKAGE( "-package",
		"ソースファイルのルートパッケージ名。\n" +
		"パッケージはルートパッケージ名を起点に階層化されます。",
		true, true  ),
	PACKAGE_BASE( "-package_base",
		"baseパッケージ名。\n" +
		"baseパッケージ名を指定パッケージ名に変更します。",
		false, true, "base" ),
	PACKAGE_VIEW( "-package_view",
		"viewパッケージ名。\n" +
		"viewパッケージ名を指定パッケージ名に変更します。",
		false, true, "view" ),
	PACKAGE_LOGIC( "-package_logic",
		"logicパッケージ名。\n" +
		"logicパッケージ名を指定パッケージ名に変更します。",
		false, true, "logic" ),
	OTHER( "-other",
		"画像などの静的コンテンツのルートパス。\n" +
		HTML_ROOT.key + " とこのパスが一致しているとみなし、\n" +
		"IMGタグ等に登場する相対パスを置換します。\n" +
		"静的コンテンツもアプリケーションサーバーに収納するならば、\n" +
		"Contextのpathと一致させて下さい。",
		false, true, "/" ),
	CLASS_PATH( "-class_path",
		"GrantTagProvider 実装クラスや URIPreProcess 実装クラスを探すクラスパスです。\n" +
		"クラスのあるディレクトリパスやjarファイルのファイルパスになります。\n" +
		"java コマンドと違い、指定ディレクトリを起点に可能性のあるファイルが\n" +
		"あれば、ターゲットとなるクラスであるか、内容を検証します。\n" +
		"ディレクトリを再帰検索し、何らかのjarファイルを見つければその内容も\n" +
		"確認します。",
		false, false ),
	URI_PP( "-uri_preprocess",
		"URI事前検証クラスを登録します。\n" +
		"paraselene.dyna.mockup.URIPreProcess を実装したクラス名を指定して下さい。\n" +
		"このクラスにクラスパスが通されている必要があります。",
		false, true ),
	CLEAR( "-clear",
		"true を指定すると、既にある出力ファイルを消去して再作成します。\n" +
		"ただし、既に存在するスケルトンソースは消去しませんし、上書きもしません。",
		false, true, "false" ),
	NO_CLEAR_DIR( "-no_clear_dir",
		CLEAR.key + " 指定された場合でも、ここで指定した\n" +
		"ディレクトリ下のファイルは保護し、削除対象としません。\n" +
		"*?を使ったワイルドカード指定が可能です。\n" +
		"ディレクトリ階層に関係なく、ディレクトリ名がマッチすると保護します。\n" +
		"指定例) .svn   Subversionの管理ファイルを削除しない\n",
		false, false ),
	IN_ENCODE( "-html_encode",
		"HTMLファイルの文字コード。\n" +
		"これを指定する時は、全てのHTMLファイルは予め、この文字コードで\n" +
		"統一しておいて下さい。\n" +
		"これを指定しなかった場合、HTMLファイルの<META>タグより決定します。\n" +
		"XHTMLファイルに対しては、このオプションは無視されます。" ,
		false, true ),
	OUT_ENCODE( "-output_encode",
		"実行時の文字コード。\n" +
		"実行時、ブラウザにはこの文字コードで出力されます。",
		false, true, "UTF-8" ),
	SOURCE_ENCODE( "-source_encode",
		"生成するスケルトンソースファイルの出力文字コード。",
		false, true, Charset.defaultCharset().name() ),
	SOURCE_LINE( "-source_line",
		"生成するスケルトンソースファイルの改行コード。\n" +
		"cr,lf,crlf,lfcrの何れかを指定可能です。\n" +
		"Windows crlf\n" + "UNIX/Linux lf\n" + "MAC OS cr,lfcr",
		false, true, LineSeparator.getOSName() ),
	SOURCE_HEAD( "-source_head",
		"生成するスケルトンソースファイルの先頭に付けるコメントを書いたファイル。\n" +
		"/* から */ で囲まれた文章、または // で始まる文章を記載したテキストファイル。\n" +
		"このテキストファイルにコピーライト等をヘッダとして付ける事ができます。\n" +
		"このテキストファイルの文字コード及び改行コードは実行環境のそれと同じもの\n" +
		"であると仮定して読み込みます。\n" +
		"出力文字コードは、" + SOURCE_ENCODE.key + " の指定に従い、\n" +
		"出力改行コードは、" + SOURCE_LINE.key + " の指定に従います。",
		false, true ),
	IGNORE_TAG_ACCESSER( "-ignore_tag_accesser",
		"name があっても、getアクセッサを生成しないタグを指定します。\n" +
		"指定例) meta param",
		false, false ),
	PARAM_URI( "-uri_param_name",
		"<PARAM>タグでURIを示すname(大文字小文字を同一視します)を指定します。\n" +
		"指定されたname属性値が一致した場合、対応するvalue属性値を\n" +
		"URIとして解決しようとします。",
		false, false ),
	TAG_PROVIDER( "-grant_tag_provider",
		"タグの独自派生クラスを登録します。\n" +
		"paraselene.dyna.GrantTagProvider を実装したクラス名を指定して下さい。\n" +
		"このクラスにクラスパスが通されている必要があります。",
		false, false ),
	INCLUDE( "-include",
		"ソースファイル化するファイル。*?を使ったワイルドカード指定が可能です。",
		false, false, HtmlExt.getParam() ),
	EXCLUDE( "-exclude",
		"ソースファイル化しないファイル。*?を使ったワイルドカード指定が可能です。\n" +
		INCLUDE.key + " 指定より優先されます。\n" +
		"静的コンテンツ扱いになるので " + OTHER.key + " の対象となります。",
		false, false  ),
	IGNORE_DIR( "-ignore_dir",
		"指定したディレクトリ下のファイルを全て無視します。\n" +
		"HTMLファイルであってもスケルトンソースを生成しませんし、\n" +
		"excludeディレクトリにも格納されず、完全に無視します。\n" +
		"*?を使ったワイルドカード指定が可能です。\n" +
		"ディレクトリ階層に関係なく、ディレクトリ名がマッチすると無視します。\n" +
		"指定例) .svn   Subversionの管理ファイルを無視\n" +
		"        _notes Dreamweaverの管理ファイルを無視",
		false, false );

	String	key, info;
	boolean req_f, one_f;
	ArrayList<String>	data = new ArrayList<String>();
	static GrantTagProvider[]	provider = null;
	static URIPreProcess	uri_pp = null;
	public static final String SITE = "http://paraselene.sourceforge.jp/";

	public String get() {
		return getData( 0 );
	}

	public int getDataCount() {
		return data.size();
	}
	public String getData( int no ) {
		if ( data.size() == 0 )	return null;
		return data.get( no );
	}
	public String getHelp() {
		return key + " " + (req_f? "(必須)": "") + "\n" + info;
	}
	public String getKey() {
		return key;
	}

	public void clear() {
		data.clear();
	}

	public void add( String str ) {
		data.add( str );
	}

	private Param( String k, String in, boolean r, boolean o, String ... d ) {
		key = k;
		info = in;
		req_f = r;
		one_f = o;
		if ( data != null ) {
			for ( int i = 0; i < d.length; i++ ) {
				data.add( d[i] );
			}
		}
	}

	private static Param getParam( String k ) {
		Param[]	param = values();
		for ( int i = 0; i < param.length; i++ ) {
			if ( param[i].key.equals( k ) )	return param[i];
		}
		return null;
	}

	private static int	param_cnt = 0;

	static void helpParam( String[] input ) {
		for ( int i = 0; i < input.length; i++ ) {
			if ( input[i].equals( "--help" ) ) {
				help();
			}
		}
	}

	static void setParam( String[] input ) throws Exception {
		Param	crt = null;
		for ( int i = 0; i < input.length; i++ ) {
			if ( input[i].charAt( 0 ) == '@' ) {
				Config.load( new File( input[i].substring( 1 ) ) );
				continue;
			}
			if ( input[i].charAt( 0 ) == '-' ) {
				crt = getParam( input[i] );
				if ( crt == null ) {
					System.err.println( "未知のパラメータです " + input[i] );
					System.exit( 1 );
				}
				continue;
			}
			if ( crt == null )	continue;
			if ( input[i].isEmpty() )	continue;
			if ( crt.one_f ) {
				crt.data = new ArrayList<String>();
			}
			crt.data.add( input[i] );
			param_cnt++;
		}
	}

	static void package_check( Param p ) {
		String[]	pkg = p.get().split( "\\." );
		for ( int i = 0; i < pkg.length; i++ ) {
			if ( pkg[i].isEmpty() || JavaDefine.isDefine( pkg[i], true ) ) {
				System.err.println( p.key + " に使用できない文字が含まれています" );
				System.exit( 1 );
			}
		}
	}

	static void package_check( Param p1, Param p2 ) {
		if ( p1.get().equals( p2.get() ) ) {
			System.err.println( p1.key + " と " + p2.key + " が同一です" );
			System.exit( 1 );
		}
	}

	static void loadParam() {
		if ( param_cnt < 1 )	help();

		Param[] param = values();
		boolean	exit_f = false;
		for ( int i = 0; i < param.length; i++ ) {
			if ( param[i].req_f ) {
				if ( param[i].data.size() == 0 ) {
					System.err.println( param[i].key + " を指定して下さい" );
					exit_f = true;
				}
			}
		}
		if ( exit_f ) {
			System.exit( 1 );
		}
		Make.SRC_PATH = OUT_SOURCE_PATH.get();
		package_check( PACKAGE );
		package_check( PACKAGE_BASE );
		package_check( PACKAGE_VIEW );
		package_check( PACKAGE_LOGIC );
		package_check( PACKAGE_BASE, PACKAGE_VIEW );
		package_check( PACKAGE_VIEW, PACKAGE_LOGIC );
		package_check( PACKAGE_LOGIC, PACKAGE_BASE );
		PrePage.setDir( PACKAGE_VIEW.get(), PACKAGE_LOGIC.get() );
		if ( !LineSeparator.setUse( SOURCE_LINE.get() ) ) {
			System.err.println( SOURCE_LINE.key + " の指定が不正です" );
			System.exit( 1 );
		}
		try {
			String	str = IN_ENCODE.get();
			if ( str != null ) {
				URLEncoder.encode( "XXX", str );
			}
		}
		catch(Exception e) {
			System.err.println( IN_ENCODE.key + " の指定が不正です" );
			System.exit( 1 );
		}
		try {
			URLEncoder.encode( "XXX", OUT_ENCODE.get() );
		}
		catch(Exception e) {
			System.err.println( OUT_ENCODE.key + " の指定が不正です" );
			System.exit( 1 );
		}
		try {
			Boolean.valueOf( CLEAR.get() );
		}
		catch(Exception e) {
			System.err.println( CLEAR.key + " の指定が不正です" );
			System.exit( 1 );
		}
		try {
			String	f = SOURCE_HEAD.get();
			if ( f != null ) {
				SourceHead.init( new File( f ) );
			}
		}
		catch(Exception e) {
			System.out.println( SOURCE_HEAD.key + " の読み込みに失敗しました。" );
			System.exit( 1 );
		}
		try {
			String	name = URI_PP.get();
			if ( name != null ) {
				Loader	loader = new Loader();
				uri_pp = (URIPreProcess)loader.loadClass( name ).getConstructor().newInstance();
				Linker.readme.echoln( "Loaded URIPreProcess " + name );
			}
		}
		catch( Exception e ) {
			System.err.println( e.toString() );
			System.err.println( URI_PP.key + " の初期化に失敗しました。" );
			System.exit( 1 );
		}
		try {
			int	cnt = TAG_PROVIDER.getDataCount();
			provider = new GrantTagProvider[cnt];
			Loader	loader = new Loader();
			for ( int i = 0; i < cnt; i++ ) {
				String	name = TAG_PROVIDER.getData( i );
				provider[i] = (GrantTagProvider)loader.loadClass( name ).getConstructor().newInstance();
				Linker.readme.echoln( "Loaded GrantTagProvider " + name );
			}
		}
		catch( Exception e ) {
			System.err.println( e.toString() );
			System.err.println( TAG_PROVIDER.key + " の初期化に失敗しました。" );
			System.exit( 1 );
		}
		for ( int i = 0; i < param.length; i++ ) {
			Linker.readme.echo( param[i].key );
			int	cnt = param[i].data.size();
			for ( int j = 0; j < cnt; j++ ) {
				Linker.readme.echo( j == 0?	": ":	", " );
				Linker.readme.echo( param[i].data.get( j ) );
			}
			Linker.readme.echo( "\n" );
		}
		Linker.readme.echo( "\n\n" );
	}

	private String getInfo() {
		String[]	str = info.split( "\n" );
		StringBuilder	buf = new StringBuilder();
		for ( int i = 0; i < str.length; i++ ) {
			buf = buf.append( "    " );
			buf = buf.append( str[i] );
			buf = buf.append( "\n" );
		}
		return buf.toString();
	}

	private static void help() {
		Param[]	param = values();
		Linker.readme.echoln(
			"オプションはテキストファイルに記載して渡す事も可能です。" +
			"@ファイル名 で読み込みます。\n" +
			"また、カレントディレクトリに " + Config.CONFIG + " があれば、" +
			"それを自動的に読み込みます。\n" +
			"コマンドに直接渡したものは、" + Config.CONFIG + " の内容を上書きします。\n" +
			"@ファイル名を指定した場合も、それ以降に直接パラメータを指定したものは上書きします。\n" +
			"テキストファイルにて、パラメータ値に空白を含むものは \" か ' で括って下さい。\n\n" +
			"--help を指定すると、このヘルプを表示します。\n\n" +
			"このコマンドには、以下のオプションがあります。\n" +
			"複数指定可なオプションは、指定したパラメータ全てが追加されていきます。\n" +
			"複数指定できないオプションは、最後に指定したもの１つが有効になります。\n"
		);
		for ( int i = 0; i < param.length; i++ ) {
			Linker.readme.echoln( param[i].key + "  " + (param[i].req_f?	"(必須)":	"") +
				(param[i].one_f?	"":	"(複数指定化)" ) );
			Linker.readme.echo( param[i].getInfo() );
			int	cnt = param[i].data.size();
			if ( cnt > 0 ) {
				Linker.readme.echo( "  (デフォルト設定値):" );
				for ( int o = 0; o < cnt; o++ ) {
					if ( o > 0 )	Linker.readme.echo( "," );
					Linker.readme.echo( "\"" + param[i].data.get( o ) + "\"" );
				}
				Linker.readme.echoln( "" );
			}
			Linker.readme.echoln( "" );
		}
		System.exit( 0 );
	}

	static boolean isHit( String filename, String syntax ) {
		char[]	syn = syntax.toCharArray();
		char[]	file = filename.toCharArray();
		int	syn_idx = 0, file_idx = 0;

		while( true ) {
			if ( syn_idx >= syn.length )	break;
			if ( file_idx >= file.length )	break;
			if ( syn[syn_idx] == '*' ) {
				for ( ; syn_idx < syn.length; syn_idx++ ) {
					if ( syn[syn_idx] != '*' && syn[syn_idx] != '?' )	break;
				}
				if ( syn_idx == syn.length ) {
					file_idx = file.length;
					break;
				}
				for ( ; file_idx < file.length; file_idx++ ) {
					if ( syn[syn_idx] == file[file_idx] )	break;
				}
				syn_idx++;
				file_idx++;
				continue;
			}
			if ( syn[syn_idx] == '?' );
			else	if ( syn[syn_idx] != file[file_idx] )	break;
			syn_idx++;
			file_idx++;
		}
		return syn_idx == syn.length && file_idx == file.length;
	}

	static boolean isHit( String filename ) {
		int	ex_cnt = EXCLUDE.data.size();
		for ( int i = 0; i < ex_cnt; i++ ) {
			if ( isHit( filename, EXCLUDE.data.get( i ) ) )	return false;
		}
		int	in_cnt = INCLUDE.data.size();
		for ( int i = 0; i < in_cnt; i++ ) {
			if ( isHit( filename, INCLUDE.data.get( i ) ) )	return true;
		}
		return false;
	}

	static boolean isIgnoreDir( String dir ) {
		int ex_cnt = IGNORE_DIR.data.size();
		for ( int i = 0; i < ex_cnt; i++ ) {
			if ( isHit( dir, IGNORE_DIR.data.get( i ) ) )	return true;
		}
		return false;
	}

	static boolean isIgnoreTagAccesser( Tag t ) {
		int	tag_cnt = IGNORE_TAG_ACCESSER.data.size();
		for ( int i = 0; i < tag_cnt; i++ ) {
			if ( IGNORE_TAG_ACCESSER.data.get( i ).equalsIgnoreCase( t.getName() ) )	return true;
		}
		return false;
	}

	static boolean isNoClear( String dir ) {
		int ex_cnt = NO_CLEAR_DIR.data.size();
		for ( int i = 0; i < ex_cnt; i++ ) {
			if ( isHit( dir, NO_CLEAR_DIR.data.get( i ) ) )	return true;
		}
		return false;
	}
}

