package ash.reverse.parser;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import ash.reverse.struct.ObjectRecord;
import ash.reverse.struct.GroupRecord;
import ash.reverse.struct.FileRecord;
import ash.reverse.struct.ClassRecord;
import ash.reverse.struct.MethodRecord;
import ash.reverse.struct.FieldRecord;

/**
 * Javaのソースコードをパーズ（解析）し、LOC情報とクラス・メソッド・フィールドの
 * 特性情報を抽出する。
 * <p>
 * コンストラクタでファイルを指定し、parse()メソッドでパーズを開始する。
 * パーズの結果は戻り値のFileRecordの中に記録される。
 * <p>
 * 注意制限事項（バグ）：
 * <ul>
 * <li> 入力するファイルは、Java言語の構文的に正しいソースコードで
 * なければならない。
 * <li> 1つのフィールド定義文で複数のフィールド名を定義した場合は、
 * 適切な解析が行われない。
 * <li> 整数値リテラルが浮動小数点リテラルとなる。
 * 列挙型の値は型をENUM_TYPEとするフィールドとして記録される。
 * </ul>
 */
public class JavaParser extends JavaTokenizer {
	/**
	 * コンストラクタでは指定のリーダからJavaのソースコードを読み取り解析を
	 * 開始する準備を行う。ファイル名はエラーメッセージの中で参照する。
	 * @param reader ソースファイルを読み取るためのリーダー
	 * @param fname ソースファイル名
	 * @param signature シグニチャの抽出が必要な場合はtrue
	 */
	public JavaParser(Reader reader, String fname, boolean signature) {
		super(reader, fname);
		this.signature = signature;
		if(signature) makeSignature();
	}
	/**
	 * レポート生成に必要な情報
	 */
	private boolean signature;			// シグニチャを抽出する場合はtrue
	private GroupRecord currentClass;	// 現在解析中のクラス

	/**
	 * 構文エラー発見時にエラー情報を組み立てる。
	 * 親クラスの情報に現在処理中のクラスの情報を追加する。
	 */
	@Override protected String getErrorInfo() {
		String msg = super.getErrorInfo();
		if(currentClass != null)
			msg += "\n" + currentClass.errMessage();
		return msg;
	}

	/** 現在解析中の対象の可視性 */
	private Visibility visibility;

	/** 現在解析中の対象の修飾情報 */
	private int modifiers;

	/**
	 * シグニチャ登録を開始する。
	 */
	@Override protected void startSignature() {
		super.startSignature();
		visibility = Visibility.PACKAGE;
		modifiers = 0;
	}

	/**
	 * トークンが修飾子情報ならばそれを記録する。
	 * @param token トークン
	 * @return トークンが修飾子情報ならばtrue
	 */
	private boolean gleanProp(String token) {
		Visibility vis = Visibility.value(token);
		if(vis != Visibility.UNDEF) {
			visibility = vis;
			return true;
		}
		int mask = Mod.mask(token);
		modifiers |= mask;
		return mask != 0;
	}

	/**
	 * 修飾子情報を設定する。
	 */
	private void setPropTo(ObjectRecord rec) {
		rec.visibility(visibility);
		rec.modifiers(modifiers);
	}

	/**
	 * Javaソースファイルのトップレベルを解析する。
	 * クラス、インタフェース、列挙型のキーワードを読み取ったら、
	 * クラスパーズの状態に遷移する。
	 * @return パーズの結果を記録したファイル記録
	 */
	public FileRecord parse() throws IOException {
		FileRecord fileRec = new FileRecord(fname);
		// trace("@@@ " + fname);
		startSignature();
		while(true) {
			switch(getToken()) {
			case TT_EOF:
				fileRec.addLoc(nLine);
				fileRec.setLineEnd(nLine + nSpaceLine);
				return fileRec;
			case TT_WORD:
				String token = this.sval;
				if(token.equals("package")) {
					startSignature();
					while(getToken() != ';') {}
					fileRec.pkgname(pruneSignature());
					startSignature();
				} else if(token.equals("import") && signature) {
					startSignature();
					while(getToken() != ';') {}
					fileRec.addImportDecl(pruneSignature());
					startSignature();
				} else if(JavaType.valid(token)) {
					parseClass(fileRec);
					startSignature();
				} else if(token.startsWith("@")) {
					parseAnnotation();	// アノテーション参照(ex. @Target)
				} else {
					gleanProp(token);
				}
				break;
			case ';':					// package/import文の終了
				startSignature();
				break;
			}
		}
	}

	/**
	 * クラスパーズ状態：
	 * クラス定義をパーズし、以下の処理を行う。
	 * <ul>
	 * <li>クラスパーズ状態に入る際に、クラス情報の記録などの初期処理を行う。
	 * <li>インナークラスの定義の開始を判定し、新しいクラスパーズの状態に
	 * 遷移する。
	 * <li>メソッド（コンストラクタ）定義の開始を判定し、メソッドパーズの状態に
	 * 遷移する。
	 * <li>クラス定義の終了を判定し、クラスパーズの状態を脱出する。
	 * </ul>
	 * @param owner 解析中のクラスを所有しているグループ
	 */
	private void parseClass(GroupRecord owner) throws IOException {
		String type = this.sval;
		getToken();
		ClassRecord theClass =
			new ClassRecord(type, this.sval, this.lineno(), owner);
		setPropTo(theClass);
		currentClass = theClass;
		owner.addMember(theClass);
		while(getToken() != '{') {} 
		theClass.setSignature(pruneSignature());
		parseClassBody(theClass);
		currentClass = owner;
	}
	/**
	 * クラス本体をパーズする。
	 * @param theClass 現在パーズ中のクラスの情報
	 */
	private void parseClassBody(ClassRecord theClass) throws IOException {
		int level = blockLevel;
		int startLine = nLine;
		String typeCnadidate = null;
		String memberCandidate = null;
		startSignature();

		if(theClass.isEnum()) {
			parseEnumValue(theClass);
			if(this.ttype == '}') pushBack();
		}

		while(true) {
			switch(getToken()) {
			case TT_WORD:
				if(JavaType.valid(this.sval)) {
					parseClass(theClass);			// インナークラス
					break;
				}
				if(this.sval.startsWith("@")) {		// アノテーション 参照
					parseAnnotation();
				} else if(!gleanProp(this.sval)) {
					typeCnadidate = memberCandidate;
					memberCandidate = this.sval;	// メソッド名の候補
				}
				continue;
			case ';':
				if(memberCandidate != null)
					addFieldDecl(theClass, memberCandidate);
				break;
			case '=':								// フィールド変数の初期値設定
				parseFieldExpr(theClass, memberCandidate);
				break;
			case '(':								// メソッド定義
				parseMethod(theClass, memberCandidate);
				break;
			case '{':								// 初期化子
				if(memberCandidate == null) {
					this.pushBack();
					parseMethod(theClass, null);
				}
				break;
			case '}':
				if(blockLevel < level) {			// クラス定義の終了
					theClass.setLineEnd(this.lineno());
					theClass.addLoc(nLine - startLine + 1);
					return;
				}
				// FALLTHROUGH
			default:
				continue;
			}
			typeCnadidate = memberCandidate = null;
			startSignature();
		}
	}

	/**
	 * 列挙型の値をパーズしClassRecord#enumListに登録する。
	 * @param theClass 現在パーズ中のクラスの情報
	 */
	private void parseEnumValue(ClassRecord theClass) throws IOException {
		// enum E { V1(...), V2(...), ...; ...}
		String enumValue = null;
		int startLevel = parenLevel;
		while(true) {
			switch(getToken()) {
			case ',':
				if(parenLevel > startLevel) break;
			case ';':
			case '}':
				if(enumValue != null) theClass.addEnum(enumValue);
				if(this.ttype == ';' || this.ttype == '}') return;
				enumValue = null;
				break;
			case TT_WORD:
				if(parenLevel == startLevel) enumValue = this.sval;
				break;
			}
		}
	}

	/**
	 * フィールド変数定義を登録する。
	 * @param theClass 現在パーズ中のクラスの情報
	 * @param name フィールド名
	 */
	private void addFieldDecl(ClassRecord theClass, String name) {
		//trace("field : " + name);
		int lineno = this.lineno();
		FieldRecord theField = new FieldRecord(name, lineno, theClass);
		setPropTo(theField);
		theClass.addMember(theField);
		theField.addLoc(1);
		theField.setLineEnd(lineno);
		theField.setFieldDecl(pruneSignature());
	}
	/**
	 * 初期化式のあるフィールド変数定義をパーズし登録する。
	 * @param theClass 現在パーズ中のクラスの情報
	 * @param name フィールド名
	 */
	private void parseFieldExpr(ClassRecord theClass, String name)
		throws IOException {
		//trace("field = " + name);
		FieldRecord theField = new FieldRecord(name, this.lineno(), theClass);
		setPropTo(theField);
		theClass.addMember(theField);
		String fieldDecl = pruneSignature();
		if(fieldDecl.endsWith("=")) 
			fieldDecl = fieldDecl.substring(0, fieldDecl.length()-1);
		theField.setFieldDecl(fieldDecl);

		int startLine = nLine;
		int level = blockLevel;
		boolean blockInitialize = false;
		startSignature();
		while(true) {
			getToken();
			if(level == blockLevel) {
				if(this.ttype == ';') break;
				if(this.ttype == TT_WORD) {
					theField.addToken(this.sval);
					if(this.sval.equals("new")) {	// new T ?
						getToken();					
						theField.addToken(this.sval);	// 型名Tを登録
						parseNewClass(theClass);
					}
				}
			} else {
				blockInitialize = true;
			}
		}
		theField.addLoc(nLine - startLine + 1);
		theField.setLineEnd(this.lineno());

		String initExpr = pruneSignature();
		if(blockInitialize) initExpr = "{...}";
		if(!initExpr.equals("}")) theField.value(initExpr);
	}
	/**
	 * "new ClassX" 以降のトークンをパーズする。
	 * new ClassX(...) {...} の形式では、匿名クラスのメソッドをパーズする。
	 * @param theClass 現在パーズ中のクラスの情報
	 */
	private void parseNewClass(ClassRecord theClass) throws IOException {
		while(true) {
			switch(getToken()) {
			case '(':					// new ClassX(
				parseNewParen();		// new ClassX(...)
				if(getToken() == '{') { // new ClassX(...) {	// 匿名クラス
					parseClassBody(theClass);
					// new ClassX(...) {...}
				} else {				// new ClassX(...);
					this.pushBack();
				}
				return;
			case '{':					// new ClassX[] {
				int level = blockLevel;
				do { getToken(); } while(blockLevel >= level);
				// new ClassX[] {...}
				return;
			case ';':					// new ClassX[];
				this.pushBack();
				return;
			}
		}
	}

	/**
	 * コンストラクタ呼び出しにおける引数をパーズする。
	 */
	private void parseNewParen() throws IOException {
		int startLevel = parenLevel;
		boolean blockEaten = false;	// CA,20111123
		do {
			int ttype = getToken();
			if(ttype == '{') blockEaten = true;
		} while(startLevel <= parenLevel);
		if(blockEaten) appendCloser("{})");
	}

	/**
	 * メソッドパーズ状態：
	 * メソッド定義をパーズし、以下の処理を行う。
	 * <ul>
	 * <li>メソッドパーズ状態に入る際に、メソッド情報の記録などの初期処理を行う。
	 * <li>抽象メソッドを判定し、メソッドパーズの状態を脱出する。
	 * <li>ブロックレベルが増加したらメソッドブロック本体の開始と判定する。
	 * <li>ブロックレベルが開始時に戻ったらメソッド定義の終了と判定する。
	 * </ul>
	 * @param theClass 現在パーズ中のクラスの情報
	 * @param name メソッド名
	 */
	private void parseMethod(ClassRecord theClass, String name)
		throws IOException {
		if(name == null) {						// 初期化子
			name = Mod.STATIC.in(modifiers) ? "static{}" : "instance{}";
		}
		MethodRecord method = theClass.addMethod(name, this.lineno());
		setPropTo(method);
		int startLine = nLine;
		int level = blockLevel;
		boolean inBody = false;
		if(!name.endsWith("{}")) parseParen();
		while(true) {
			getToken();
			if(!inBody) {
				if(this.ttype == ';') break;	// 抽象メソッドの終了
				if(blockLevel > level)			// メソッド本体の開始
					inBody = true;
			} else {
				if(blockLevel == level) break;	// メソッド定義の終了
				if(signature && this.ttype == TT_WORD) {
					//trace("@@@ " + this.sval);
					theClass.addMethodToken(this.sval);
				}
			}
		}		
		theClass.setMethodLoc(nLine - startLine + 1, this.lineno());

		String sig = pruneSignature();
		if(sig.isEmpty()) sig = "instance()" ;
		if(sig.equals("static")) sig = "static()";
		theClass.setMethodSignature(sig);
	}

	/**
	 * アノテーション参照をパーズする。
	 * ex. @Documented @Target({TYPE, FIELD, METHOD})
	 *                 ^      ^
	 */
	private void parseAnnotation() throws IOException {
		appendSignature("\t");
		if(getToken() == '(') {
			startAnnotation();
			parseParen();
			endAnnotation();
			appendSignature("\t");
		} else {
			this.pushBack();
		}
	}
	/**
	 * 右丸括弧までをパーズし定義情報バッファに追加する。
	 */
	private void parseParen() throws IOException {
		int startLevel = parenLevel;
		do { getToken(); } while(startLevel <= parenLevel);
	}
}
