/*
 * 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.util.HybsEntry ;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.system.Closer ;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.model.Formatter;				// 6.3.2.0 (2015/07/10)

import java.util.Map ;
import java.util.LinkedHashMap ;

import java.io.File;
import java.io.PrintWriter;

/**
 * Process_TableWriter は、上流から受け取ったデータをファイルに書き込む
 * CainProcess インターフェースの実装クラスです。
 *
 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から
 * 受け取ったLineModel を元に、DBTableModel 形式ファイルを出力します。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  Process_TableWriter -outfile=OUTFILE -sep=, -encode=UTF-8 -append=true
 *
 *    -outfile=出力ﾌｧｲﾙ名         ：出力ﾌｧｲﾙ名
 *   [-sep=ｾﾊﾟﾚｰﾀ文字           ] ：区切り文字(初期値:タブ)
 *   [-encode=文字ｴﾝｺｰﾄﾞ        ] ：出力ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ
 *   [-append=[false/true]      ] ：出力ﾌｧｲﾙを、追記する(true)か新規作成する(false)か。
 *   [-useHeader=[true/false]   ] ：ﾍｯﾀﾞｰ情報(#NAME行)を出力する(true)か出力しない(false)か。
 *   [-useNumber=[true/false]   ] ：行番号を出力する(true)か出力しない(false)か。
 *   [-useWquot=[false/true]    ] ：出力ﾃﾞｰﾀをﾀﾞﾌﾞﾙｸｵｰﾃｰｼｮﾝで括る(true)かそのまま(false)か。
 *   [-useDataWquot=[true/false]] ：出力ﾃﾞｰﾀ上のﾀﾞﾌﾞﾙｸｵｰﾃｰｼｮﾝを2重にする(true)かそのまま(false)か。
 *   [-omitCTRL=[false/true]    ] ：ｺﾝﾄﾛｰﾙ文字を削除する(true)かそのまま(false)か。
 *   [-const_XXXX=固定値        ] ：-const_FGJ=1
 *                                      LineModel のキー(const_ に続く文字列)の値に、固定値を設定します。
 *                                     キーが異なれば、複数のカラム名を指定できます。
 *   [-lineFormat=出力形式      ] ：1行分のフォーマットを指定します。
 *   [-display=[false/true]     ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [-debug=[false/true]       ] ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_TableWriter extends AbstractProcess implements ChainProcess {
	private static final String CNST_KEY = "const_" ;

	private String	outfile		;
	private PrintWriter writer	;
	private char	separator	= TAB;			// 6.0.2.5 (2014/10/31) TAB を char 化

	private String[]	cnstClm		;			// 固定値を設定するカラム名
	private int[]		cnstClmNos	;			// 固定値を設定するのカラム番号
	private String[]	constVal	;			// カラム番号に対応した固定値
	private File		file		;			// 出力ファイル
	private String		encode		= System.getProperty("file.encoding");	// 出力ファイルエンコード
	private boolean		append		;			// ファイル追加(true:追加/false:通常)
	private boolean		useHeader	= true;		// ヘッダー情報(#NAME行)を出力する(true)か出力しない(false)か。
	private boolean		useNumber	= true;		// 行番号を出力する(true)か出力しない(false)か。
	private boolean		useWquot	;			// 出力データをダブルクオーテーションで括る(true)かそのまま(false)か。
	private boolean		useDataWquot= true;		// 5.9.10.3 (2016/07/15) データ上のダブルクオーテーションを重ねる(true)かそのまま(false)か。
	private boolean		omitCTRL	;			// コントロール文字を削除する(true)かそのまま(false)か。
	private String		lineFormat	;			// 6.3.2.0 (2015/07/10) 1行分のフォーマット
	private boolean		display		;			// 表示しない
	private boolean		debug		;			// 5.7.3.0 (2014/02/07) デバッグ情報

	private boolean firstRow	= true;			// 最初の一行目
	private int		count		;

	private	Formatter format	;				// 6.3.2.0 (2015/07/10)

	/** 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( "outfile",	"出力ﾌｧｲﾙ名 (必須)" );

		USABLE_PROPARTY = new LinkedHashMap<>();
		USABLE_PROPARTY.put( "sep",			"区切り文字(初期値:ﾀﾌﾞ)" );
		USABLE_PROPARTY.put( "encode",		"出力ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ" );
		USABLE_PROPARTY.put( "append",		"出力ﾌｧｲﾙを、追記する(true)か新規作成する(false)か。" );
		USABLE_PROPARTY.put( "useHeader",	"ﾍｯﾀﾞｰ情報(#NAME行)を出力する(true)か出力しない(false)か。" );
		USABLE_PROPARTY.put( "useNumber",	"行番号を出力する(true)か出力しない(false)か。" );
		USABLE_PROPARTY.put( "useWquot",	"出力ﾃﾞｰﾀをﾀﾞﾌﾞﾙｸｵｰﾃｰｼｮﾝで括る(true)かそのまま(false)か。" );
		USABLE_PROPARTY.put( "useDataWquot","出力ﾃﾞｰﾀ中のﾀﾞﾌﾞﾙｸｵｰﾃｰｼｮﾝを重ねる(true)かそのまま(false)か。" );	// 5.9.10.3 (2016/07/15)
		USABLE_PROPARTY.put( "omitCTRL",	"ｺﾝﾄﾛｰﾙ文字を削除する(true)かそのまま(false)か。" );
		USABLE_PROPARTY.put( "const_",		"LineModel のキー(const_ に続く文字列)の値に、固定値を" +
										CR + "設定します。キーが異なれば、複数のカラム名を指定できます。" +
										CR + "例: -const_FGJ=1" );
		USABLE_PROPARTY.put( "lineFormat","1行分のフォーマットを指定します。" );		// 6.3.2.0 (2015/07/10)
		USABLE_PROPARTY.put( "display",		"結果を標準出力に表示する(true)かしない(false)か" +
										CR + " (初期値:false:表示しない)" );
		USABLE_PROPARTY.put( "debug",		"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );		// 5.7.3.0 (2014/02/07) デバッグ情報
	}

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

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
	 * @og.rev 5.9.10.3 (2016/07/15) ダブルクオートを重ねるかどうかの判定useDataWquot追加
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		outfile			= arg.getProparty("outfile");
		encode	 		= arg.getProparty("encode",encode);
		append			= arg.getProparty("append",append);
		useHeader		= arg.getProparty("useHeader",useHeader);
		useNumber		= arg.getProparty("useNumber",useNumber);
		useWquot		= arg.getProparty("useWquot",useWquot);
		useDataWquot	= arg.getProparty("useDataWquot",useDataWquot);	// 5.9.10.3 (2016/07/15)
		omitCTRL		= arg.getProparty("omitCTRL",omitCTRL);
		final HybsEntry[] cnstKey = arg.getEntrys( CNST_KEY );			// 配列
		lineFormat		= arg.getProparty("lineFormat",lineFormat);		// 6.3.2.0 (2015/07/10)
		display			= arg.getProparty("display",display);
		debug			= arg.getProparty("debug",debug);				// 5.7.3.0 (2014/02/07) デバッグ情報

		// 6.0.2.5 (2014/10/31) TAB を char 化
		final String sep = arg.getProparty( "sep",null );
		if( sep != null && sep.length() > 0 ) { separator = sep.charAt(0); }

		final int size   = cnstKey.length;
		cnstClm    = new String[size];
		constVal   = new String[size];
		for( int i=0; i<size; i++ ) {
			cnstClm[i]  = cnstKey[i].getKey();
			constVal[i] = cnstKey[i].getValue();
		}

		if( outfile == null ) {
			final String errMsg = "ファイル名が指定されていません。" ;
			throw new OgRuntimeException( errMsg );
		}

		file = new File( outfile );
		final File dir = file.getParentFile() ;

		// ディレクトリが存在しない場合の処理
		if( ! dir.exists() && ! dir.mkdirs() ) {
			final String errMsg = "ディレクトリが作成できませんでした。[" + dir + "]" ;
			throw new OgRuntimeException( errMsg );
		}
	}

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

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @og.rev 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
	 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
	 *
	 * @param   data	オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		count++ ;
	//	if( display ) { println( data.dataLine() ); }
		if( firstRow ) {
			writer = FileUtil.getPrintWriter( file,encode,append );
			if( useHeader && useNumber ) { writeName( data ); }

			final int size   = cnstClm.length;
			cnstClmNos = new int[size];
			for( int i=0; i<size; i++ ) {
				cnstClmNos[i] = data.getColumnNo( cnstClm[i] );
			}

			// 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
			if( lineFormat != null ) {
				format = new Formatter( data,lineFormat );		// 6.4.3.4 (2016/03/11)
			}

			firstRow = false;
			if( display ) { println( data.nameLine() ); }		// 5.7.3.0 (2014/02/07) デバッグ情報
		}

		// 固定値置き換え処理
		for( int j=0; j<cnstClmNos.length; j++ ) {
			data.setValue( cnstClmNos[j],constVal[j] );
		}

		// 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
		if( lineFormat == null ) {
			writeData( data );
		}
		else {
			final String fmtVal = format.getLineFormatString( data );
			writer.println( fmtVal );
		}

		if( display ) { println( data.dataLine() ); }	// 5.1.2.0 (2010/01/01) display の条件変更
		return data;
	}

	/**
	 * PrintWriter に LineModelの項目名情報を書き込みます。
	 * 第一カラム目は、項目名情報を示す "#Name" を書き込みます。
	 *
	 * seHeader=true && useNumber=true の場合のみ、このメソッドが呼ばれます。
	 *
	 * @og.rev 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を利用する。
	 *
	 * @param	data ラインモデル
	 */
	private void writeName( final LineModel data ) {
		final int size = data.size();
		writer.print( "#Name" );
		for( int clm=0; clm<size; clm++ ) {
			writer.print( separator );				// 6.0.4.0 (2014/11/28) #NAME 行の区切り文字
			writer.print( data.getName(clm) );
		}
		writer.println();
	}

	/**
	 * PrintWriter に LineModelのテーブル情報を書き込みます。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 改行を含む場合は、ダブルクオートを強制的に前後に追加する。
	 * @og.rev 5.2.2.0 (2010/11/01) ダブルクオートを含む場合は、その直前にダブルクオートを強制的に追加する。
	 * @og.rev 5.9.10.3 (2016/07/15) ダブルクオートを重ねるかどうかの判定useDataWquot追加
	 *
	 * @param	data ラインモデル
	 */
	private void writeData( final LineModel data ) {
		final int size = data.size();

		if( useNumber ) { writer.print( data.getRowNo() ); }		// 行番号
		for( int clm=0; clm<size; clm++ ) {
			if( useNumber || clm!=0 ) { writer.print( separator ); }
			Object val = data.getValue(clm);
			if( val == null ) { val = ""; }

			String sval = String.valueOf( val );
			// 5.2.2.0 (2010/11/01) ダブルクオートを含む場合は、その直前にダブルクオートを強制的に追加する。
			if( useDataWquot && sval.indexOf( '"' ) >= 0 ) { sval = sval.replaceAll( "\"" ,"\"\"" ) ; }	// 5.9.10.3
			if( omitCTRL ) { sval = sval.replaceAll( "\\s" ," " ) ; }
			// 5.2.2.0 (2010/11/01) 改行を含む場合は、ダブルクオートを強制的に前後に追加する。
			if( useWquot || ( !omitCTRL && sval.indexOf( CR ) >= 0 ) ) {
				sval = "\"" + sval + "\"" ;
			}
			writer.print( sval );
		}
		writer.println();
	}

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

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( BUFFER_LARGE )
		.append( "Process_TableWriter は、上流から受け取ったデータをファイルに書き込む" 		).append( CR )
		.append( "CainProcess インターフェースの実装クラスです。"								).append( CR )
		.append( CR )
		.append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"				).append( CR )
		.append( "受け取ったLineModel を元に、DBTableModel 形式ファイルを出力します。"			).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_TableWriter().usage() );
	}
}
