package ash.reverse.parser;
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;

/**
 * Javaソースファイルの語彙解析を行う。
 * StreamTokenizerと同様に、拡張して使用する。
 * <p>
 * 拡張機能：
 * <ul>
 * <li>Javaソースコード用の設定をする。
 * <li>空白行と有効行のカウントを行う。
 * <li>ブロックとパーレンのレベルを管理する。
 * <li>不正なEOFを読みとったら例外を発生させ、無限ループの発生を防ぐ。
 * <li>シグニチャ情報を管理する。<br>
 * 		make/start/append/pruneSignature() を使用する。
 * <li>トークンの取得は getToken()メソッドで行う。
 * <ul>
 */
public class JavaTokenizer extends StreamTokenizer {
	/**
	 * コンストラクタの定義：
	 * StreamTokenizer を拡張し、Javaソースファイルの語彙解析のための
	 * 初期設定を行う。
	 * @param reader ソースファイルを読み取るためのリーダー
	 * @param fname ソースファイル名
	 */
	public JavaTokenizer(Reader reader, String fname) {
		super(reader);
		this.fname = fname;
		this.eof = false;
		// Java用の設定
		//this.resetSyntax();
		this.eolIsSignificant(true);	// 改行コードを検出する
 		this.slashStarComments(true);	// /* */ 型のコメント処理を有効にする
 		this.slashSlashComments(true);	// //型コメントの処理を有効にする
		this.ordinaryChar('/');			// ★`/' のコメント文字設定を無効にする
		this.wordChars('_', '_');		// 英数字の他に識別子に使える文字の定義
		this.wordChars('$', '$');		// 英数字の他に識別子に使える文字の定義
		this.parseNumbers();			// 数値リテラルを解析する
	}
	/**
	 * デバッグ用トレースルーチン
	 */
	protected void trace() { System.err.println(tokenString()); }
	protected void trace(Object msg) { System.err.println(msg); }

	/**
	 * 拡張クラスから参照される情報
	 */
	protected String fname;				// ソースファイル名
	protected int nSpaceLine = 0;		// 空白行の行数カウント
	protected int nLine = 0;			// 有効行の行数カウント
	protected int blockLevel = 0;		// ブロック{}のネスト
	protected int parenLevel = 0;		// パーレン()のネスト
	/**
	 * パーズ途中の状態を保持するための内部情報
	 */
	private boolean makeSignature = false;
	private int lastLineNum = 1;		// 直前の有効行の行番号(先頭行は1)
	private boolean tokenEaten = false;	// 行頭のtokenが処理されたらtrue
	private boolean eof = false;		// ソースファイルの終了
	private boolean pushback = false;	// pushBack() 処理のフラグ
	private boolean atmark = false;		// アノテーション定義／参照
	private StringBuilder signatureBuffer = new StringBuilder();
	private boolean signatureSwitch = false; // シグニチャバッファへの書出し制御
	private int c1;						// シグニチャバッファの最後の文字種別
	private boolean parsingAnnotation;	// アノテーションのパーズ中を判定

	protected void appendCloser(String closer) {
		signatureBuffer.append(closer);
	}

	/**
	 * アノテーションの解析の開始時と終了時に呼び出す必要がある。
	 * この情報はシグニチャ情報の書き出し制御に必要。
	 */
	protected void startAnnotation() { parsingAnnotation = true; }
	protected void endAnnotation() { parsingAnnotation = false; }

	/**
	 * 構文エラーによるEOF到達時のエラー情報を組み立てる。
	 * 拡張クラスで付加情報がある場合はこのメソッドをオーバライドすれば良い。
	 */
	protected String getErrorInfo() {
		String msg = "*** unexpected EOF:";
		if(fname != null) msg += "\n" + fname;
		return msg;
	}

	/**
	 * 次のtokenを読み取り、空白行・有効行のカウントとファイルの終端処理を行う。
	 */
	@Override public int nextToken() throws IOException {
		switch(super.nextToken()) {
		case TT_EOF:
			if(eof)	throw new IOException(getErrorInfo());
			eof = true;
			return this.ttype;
		case TT_EOL:
			// 空白行か否かを判定し、空白行ならばカウントする。
			// 前の改行後に行頭のtokenが処理されていなければ、空白行と判定する。
			if(!tokenEaten)
				nSpaceLine += this.lineno() - lastLineNum;
			tokenEaten = false;
			lastLineNum = this.lineno();
			return this.ttype;
		default:
			// 行頭のtokenを読み取ったら有効行としてカウントする
			if(!tokenEaten) nLine++;
			tokenEaten = true;
			return this.ttype;
		}
	}
	@Override public void pushBack() {
		if(this.ttype == '{') blockLevel--;
		if(this.ttype == '(') parenLevel--;
		pushback = true;
	}
	/**
	 * シグニチャを生成する場合に呼ぶ。
	 */
	protected void makeSignature() { makeSignature = true; }
	/**
	 * シグニチャ登録を開始する。
	 */
	protected void startSignature() {
		signatureBuffer.delete(0, Integer.MAX_VALUE);
		signatureSwitch = true;
		c1 = 0;
	}
	/**
	 * シグニチャ登録を終了し、シグニチャ情報を返す。
	 * @return シグニチャの文字列
	 */
	protected String pruneSignature() {
		signatureSwitch = false;
		return signatureBuffer.toString();
	}
	/**
	 * トークンをシグニチャバッファに追加する。
	 * @param token 
	 */
	protected void appendSignature(String token) {
		if(!makeSignature || !signatureSwitch) return;
		if((token.equals(";") || token.equals("{")) && !parsingAnnotation) {
			signatureSwitch = false;
			return;
		}
		// c1: シグニチャバッファの最後の文字種別
		// c2: 追加するトークンの先頭文字種別
		int c2 = token.charAt(0);
		if(c1 != 0 && c2 != '\t') {
			if(Character.isJavaIdentifierPart(c2)) c2 = 'A';
			if((c1 == 'A' || c1 == ',' || c1 == '?') && c2 == 'A'
			   || c1 == ']' && c2 != '[' && c2 != '>'
			   || c1 == '>' && c2 != '>'
			   || c1 == ')'
			   || c1 != '\t' && c2 == '@'
			   || signatureBuffer.toString().endsWith("..."))
				signatureBuffer.append(' ');
		}
		signatureBuffer.append(token);
		c1 = token.charAt(token.length()-1);
		if(Character.isJavaIdentifierPart(c1)) c1 = 'A';
	}

	/**
	 * 次のtokenを読み取り、アノテーション処理(@記法処理)、プッシュバック処理、
	 * ブロックとパーレンのレベル管理を行う。
	 * @return トークン種別（this.ttypeでも参照できる）
	 */
	protected int getToken() throws IOException {
		while(true) {
			if(!pushback) nextToken();
			if(this.ttype == TT_EOF) return TT_EOF;	// ファイル終端では何もしない
			if(this.ttype == TT_EOL) continue;	// 改行コードは読み飛ばす
			if(pushback) {
				pushback = false;
			} else {
				appendSignature(tokenString());
			}
			
			if(atmark && this.ttype == TT_WORD) { // アノテーション処理
				this.sval = "@" + this.sval;
				atmark = false;
				return TT_WORD;
			}
			atmark = (this.ttype == '@');		// アノテーションの前処理
			if(atmark) continue;

			// ブロックとパーレンのレベル管理
			if(this.ttype == '{') blockLevel++;
			else if(this.ttype == '}') blockLevel--;
			else if(this.ttype == '(') parenLevel++;
			else if(this.ttype == ')') parenLevel--;
			return this.ttype;
		}
	}
	private String tokenString() {
		switch(this.ttype) {
		case TT_WORD:   return this.sval;
		case TT_NUMBER: return "" + this.nval;
		case '\'':      return "'" + antiChar(this.sval) + "'";
		case '"':       return "\"" + antiString(this.sval) + "\"";
		default:        return "" + (char)this.ttype;
		}
	}
	private String antiString(String s) {
		StringBuilder sb = new StringBuilder();
		for(int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			switch(c) {
			case '\\': sb.append("\\\\"); break;
			case '\n': sb.append("\\n"); break;
			case '\t': sb.append("\\t"); break;
			case '\f': sb.append("\\f"); break;
			case '\r': sb.append("\\r"); break;
			case '"':  sb.append("\\\""); break;
				//case 'u': sb.append("\\"+c); break;
			default: sb.append(c); break;
			}
		}
		return sb.toString();
	}
	private String antiChar(String s) {
		if(s.length() == 1) {
			switch(s.charAt(0)) {
			case '\\': return "\\\\";
			case '\n': return "\\n";
			case '\t': return "\\t";
			case '\f': return "\\f";
			case '\r': return "\\r";
			case '\'': return "\\'";
			default: return s;
			}
		}
		if(s.length() == 5 && s.charAt(0) == 'u') return "\\" + s;
		return s;
	}
}
