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

import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.db.DBColumn;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.Attributes;
import org.opengion.fukurou.model.Formatter;
import static org.opengion.fukurou.system.HybsConst.BUFFER_LARGE;		// 6.1.0.0 (2014/12/26) refactoring

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.List;
import java.util.ArrayList;
import java.util.Arrays ;

/**
 * ViewMarker インターフェース の実装オブジェクトです。
 * これを,共通のスーパークラスとして 各種表示フォーム(例：HTML表示等)に使います。
 *
 * このクラスは、setter/getterメソッドのデフォルト実装を提供しています。
 * 各種表示フォームに対応したサブクラス上で, create() をオーバーライドして下さい。
 *
 * @og.group 画面表示
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ViewMarker_MARKER implements ViewMarker {

	private static final int	MARK_NULL   = -1;	// マーカー未設定
	private static final int	MARK_TRUE   = 1;	// マーカー作成
	private static final int	MARK_FALSE  = 0;	// マーカー作成せず

	private List<Attributes>		markData	;		// 4.0.0 (2005/08/31)
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private final ConcurrentMap<Integer,Formatter>	formMap		= new ConcurrentHashMap<>();	// 6.4.3.1 (2016/02/12)
	private DBTableModel		table			;
	private int[]				markCmlNo		;
	private int[]				isMark			;
	// 3.5.2.0 (2003/10/20)
	private String[]			markKey			;
	private String[]			markLists		;
	private String[]			instrVals		;		// 3.8.8.1 (2007/01/06)
	private int[]				markListNo		;
	private boolean[]			useFmtDeco		;		// 5.6.3.0 (2013/04/01) [$XXXX],[#XXXX]機能を有効にするかどうか(true:有効)

	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private final ConcurrentMap<Integer,List<Integer>>	clmMap	= new ConcurrentHashMap<>();	// 6.4.3.1 (2016/02/12)

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public ViewMarker_MARKER() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * 内容をクリア(初期化)します。
	 *
	 * @og.rev 3.1.1.0 (2003/03/28) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 3.5.2.0 (2003/10/20) markLists,markListNo,markKey属性を追加
	 * @og.rev 3.5.6.1 (2004/06/25) formMap属性を追加
	 * @og.rev 3.8.8.1 (2007/01/06) instrVals属性を追加
	 * @og.rev 5.6.3.0 (2013/04/01) useFmtDeco属性を追加
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 *
	 */
	public void clear() {
		markData	= null;		// 4.0.0 (2005/08/31)
		formMap.clear();		// 6.4.3.3 (2016/03/04)
		table		= null;
		isMark		= null;
		markKey		= null;
		markLists	= null;
		instrVals	= null;		// 3.8.8.1 (2007/01/06)
		markListNo	= null;
		clmMap.clear();			// 6.4.3.3 (2016/03/04)
		useFmtDeco	= null;		// 5.6.3.0 (2013/04/01) [$XXXX],[#XXXX]機能を有効にするかどうか(true:有効)
	}

	/**
	 * カラムに対するマーカーアトリビュートをセットします。
	 *
	 * @og.rev 3.1.0.0 (2003/03/20) Hashtable を使用している箇所で、非同期でも構わない箇所を、HashMap に置換え。
	 * @og.rev 3.1.1.0 (2003/03/28) 同期メソッド(synchronized付き)を非同期に変更する。
	 *
	 * @param	attri	アトリビュート
	 */
	public void addAttribute( final Attributes attri ) {
		if( markData == null ) { markData = new ArrayList<>(); }
		markData.add( attri );
	}

	/**
	 * このマーカーが、初期化されているかどうかを判定します。
	 *
	 * 使用できる状態の場合は、true , 初期化が出来ていない場合は、false を返します。
	 *
	 * @og.rev 6.7.2.0 (2017/01/16) caseKey,caseVal等で未使用のときの対応。
	 *
	 * @return	初期化状況 [true:初期化済み/false:未初期化]
	 */
	public boolean isUsable() {
//		return markData != null && markData.size() > 0 ;		// 本当は、ゼロということは無いはず。
		return markData != null && !markData.isEmpty() ;		// 本当は、ゼロということは無いはず。	// 6.9.7.0 (2018/05/14) PMD
	}

	/**
	 * 内部に DBTableModel をセットします。
	 *
	 * @og.rev 3.1.1.0 (2003/03/28) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 3.5.2.0 (2003/10/20) markLists,markListNo,markKey属性を追加
	 * @og.rev 3.5.6.1 (2004/06/25) DBTableModel の再設定に対応。
	 * @og.rev 3.8.8.1 (2007/01/06) instrVals属性を追加
	 * @og.rev 5.6.3.0 (2013/04/01) useFmtDeco属性を追加
	 * @og.rev 6.4.3.3 (2016/03/04) Map#computeIfAbsent で対応する。
	 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
	 * @og.rev 6.7.6.0 (2017/03/17) strictCheck 追加。
	 *
	 * @param  tbl DBTableModelオブジェクト
	 */
	public void setDBTableModel( final DBTableModel tbl ) {
		table = tbl;
		final int count = markData.size();			// 4.0.0 (2005/08/31)

		isMark		= new int[count];
		markKey		= new String[count];
		markCmlNo	= new int[count];
		markLists	= new String[count];
		instrVals	= new String[count];
		markListNo	= new int[count];
		useFmtDeco	= new boolean[count];	// 5.6.3.0 (2013/04/01) [$XXXX],[#XXXX]機能を有効にするかどうか(true:有効)

		Arrays.fill( isMark,MARK_FALSE );	// マーカーの表示可否
		Arrays.fill( markCmlNo,-1 );		// マーカーの可否を判断するカラム番号
		Arrays.fill( useFmtDeco,false );	// [$XXXX],[#XXXX]機能を無効にする。(互換性のため)

		for( int intKey=0; intKey<count; intKey++ ) {
			final Attributes attri = markData.get( intKey );

			final String column = attri.get( "column" );

			// 6.7.6.0 (2017/03/17) カラムのDBTableModel存在チェック。初期値が true なので、attri に無い場合も、true になる。
			final String strChk = attri.get( "strictCheck" );
			final boolean strictCheck = ! "false".equalsIgnoreCase( strChk );

			// 6.4.3.1 (2016/02/12) ConcurrentMap 系は、key,val ともに not null 制限です。
			final int clm = table.getColumnNo( column,strictCheck );	// 6.7.6.0 (2017/03/17) strictCheck で、true の場合は、clm番号が見つからないときは、Exception発生

			if( clm < 0 ) { continue; }		// 6.7.6.0 (2017/03/17) 存在しない場合、以下の処理を行わない。＝ clmMap に、カラムが登録されない。

			// 6.4.3.3 (2016/03/04) Map#computeIfAbsent で対応する。
			// Map#computeIfAbsent ： 戻り値は、既存の、または計算された値。追加有り、置換なし、削除なし
			clmMap.computeIfAbsent( clm,k -> new ArrayList<>() ).add( intKey );

			final String body = attri.get( "body" );
			final Formatter formatter = new Formatter( table,body );		// 6.4.3.4 (2016/03/11)
			// 6.4.3.1 (2016/02/12) ConcurrentMap 系は、key,val ともに not null 制限です。
			formMap.put( intKey, formatter );

			makeOnMarkFormat( intKey,attri );

			useFmtDeco[intKey] = "true".equalsIgnoreCase( attri.get( "useFormatDeco" ) );	// 5.6.3.0 (2013/04/01)
		}
	}

	/**
	 * 指定の行列に対するマーカー文字列を返します。
	 * この値は,すでにマーカー文字列処理されている為, RendererValue で
	 * 変換する必要はありません。
	 * 引数の value はそのカラムの値として利用されます。この値は、修飾済みの
	 * 値を与えることが可能です。
	 *
	 * @og.rev 3.5.6.1 (2004/06/25) formMap属性を使用します。
	 * @og.rev 3.8.8.1 (2007/01/06) instrVals属性を追加
	 * @og.rev 5.3.9.0 (2011/09/01) カラム名の先頭に'$'を付加した場合に、URLEncodeされた値を返すように対応
	 * @og.rev 5.6.3.0 (2013/04/01) useFmtDeco属性を追加([$XXXX],[#XXXX]機能を有効にするかどうか)
	 * @og.rev 6.2.4.0 (2015/05/15) useFmtDeco属性とは無関係に、[!XXXX](値)を対応します。
	 * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
	 *
	 * @param   row 指定の行
	 * @param   clm 指定の列
	 * @param   value カラムの値(マーカー文字列処理済)
	 *
	 * @return  row行，colum列 のマーカー文字列
	 */
	public String getMarkerString( final int row,final int clm,final String value ) {
		final int intKey = isOnMark(row,clm) ;
		if( intKey < 0 ) { return value; }

		final Formatter formatter = formMap.get( intKey );
		final int[]    clmNo  = formatter.getClmNos();
		final String[] format = formatter.getFormat();
		final char[]   types  = formatter.getType();

		final StringBuilder buf = new StringBuilder( BUFFER_LARGE );
		int j=0;
		String val ;
		for( ; j<clmNo.length; j++ ) {
			// 6.2.4.0 (2015/05/15) [!XXXX](値)を対応
			if( clm == clmNo[j] && types[j] != '!' ) {
				val = value;								// 一致する場合の valueは、通常レンデラー
			}
			else {
				val = formatter.getValue(row,clmNo[j]);		// 生の値
			}

			// 5.6.3.0 (2013/04/01) useFmtDeco属性を追加(trueの場合は、[$XXXX],[#XXXX]機能を有効にする)
			if( useFmtDeco[intKey] ) {
				final DBColumn dbClm = table.getDBColumn( clmNo[j] );
				final String   prm   = formatter.getClmParam(j);	// 6.8.3.1 (2017/12/01)
				if( types[j] == '$' ) {			// レンデラー
					// 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
					val = formatter.getValue(row,clmNo[j]);			// ※ 互換性の関係で、useFmtDecoで、'$' のときのみ、生の値を再取得します。
					val = prm == null || prm.isEmpty()
									? dbClm.getRendererValue( row,val )
									: dbClm.getRendererValue( row,val,prm );
				}
				else if( types[j] == '#' ) {	// ラベル
					if( prm == null || prm.isEmpty() ) {
						val = dbClm.getLabel();
					}
					else {
						// 6.8.3.1 (2017/12/01) [#XXXX param] で、RendererValueのパラメータを動的に変更できます。
						switch( prm.charAt(0) ) {		// 先頭の文字(文字列のswitchでもよい)
							case 'L': val = dbClm.getLabel();			break;
							case 'S': val = dbClm.getShortLabel();		break;
							case 'T': val = dbClm.getLongLabel();		break;
							case 'D': val = dbClm.getDescription();		break;
							default : val = dbClm.getLabel();			break;
						}
					}
				}
			}
			// false が以前と同じ処理（互換処理）ただし、view などのフォーマット処理とは異なる。
			else {
				// 5.3.9.0 (2011/09/01) カラム名の先頭に'$'を付加した場合URLEncodeされた値を返すように対応
				if( types[j] == '$' ) {
					val = StringUtil.urlEncode( val );
				}
			}

			buf.append( format[j] ).append( val );
		}
		if( j < format.length ) { buf.append( format[j] ); }
		String rtn = StringUtil.replace( buf.toString(),"{I}",String.valueOf( row ) );

		// 3.8.8.1 (2007/01/06) instrVals属性を追加
		if( instrVals[intKey] != null ) {
			final String[] vals = StringUtil.csv2Array( instrVals[intKey],' ' );
			for( int i=0; i<vals.length; i++ ) {
				final String css = "<span class=\"instr" + i + "\">" + vals[i] + "</span>";
				rtn = StringUtil.replace( rtn,vals[i],css );
			}
		}
		return rtn ;
	}

	/**
	 * マーカーを作成する/作成しないの指定カラム番号を求めます。
	 * また、int[列番号] isMark を初期化します。
	 *
	 * @og.rev 3.5.2.0 (2003/10/20) markLists,markListNo,markKey属性を追加
	 * @og.rev 3.8.8.1 (2007/01/06) instrVals属性を追加
	 *
	 * @param	intKey	カラムキーの番号
	 * @param	attri	アトリビュート
	 */
	private void makeOnMarkFormat( final int intKey,final Attributes attri ) {
		final String onMark   = attri.get( "onMark" );
		final String markList = attri.get( "markList" );
		instrVals[intKey] = attri.get( "instrVals" );	// 3.8.8.1 (2007/01/06)

		// 3.5.6.0 (2004/06/18) nullポインタの参照外しバグの対応
		// このロジックで値が設定済みであれば、以下の処理は不要である。
		isMark[intKey] = MARK_NULL;
		if( onMark == null || onMark.isEmpty() ||
			markList == null || markList.isEmpty() ) {
				isMark[intKey] = MARK_FALSE;
				return ;	// 3.5.6.0 (2004/06/18)
		}
		else if( onMark.charAt(0) != '[' && markList.charAt(0) != '[' ) {
			isMark[intKey] = markList.indexOf( onMark ) >= 0 ? MARK_TRUE : MARK_FALSE;
			return ;	// 3.5.6.0 (2004/06/18)
		}

		if( onMark.charAt(0) == '[' ) {
			markCmlNo[intKey] = table.getColumnNo( onMark.substring( 1,onMark.length()-1 ));
		}
		else {
			markCmlNo[intKey]  = -1;
			markKey[intKey]    = onMark ;
		}

		if( markList.charAt(0) == '[' ) {
			markListNo[intKey] = table.getColumnNo( markList.substring( 1,markList.length()-1 ));
		}
		else {
			markListNo[intKey] = -1;
			markLists[intKey] = markList;
		}
	}

	/**
	 * マーカーを作成するかどうかを判断します。
	 * int[列番号] isMark には、 未設定 FALSE TRUE の状態を持っており、
	 * 列でマーカーを作成する状態が固定の場合(例えば,onMark属性がデフォルト "true" の場合)
	 * カラムに関係なく、同じ値を返すときに、使用します。
	 *
	 * @og.rev 3.5.2.0 (2003/10/20) markLists,markListNo,markKey属性を追加
	 * @og.rev 3.5.4.0 (2003/11/25) onMark ,markList が null(またはゼロストリング)の場合は、false とする。
	 * @og.rev 4.0.0.0 (2005/08/31) 同一カラムの複数登録を許可します。
	 * @og.rev 6.7.2.0 (2017/01/16) markListの先頭が、"?" の場合、正規表現で判定します。
	 *
	 * @param	row	列番号
	 * @param	clm	カラムキーの名称
	 *
	 * @return	処理するリスト番号、-1 の場合は、該当なし
	 */
	private int isOnMark( final int row,final int clm ) {
		final List<Integer> list = clmMap.get( clm );
		if( list == null ) { return -1; }

		for( int i=0; i<list.size(); i++ ) {
			final int intKey = list.get( i );
			if( isMark[intKey] != MARK_NULL ) {
				if( isMark[intKey] == MARK_TRUE ) { return intKey; }
				else { continue; }
			}

			final String onMark ;
			if( markCmlNo[intKey] < 0 ) { onMark = markKey[intKey] ; }
			else { onMark = table.getValue( row,markCmlNo[intKey] ); }

			// 3.5.4.0 (2003/11/25) 追加
			if( onMark == null || onMark.isEmpty() ) { continue; }

			final String markList ;
			if( markListNo[intKey] < 0 ) { markList = markLists[intKey] ; }
			else { markList = table.getValue( row,markListNo[intKey] ); }

			// 3.5.4.0 (2003/11/25) 修正
			if( markList == null || markList.isEmpty() ) { continue; }

			// 6.7.2.0 (2017/01/16) markListの先頭が、"?" の場合、正規表現で判定します。
			if( markList.charAt(0) == '?' ) {
				if( onMark.matches( markList.substring(1) ) ) {
					return intKey;
				}
			}
			else if( markList.indexOf( onMark ) >= 0 ) { return intKey; }
		}
		return -1;
	}

	/**
	 * マーカーされたカラム番号の配列を返します。
	 *
	 * これは特殊処理で、Edit機能で、カラム列をキャッシュしているときに、
	 * JSPのソース等の変更時に、変更が反映されない対応を行う場合、
	 * 通常の ViewFormのサブクラスから、Edit専用の ViewForm_HTMLSeqClmTable で
	 * 制御する場合、ViewMarkerのEditMarkerでは、通常非表示（検索の場合）ですが
	 * Editのポップアップ画面に、表示されてしまうのを避けるため、noDisplay に
	 * 強制的にするカラム番号が必要です。
	 * あくまで、暫定処置です。Edit機能を改修するときに、この機能は削除します。
	 *
	 * ※ この処理は、EditMarkerでのみ有効にします。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) Edit機能で、JSPソース変更時の対応
	 *
	 * @return  マーカーされたカラム番号の配列
	 */
	public int[] getColumnNos() {
		final int[] rtn = new int[clmMap.size()];
		int i=0;
		for( final Integer obj : clmMap.keySet() ) {
			rtn[i++] = obj.intValue();
		}

		return rtn;
	}
}
