/*
 * 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 static org.opengion.fukurou.util.HybsConst.CR ;		// 6.1.0.0 (2014/12/26)
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.model.Formatter;
// import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.db.DBTableModel;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

/**
 * [PN],[OYA] などの [] で指定されたカラムで表されたフォーマットデータに対して、
 * DBTableModelオブジェクトを適用して 各カラムに実データを割り当てるオブジェクトです。
 *
 * 特に、[XXXX]に対して、[#XXXX]、[$XXXX]、[$XXXX]などの特殊記号が使用できます。
 * 特殊記号の解釈は、HTMLFormatTextField系とHTMLFormatTable系で異なりますので
 * ご注意ください。
 *
 * @og.rev 3.5.4.0 (2003/11/25) 新規追加
 * @og.group 画面表示
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class TableFormatter {

	/** フォーマットタイプの指定の特殊なマーク {@value} */
	public static final String HYBS_ITD_MARKER = "h_itd_marker";
	private static final Pattern ptnKey = Pattern.compile( "[ \t]+</td" ); // 4.3.2.0 (2008/09/10)

	private	FormatterType	formatType	;
	private int[]			location	;
	private String[]		format		;
	private String			formatTag	;
	private String			rowspan		= " rowspan=\"2\"";
	private String			trTag		;
	private boolean			noClass 	;
	// 3.5.6.0 (2004/06/18) '!' 値のみ 追加 既存の '$' は、レンデラー
	private char[]			type		;			// '#':ラベルのみ  '$':レンデラー '!':値のみ  その他:通常
	private String			usableKey	;			// キー情報のカラム文字列
	private int				usableKeyNo	= -1;		// キー情報のカラム番号
	private String			usableList	= "1";

	private String			keyBreakClm	;			// 5.7.6.3 (2014/05/23) キーブレイクをチェックするカラムID
	private int				breakClmNo	= -1;		// 5.7.6.3 (2014/05/23) キーブレイクカラム番号
	private String			breakVal	;			// 5.7.6.3 (2014/05/23) キーブレイクをチェックする値

	private String			itdBody		= "";		// 3.5.6.0 (2004/06/18) 追加
	private Formatter		formatter	;

	/**
	 * フォーマットをセットします。
	 * フォーマットに、&lt;table&gt;を含む場合、TextField扱いなので、フォーマット分割
	 * しません。table を含まず、tr を含む場合は、１行分のデータとして扱う為、
	 * trTag を求めます。
	 * trTag と format との間に、行ヘッダーが入ります。
	 * Tomcat6では、JSPのパース時に、tabやspaceはそのままパースされるため、&lt;/td&gt;前
	 * のスペース削除処理も行います。
	 *
	 * @og.rev 4.3.2.0 (2008/09/10) &lt;/td&gt;前のスペースを取り消します。
	 * @og.rev 5.5.0.3 (2012/03/13) &lt;tr&gt;を取らないフラグ追加
	 *
	 * @param	fmt  [カラム名] 形式のフォーマットデータ
	 * @param   flag  falseにすると先頭のtrタグを取る処理を行いません(5.5.0.3)
	 */
	public void setFormat( final String fmt , final boolean flag ) {
		final int tbl = fmt.indexOf( "<table" );
		final int str = fmt.indexOf( "<tr" );

		// tr を含み、かつ、tableを含まないか、含んでも tr の後ろにtableがある場合。
		if( str >= 0 && ( tbl < 0 || str < tbl ) && flag ) { // 5.5.0.3(2012/03/13)
			final int end = fmt.indexOf( '>',str );
			formatTag = fmt.substring(end+1);
			trTag = fmt.substring(0,end+1) ;
		}
		else {
			formatTag = fmt;
			trTag     = null;
		}
		// 4.3.2.0 (2008/09/10) </td>前のスペースを取り消す。
		final Matcher matcher = ptnKey.matcher( formatTag );
		formatTag = matcher.replaceAll( "</td" );

	}

	/**
	 * フォーマットをセットします。
	 * フォーマットに、&lt;table&gt;を含む場合、TextField扱いなので、フォーマット分割
	 * しません。table を含まず、tr を含む場合は、１行分のデータとして扱う為、
	 * trTag を求めます。
	 * trTag と format との間に、行ヘッダーが入ります。
	 * Tomcat6では、JSPのパース時に、tabやspaceはそのままパースされるため、&lt;/td&gt;前
	 * のスペース削除処理も行います。
	 *
	 * @og.rev 5.5.0.3 (2012/03/13) 引数追加につき。
	 *
	 * @param	fmt  [カラム名] 形式のフォーマットデータ
	 */
	public void setFormat( final String fmt ) {
		setFormat( fmt , true );
	}

	/**
	 * フォーマットを取得します。
	 *
	 * @og.rev 3.5.5.8 (2004/05/20) 新規追加
	 * @og.rev 5.1.7.0 (2010/06/01) サニタイズ戻し処理("\\]\\"から"["に戻し)を追加
	 *
	 * @return	フォーマットデータ
	 */
	public String getFormat() {
		if( trTag != null ) {
			return decodeSanitizedStr( trTag + formatTag );
		}
		else {
			return decodeSanitizedStr( formatTag );
		}
	}

	/**
	 * DBTableModelを利用して、フォーマットデータを初期化します。
	 *
	 * @og.rev 3.5.5.0 (2004/03/12) [KEY.カラム名] 機能追加
	 * @og.rev 3.5.5.2 (2004/04/02) [I] で、行番号を作成します。
	 * @og.rev 3.5.6.0 (2004/06/18) '!' 値のみ 追加 既存の '$' は、レンデラー
	 * @og.rev 3.6.0.0 (2004/09/17) [ROW.ID] で、行毎のチェックボックスのIDを返します。
	 * @og.rev 5.1.7.0 (2010/06/01) サニタイズ戻し処理("\\]\\"から"["に戻し)を追加
	 * @og.rev 5.7.6.3 (2014/05/23) キーブレイクをチェックする keyBreakClm 属性追加
	 *
	 * @param	table	DBTableModelオブジェクト
	 */
	public void makeFormat( final DBTableModel table ) {
		formatter = new Formatter( table );
		formatter.setFormat( formatTag );
		location = formatter.getClmNos();
		format   = formatter.getFormat();

		// 5.1.7.0 (2010/06/01) サニタイズ戻し処理("\\]\\"から"["に戻し)を追加
		// 6.0.2.5 (2014/10/31)  null でないことがわかっている値の冗長な null チェックがあります。 
//		if( format != null ) {
			for( int i=0; i<format.length; i++ ) {
				format[i] = decodeSanitizedStr( format[i] );
			}
//		}

		type = formatter.getType();

		// このフォーマットを使用するかどうかを指定する判定条件の初期設定です。
		if( usableKey != null ) {
			usableKeyNo = table.getColumnNo( usableKey );
		}

		// 5.7.6.3 (2014/05/23) キーブレイクをチェックする keyBreakClm 属性追加
		if( keyBreakClm != null ) {
			breakClmNo = table.getColumnNo( keyBreakClm );
			breakVal   = null;		// 初期化します。
		}
	}

	/**
	 * テーブルフォーマットのタイプを指定します。
	 * enum FormatterType で、指定します。
	 *
	 * @og.rev 4.0.0.0 (2007/05/02) enum 定義に変更
	 *
	 * @param  ftype フォーマットのタイプ
	 */
	public void setFormatType( final FormatterType ftype ) {
		formatType = ftype;
	}

	/**
	 * このフォーマットのタイプを返します。
	 *
	 * このフォーマットのタイプを返します。
	 *
	 * @og.rev 4.0.0.0 (2007/05/02) enum 定義に変更
	 *
	 * @return	このフォーマットのタイプを返します。
	 */
	public FormatterType getFormatType() {
		return formatType;
	}

	/**
	 * テーブルの rowspan 属性をセットします。
	 * rowspan は、ヘッダー部のフォーマットの行数です。初期値は ２行 です。
	 * 設定は、"2" などの、数字部のみをセットします。
	 *
	 * @param  rowspan 属性
	 */
	public void setRowspan( final String rowspan ) {
		if( rowspan == null || rowspan.isEmpty() || rowspan.equals( "1" ) ) {
			this.rowspan = "";
		}
		else {
			this.rowspan = " rowspan=\"" + rowspan + "\"";
		}
	}

	/**
	 * 設定された rowspan を返します。
	 * これは、フォーマットの段組の数を取り出します。
	 * 文字列としては、rowspan="2" という形で取り出します。
	 *
	 * @return フォーマット文字列
	 */
	public String getRowspan() {
		return rowspan;
	}

	/**
	 * ロケーション番号のサイズを返します。
	 * フォーム位置番号は、0 から getLocationSize()-1 までの数字を指定します。
	 * ロケーションサイズは、aaa[ABC]bbb[DEF]ccc[GHI]ddd となっている場合、
	 * aaa , bbb , ccc , ddd は、フォーマットで、サイズは４。
	 * ABC , DEF , GHI に対応するカラム番号がロケーションで、サイズは３。
	 * このメソッドで返すのは、ロケーション番号(３)の方です。
	 *
	 * @return  ロケーション番号のサイズ
	 */
	public int getLocationSize() {
		return location.length;
	}

	/**
	 * カラムのロケーション番号を返します。
	 * 引数は、0 から、getLocationSize()-1 までの数で指定します。
	 * 指定の位置の、フォーマットのカラム名に対応するロケーション番号
	 * を返します。
	 *
	 * @param no フォーム位置番号
	 *
	 * @return ロケーション番号
	 */
	public int getLocation( final int no ) {
		return location[no];
	}

	/**
	 * 指定のロケーション番号の値をクリアします。
	 * ただし、直前のフォーマットに、td タグが存在する場合は、
	 * style="display:none;" を設定することで、td タグそのものが
	 * 無くなります。
	 * その場合、段組みなどのレイアウトを行っていると、フォーマットが
	 * 崩れますので、十分ご確認ください。
	 * また、同一 td 内に複数のカラムを指定した場合は、tdタグ内のすべての
	 * カラムが消えます。
	 * td タグが存在しない場合は、非表示というより、データを空に
	 * するだけになります。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
	 * @og.rev 6.2.0.1 (2015/03/06) 非表示のマーカーに、Formatter#NO_DISPLAY を使用する。
	 *
	 * @param no フォーム位置番号
	 */
//	protected void setNoLocation( final int no ) {
//		location[no] = -1;
	protected void setNoDisplay( final int no ) {
		location[no] = Formatter.NO_DISPLAY ;

		int tdIdx = format[no] == null ? -1 : format[no].indexOf( "<td" ) ;		// nullチェックも兼ねる

		// 6.2.0.1 (2015/03/06) 非表示のマーカーの td に、style="display:none;" 追加
		if( tdIdx >= 0 ) {
			int adrs = format[no].indexOf( "style=\"" );
			if( adrs >= 0 ) {					// style 属性が既に存在する場合。
				adrs += "style=\"".length();	// style=" の直後の位置を求める。
				format[no] = format[no].substring( 0,adrs )
								+ "display:none;"
								+ format[no].substring( adrs ) ;
			}
			else {								// style 属性がないので、td の直後に入れる。
				tdIdx += "<td".length();		// td の直後の位置を求める。
				format[no] = format[no].substring( 0,tdIdx )
								+ " style=\"display:none;\""
								+ format[no].substring( tdIdx ) ;
			}
		}
	}

	/**
	 * フォーマット文字列を返します。
	 * 引数は、0 から、getLocationSize() までの数で指定します。
	 * 指定のフォーマットが、aaa[ABC]bbb[DEF]ccc[GHI]ddd となっている場合、
	 * aaa , bbb , ccc , ddd を引数 0 , 1 , 2 , 3 で返します。
	 *
	 * @param no フォーム位置番号
	 *
	 * @return フォーマット文字列
	 */
	public String getFormat( final int no ) {
		return format[no];
	}

	/**
	 * システムフォーマット文字列を返します。
	 * システムフォーマット文字列は、[KEY.カラム名] などの特殊記号で指定された
	 * カラム名の事で、location には、マイナスの値が設定されます。
	 * マイナスの値に応じて、処理を変えることが出来ます。
	 *
	 * [KEY.カラム名] : 行番号付きカラム名
	 * [I]            : 行番号
	 * [ROW.ID]       : 行毎のチェックボックスのID
	 * [ROW.JSON]     : 行毎の全データのJavaScriptオブジェクト形式
	 *
	 * @og.rev 3.5.5.0 (2004/03/12) [KEY.カラム名] 機能追加
	 * @og.rev 3.5.5.2 (2004/04/02) [I] で、行番号を作成します。
	 * @og.rev 3.6.0.0 (2004/09/17) [ROW.ID] で、行毎のチェックボックスのIDを返します。
	 * @og.rev 4.0.0.0 (2007/05/02) Formatter を使用するように変更
	 * @og.rev 6.2.0.1 (2015/03/06) 非表示のマーカーに、Formatter#NO_DISPLAY を使用する。
	 *
	 * @param	row 	行番号
	 * @param	loc 	位置番号
	 *
	 * @return フォーマット文字列
	 * @og.rtnNotNull
	 */
	public String getSystemFormat( final int row,final int loc ) {
		if( loc == Formatter.SYS_ROWNUM ) {
			return String.valueOf( row );
		}
		else if( loc == Formatter.SYS_JSON ) {
			return formatter.getJson( row );
		}
		else if( loc == Formatter.NO_DISPLAY ) {		// 6.2.0.1 (2015/03/06) 非表示のマーカー
			return "";
		}

		final String errMsg = "システムフォーマットは、下記の形式しか使用できません。[" + loc + "]" + CR
				+ "  : [KEY.カラム名] : 行番号付きカラム名" + CR
				+ "  : [I]            : 行番号" + CR
				+ "  : [ROW.ID]       : 行毎のチェックボックスのID" + CR
				+ "  : [ROW.JSON]     : 行毎の全データのJavaScriptオブジェクト形式" ;
		throw new HybsSystemException( errMsg );
	}

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

	/**
	 * 設定された フォーマットの trタグを返します。
	 * これは、trタグにclass属性他の設定がされていた場合に、変換後の
	 * 文字列にも反映させる為に必要です。
	 *
	 * @og.rev 5.1.7.0 (2010/06/01) サニタイズ戻し処理("\\]\\"から"["に戻し)を追加
	 *
	 * @return フォーマットの trタグ
	 * @og.rtnNotNull
	 */
	public String getTrTag() {
		if( trTag == null ) { return ""; }

		return decodeSanitizedStr( trTag ) ;
	}

	/**
	 * カラムのクラス名(X,S9 など)のセットを行うかどうか指定します。
	 *
	 * "true" で、クラス属性を設定しません。これは、ＣＳＳファイルに書かれている属性を
	 * 使用しないことを意味します。
	 * 初期値は、"false" です。
	 *
	 * @param	flag クラス名使用の有無(true:使用しない/false:使用する。)
	 */
	public void setNoClass( final String flag ) {
		noClass = StringUtil.nval( flag,noClass );
	}

	/**
	 * カラムのクラス名(X,S9 など)のセットを行うかどうか取得します。
	 *
	 * "true" で、クラス属性を設定しません。これは、ＣＳＳファイルに書かれている属性を
	 * 使用しないことを意味します。
	 * 初期値は、"false" です。
	 *
	 * @return	クラス名使用の有無(true:使用しない/false:使用する。)
	 */
	public boolean isNoClass() {
		return noClass;
	}

	/**
	 * フォーマットの使用可否を判断するキーとなるカラム名を指定します。
	 *
	 * キーが、usableList に含まれる場合は、このフォームを使用できます。
	 * キー(カラム名)が指定されない場合は、常に使用されます。
	 * ※ 現時点では、BODYタイプのみ使用しています。
	 *
	 * @param  key フォーマットの使用可否を判断するカラム名
	 */
	public void setUsableKey( final String key ) {
		usableKey = key;
	}

	/**
	 *  フォーマットの使用可否を判断する文字列リストを指定します。
	 *
	 * キーが、この文字列リスト中に存在する場合は、このフォームを使用できます。
	 * この文字列リストは、固定な文字列です。{&#064;XXXX}は使用できますが、[XXXX]は
	 * 使用できません。
	 * 初期値は、"1" です。
	 * ※ 現時点では、BODYタイプのみ使用しています。
	 *
	 * @param  list フォーマットの使用可否を判断する文字列リスト
	 * @see TableFormatter#isUse( int,DBTableModel )
	 */
	public void setUsableList( final String list ) {
		if( list != null ) {
			usableList = list;
		}
	}

	/**
	 * ここで指定したカラムの値が、キーブレイクした場合、このタグを使用します。
	 *
	 * キーブレイクで 使用可否を指定する為の機能です。
	 * この設定値は、usableKey,usableList とは、独立しているため、それぞれで
	 * 有効になれば、使用されると判断されます。
	 * キーブレイク判定では、最初の1件目は、必ず使用されると判断されます。
	 *
	 * @og.rev 5.7.6.3 (2014/05/23) 新規追加
	 *
	 * @param  kclm  キーブレイクをチェックするカラムID
	 */
	public void setKeyBreakClm( final String kclm ) {
		keyBreakClm = kclm;
	}

	/**
	 * このフォーマットを使用するかどうかの問い合わせを返します。
	 *
	 * "true" で、使用します。setUsableKey( String ) で、指定された
	 * カラム名の値が、setUsableList( String ) で指定された文字列に含まれていれば、
	 * 使用します。カラム名がセットされない場合は、デフォルト値("true")が使用されます。
	 * ※ 現時点では、BODYタイプのみ使用しています。
	 * カラムのデータに、不正なスペースが入る場合を想定して、trim() しています。
	 * よって、usableList の値にスペースは使用できません。
	 *
	 * 5.7.6.3 (2014/05/23) 以降は、keyBreakClm によるキーブレイクチェックも追加されました。
	 * 従来の usableKey,usableList とは、独立しているため、それぞれで有効になれば、
	 * 使用されると判断されます。
	 *
	 * @og.rev 3.5.6.2 (2004/07/05) 判定評価用カラムの値を trim() します。
	 * @og.rev 5.7.6.3 (2014/05/23) キーブレイクをチェックする keyBreakClm 属性追加
	 *
	 * @param  row 行番号
	 * @param	table	DBTableModelオブジェクト
	 *
	 * @return	このフォームを使用するかどうか(true:使用する/false:使用しない)
	 * @see TableFormatter#setUsableKey( String )
	 * @see TableFormatter#setUsableList( String )
	 */
	public boolean isUse( final int row, final DBTableModel table ) {
// 5.7.6.3 (2014/05/23) keyBreakClm 属性の追加で、処理ロジックの見直し
//		if( usableKeyNo < 0 ) { return true; }
//		String val = table.getValue( row,usableKeyNo ).trim();
//		return val.isEmpty() ? false : usableList.indexOf( val ) >= 0 ;

		// どちらも設定されていなければ、使用される(=true)
		if( usableKeyNo < 0 && breakClmNo < 0 ) { return true; }

		// 以下、どちらかは設定されているため、true の時点で、使用される(=true)を返す。
		if( usableKeyNo >= 0 ) {
			final String val = table.getValue( row,usableKeyNo ).trim();
			if( usableList.indexOf( val ) >= 0 ) { return true; }
		}

		if( breakClmNo >= 0 ) {
			final String val = table.getValue( row,breakClmNo ).trim();
			if( !val.equals( breakVal ) ) {			// 同じでない場合は、true
				breakVal = val;
				return true;
			}
		}

		return false ;			// 最後まで残ると、使用されないと判断、false を返す。
	}

	/**
	 *  itdフォーマット文字列を設定します。
	 *
	 * itd ボディ部の文字列を指定します。
	 * itd ボディは、繰り返し処理を行います。これを、上位のボディ文字列の中の
	 * HYBS_ITD_MARKER 文字列 と置き換えます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) itdフォーマット文字列の取り込み
	 *
	 * @param  itd itdフォーマットの文字列
	 */
	public void setItdBody( final String itd ) {
		if( itd != null ) {
			itdBody = itd;
		}
	}

	/**
	 *  itdフォーマット文字列を取得します。
	 *
	 * itd ボディ部の文字列を取得します。
	 * itd ボディは、繰り返し処理を行います。これを、上位のボディ文字列の中の
	 * HYBS_ITD_MARKER 文字列 と置き換えます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) itdフォーマット文字列の取り込み
	 *
	 * @return	itdフォーマットの文字列
	 */
	public String getItdBody() {
		return itdBody;
	}

	/**
	 * サニタイズの戻し("\\]\\"から"["に戻し)を行います。
	 *
	 * @og.rev 5.1.7.0 (2010/06/01) 新規作成
	 *
	 * @param str サニタイズされた文字列
	 *
	 * @return サニタイズ戻し処理後の文字列
	 */
	private String decodeSanitizedStr( final String str ) {
		if( str != null && str.indexOf( "\\]\\" ) >= 0 ) {
			return str.replace( "\\]\\", "[" );
		}
		else {
			return str;
		}
	}
}
