/*
 * 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.process;

import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.util.Argument;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.util.StringUtil ;
import org.opengion.fukurou.util.FileUtil;					// 6.4.5.2 (2016/05/06)

import org.opengion.fukurou.model.ExcelModel;				// 6.0.2.0 (2014/09/19)

import java.util.Map ;
import java.util.LinkedHashMap ;
import java.util.List ;
import java.util.ArrayList ;

import java.io.File;

/**
 * Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を
 * 置換する、ChainProcess インターフェースの実装クラスです。
 *
 * Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、
 * ネイティブEXCELファイルなのかの違いです。
 *
 * keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、
 * 対象とする語句をセル単位に置換します。
 * keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、
 * その行を読み飛ばします。また、区切りタブは何個存在しても構いません。
 * ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース
 * が前後に存在している場合は、ご注意ください。
 * 置換文字(値)は、\t と \n の特殊文字が使用できます。
 * この GrepChangeExcel では、語句に、正規表現は使用できません。正規表現のキーワード
 * や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用してください。
 * このプログラムでは、上流から受け取った FileLineModel のファイルに対して、
 * 置き換えた結果も、同じファイルにセーブします。
 * 元のファイルを保存したい場合は、予めバックアップを取得しておいてください。
 * -inEncode は、keywordFileのエンコード指定になります。
 * 初期値は、互換性を持つため、System.getProperty("file.encoding") ですが、
 * 明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。
 *
 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
 * できれば、使用可能です。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 *  Process_GrepChangeExcel -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
 *
 *    -keywordFile=キーワード    ：置換する語句を含むキーと値のペアー(タブ区切り)
 *   [-ignoreCase=大文字小文字 ] ：検索時に大文字小文字を区別しない(true)かどうか(初期値:false[区別する])
 *   [-isChange=置換可否       ] ：置換処理を実施する(true)かどうか(初期値:true[置換する])
 *   [-inEncode=入力エンコード ] ：keywordFileのエンコード
 *   [-display=[false/true]    ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [-debug=[false/true]      ] ：デバッグ用に実行内容を表示するかどうかを指定(初期値:false[表示しない])
 *
 * @og.rev 5.5.1.7 (2012/04/16) 新規追加
 * @og.rev 6.0.2.0 (2014/09/19) fukurou.model.ExcelModel を使用するように変更
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_GrepChangeExcel extends AbstractProcess implements ChainProcess {
	private String[]	keyword	;
	private String[]	change	;
	private boolean		ignoreCase	;
	private boolean		isChange	= true;		// 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする
	private boolean		display		;			// false:表示しない
	private boolean		debug		;			// false:表示しない

	private int		inCount		;
	private int		findCount	;
	private int		cngCount	;

	/** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> MUST_PROPARTY   ;		// ［プロパティ］必須チェック用 Map
	/** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> USABLE_PROPARTY ;		// ［プロパティ］整合性チェック Map

	static {
		MUST_PROPARTY = new LinkedHashMap<>();
		MUST_PROPARTY.put( "keywordFile",	"置換する語句を含むキーと値のペアー(タブ区切り)(必須)" );

		USABLE_PROPARTY = new LinkedHashMap<>();
		USABLE_PROPARTY.put( "ignoreCase",	"検索時に大文字小文字を区別しない(true)かどうか。" +
										CR + "(初期値:区別する[false])" );
		USABLE_PROPARTY.put( "isChange",		"置換処理を実施する(true)かどうか" +
										CR + "(初期値:置換する[true])" );
		USABLE_PROPARTY.put( "inEncode",		"keywordFileのエンコード" );
		USABLE_PROPARTY.put( "display",		"結果を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );
		USABLE_PROPARTY.put( "debug",		"デバッグ用に実行内容を表示するかどうかを指定" +
										CR + "(初期値:false:表示しない)" );
	}

	/**
	 * デフォルトコンストラクター。
	 * このクラスは、動的作成されます。デフォルトコンストラクターで、
	 * super クラスに対して、必要な初期化を行っておきます。
	 *
	 */
	public Process_GrepChangeExcel() {
		super( "org.opengion.fukurou.process.Process_GrepChangeExcel",MUST_PROPARTY,USABLE_PROPARTY );
	}

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
	 * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		final String keywordFile = arg.getProparty("keywordFile" );
		ignoreCase		= arg.getProparty("ignoreCase",ignoreCase);
		isChange		= arg.getProparty("isChange",isChange);			// 5.1.2.0 (2010/01/01)
		final String inEncode	= arg.getProparty("inEncode",System.getProperty("file.encoding"));
		display			= arg.getProparty("display",display);
		debug			= arg.getProparty("debug",debug);

		// 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
		final List<String> list = FileUtil.getLineList( keywordFile , inEncode );		// 6.4.5.2 (2016/05/06)
		final int len = list.size();								// 6.4.5.2 (2016/05/06)
		if( len == 0 ) {
			final String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
			throw new OgRuntimeException( errMsg );
		}

		println( "keywordFile を、" + len + "件読み取りました。" );
		final List<String> keyList = new ArrayList<>( len );
		final List<String> cngList = new ArrayList<>( len );

		for( final String line : list ) {
	//		String line = lines[i].trim();
			final int indx = line.indexOf( '\t' );
			if( indx <= 0 ) { continue ; }	// TAB が先頭や、存在しない行は読み飛ばす。
			keyList.add( line.substring( 0,indx ).trim() );
			String cng = line.substring( indx+1 ).trim();
			cng = StringUtil.replace( cng,"\\n",CR );
			cng = StringUtil.replace( cng,"\\t","\t" );
			cngList.add( cng );
		}
		keyword	= keyList.toArray( new String[keyList.size()] );
		change	= cngList.toArray( new String[cngList.size()] );
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
	 *
	 * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		// ここでは処理を行いません。
	}

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
	 * @og.rev 6.0.2.0 (2014/09/19) org.opengion.fukurou.model.ExcelModel を使うように変更。
	 * @og.rev 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
	 *
	 * @param   data	オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		inCount++ ;
		final FileLineModel fileData ;
		if( data instanceof FileLineModel ) {
			fileData = (FileLineModel)data ;
		}
		else {
			final String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
			throw new OgRuntimeException( errMsg );
		}

		final File org = fileData.getFile() ;
		if( ! org.isFile() ) { return data; }

		boolean	nextFlag	= false;
		final File			orgFile	= org.getAbsoluteFile();				// 6.2.0.0 (2015/02/27)
		final ExcelModel	excel	= new ExcelModel( orgFile );			// 6.2.0.0 (2015/02/27)

		final int maxStNo = excel.getNumberOfSheets();						// 6.0.2.0 (2014/09/19)
		for( int stNo=0; stNo<maxStNo; stNo++ ) {
			final String sheetName = excel.getSheetName( stNo );		// ここで、ExcelModel内部に処理Sheetをセットします。
			if( display ) { println( orgFile + ":" + sheetName ); }
			final int nFirstRow = excel.getFirstRowNum();
			final int nLastRow  = excel.getLastRowNum();
			for( int rowNo=nFirstRow; rowNo<=nLastRow; rowNo++ ) {
				final String[] vals = excel.getValues( rowNo );
				// 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
				for( int colNo=0; colNo<vals.length; colNo++ ) {
					final String orgText = vals[colNo];
					if( orgText != null && orgText.length() > 0 ) {
						if( debug ) { println( "\tDATA: [" + rowNo + "," + colNo + "]=" + orgText ); }
						final String strText = changeString( orgText );		// 文字列変換。無変換の場合は、null が返る。
						if( strText != null ) {
							if( display ) { println( "\tFIND: [" + rowNo + "," + colNo + "]=" + orgText + "→" + strText ); }
							excel.setCellValue( strText,colNo );		// 書き戻し。行は先ほど getValues(int) した行
							nextFlag = true;
							findCount++;			// 5.5.2.4 (2012/05/16)
						}
					}
				}
			}

			// シート名も変換対象とする。
			final String newSheetName = changeString( sheetName );	// 無変換の場合は、null が返る。
			if( newSheetName != null ) {
				if( display ) { println( "\tFIND sheetName=" + sheetName + "→" + newSheetName ); }
				excel.setSheetName( stNo,newSheetName );
				nextFlag = true;
				findCount++;					// 5.5.2.4 (2012/05/16)
			}
		}

		if( isChange && nextFlag ) {
			excel.saveFile( orgFile ) ;			// 6.2.0.0 (2015/02/27)
			cngCount = findCount ;				// 5.5.2.4 (2012/05/16) 置換時には、findCount を、cngCount にセットしておく。
		}

		return nextFlag ? data : null ;
	}

	/**
	 * 引数の文字列から、keyword ファイルを元に文字列変換を行います。
	 *
	 * ここでは、変換が行われたかどうかを判定するため、変換された場合
	 * のみ、値を返します。変換されない場合は、null を返しますので、
	 * ご注意ください。
	 *
	 * @param	org	変換前の文字列
	 *
	 * @return   変換後の文字列(変換がなければ、null を返します。)
	 */
	public String changeString( final String org ) {
		if( org == null || org.isEmpty() ) { return null; }

		String tgt = org;
		for( int i=0; i<keyword.length; i++ ) {
			tgt = tgt.replaceAll( keyword[i],change[i] );
		}

		// 元と同じ場合は、null を返します。
		if( ignoreCase && org.equalsIgnoreCase( tgt ) || org.equals( tgt ) ) {		// 6.9.7.0 (2018/05/14) PMD Useless parentheses.
			tgt = null;
		}

		return tgt ;
	}

	/**
	 * プロセスの処理結果のレポート表現を返します。
	 * 処理プログラム名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のレポート
	 */
	public String report() {
		final String report = "[" + getClass().getName() + "]" + CR
				+ TAB + "Search File Count : " + inCount    + CR
				+ TAB + "Key Find    Count : " + findCount  + CR
				+ TAB + "Key Change  Count : " + cngCount ;

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( 1200 )
			.append( "Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を"		).append( CR )
			.append( "置換する、ChainProcess インターフェースの実装クラスです。"					).append( CR )
			.append( "Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、"	).append( CR )
			.append( "ネイティブEXCELファイルなのかの違いです。"									).append( CR )
			.append( CR )
			.append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、"	).append( CR )
			.append( "対象とする語句を置換します。"													).append( CR )
			.append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"		).append( CR )
			.append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"			).append( CR )
			.append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"	).append( CR )
			.append( "が前後に存在している場合は、ご注意ください。"									).append( CR )
			.append( "置換文字(値)は、\t と \n の特殊文字が使用できます。" 							).append( CR )
			.append( "この GrepChangeExcel では、語句に、正規表現は使用できません。"				).append( CR )
			.append( "正規表現のキーワードや文字列を複数行の文字列と置き換える場合は、Process_Grep"	).append( CR )
			.append( "を使用して下さい。"															).append( CR )
			.append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、"	).append( CR )
			.append( "置き換えた結果も、同じファイルにセーブします。"								).append( CR )
			.append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"	).append( CR )
			.append( "-inEncode は、keywordFileのエンコード指定になります。"						).append( CR )
			.append( "初期値は、互換性を持つため、System.getProperty(\"file.encoding\") ですが、"	).append( CR )
			.append( "明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。"				).append( CR )
			.append( CR )
			.append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"	).append( CR )
			.append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"		).append( CR )
			.append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"	).append( CR )
			.append( "できれば、使用可能です。"														).append( CR )
			.append( CR )
			.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"	).append( CR )
			.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"		).append( CR )
			.append( "繋げてください。"																).append( CR )
			.append( CR ).append( CR )
			.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

	/**
	 * このクラスは、main メソッドから実行できません。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		LogWriter.log( new Process_GrepChangeExcel().usage() );
	}
}
