/*
 * 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 java.util.Locale;											// 6.4.0.2 (2015/12/11)

import org.opengion.fukurou.system.OgRuntimeException ;				// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.OgCharacterException ;			// 6.5.0.1 (2016/10/21)
import org.opengion.fukurou.system.Closer;							// 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system

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

/**
 * CommentLineParser.java は、ファイルを行単位に処理して、コメントを除去するクラスです。
 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
 *
 * ブロックコメントの状態や、コメント除外の状態を管理しています。
 * オブジェクト作成後、line( String ) メソッドに、ファイルから読み取った１行分の文字列を渡せば、
 * コメントが除外された形で返されます。
 * 
 * コメントが除去された行は、rTrim しますが、行の削除は行いません。
 * これは、Grep等で、文字列を発見した場合に、ファイルの行番号がずれるのを防ぐためです。
 * 逆に、Diff等で、複数のコメント行は、１行の空行にしたい場合や、空行自体をなくして
 * 比較したい場合は、戻ってきた行が、空行かどうかで判定して呼び出し元で処理してください。
 *
 * 引数の行文字列が、null の場合は、null を返します。(読み取り行がなくなった場合)
 *
 * 文字列くくり指定 は、例えば、ラインコメント(//) が、文字列指定("//") や、"http://xxxx" などの
 * プログラム本文で使用する場合のエスケープ処理になります。
 * つまり、文字列くくり指定についても、IN-OUT があり、その範囲内は、コメント判定外になります。
 *
 * ※ 6.3.1.1 (2015/07/10)
 *    コメントセットを、add で、追加していく機能を用意します。
 *    現状では、Java,ORACLE,HTML のコメントを意識せず処理したいので、すべてを
 *    処理することを前提に考えておきます。
 *
 * ※ 6.4.0.2 (2015/12/11)
 *    行コメントが先頭行のみだったのを修正します。
 *    og:comment タグを除外できるようにします。そのため、
 *    終了タグに、OR 条件を加味する必要があるため、CommentSet クラスを見直します。
 *    可変長配列を使うため、文字列くくり指定を前に持ってきます。
 *
 * @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 final List<CommentSet> cmntSetList ;

	/**
	 * 処理するコメントの種類を拡張子で指定するコンストラクターです。
	 * これは、ORACLE系のラインコメント(--)が、Java系の演算子(i--;など)と
	 * 判定されるため、ひとまとめに処理できません。
	 * ここで指定する拡張子に応じて、CommentSet を割り当てます。
	 *
	 * ・sql , tri , spc は、ORACLE系を使用。
	 * ・xml , htm , html , は、Java,C,JavaScript系 ＋ HTML,XML系を使用。
	 * ・jsp は、 Java,C,JavaScript系 ＋ HTML,XML系 ＋ ORACLE系 ＋ openGion JSP系 を使用。
	 * ・それ以外は、Java,C,JavaScript系を使用。
	 *     css は、それ以外になりますが、//（ラインコメント）はありませんが、コメントアウトされます。
	 *
	 * @og.rev 6.4.0.2 (2015/12/11) sufix によるコメント処理方法の変更。
	 * @og.rev 6.4.1.0 (2016/01/09) comment="***"のコメント処理方法の追加。
	 * @og.rev 6.4.1.1 (2016/01/16) sufixを小文字化。
	 * @og.rev 6.8.1.7 (2017/10/13) COMMENT ON で始まる行(大文字限定)は、コメントとして扱う
	 *
	 * @param	sufix 拡張子
	 */
	public CommentLineParser( final String sufix ) {
		final String type = sufix == null ? "null" : sufix.toLowerCase( Locale.JAPAN );

		if( "sql , tri , spc".contains( type ) ) {
			cmntSetList = Arrays.asList(
					new CommentSet( "--" , "/*"			 , "*/"  )						// ORACLE系
				,	new CommentSet( "COMMENT ON" , null	 , (String)null  )				// 大文字のみ除外する。
			);
		}
		else if( "xml , htm , html".contains( type ) ) {
			cmntSetList = Arrays.asList(
					new CommentSet( "//" , "/*"			 , "*/"  )						// Java,C,JavaScript系
				,	new CommentSet( null , "<!--"		 , "-->" )						// HTML,XML系
			);
		}
		else if( "jsp".contains( type ) ) {
			cmntSetList = Arrays.asList(
					new CommentSet( "//" , "/*"			 , "*/"  )						// Java,C,JavaScript系
				,	new CommentSet( null , "<!--"		 , "-->" )						// HTML,XML系
				,	new CommentSet( "--" , "/*"			 , "*/"  )						// ORACLE系
				,	new CommentSet( null , "<og:comment" , "/>" , "</og:comment>"  )	// openGion JSP系
				,	new CommentSet( null , "comment=\""  , "\""  )						// openGion comment="***"		6.4.1.0 (2016/01/09)
			);
		}
		else {
			cmntSetList = Arrays.asList(
					new CommentSet( "//" , "/*"			 , "*/"  )						// Java,C,JavaScript系
			);
		}
	}

	/**
	 * １行分の文字列を読み取って、コメント部分を削除した文字列を返します。
	 * 行として存在しない場合は、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	コメント削除後の１行の文字列
	 */
	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( !cmntSet.checkEsc( ch ) ) {							// エスケープ文字でないなら、判定処理を進める
				// ブロックコメント継続中か、先頭がブロックコメント
				if( cmntSet.isBlockIn( inLine,st ) ) {
					final int ed = cmntSet.blockOut( inLine,st ) ;	// 終了を見つける
					if( ed >= 0 ) {									// 終了があれば、そこまで進める。
						st = ed;
						continue;									// ブロックコメント脱出。再読み込み
					}
					break;											// ブロックコメント継続中。次の行へ
				}

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

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

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

		return buf.toString() ;
	}

	/**
	 * コメントセットを管理する内部クラスです。
	 *
	 * コメントの種類を指定します。
	 *
	 * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
	 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
	 * ORACLE系             -- , /&#042; , &#042;/
	 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
	 *
	 * @og.rev 6.3.1.1 (2015/07/10) CommentSet で管理します。
	 * @og.rev 6.4.0.2 (2015/12/11) CommentSet の見直し。
	 */
	private static final class CommentSet {
		private final String   LINE_CMNT	;			// ラインコメント
		private final String   BLOCK_CMNT1	;			// ブロックコメントの開始
		private final String[] BLOCK_CMNT2	;			// ブロックコメントの終了

		private static final char ESC_CHAR1 = '"'; ;	// コメント判定除外("")
		private static final char ESC_CHAR2 = '\'' ;	// コメント判定除外('')
		private static final char CHAR_ESC  = '\\' ;	// エスケープ文字('\\')

		private boolean escIn1   ;						// コメント判定除外中かどうか("")
		private boolean escIn2   ;						// コメント判定除外中かどうか('')
		private boolean chEsc    ;						// コメント除外のエスケープ処理中かどうか

		private boolean isBlkIn ;						// ブロックコメントが継続しているかどうか		6.4.1.1 (2016/01/16) refactoring isBlockIn → isBlkIn

		/**
		 * コメントの種類を指定するコンストラクタです。
		 *
		 * Java,C,JavaScript系、&#47;&#47; , /&#042; , &#042;/
		 * HTML,XML系、         &#47;&#47; , &lt;!-- , --&gt;
		 * ORACLE系             -- , /&#042; , &#042;/
		 * openGion JSP系       null , &lt;og:comment , /&gt; ,&lt;/og:comment&gt;
		 *
		 * @param	lineCmnt	ラインコメント
		 * @param	blockCmnt1	ブロックコメントの開始
		 * @param	blockCmnt2	ブロックコメントの終了(可変長配列)
		 */
		CommentSet( final String lineCmnt,final String blockCmnt1,final String... blockCmnt2 ) {
			LINE_CMNT   = lineCmnt ;		// ラインコメント
			BLOCK_CMNT1 = blockCmnt1 ;		// ブロックコメントの開始
			BLOCK_CMNT2 = blockCmnt2 ;		// ブロックコメントの終了
		}

		/**
		 * ブロック外で、エスケープ文字の場合は、内外反転します。
		 *
		 * @og.rev 6.4.1.1 (2016/01/16) Avoid if (x != y) ..; else ..; refactoring
		 *
		 * @param	ch	チェックするコメント除外char
		 * @return	エスケープ文字中の場合は、true
		 */
		/* default */ boolean checkEsc( final char ch ) {
			if( isBlkIn || chEsc ) {
				chEsc = false;
			}
			else {
				chEsc = CHAR_ESC == ch;
				if( !escIn2 && ESC_CHAR1 == ch ) { escIn1 = !escIn1 ; }		// escIn2 でない場合に、escIn1 の判定を行う。
				if( !escIn1 && ESC_CHAR2 == ch ) { escIn2 = !escIn2 ; }		// 同様にその逆
			}
			return escIn1 || escIn2;	// どちらかで、エスケープ中
		}

		/**
		 * ブロックコメント中かどうかの判定を行います。
		 *
		 * @og.rev 6.4.5.1 (2016/04/28) ブロックコメントを指定しないケースに対応。
		 *
		 * @param	line	チェックする行データ
		 * @param	st		チェック開始文字数
		 * @return	ブロックコメント中の場合は、true を返します。
		 */
		/* default */ boolean isBlockIn( final String line , final int st ) {
			if( !isBlkIn && BLOCK_CMNT1 != null ) { isBlkIn = line.startsWith( BLOCK_CMNT1,st ); }

			return isBlkIn ;
		}

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

		/**
		 * ブロックコメントの終了を見つけます。
		 * 終了は、複数指定でき、それらのもっとも最初に現れる方が有効です。
		 * 例：XMLタグで、BODYが、あるなしで、終了条件が異なるケースなど。
		 * この処理では、ブロックコメントが継続中かどうかの判定は行っていないため、
		 * 外部(呼び出し元)で、判定処理してください。
		 *
		 * ※ このメソッドは、ブロックコメント中にしか呼ばれないため、
		 *    ブロックコメントを指定しないケース(BLOCK_CMNT1==null)では呼ばれません。
		 *
		 * @param	line	チェックする行データ
		 * @param	st		チェック開始文字数
		 * @return	ブロックコメントの終了の位置。なけらば、-1
		 */
		/* default */ int blockOut( final String line , final int st ) {
			int ed = line.length();
			for( final String key : BLOCK_CMNT2 ) {
				final int tmp = line.indexOf( key,st + BLOCK_CMNT1.length() );			// 6.4.1.0 (2016/01/09) 開始位置の計算ミス
				if( tmp >= 0 && tmp < ed ) {	// 存在して、かつ小さい方を選ぶ。
					ed = tmp + key.length();	// アドレスは、終了コメント記号の後ろまで。
					isBlkIn = false;			// ブロックコメントから抜ける。
				}
			}

			return isBlkIn ? -1 : ed ;		// 見つからない場合は、-1 を返す。
		}
	}

	/**
	 * このクラスの動作確認用の、main メソッドです。
	 *
	 * Usage: java org.opengion.fukurou.util.CommentLineParser inFile outFile [encode] [-rowTrim]
	 *
	 * -rowTrim を指定すると、空行も削除します。これは、コメントの増減は、ソースレベルで比較する場合に
	 * 関係ないためです。デフォルトは、空行は削除しません。grep 等で検索した場合、オリジナルの
	 * ソースの行数と一致させるためです。
	 *
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
	 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
	 *
	 * @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] [-rowTrim]" );
		}

		final File inFile  = new File( args[0] );
		final File outFile = new File( args[1] );
		String  encode  = "UTF-8" ;
		boolean rowTrim = false;
		for( int i=2; i<args.length; i++ ) {
			if( "-rowTrim".equalsIgnoreCase( args[i] ) ) { rowTrim = true; }
			else { encode = args[i]; }
		}

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

		final CommentLineParser clp = new CommentLineParser( FileInfo.getSUFIX( inFile ) );

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