/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.File;
import java.io.IOException;
import java.util.List;												// 6.3.1.1 (2015/07/10)
import java.util.Arrays;											// 6.3.1.1 (2015/07/10)
import java.nio.charset.CharacterCodingException;					// 6.3.1.0 (2015/06/28)

import static org.opengion.fukurou.util.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring

/**
 * CommentLineParser.java は、ファイルを行単位に処理して、コメントを除去するクラスです。
 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
 *
 * ブロックコメントの状態や、コメント除外の状態を管理しています。
 * オブジェクト作成後、line( String ) メソッドに、ファイルから読み取った１行分の文字列を渡せば、
 * コメントが除外された形で返されます。
 * 行として存在しない場合は、null を返します。
 *
 * ※ 6.3.1.1 (2015/07/10)
 *    コメントセットを、add で、追加していく機能を用意します。
 *    現状では、Java,ORACLE,HTML のコメントを意識せず処理したいので、すべてを
 *    処理することを前提に考えておきます。
 *
 * @og.rev 5.7.4.0 (2014/03/07) 新規追加
 * @og.rev 6.3.1.1 (2015/07/10) 内部構造大幅変更
 * @og.group ユーティリティ
 *
 * @version  6.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK7.0,
 */
public class CommentLineParser {
//	private String LINE_CMNT   = "//" ;		// ラインコメント
//	private String BLOCK_CMNT1 = "/*" ;		// ブロックコメントの開始
//	private String BLOCK_CMNT2 = "*/" ;		// ブロックコメントの終了
//	private char   ESC_CHAR    = '"'  ;		// コメント除外(プログラム等で使用)

//	private boolean escIn   ;				// コメント除外中かどうか
//	private boolean blockIn ;				// ブロックコメントが継続しているかどうか
//	private boolean rtnOnly ;				// 前の行が改行のみだったかどうか。

	private final List<CommentSet> cmntSetList = Arrays.asList(
			new CommentSet( "//" , "/*"   , "*/"  , '"'  )		// Java,C,JavaScript系
		,	new CommentSet( null , "<!--" , "-->" , '\'' )		// HTML,XML系
		,	new CommentSet( "--" , "/*"   , "*/"  , '\'' )		// ORACLE系
	);

//	/**
//	 * コメントの種類を指定します。何も指定しない場合は、Javaコメントを初期値で設定します。
//	 *
//	 * Javaの場合は、順番に、&#47;&#47; , /&#042; , &#042;/ , &#034;(二重引用符) になります。
//	 * JavaScriptなら、&#47;&#47; , &lt;!-- , --&gt; , &#034;
//	 * ORACLEなら、-- , /&#042; , &#042;/ , &#034;(一重引用符) になります。
//	 *
//	 * ※ サブクラスで分けてもよかったのですが、とりあえず引数私にしました。
//	 *
//	 * @og.rev 5.7.4.0 (2014/03/07) 新規追加
//	 *
//	 * @param	lineCmnt	ラインコメント
//	 * @param	blockCmnt1	ブロックコメントの開始
//	 * @param	blockCmnt2	ブロックコメントの終了
//	 * @param	escChar		コメント除外(プログラム等で使用)
//	 */
//	public void init( final String lineCmnt,final String blockCmnt1,final String blockCmnt2,final char escChar ) {
//		LINE_CMNT   = lineCmnt ;		// ラインコメント
//		BLOCK_CMNT1 = blockCmnt1 ;		// ブロックコメントの開始
//		BLOCK_CMNT2 = blockCmnt2 ;		// ブロックコメントの終了
//		ESC_CHAR    = escChar  ;		// コメント除外(プログラム等で使用)
//	}

	/**
	 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
	 * 行として存在しない場合は、null を返します。
	 *
	 * @og.rev 5.7.4.0 (2014/03/07) 新規追加
	 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
	 *
	 * @param	inLine １行の文字列
	 * @return	コメント削除後の１行の文字列
	 */
	public String line( final String inLine ) {

		String outLine = inLine ;
		for( final CommentSet cmntSet : cmntSetList ) {
			outLine = line( outLine,cmntSet );
		}
		return outLine ;
	}

	/**
	 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
	 * 行として存在しない場合は、null を返します。
	 *
	 * @og.rev 5.7.4.0 (2014/03/07) 新規追加
	 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
	 *
	 * @param	inLine  １行の文字列
	 * @param	cmntSet コメントを管理するオブジェクト
	 * @return	コメント削除後の１行の文字列
	 */
//	public String line( final String inLine ) {
	private String line( final String inLine , final CommentSet cmntSet ) {
		if( inLine == null ) { return null; }

		final int size = inLine.length();

		final StringBuilder buf = new StringBuilder( size );

		for( int st=0; st<size; st++ ) {
			final char ch = inLine.charAt(st);

			// ブロック外で、エスケープ文字の場合は、内外反転
//			if( !blockIn && ESC_CHAR == ch ) { escIn = !escIn ; }
			cmntSet.checkEsc( ch );

//			if( !escIn ) {										// エスケープ外ら、処理を進める
			if( !cmntSet.escIn ) {								// エスケープ外ら、処理を進める
				// ブロックコメント継続中
//				if( blockIn ) {
				if( cmntSet.blockIn ) {
//					final int ed = inLine.indexOf( BLOCK_CMNT2,st ) ;				// 終了を見つける
					final int ed = inLine.indexOf( cmntSet.BLOCK_CMNT2,st ) ;		// 終了を見つける
					if( ed >= 0 ) {									// 終了があれば、そこまで進める。
//						blockIn = false;
						cmntSet.blockIn = false;
//						st = ed+BLOCK_CMNT2.length();
						st = ed+cmntSet.BLOCK_CMNT2.length();
						continue;									// ブロックコメント脱出。再読み込み
					}
					break;											// ブロックコメント未発見。次の行へ
				}

				// ラインコメント発見。次の行へ
//				if( inLine.startsWith( LINE_CMNT,st ) ) { break; }	
				if( cmntSet.isLineCmnt( inLine,st ) ) { break; }

				// ブロックコメントを見つける
//				if( inLine.startsWith( BLOCK_CMNT1,st ) ) {
				if( inLine.startsWith( cmntSet.BLOCK_CMNT1,st ) ) {
//					final int ed = inLine.indexOf( BLOCK_CMNT2,st ) ;			// 終了を見つける
					final int ed = inLine.indexOf( cmntSet.BLOCK_CMNT2,st ) ;	// 終了を見つける
					if( ed >= 0 ) {
//						st = ed+BLOCK_CMNT2.length();
						st = ed+cmntSet.BLOCK_CMNT2.length();
						continue;									// ブロックコメント脱出。再読み込み
					}
					else {
//						blockIn = true;
						cmntSet.blockIn = true;
					}
					break;											// ブロックコメント未発見。次の行へ
				}
			}

			// 通常の文字なので、追加する。
			buf.append( ch );
		}

		// rTrim() と同等の処理
		int len = buf.length();
		while( 0 < len && buf.charAt(len-1) <= ' ' ) {
			len--;
		}
		buf.setLength( len );

		String rtn = null;
		// 長さが 0 の行は、連続で現れないようにします。
		if( len == 0 ) {
//			if( !rtnOnly ) {
			if( !cmntSet.rtnOnly ) {
//				rtnOnly = true;
				cmntSet.rtnOnly = true;
				rtn = "";
			}
		}
		else {
//			rtnOnly = false;
			cmntSet.rtnOnly = false;
			rtn = buf.toString();
		}

		return rtn ;
	}

	/**
	 * コメントセットを管理する内部クラスです。
	 *
	 * コメントの種類を指定します。何も指定しない場合は、Javaコメントを初期値で設定します。
	 *
	 * Javaの場合は、順番に、&#47;&#47; , /&#042; , &#042;/ , &#034;(二重引用符) になります。
	 * JavaScriptなら、&#47;&#47; , &lt;!-- , --&gt; , &#034;
	 * ORACLEなら、-- , /&#042; , &#042;/ , &#034;(一重引用符) になります。
	 *
	 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
	 */
	private static final class CommentSet {
		private final String LINE_CMNT		;	// ラインコメント
		private final String BLOCK_CMNT1	;	// ブロックコメントの開始
		private final String BLOCK_CMNT2	;	// ブロックコメントの終了
		private final char   ESC_CHAR		;	// コメント除外(プログラム等で使用)

		private boolean escIn   ;				// コメント除外中かどうか
		private boolean blockIn ;				// ブロックコメントが継続しているかどうか
		private boolean rtnOnly ;				// 前の行が改行のみだったかどうか。

		/**
		 * コメントの種類を指定するコンストラクタです。
		 *
		 * Javaの場合は、順番に、&#47;&#47; , /&#042; , &#042;/ , &#034;(二重引用符) になります。
		 * JavaScriptなら、&#47;&#47; , &lt;!-- , --&gt; , &#034;
		 * ORACLEなら、-- , /&#042; , &#042;/ , &#034;(一重引用符) になります。
		 *
		 * @param	lineCmnt	ラインコメント
		 * @param	blockCmnt1	ブロックコメントの開始
		 * @param	blockCmnt2	ブロックコメントの終了
		 * @param	escChar		コメント除外(プログラム等で使用)
		 */
		CommentSet( final String lineCmnt,final String blockCmnt1,final String blockCmnt2,final char escChar ) {
			LINE_CMNT   = lineCmnt ;		// ラインコメント
			BLOCK_CMNT1 = blockCmnt1 ;		// ブロックコメントの開始
			BLOCK_CMNT2 = blockCmnt2 ;		// ブロックコメントの終了
			ESC_CHAR    = escChar  ;		// コメント除外(プログラム等で使用)
		}

		/**
		 * ブロック外で、エスケープ文字の場合は、内外反転します。
		 *
		 * @param	ch	チェックするコメント除外char
		 */
		void checkEsc( final char ch ) {
			if( !blockIn && ESC_CHAR == ch ) { escIn = !escIn ; }
		}

		/**
		 * ラインコメントかどうかの判定を行います。
		 *
		 * @param	line	チェックする行データ
		 * @param	st		チェック開始文字数
		 * @return	ラインコメントの場合は、true を返します。
		 */
		boolean isLineCmnt( final String line , final int st ) {
			return LINE_CMNT != null && line.startsWith( LINE_CMNT,st ) ;
		}
	}

	/**
	 * このクラスの動作確認用の、main メソッドです。
	 *
	 * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode]
	 *
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		if( args.length < 2 ) {
			System.out.println( "Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode]" );
		}

		final File inFile  = new File( args[0] );
		final File outFile = new File( args[1] );
		final String encode  = (args.length >= 3 ) ? args[3] : "UTF-8" ;

		final BufferedReader reader = FileUtil.getBufferedReader( inFile ,encode );
		final PrintWriter    writer = FileUtil.getPrintWriter(   outFile ,encode );

		final CommentLineParser clp = new CommentLineParser();

		try {
			String line1;
			while((line1 = reader.readLine()) != null) {
				line1 = clp.line( line1 );
				if( line1 != null ) {
					writer.println( line1 );
				}
			}
		}
		// 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
		catch ( CharacterCodingException ex ) {
			final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
								+	"  ファイルのエンコードが指定のエンコードと異なります。" + CR
								+	" [" + inFile.getPath() + "] , Encode=[" + encode + "]" ;
			throw new RuntimeException( errMsg,ex );
		}
		catch( IOException ex ) {
			final String errMsg = "ファイルコピー中に例外が発生しました。\n"
						+ " inFile=[" + inFile + "] , outFile=[" + outFile + "]\n" ;
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( reader ) ;
			Closer.ioClose( writer ) ;
		}
	}
}
