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

import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.1 (2016/02/12) refactoring
import java.util.Set;												// 6.0.2.4 (2014/10/17)
import java.util.HashSet;											// 6.0.2.4 (2014/10/17)
import java.util.function.BiConsumer;								// 6.4.5.0 (2016/04/08)

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

/**
 * 編集設定情報を管理するためのデータ管理クラスです。
 * ここで管理される各パラメーターの意味は以下の通りです。
 * (各インデックス番号は、内部的に管理されているインデックス番号を意味します)
 *
 * ・0:編集名
 *       この編集設定オブジェクトの名称です。
 * ・1:表示カラム
 *       表示対象となるカラム一覧です。CSV形式で指定します。
 *       この一覧には、非表示のカラムも合わせて管理され、非表示カラムについては、
 *       カラム名の先頭に"!"をつけます。
 *       例) AAA,!BBB,CCC ⇒ AAA,CCCの順に表示(BBBは非表示)
 * ・2:集計カラム
 *       各値をSUMする対象となるカラムです。(CSV形式で複数指定が可能)
 *       ここで指定されたカラムは数値型である必要があります。
 *       SQL構文における、SUM関数の引数として指定するカラムに相当します。
 * ・3:グループカラム
 *       集計カラムの各値をグルーピングするためのカラムです。(CSV形式で複数指定が可能)
 *       SQL構文における、GROUP BYに指定するカラムに相当します。
 * ・4:小計カラム
 *       集計カラムの各値に対し、小計行を付加するためのブレイクキーを指定します。(CSV形式で複数指定が可能)
 * ・5:合計カラム
 *       集計カラムの各値に対し、合計行を付加するためのブレイクキーを指定します。(CSV形式で複数指定が可能)
 * ・6:総合計フラグ
 *       集計カラムの各値に対し、総合計行を付加するかどうかを指定します。(0以外:追加する 0:追加しない)
 * ・7:表示順カラム
 *       データの表示順をその順番にCSV形式で指定します。
 *       カラム名の先頭に"!"をつけた場合は、そのカラムは降順で表示されます。
 *       SQL構文における、orderby句に相当します。
 * ・8:共通フラグ
 *       この編集設定オブジェクトが、共通(全ユーザー公開)編集かどうかを
 *       指定します。(0以外:共通 0:個人のみ)
 *
 * @og.rev 5.3.6.0 (2011/06/01) 新規追加
 *
 * @version  5.0
 * @author   Hiroki Nakamura
 * @since    JDK6.0,
 */
public class DBEditConfig {

	private static final int EDIT_KEY_NAME		= 0;
	private static final int EDIT_KEY_VIEW		= 1;
	private static final int EDIT_KEY_SUM		= 2;
	private static final int EDIT_KEY_GROUP		= 3;
	private static final int EDIT_KEY_SUBTOTAL	= 4;
	private static final int EDIT_KEY_TOTAL		= 5;
	private static final int EDIT_KEY_GRANDTOTAL= 6;
	private static final int EDIT_KEY_FIRSTTOTAL= 7;		// 6.1.1.0 (2015/01/17) FIRSTTOTAL 追加
	private static final int EDIT_KEY_ORDERBY	= 8;
	private static final int EDIT_KEY_COMMON	= 9;

	private static final String[] EDIT_KEYS			// 6.3.9.1 (2015/11/27) 修飾子を、なし → private に変更。キーに、"EDIT_" を最初から付けておきます。
		= { "EDIT_NAME", "EDIT_VIEW", "EDIT_SUM", "EDIT_GROUP", "EDIT_SUBTOTAL", "EDIT_TOTAL", "EDIT_GRANDTOTAL", "EDIT_FIRSTTOTAL", "EDIT_ORDERBY", "EDIT_COMMON" };

	private final String[] editVals = new String[EDIT_KEYS.length];		// 6.3.9.1 (2015/11/27) 再利用率が低いのと、コンパイラが何とかしてくれるでしょう。

	private int sumClmCount;
	private int groupClmCount;
	private int subTotalClmCount;
	private int totalClmCount;
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private final ConcurrentMap<String,String> orderMap = new ConcurrentHashMap<>();
	private String orderByDescClms;

	/**
	 * コンストラクタ
	 *
	 * 空の編集設定オブジェクトを構築します。
	 */
	public DBEditConfig() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * コンストラクタ
	 *
	 * 各種パラメーターを指定して編集設定オブジェクトを構築します。
	 *
	 * @og.rev 6.1.1.0 (2015/01/17) 総合計を最初の行に追加するかどうか(FirstTotal)の属性を追加
	 *
	 * @param editName 編集名称
	 * @param viewClms 画面表示カラム
	 * @param sumClms 集計カラム
	 * @param groupClms グループカラム
	 * @param subTotalClms 小計カラム
	 * @param totalClms 合計カラム
	 * @param useGrandTotal 総合計行を追加するか(1:追加する 1以外:追加しない)
	 * @param useFirstTotal 総合計行を追加するか(1:追加する 1以外:追加しない)
	 * @param orderByClms 表示順
	 * @param isCommon 共通編集かどうか(1:共通 1以外:個人のみ)
	 */
	public DBEditConfig( final String editName, final String viewClms
						, final String sumClms, final String groupClms
						, final String subTotalClms, final String totalClms
						, final String useGrandTotal, final String useFirstTotal
						, final String orderByClms, final String isCommon ) {

		editVals[EDIT_KEY_NAME]			= editName;
		editVals[EDIT_KEY_VIEW]			= viewClms;
		editVals[EDIT_KEY_SUM]			= sumClms;
		editVals[EDIT_KEY_GROUP]		= groupClms;
		editVals[EDIT_KEY_SUBTOTAL]		= subTotalClms;
		editVals[EDIT_KEY_TOTAL]		= totalClms;
		editVals[EDIT_KEY_GRANDTOTAL]	= useGrandTotal;
		editVals[EDIT_KEY_FIRSTTOTAL]	= useFirstTotal;	// 6.1.1.0 (2015/01/17)
		editVals[EDIT_KEY_ORDERBY]		= orderByClms;
		editVals[EDIT_KEY_COMMON]		= isCommon;

		init();
	}

	/**
	 * コンストラクタ
	 *
	 * 各種パラメーターを配列で指定して編集設定オブジェクトを構築します。
	 * 各パラメータの配列インデックスは、{@link #getEditKeys(String,String)}で返される
	 * キー一覧の配列インデックスと一致します。
	 * 各パラメーターの意味については、クラスのJavadoc{@link DBEditConfig}を参照して下さい。
	 *
	 * @param editVals 設定値(配列)
	 */
	public DBEditConfig( final String[] editVals ) {
		System.arraycopy( editVals, 0, this.editVals, 0, editVals.length );
		init();
	}

	/**
	 * 編集設定オブジェクト作成時の初期化処理です。
	 * コンストラクタの引数に基づき内部変数の初期設定を行います。
	 */
	private void init() {
		sumClmCount		= StringUtil.csv2Array( editVals[EDIT_KEY_SUM]		).length;
		groupClmCount	= StringUtil.csv2Array( editVals[EDIT_KEY_GROUP]	).length;
		subTotalClmCount= StringUtil.csv2Array( editVals[EDIT_KEY_SUBTOTAL]	).length;
		totalClmCount	= StringUtil.csv2Array( editVals[EDIT_KEY_TOTAL]	).length;

		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
		if( editVals[EDIT_KEY_ORDERBY] == null ) {
			orderByDescClms = null;
		}
		else {
			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
			final String[] ary = StringUtil.csv2Array( editVals[EDIT_KEY_ORDERBY] );
			for( int i=0; i<ary.length ;i++ ) {
				String str = ary[i];
				if( StringUtil.startsChar( str , '!' ) ) {				// 6.2.0.0 (2015/02/27) １文字 String.startsWith
					str = str.substring( 1 );
					if( buf.length() > 0 ) { buf.append( ',' ); }		// 6.0.2.5 (2014/10/31) char を append する。
					buf.append( str );
				}
				orderMap.put( str, String.valueOf( i+1 ) );
			}
			orderByDescClms = buf.toString();
		}
	}

	/**
	 * 画面ID、編集名をキーに、編集設定オブジェクトの各設定値の管理キーを指定します。
	 *
	 * 編集設定オブジェクトで管理される各キーに対して、
	 * "EDIT_[KEY]_(画面ID)_(編集名)"というキーを生成し、これを配列にして返します。
	 *
	 * @og.rev 6.0.2.2 (2014/10/03) 新規追加。DBEditConfig から、移動
	 * @og.rev 6.3.9.1 (2015/11/27) DBEditConfigManager から、移動。
	 *
	 * @param guikey 画面ID
	 * @param editName 編集名
	 *
	 * @return 編集設定を管理するためのキー一覧
	 */
	public static String[] getEditKeys( final String guikey, final String editName ) {
		final String[] rtn = new String[EDIT_KEYS.length];
		final String keyNm = "_" + guikey + "_" + editName;

		for( int i=0; i<EDIT_KEYS.length; i++ ) {
			rtn[i] = EDIT_KEYS[i] + keyNm;
		}
		return rtn;
	}

	/**
	 * 編集設定オブジェクトの各設定値を配列にして返します。
	 *
	 * 配列のインデックス番号は、{@link #getEditKeys(String,String)}で生成されるキーの
	 * インデックス番号と一致します。
	 *
	 * @return 編集設定オブジェクトの設定値一覧(配列)
	 */
	public String[] getEditVals() {
		final String[] rtn = new String[editVals.length];
		System.arraycopy( editVals, 0, rtn, 0, editVals.length );
		return rtn;
	}

	/**
	 * 編集名を返します。
	 *
	 * @return 編集名
	 */
	public String getEditName() {
		return editVals[EDIT_KEY_NAME];
	}

	/**
	 * 表示カラム名の一覧をCSV形式で返します。
	 * 非表示カラムについては、カラム名の先頭に"!"をつけて返されます。
	 * 例) AAA,!BBB,CCC ⇒ AAA,CCCの順に表示(BBBは非表示)
	 *
	 * @return 表示カラム名一覧(CSV形式)
	 */
	public String getViewClms() {
		return editVals[EDIT_KEY_VIEW];
	}

	/**
	 * 表示カラム(CSV形式)をチェックし、変更があれば、反映したカラムを作成します。
	 *
	 * 表示カラムは、並び順や非表示マーカー(!)などが加味され、ユーザー、画面ごとに
	 * データベースに記録されています。JSPソースを修正した場合、データベースに
	 * 書き込まれた表示カラムは、反映されないため、カラム選択画面等に表示されません。
	 * そこで、オリジナルのカラムに追加された場合は、カラムを比較することで、
	 * 追加分のカラムを、非表示カラムとして、後ろに追記します。
	 * 削除された場合は、ViewForm で警告表示することで、ユーザーに変更を促します。
	 *
	 * @og.rev 6.0.2.4 (2014/10/17) JSP修正時の追加カラム対応
	 *
	 * @param	orgClms		オリジナルのカラム(CSV形式)
	 *
	 * @return	変更後の表示カラム(CSV形式)
	 */
	public String getViewClms( final String orgClms ) {
		String viewClms = editVals[EDIT_KEY_VIEW];

		if( orgClms == null || orgClms.isEmpty() ) { return viewClms; }		// orgClms がなければ、viewClms を返す。
		// 基本的には、両者のカラムは、一致するはず。
		final String[] vclms = viewClms.split( "," );		// 表示順、非表示処理を行ったカラム
		final String[] fclms = orgClms.split( "," );		// 元々の表示可能カラムすべて(fullClms)

		// 表示可能カラムすべての Set を作成します。
		final Set<String> fset = new HashSet<>();
		for( int i=0; i<fclms.length; i++ ) {		// orgClms をSet に追加します。
			fset.add( fclms[i] );
		}

		// 非表示カラムの内、表示可能カラムに存在しない分だけの Set を作成します。	
		// また、表示可能カラムから、順番に、viewClms の値を削除していきます。
		final Set<String>   vset = new HashSet<>();
		final StringBuilder vbuf = new StringBuilder( BUFFER_MIDDLE );	// 新しい viewClms 作成用
		for( int i=0; i<vclms.length; i++ ) {		// viewClms をSet に追加します。
			String clm = vclms[i];
			if( clm == null || clm.isEmpty() ) { continue; }			// 6.0.2.5 (2014/10/31) 潜在バグ？ 先頭に"," が来るとアベンドする。
			clm = clm.charAt(0) == '!' ? clm.substring(1) : clm ;		// 非表示の (!) は削除します。
			if( fset.remove( clm ) ) {				// fullSet にあれば、削除するとともに、新viewClmsを作成する。
				if( vbuf.length() > 0 ) { vbuf.append(','); }			// 最初以降は、カンマで連結する。		// 6.0.2.5 (2014/10/31) char を append する。
				vbuf.append( vclms[i] );			// append するのは、(!) 付のカラム
			}
			else {
				vset.add( clm );					// fullSet になければ、viewSet に追加
			}
		}

		// この段階で、fset、vset ともに、それぞれ独自のカラムが残っている。
		// どちらも、残っていなければ、正常なので、viewClms を返す。
		if( vset.isEmpty() && fset.isEmpty() ) { return viewClms; }

		// fullSet にカラムが残っていれば、非表示で、新viewClmsに、追加する。
		if( !fset.isEmpty() ) {
			final String[] defClms = fset.toArray( new String[fset.size()] );
			for( int i=0; i<defClms.length; i++ ) {
				if( vbuf.length() > 0 ) { vbuf.append(','); }	// 6.0.2.5 (2014/10/31) 最初以降は、カンマで連結する。
				vbuf.append('!').append( defClms[i] );						// 非表示カラムとして、後ろに追加する。
			}
		}
		viewClms = vbuf.toString();

		editVals[EDIT_KEY_VIEW] = viewClms;
		return viewClms;
	}

	/**
	 * 集計カラムの一覧をCSV形式で返します。
	 *
	 * @return 集計カラムの一覧(カンマ区切)
	 */
	public String getSumClms() {
		return editVals[EDIT_KEY_SUM];
	}

	/**
	 * 集計処理を行うかどうかを返します。
	 * これは、集計カラムが指定されているか、と同じ意味です。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useSum() {
		return editVals[EDIT_KEY_SUM] != null && editVals[EDIT_KEY_SUM].length() > 0 ;
	}

	/**
	 * 指定されたカラムが集計対象のカラムかどうかを返します。
	 *
	 * @param clm カラム
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean isSumClm( final String clm ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		// 条件反転注意
		return clm != null && editVals[EDIT_KEY_SUM] != null && ( ","+editVals[EDIT_KEY_SUM]+"," ).indexOf( ","+clm+"," ) >= 0 ;

	}

	/**
	 * 集計カラムのカラム数を返します。
	 *
	 * @return 集計カラムのカラム数
	 */
	public int getSumClmCount() {
		return sumClmCount;
	}

	/**
	 * グループカラムの一覧をCSV形式で返します。
	 *
	 * @return グループカラムの一覧(カンマ区切)
	 */
	public String getGroupClms() {
		return editVals[EDIT_KEY_GROUP];
	}

	/**
	 * グループ処理を行うかどうかを返します。
	 * これは、グループカラムが指定されているか、と同じ意味です。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useGroup() {
		return editVals[EDIT_KEY_GROUP] != null && editVals[EDIT_KEY_GROUP].length() > 0 ;
	}

	/**
	 * 指定されたカラムがグループ対象のカラムかどうかを返します。
	 *
	 * @param clm カラム
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean isGroupClm( final String clm ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		// 条件反転注意
		return clm != null && editVals[EDIT_KEY_GROUP] != null && ( ","+editVals[EDIT_KEY_GROUP]+"," ).indexOf( ","+clm+"," ) >= 0 ;

	}

	/**
	 * グループカラムのカラム数を返します。
	 *
	 * @return グループカラムのカラム数
	 */
	public int getGroupClmCount() {
		return groupClmCount;
	}

	/**
	 * 小計カラムの一覧をCSV形式で返します。
	 *
	 * @return 小計カラムの一覧(カンマ区切)
	 */
	public String getSubTotalClms() {
		return editVals[EDIT_KEY_SUBTOTAL];
	}

	/**
	 * 小計処理を行うかどうかを返します。
	 * これは、小計カラムが指定されているか、と同じ意味です。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useSubTotal() {
		return editVals[EDIT_KEY_SUBTOTAL] != null && editVals[EDIT_KEY_SUBTOTAL].length() > 0 ;
	}

	/**
	 * 指定されたカラムが小計対象のカラムかどうかを返します。
	 *
	 * @param clm カラム
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean isSubTotalClm( final String clm ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		// 条件反転注意
		return clm != null && editVals[EDIT_KEY_SUBTOTAL] != null && ( ","+editVals[EDIT_KEY_SUBTOTAL]+"," ).indexOf( ","+clm+"," ) >= 0 ;

	}

	/**
	 * 小計カラムのカラム数を返します。
	 *
	 * @return グループカラムのカラム数
	 */
	public int getSubTotalClmCount() {
		return subTotalClmCount;
	}

	/**
	 * 合計カラムの一覧をCSV形式で返します。
	 *
	 * @return 合計カラムの一覧(カンマ区切)
	 */
	public String getTotalClms() {
		return editVals[EDIT_KEY_TOTAL];
	}

	/**
	 * 合計処理を行うかどうかを返します。
	 * これは、合計カラムが指定されているか、と同じ意味です。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useTotal() {
		return editVals[EDIT_KEY_TOTAL] != null && editVals[EDIT_KEY_TOTAL].length() > 0 ;
	}

	/**
	 * 指定されたカラムが合計対象のカラムかどうかを返します。
	 *
	 * @param clm カラム
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean isTotalClm( final String clm ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		// 条件反転注意
		return clm != null && editVals[EDIT_KEY_TOTAL] != null && ( ","+editVals[EDIT_KEY_TOTAL]+"," ).indexOf( ","+clm+"," ) >= 0 ;

	}

	/**
	 * 合計カラムのカラム数を返します。
	 *
	 * @return グループカラムのカラム数
	 */
	public int getTotalClmCount() {
		return totalClmCount;
	}

	/**
	 * 総合計行を付加するかどうかを返します。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useGrandTotal() {
		return StringUtil.nval( editVals[EDIT_KEY_GRANDTOTAL], false );
	}

	/**
	 * 総合計を最初の行に追加するかどうかを返します。
	 *
	 * @og.rev 6.1.1.0 (2015/01/17) 総合計を最初の行に追加するかどうか(FirstTotal)の属性を追加
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useFirstTotal() {
		return StringUtil.nval( editVals[EDIT_KEY_FIRSTTOTAL], false );
	}

	/**
	 * 表示順カラムをCSV形式で返します。
	 * カラムの並び順が表示順としての優先順になります。
	 * また、降順で表示するカラムについては、カラム名の先頭に"!"が付加されます。
	 *
	 * @return 標準順カラムの一覧(カンマ区切)
	 */
	public String getOrderByClms() {
		return editVals[EDIT_KEY_ORDERBY];
	}

	/**
	 * 指定されたカラムの表示順の優先番号を返します。
	 * 指定カラムが標準として指定されていない場合は、""(ゼロストリング)を返します。
	 *
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 *
	 * @param clm カラム
	 *
	 * @return 表示順の優先番号
	 * @og.rtnNotNull
	 */
	public String getOrder( final String clm ) {
		// ConcurrentMap#getOrDefault(Object,V) を使用して、Map の値が null のときの初期値を返します。
		return clm == null || editVals[EDIT_KEY_ORDERBY] == null
					? ""
					: orderMap.getOrDefault( clm , "" );

	}

	/**
	 * 指定されたカラムの表示順指定が降順であるかどうかを返します。
	 * 標準と指定されていない場合は、falseを返します。
	 *
	 * @param clm カラム
	 *
	 * @return true:降順 false:昇順
	 */
	public boolean isOrderByDesc( final String clm ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		// 条件反転注意
		return clm != null && orderByDescClms != null && ( ","+orderByDescClms+"," ).indexOf( ","+clm+"," ) >= 0 ;

	}

	/**
	 * 並び替え処理を行うかどうかを返します。
	 * これは、表示順カラムが指定されているか、と同じ意味です。
	 *
	 * @return true:対象 false:非対象
	 */
	public boolean useOrderBy() {
		return editVals[EDIT_KEY_ORDERBY] != null && editVals[EDIT_KEY_ORDERBY].length() > 0 ;
	}

	/**
	 * この編集設定オブジェクトが、共通(全ユーザー公開)編集か
	 * どうかを返します。
	 *
	 * @return 0以外:共通 0:個人のみ
	 */
	public boolean isCommon() {
		return StringUtil.nval( editVals[EDIT_KEY_COMMON], false );
	}

	/**
	 * 画面IDに対応した、内部のキーと値の各要素に対して指定されたアクションを実行します。
	 *
	 * getEditKeys(String,String) で得られるキーの文字列配列と、getEditVals()で得られる値の文字列配列
	 * を、順次、action メソッドに渡していきます。
	 * キーの文字列配列の作成時の編集名は、このオブジェクトの編集名を使用します。
	 *
	 * @og.rev 6.4.5.0 (2016/04/08) UserInfo のEditConfig関連機能を、DBEditConfigManagerに移植します。新規追加
	 *
	 * @param guikey 画面ID
	 * @param action 各要素に対して実行される関数型インタフェース( editKey、editVal )
	 */
	public void forEach( final String guikey, final BiConsumer<String ,String> action ) {
		final String keyNm = "_" + guikey + "_" + editVals[EDIT_KEY_NAME];

		for( int i=0; i<EDIT_KEYS.length; i++ ) {
			action.accept( EDIT_KEYS[i] + keyNm , editVals[i] );
		}
	}
}
