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

import java.util.ArrayList;
import java.util.List;

import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring
import org.opengion.fukurou.system.OgRuntimeException ;				// 6.4.2.0 (2016/01/29)

/**
 * [PN],[OYA] などの [] で指定されたカラムで表されたフォーマットデータに対して、
 * DataModel オブジェクトを適用して 各カラムに実データを割り当てるオブジェクトです。
 *
 * カラム名には、特殊カラム名が使用できます。これは、DataModel に存在しないカラム名
 * ですが、値を返すことが出来ます。
 * <pre>
 * [KEY.カラム名] : 行番号付きカラム名
 * [I]            : 行番号
 * [J]            : 登録時の件数(1～)		7.3.1.3 (2021/03/09)
 * [ROW.ID]       : 行毎のチェックボックスのID
 * [ROW.JSON]     : 行毎の全データのJavaScriptオブジェクト形式 { key:val,key:val,... }
 * カラムの前に修飾記号(#,$,!)を付けるとフォーマットを変更できます。
 * ただし、FormatTextField 系 と FormatTable 系で、出力される形式が異なります。
 *                  FormatTextField 系               FormatTable 系
 * [#カラム名]    : TDなしのラベルと入力フィールド   ラベルを出力
 * [$カラム名]    : TDなしの入力フィールドのみ       レンデラーを出力
 * [!カラム名]    : TDなしの値のみ                   値を出力
 *
 * </pre>
 * @og.group 画面表示
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Formatter {
	/** カラムID(連結文字列)行番号の連結文字列を定義 {@value} */
	public static final String JOINT_STRING = "__" ;

	/** テーブル表示のチェックボックスを特定する id の 名称( id は、この名称＋行番号) {@value} */
	public static final String ROW_ID_KEY = "cb";	// 3.6.0.0 (2004/09/17)

//	/** 特殊カラム名の定義: 行番号 [I]	*/
//	public static final int SYS_ROWNUM	= -1;		// [KEY.カラム名],[I],[ROW.ID]
	/** 特殊カラム名の定義: [ROW.JSON]	*/
	public static final int SYS_JSON	= -2;		// [ROW.JSON]
	/** 6.9.5.0 (2018/04/23) 特殊カラム名の定義: 行番号 [I]	*/
	public static final int SYS_ROWNUM	= -3;		// [KEY.カラム名],[I],[ROW.ID] 6.9.5.0 (2018/04/23) -1 は、カラム無しと同じなので、-3 に変更
	/** 6.9.5.0 (2018/04/23) 特殊カラム名の定義: 行番号 [I]	*/
	public static final int SYS_CNT		= -4;		// 7.3.1.3 (2021/03/09) [J] は特殊記号で、登録時の件数(1～)
	/** 特殊カラム名の定義: 非表示		*/
	public static final int NO_DISPLAY	= -9;		// 6.2.0.1 (2015/03/06) 非表示のマーカー

	private final DataModel<?>	model	;			// 4.3.3.6 (2008/11/15) Generics警告対応
	private int[]				clmNos	;			// フォーマットのカラム番号配列
	private String[]			format	;
	private String[]			clmKeys	;			// フォーマットのカラム名配列
	private String[]			clmPrms	;			// 6.8.3.1 (2017/12/01) フォーマットのカラムのパラメータ
	private char[]				type	;			// '#':ラベルのみ  '$':レンデラー '!':値のみ  その他:通常

	/**
	 * データモデルとフォーマットを指定してフォーマッターを構築します。
	 *
	 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
	 *
	 * @param	model データモデル
	 * @param	fmt  [カラム名]形式のフォーマットデータ
	 */
	public Formatter( final DataModel<?> model , final String fmt ) {
		this.model = model;
		makeFormatList( fmt );
		advanceFormat();
	}

	/**
	 * フォーマットをセットします。
	 *
	 * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
	 *
	 * @param	fmt  [カラム名]形式のフォーマットデータ
	 */
	private void makeFormatList( final String fmt ) {
		int start = 0;
		int index = fmt.indexOf( '[' );
		final List<String> formatList = new ArrayList<>();
		final List<String> clmKeyList = new ArrayList<>();
		while( index >= 0 ) {
			final int end = fmt.indexOf( ']',index );
			if( end < 0 ) {
				final String errMsg = "[ と ] との対応関係がずれています。"
								+ "format=[" + fmt + "] : index=" + index ;
				throw new OgRuntimeException( errMsg );
			}

			// [ より前方の文字列は、formatList へ
			if( index > 0 ) { formatList.add( fmt.substring( start,index ) ); }
			else 			{ formatList.add( "" ); }	// ][ と連続しているケース

			// [XXXX] の XXXX部分を処理
			clmKeyList.add( fmt.substring( index+1,end ) );

			start = end+1 ;
			index = fmt.indexOf( '[',start );
		}
		// ] の後方部分は、formatList へ
		formatList.add( fmt.substring( start ) );

		format  = formatList.toArray( new String[formatList.size()] );
		clmKeys = clmKeyList.toArray( new String[clmKeyList.size()] );
		clmPrms = new String[clmKeyList.size()];		// 6.8.3.1 (2017/12/01)
	}

	/**
	 * 追加機能フォーマットを作成します。
	 *
	 * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
	 * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1～) を表現する。
	 */
	private void advanceFormat() {
		final int size = clmKeys.length ;
		clmNos = new int[size];
		type   = new char[size];

		// カラム番号の設定と、特殊カラム名処理
		String clm ;
		for( int i=0; i<size; i++ ) {
			clm = clmKeys[i];
			final char ch = clm.charAt(0);
			if( ch == '#' || ch == '$' || ch == '!' ) {
				type[i] = ch;
				clm = clm.substring(1);
				// 6.8.3.1 (2017/12/01) [$XXXX param] 対応。
				final int sp = clm.indexOf( ' ' );		// スペース分割
				if( sp > 0 ) {
					clmPrms[i] = clm.substring( sp+1 );	// 先にパラメータを取得
					clm = clm.substring( 0,sp );
				}
				clmKeys[i] = clm;
				clmNos[i]  = model.getColumnNo( clm );	// 指定されたセルのカラム番号。存在しなければ、-1
			}
			// [KEY.カラム名] 機能追加
			else if( clm.startsWith( "KEY." ) ) {
				clmNos[i] = SYS_ROWNUM;
				format[i] = format[i] + clm.substring(4) + JOINT_STRING ;
			}
			// [I] 機能追加
			else if( "I".equals( clm ) ) {
				clmNos[i] = SYS_ROWNUM;
			}
			// 7.3.1.3 (2021/03/09) [J] 機能追加
			else if( "J".equals( clm ) ) {
				clmNos[i] = SYS_CNT;
			}
			// [ROW.ID] 機能追加
			else if( "ROW.ID".equals( clm ) ) {
				clmNos[i] = SYS_ROWNUM;
				format[i] = format[i] + ROW_ID_KEY ;
			}
			// [ROW.JSON] 機能追加
			else if( "ROW.JSON".equals( clm ) ) {
				clmNos[i] = SYS_JSON;
			}
			else {
				clmNos[i] = model.getColumnNo( clm );	// 指定されたセルのカラム番号。存在しなければ、-1
			}
		}
	}

	/**
	 * column にあるセルの属性値をStringに変換して返します。
	 *
	 * ここでは、[KEY.カラム名] , [I] , [J] , [ROW.ID] は同じ値(row)を返します。
	 *
	 * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
	 * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1～) を表現する。
	 *
	 * @param	row	処理中の行番号
	 * @param	clm	値が参照されるカラム番号
	 *
	 * @return	指定されたセルの値
	 *
	 */
	public String getValue( final int row,final int clm ) {
		final String rtn ;
		if( clm >= 0 ) {
			// 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
			final Object obj = model.getValue( row,clm );
			rtn = obj == null ? "" : String.valueOf( obj );
		}
//		else if( clm == SYS_ROWNUM ) {
		else if( clm == SYS_ROWNUM || clm == SYS_CNT ) {	// 7.3.1.3 (2021/03/09)
			rtn = String.valueOf( row );
		}
		else if( clm == SYS_JSON ) {
			rtn = getJson( row );
		}
		else if( clm == NO_DISPLAY ) {		// 7.3.1.3 (2021/03/09) ここで指定されるかどうか不明だが、入れておきます。
			rtn = "";
		}
		else {
			final String errMsg = "指定のカラム番号に該当する処理が見つかりません。"
							+ "clm=[" + clm + "]" ;
			throw new OgRuntimeException( errMsg );
		}

		return rtn ;
	}

	/**
	 * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
	 *
	 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
	 *
	 * @param	row 	行番号( [I]フォーマット処理用 )
	 *
	 * @return  指定のObject配列を元に作成したフォーマット文字列
	 * @og.rtnNotNull
	 */
	public String getFormatString( final int row ) {
//		return getFormatString( row, null );
		return getFormatString( row, null, (val,typ,prm) -> val );
	}

	/**
	 * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
	 * データはseparatorで指定された区切り文字で囲まれて返されます。
	 *
	 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
	 *
	 * @param	row 	行番号( [I]フォーマット処理用 )
	 * @param	separator	セパレーター
	 *
	 * @return  指定のObject配列を元に作成したフォーマット文字列
	 * @og.rtnNotNull
	 */
	public String getFormatString( final int row, final String separator ) {
//		return getFormatString( row, null );
		return getFormatString( row, separator, (val,typ,prm) -> val );
	}

	/**
	 * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
	 * データはseparatorで指定された区切り文字で囲まれて返されます。
	 *
	 * ここでは、[KEY.カラム名] , [I] , [J] , [ROW.ID] は同じ値(row)を返します。
	 *
	 * @og.rev 4.3.1.1 (2008/08/23) switch に、default label が存在しないため、追加
	 * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
	 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
	 * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1～) を表現する。
	 *
	 * @param	row 	行番号( [I]フォーマット処理用 )
	 * @param	separator	セパレーター
	 * @param	triFunc		TriFunction関数
	 *
	 * @return  内部のDataModelを元に作成したフォーマット文字列
	 * @og.rtnNotNull
	 */
//	public String getFormatString( final int row, final String separator ) {
	public String getFormatString( final int row, final String separator, final TriFunction<String,Character,String,String> triFunc ) {
		final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );

		final int count = clmNos.length;
		for( int i=0; i<count; i++ ) {
			final int clm = clmNos[i];					// 7.3.1.3 (2021/03/09)
			rtnStr.append( format[i] );
//			if( clmNos[i] == SYS_ROWNUM ) {
			if( clm == SYS_ROWNUM || clm == SYS_CNT ) {	// 7.3.1.3 (2021/03/09)
				rtnStr.append( row );
			}
			else if( clm == SYS_JSON ) {
				rtnStr.append( getJson( row ) );
			}
			else if( clm == NO_DISPLAY ) {		// 7.3.1.3 (2021/03/09) ここで指定されるかどうか不明だが、入れておきます。
				// なにも append しない = 空文字列を追加と同じ
			}
			else {
				// 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
				final Object obj = model.getValue( row,clm );
				final String val = obj == null ? "" : String.valueOf( obj );

				if( separator == null || separator.isEmpty() ) {
//					rtnStr.append( val );
					// separator の使い方がおかしい。
					rtnStr.append( triFunc.apply( val,type[i],clmPrms[i] ) );		// 7.2.9.0 (2020/10/12)
				}
				else {
					// 4.3.1.1 (2008/08/23) default label が存在しないため、追加
					switch( model.getNativeType( clm ) ) {
						case INT:
						case LONG:
						case DOUBLE:
							rtnStr.append( val );
							break;
						case STRING:
						case CALENDAR:
							rtnStr.append( separator ).append( val ).append( separator );
							break;
						default:
							throw new AssertionError( "Unexpected enumrated value! " + model.getNativeType( clm ) );
					}
				}
			}
		}
		rtnStr.append( format[count] );

		return rtnStr.toString();
	}

	/**
	 * 引数を3つ取る Function の 関数型インターフェースを定義します。
	 *
	 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
	 *
	 * @param	<V> 	引数1の総称型 変換前の値を想定
	 * @param	<T> 	引数2の総称型 タイプ(#,$,!)を想定
	 * @param	<P> 	引数3の総称型 パラメータ(%L,%Sなど)を想定
	 * @param	<R> 	returnの総称型 Format後の値を想定
	 */
	public interface TriFunction<V,T,P,R> {
//	public static interface TriFunction<V,T,P,R> {
		/**
		 * 指定された引数にこの関数を適用します。
		 *
		 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
		 *
		 * @param	val 	引数1の総称型 変換前の値を想定
		 * @param	typ 	引数2の総称型 タイプ(#,$,!)を想定
		 * @param	prm 	引数3の総称型 パラメータ(%L,%Sなど)を想定
		 * @return	returnの総称型 Format後の値を想定
		 */
		R apply( V val,T typ,P prm );
	}

	/**
	 * 引数の DataModel を元に作成したフォーマット文字列を返します。
	 * これは、簡易処理で、DataModel オブジェクトは、実質的には、LineModel です。
	 * パッケージの関連から、引数が、DataModel オブジェクトになっています。
	 *
	 * @og.rev 6.3.2.0 (2015/07/10) LineModelで、Formatter処理できるように、対応します。
	 *
	 * @param	model 	DataModelオブジェクト(実質的には、LineModelオブジェクト)
	 *
	 * @return  引数のDataModelを元に作成したフォーマット文字列
	 * @og.rtnNotNull
	 */
	public String getLineFormatString( final DataModel<Object> model ) {
		final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );

		final int count = clmNos.length;
		for( int i=0; i<count; i++ ) {
			rtnStr.append( format[i] )
				.append( model.getValue( 0,clmNos[i] ) );		// 行番号は、0 にしておきます。
		}
		rtnStr.append( format[count] );

		return rtnStr.toString();
	}

	/**
	 * 先のフォーマット情報の［カラム名］を、クエスチョンマークに置き換えたフォーマットを返します。
	 *
	 * これは、java.sql.PreparedStatement 形式のQuery文字列を合成する場合に使用します。
	 *
	 * @return  PreparedStatement形式のQuery文字列
	 * @og.rtnNotNull
	 */
	public String getQueryFormatString() {
		final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );

		final int count = clmKeys.length;
		for( int i=0; i<count; i++ ) {
			rtnStr.append( format[i] ).append( '?' );
		}
		rtnStr.append( format[count] );

		return rtnStr.toString();
	}

	/**
	 * フォーマットのカラム名配列を返します。
	 *
	 * @return	フォーマットのカラム名配列
	 * @og.rtnNotNull
	 */
	public String[] getClmKeys() {
		return clmKeys.clone();
	}

	/**
	 * フォーマットのカラムのパラメータ配列を返します。
	 *
	 * [#XXX] 、[$XXX]、[!XXX] で、カラムオブジェクトに渡すパラメータを、[$XXX param]形式で
	 * 指定できます。param 部分を、カラム配列と関連付けて、返します。
	 * 未設定のパラメータは、null が設定されています。
	 *
	 * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
	 *
	 * @return	フォーマットのパラメータ名配列
	 * @og.rtnNotNull
	 */
	public String[] getClmPrms() {
		return clmPrms.clone();
	}

	/**
	 * フォーマットの指定の位置のカラムのパラメータを返します。
	 *
	 * [#XXX] 、[$XXX]、[!XXX] で、カラムオブジェクトに渡すパラメータを、[$XXX param]形式で
	 * 指定できます。param 部分を、カラム番号を指定することで、返します。
	 * 未設定のパラメータは、null が設定されています。
	 *
	 * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
	 *
	 * @param	ad パラメータのアドレス(カラムと同じ位置)
	 * @return	フォーマットのパラメータ
	 * @og.rtnNotNull
	 */
	public String getClmParam( final int ad ) {
		return ad >= 0 && ad < clmPrms.length ? clmPrms[ad] : null;
	}

	/**
	 * フォーマットのカラム番号配列を返します。
	 *
	 * @return	フォーマットのカラム番号配列
	 * @og.rtnNotNull
	 */
	public int[] getClmNos() {
		return clmNos.clone();
	}

	/**
	 * フォーマット配列を返します。
	 *
	 * @return	フォーマット配列
	 * @og.rtnNotNull
	 */
	public String[] getFormat() {
		return format.clone();
	}

	/**
	 * タイプ文字列配列を返します。
	 * タイプとは、[XXX] の記述で、[#XXX] は、XXXカラムのラベルを、[$XXX]は、XXXカラムの
	 * レンデラーを、[!XXX] は、値のみ取り出す指定を行います。
	 * 主に、TextField系のフォーマットとTable系では、意味合いが異なりますので、
	 * ご注意ください。
	 *
	 * @return タイプ文字列配列 '#':ラベルのみ  '$':レンデラー '!':値のみ  その他:通常
	 * @og.rtnNotNull
	 */
	public char[] getType() {
		return type.clone();
	}

	/**
	 * 行毎の全データのJavaScriptオブジェクト形式 を返します。
	 *
	 * JavaScriptオブジェクト形式とは、{ key:val,key:val,... }の形式で、
	 * ここでは、内部設定された DataModel のすべてのカラム名をキーに、
	 * 引数で渡された 配列を 値として使用します。
	 *
	 * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
	 *
	 * @param	row	(DataModelの)行番号
	 *
	 * @return  指定の行番号に対応する全データのJSON形式データ
	 * @og.rtnNotNull
	 */
	public String getJson( final int row ) {
		final String[] names = model.getNames();
		final Object[] vals  = model.getValues( row );

		final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );

		rtnStr.append( "{'I':'" ).append( row ).append( '\'' );	// 行番号

		for( int i=0; i<names.length; i++ ) {
			// 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
			rtnStr.append( ",'" ).append( names[i] ).append( "':'" )
				.append( vals[i] == null ? "" : vals[i] ).append( '\'' );
		}
		rtnStr.append( '}' );		// 6.0.2.5 (2014/10/31) char を append する。

		return rtnStr.toString();
	}
}
