/*
 * 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.util.Argument;
import org.opengion.fukurou.util.FileString;
import org.opengion.fukurou.util.Closer ;
import org.opengion.fukurou.util.StringUtil ;
import org.opengion.fukurou.util.LogWriter;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 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) 新規追加
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_GrepChangeExcel extends AbstractProcess implements ChainProcess {
	private String[]	keyword	= null;
	private String[]	change	= null;
	private boolean		ignoreCase	= false;
	private boolean		isChange	= true;		// 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする
	private boolean		display		= false;	// 表示しない
	private boolean		debug		= false;	// 表示しない

	private int		inCount		= 0;
	private int		findCount	= 0;
	private int		cngCount	= 0;

	private static final Map<String,String> mustProparty   ;		// ［プロパティ］必須チェック用 Map
	private static final Map<String,String> usableProparty ;		// ［プロパティ］整合性チェック Map

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

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

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

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		Argument arg = getArgument();

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

		FileString fs = new FileString();
		fs.setFilename( keywordFile );
		fs.setEncode( inEncode );
		String[] lines = fs.getValue( "\n" );
		int len = lines.length;
		if( len == 0 ) {
			String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
			throw new RuntimeException( errMsg );
		}

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

		for( int i=0; i<len; i++ ) {
	//		String line = lines[i].trim();
			String line = lines[i];
			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) エラー時にデータも出力します。
	 *
	 * @param   data	オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		inCount++ ;
		final FileLineModel fileData ;
		if( data instanceof FileLineModel ) {
			fileData = (FileLineModel)data ;
		}
		else {
			String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
			throw new RuntimeException( errMsg );
		}

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

		boolean nextFlag  = false;

		FileInputStream	in	  = null;
		Workbook		wb	  = null;
		Sheet			sheet = null;
		int  stNo = -1 , rowNo = -1 , cellNo = -1 ;	// エラー発生時に場所を特定する為の情報
		String sheetName = null;		// エラー発生時に場所を特定する為の情報
		try {
			in = new FileInputStream(org);
			wb = WorkbookFactory.create(in);	// HSSFとXSSFの違いをPOIが吸収してくれる

			for( stNo=0; stNo<wb.getNumberOfSheets(); stNo++ ) {
				sheet = wb.getSheetAt(stNo);
				sheetName = sheet.getSheetName();
				if( display ) { println( org.getPath() + ":" + sheetName ); }

				int nFirstRow = sheet.getFirstRowNum();
				int nLastRow  = sheet.getLastRowNum();
				for( rowNo = nFirstRow; rowNo <= nLastRow; rowNo++) {
					Row oRow = sheet.getRow(rowNo);
					if( oRow == null ) { continue; }
					int nFirstCell = oRow.getFirstCellNum();
					int nLastCell  = oRow.getLastCellNum();
					for( cellNo = nFirstCell; cellNo <= nLastCell; cellNo++) {
						Cell oCell = oRow.getCell( cellNo );
						if( oCell != null ) {
							int nCellType = oCell.getCellType();
							if( nCellType == Cell.CELL_TYPE_STRING ) {
								RichTextString richText = oCell.getRichStringCellValue();
								if( richText != null ) {
									String orgText = richText.getString();
									if( debug ) { println( "DEBUG:  [" + rowNo + "," + cellNo + "]=" + orgText ); }

									String strText = changeString( orgText );		// 文字列変換。無変換の場合は、null が返る。
									if( strText != null ) {
										if( display ) { println( "CHANGE: [" + rowNo + "," + cellNo + "]=" + orgText + "→" + strText ); }
										oCell.setCellValue( strText );				// Cell に書き戻し(RichTextStringでないが大丈夫？)
										nextFlag = true;
										findCount++;			// 5.5.2.4 (2012/05/16)
									}
								}
							}
						}
					}
				}

				// シート名も変換対象とする。
				String newSheetName = changeString( sheetName );	// 無変換の場合は、null が返る。
				if( newSheetName != null ) {
					if( display ) { println( "  sheetName=" + sheetName + "→" + newSheetName ); }
					wb.setSheetName(stNo, newSheetName);
					nextFlag = true;
					findCount++;			// 5.5.2.4 (2012/05/16)
				}
			}
		}
		catch ( IOException ex ) {
			String errMsg = "処理中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
						+ org.toString() + CR
						+ "Sheet=[" + sheetName + "],SheetNo=[" + stNo + "],rowNo=[" + rowNo + "],cellNo=[" + cellNo + "]" + CR
						+ "data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
			throw new RuntimeException( errMsg,ex );
		}
		catch ( InvalidFormatException ex ) {
			String errMsg = "読み込みファイルの形式エラーが発生しました。[" + data.getRowNo() + "]件目" + CR
						+ org.toString() + CR
						+ "Sheet=[" + sheetName + "],SheetNo=[" + stNo + "],rowNo=[" + rowNo + "],cellNo=[" + cellNo + "]" + CR
						+ "data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
			throw new RuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( in );
		}

		if( isChange && nextFlag ) {
			FileOutputStream fileOut = null ;
			try {
				fileOut = new FileOutputStream( org );
				wb.write(fileOut);
				cngCount = findCount ;	// 5.5.2.4 (2012/05/16) 置換時には、findCount を、cngCount にセットしておく。
			}
			catch( IOException ex ) {
				String errMsg = "ファイルへ書込み中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
							+ org.toString() + CR
							+ "data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
				throw new RuntimeException( errMsg,ex );
			}
			finally {
				Closer.ioClose( fileOut );
			}
		}

		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( org.equals( tgt ) || (ignoreCase && org.equalsIgnoreCase( tgt )) ) {
			tgt = null;
		}

		return tgt ;
	}

	/**
	 * プロセスの処理結果のレポート表現を返します。
	 * 処理プログラム名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のレポート
	 */
	public String report() {
		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	このクラスの使用方法
	 */
	public String usage() {
		StringBuilder buf = new StringBuilder();

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

		buf.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

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