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

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.html.TableFormatter;
import org.opengion.hayabusa.html.ViewGanttTableParam;

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

import java.util.Locale;
import java.util.TreeSet;
import java.util.List;
import java.util.Date;
import java.util.Calendar;
import java.text.SimpleDateFormat;
import java.text.ParseException;

/**
 * ガントチャート(テーブル形式)を作成する、ガントチャート表示クラスです。
 *
 * AbstractViewForm により、setter/getterメソッドのデフォルト実装を提供しています。
 * 各HTMLのタグに必要な setter/getterメソッドのみ，追加定義しています。
 *
 * AbstractViewForm を継承している為,ロケールに応じたラベルを出力させる事が出来ます。
 *
 * @og.group 画面表示
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ViewForm_HTMLGanttTable extends ViewForm_HTMLTable {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.4.2.0 (2016/01/29)" ;

	// 3.5.4.0 (2003/11/25) TableFormatter クラス追加
	private TableFormatter 		headerFormat	;
	private TableFormatter[]	bodyFormats		;
	private TableFormatter		footerFormat	;
	private int					bodyFormatsCount;

	// 繰り返すTD用 3.5.6.0 (2004/06/18) 廃止
	private TableFormatter[]	itdFormats		;		// 追加

	private String ganttHeadLine		;
	private int[]  groupCols			;
	private int    posDuration			= -1;
	private int    posDystart			= -1;

	private String formatDystart		= ViewGanttTableParam.DYSTART_FORMAT_VALUE;
	private double  minDuration			= StringUtil.parseDouble( ViewGanttTableParam.MIN_DURATION_VALUE ) ;	// 3.5.5.8 (2004/05/20)
	private double  headerDuration		= minDuration ;	// 3.5.5.8 (2004/05/20)

	// 3.5.4.6 (2004/01/30) 初期値変更
	private static final int BODYFORMAT_MAX_COUNT = 10;

	// <(td|th)(.*)>(.*)</(td|th)>を確認する
	private static final Pattern TDTH_PTN =									// 6.4.1.1 (2016/01/16) cpTdTh  → TDTH_PTN  refactoring
		Pattern.compile("\\<(td|th)([^\\>]*)\\>(.*)\\</(td|th)\\>"
				, Pattern.DOTALL + Pattern.CASE_INSENSITIVE);
	// 3.5.5.9 (2004/06/07)
	private int maxDayCnt				;
	private String headerLocale			;
	private String[] headDays			;
	private int headDaysCnt				;

	// 3.5.6.3 (2004/07/12) 行チェックによる編集用の hidden
	private static final String CHECK_ROW =
		"<input type=\"hidden\" name=\"" + HybsSystem.ROW_SEL_KEY + "\" value=\"";

	// 3.6.1.0 (2005/01/05) 開始日付けと終了日付けの指定
	private boolean useSeqDay		;
	private String startDay			;
	private String endDay			;

	private boolean useItd			;		// 5.0.0.3 (2009/09/22)

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

	/**
	 * 内容をクリア(初期化)します。
	 *
	 * @og.rev 3.1.1.0 (2003/03/28) 同期メソッド(synchronized付き)を非同期に変更する。
	 * @og.rev 3.5.0.0 (2003/09/17) Noカラムに、表示を全て消せるように、class 属性を追加。
	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
	 * @og.rev 3.5.5.8 (2004/05/20) minDuration ,  headerDuration 追加。 不要な変数削除
	 * @og.rev 3.5.6.0 (2004/06/18) ithFormat ,  itdFormat 属性削除、itdFormats属性を追加
	 * @og.rev 3.6.1.0 (2005/01/05) startDay,endDay,useSeqDay 属性追加
	 * @og.rev 5.0.0.3 (2009/09/22) itdタグの有無でcolspan対策のtdの出力個数を調整
	 */
	@Override
	public void clear() {
		super.clear();

		headerFormat		= null;
		bodyFormats			= null;
		footerFormat		= null;
		bodyFormatsCount	= 0;

		ganttHeadLine		= null;
		itdFormats			= null;		// 3.5.6.0 (2004/06/18)
		groupCols			= null;
		posDuration			= -1  ;
		posDystart			= -1;
		formatDystart		= ViewGanttTableParam.DYSTART_FORMAT_VALUE;
		minDuration			= StringUtil.parseDouble( ViewGanttTableParam.MIN_DURATION_VALUE ) ;	// 3.5.5.8 (2004/05/20)
		headerDuration		= minDuration ;		// 3.5.5.8 (2004/05/20)

		maxDayCnt			= 0;	// 3.5.5.9 (2004/06/07)
		headerLocale		= null;	// 3.5.5.9 (2004/06/07)
		headDays			= null;	// 3.5.5.9 (2004/06/07)
		headDaysCnt			= 0;	// 3.5.5.9 (2004/06/07)

		useSeqDay			= false;	// 3.6.1.0 (2005/01/05)
		startDay			= null;	// 3.6.1.0 (2005/01/05)
		endDay				= null;	// 3.6.1.0 (2005/01/05)

		useItd			= false; // 5.0.0.3 (2009/09/22)
	}

	/**
	 * DBTableModel から HTML文字列を作成して返します。
	 * startNo(表示開始位置)から、pageSize(表示件数)までのView文字列を作成します。
	 * 表示残りデータが pageSize 以下の場合は,残りのデータをすべて出力します。
	 *
	 * @og.rev 3.5.0.0 (2003/09/17) BODY要素の noClass 属性を追加。
	 * @og.rev 3.5.0.0 (2003/09/17) &lt;tr&gt;属性は、元のフォーマットのまま使用します。
	 * @og.rev 3.5.2.0 (2003/10/20) ヘッダー繰り返し属性( headerSkipCount )を採用
	 * @og.rev 3.5.3.1 (2003/10/31) skip属性を採用。headerLine のキャッシュクリア
	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
	 * @og.rev 3.5.5.0 (2004/03/12) systemFormat(例：[KEY.カラム名]形式等)の対応
	 * @og.rev 3.5.5.9 (2004/06/07) IEの colspan が上手く動かない対策。
	 * @og.rev 3.5.5.9 (2004/06/07) [#カラム名] , [$カラム名] に対応
	 * @og.rev 3.5.6.0 (2004/06/18) itdFormat を、BODY毎のFormatを使用するように修正
	 * @og.rev 3.5.6.0 (2004/06/18) '!' 値のみ 追加 既存の '$' は、レンデラー
	 * @og.rev 3.5.6.3 (2004/07/12) 行チェックによる編集が出来るように機能を追加
	 * @og.rev 3.5.6.4 (2004/07/16) ヘッダーとボディー部をJavaScriptで分離
	 * @og.rev 3.6.1.0 (2005/01/05) 行チェックによる編集が、検索即登録時も可能なようにします。
	 * @og.rev 4.0.0.0 (2005/01/31) 新規作成(getColumnClassName ⇒ getColumnDbType)
	 * @og.rev 3.7.0.1 (2005/01/31) E の colspan バグ対応で入れた最終行の 空タグを消す為の修正
	 * @og.rev 4.3.1.0 (2008/09/08) フォーマットが設定されていない場合のエラー追加
	 * @og.rev 4.3.7.4 (2009/07/01) tbodyタグの入れ子を解消(FireFox対応)
	 * @og.rev 5.0.0.3 (2009/09/22) itdタグの有無でcolspan対策のtdの出力個数を調整
	 * @og.rev 5.5.4.4 (2012/07/20) 二重チェック状態になってしまう対策
	 * @og.rev 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
	 *
	 * @param  stNo	  表示開始位置
	 * @param  pgSize   表示件数
	 *
	 * @return	DBTableModelから作成された HTML文字列
	 * @og.rtnNotNull
	 */
	@Override
	public String create( final int stNo, final int pgSize )  {
//		// ガントは、キーブレイクがあるため、全件表示します。
//		final int startNo  = 0;
		final int pageSize = getRowCount() ;
		if( pageSize == 0 ) { return ""; }	// 暫定処置
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//		boolean outputCheck = true; // 5.5.4.4. (2012/07/20) チェックボックス出力判定

		// 4.3.1.0 (2008/09/08)
		if( headerFormat == null ) {
			final String errMsg = "ViewTagで canUseFormat() = true の場合、Formatter は必須です。";
			throw new HybsSystemException( errMsg );
		}

		headerLine	 = null;		// 3.5.3.1 (2003/10/31) キャッシュクリア

		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
		// ガントは、キーブレイクがあるため、全件表示します。
		final int startNo  = 0;
		final int lastNo = getLastNo( startNo, pageSize );
		final int blc = getBackLinkCount();
		final int hsc = getHeaderSkipCount();		// 3.5.2.0 (2003/10/20)
		int hscCnt = 1;						// 3.5.2.0 (2003/10/20)

		// このビューの特有な属性を初期化
		paramInit();

		final StringBuilder out = new StringBuilder( BUFFER_LARGE );

		out.append( getCountForm( startNo,pageSize ) );

		if( posDuration < 0 ) { ganttHeadLine = getGanttHeadNoDuration(startNo, lastNo); }
		else {					ganttHeadLine = getGanttHead(startNo, lastNo); }

		out.append( getHeader() );

		if( bodyFormatsCount == 0 ) {
			bodyFormats[0] = headerFormat ;
			bodyFormatsCount ++ ;
		}
		else {
			for( int i=0; i<bodyFormatsCount; i++ ) {
				bodyFormats[i].makeFormat( getDBTableModel() );
				// 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
				setFormatNoDisplay( bodyFormats[i] );

				if( itdFormats[i] != null ) {
					itdFormats[i].makeFormat( getDBTableModel() );
					// 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
					setFormatNoDisplay( itdFormats[i] );
				}
			}
		}

		String[] astrOldGroupKeys = new String[groupCols.length];
		for( int nIndex =0; nIndex<astrOldGroupKeys.length; nIndex++ ) {
			astrOldGroupKeys[nIndex] = "";
		}

		final StringBuilder bodyBuf = new StringBuilder( BUFFER_LARGE );		// 6.1.0.0 (2014/12/26) refactoring
		final StringBuilder itdBuf  = new StringBuilder( BUFFER_LARGE );		// 6.1.0.0 (2014/12/26) refactoring
		boolean checked = false;		// 3.5.6.3 (2004/07/12)
		int bgClrCnt = 0;
		TableFormatter itdFormat = null;
		for( int row=startNo; row<lastNo; row++ ) {
			// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//			outputCheck = true;					// 5.5.4.4. (2012/07/20) チェックボックス出力判定
			boolean outputCheck = true;			// 5.5.4.4. (2012/07/20) チェックボックス出力判定
			// ガントでは、データのスキップは行いません。

			if( !isSameGroup(row, astrOldGroupKeys) ) {
				// 3.5.6.3 (2004/07/12) キーブレイク時にチェック行かどうかを記録
				checked = getDBTableModel().isRowChecked( row );

				if( row != startNo ) {
					// ヘッダー日付けが残っているのに、データがなくなった場合
					while( headDays != null && headDaysCnt < headDays.length ) {
						itdBuf.append( "<td></td>" );
						headDaysCnt++;
					}
					out.append(StringUtil.replace(bodyBuf.toString(), TableFormatter.HYBS_ITD_MARKER, itdBuf.toString()));
					itdBuf.setLength(0);		// 6.1.0.0 (2014/12/26) refactoring
					headDaysCnt	= 0;
				}

				bodyBuf.setLength(0);

				// カラムのグループがブレイクするまで、BodyFormatは同一である前提
				for( int i=0; i<bodyFormatsCount; i++ ) {
					final TableFormatter bodyFormat = bodyFormats[i];
					if( ! bodyFormat.isUse( row,getDBTableModel() ) ) { continue; }		// 3.5.4.0 (2003/11/25)
					itdFormat = itdFormats[i];		//

					bodyBuf.append("<tbody")
						.append( getBgColorCycleClass( bgClrCnt++ ) )
						.append('>')										// 6.0.2.5 (2014/10/31) char を append する。
						.append( bodyFormat.getTrTag() );

					// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
					if( isNumberDisplay() ) {
						final String ckboxTD = "<td" + bodyFormat.getRowspan() + ">";
						bodyBuf.append( makeCheckbox( ckboxTD,row,blc ) );
						outputCheck = false; // 5.5.4.4. (2012/07/20)
					}

					int cl = 0;
					for( ; cl<bodyFormat.getLocationSize(); cl++ ) {
						String fmt = bodyFormat.getFormat(cl);
						final int loc = bodyFormat.getLocation(cl);	// 3.5.5.0 (2004/03/12)
						// 6.1.0.0 (2014/12/26) refactoring
						if( ! bodyFormat.isNoClass() && loc >= 0 ) {
							final String tdclass = "<td class=\"" + getColumnDbType(loc) + "\" ";
//							fmt = StringUtil.replace( bodyFormat.getFormat(cl) ,"<td", tdclass );
							fmt = StringUtil.replace( fmt ,"<td", tdclass );						// 6.3.9.1 (2015/11/27)
						}
						bodyBuf.append( fmt );					// 3.5.0.0 (2003/09/17)
						// 3.5.5.9 (2004/06/07) #,$ 対応
						if( loc >= 0 ) {
							switch( bodyFormat.getType(cl) ) {
								case '#' : bodyBuf.append( getColumnLabel(loc) );		break;
								case '$' : bodyBuf.append( getRendererValue(row,loc) );	break;
								case '!' : bodyBuf.append( getValue(row,loc) );			break;
								default  : bodyBuf.append( getValueLabel(row,loc) );	break;
							}
						}
						else {
							bodyBuf.append( bodyFormat.getSystemFormat(row,loc) );
						}
					}
					bodyBuf.append( bodyFormat.getFormat(cl) )
						.append("</tbody>").append( CR );
				}

				// 3.5.2.0 (2003/10/20) ヘッダー繰り返し属性( headerSkipCount )を採用
				if( hsc > 0 && hscCnt % hsc == 0 ) {
					bodyBuf.append("<tbody class=\"row_h\" >")
						.append( getHeadLine() )
						.append("</tbody>");
					hscCnt = 1;
				}
				else {
					hscCnt ++ ;
				}
			}

			// 3.5.6.3 (2004/07/12) キーブレイク時のチェック行の状態を繰り返し行に反映
			// 3.6.1.0 (2005/01/05) さらに、外部でチェックを入れている場合は、個別対応する。
			if( (checked || isChecked( row )) && outputCheck ) {	 // 5.5.4.4. (2012/07/20)
				getDBTableModel().setRowWritable( row,true );
				out.append( CHECK_ROW )
					.append( row )
					.append( "\" />" );
			}

			formatItd(row, itdFormat, itdBuf);
		}

		// 残ったデータを出力
		if( itdBuf.length() > 0 ) {
			// ヘッダー日付けが残っているのに、データがなくなった場合
			while( headDays != null && headDaysCnt < headDays.length ) {
				itdBuf.append( "<td></td>" );
				headDaysCnt++;
			}
			out.append(StringUtil.replace(bodyBuf.toString(), TableFormatter.HYBS_ITD_MARKER, itdBuf.toString()));
		}

		// 3.5.5.9 (2004/06/07) IEの colspan が上手く動かない対策。
		// minDuration が、1.0 以下の場合のみ実行
		if( minDuration < 1.0d ) {
			// int tdCount = (int)Math.round( maxDayCnt / minDuration );
			// 5.0.0.3 (2009/09/22) itdタグの有無でtdCountを変える。
			final int tdCount = useItd ? (int) Math.round( maxDayCnt / minDuration ) : headerFormat.getLocationSize();

	 		// 3.7.0.1 (2005/01/31) E の colspan バグ対応で入れた最終行の 空タグを消す為の修正
			out.append("<tbody><tr class=\"dummy\">").append( CR );
			for( int i=0; i<tdCount; i++ ) {
				out.append( "<td/>" );
			}
			out.append("</tr></tbody>").append( CR );
		}

		if( footerFormat != null ) {
			// 6.3.9.0 (2015/11/06) 引数にTableFormatterを渡して、処理の共有化を図る。
//			out.append( getTableFoot() );
			out.append( getTableFoot( footerFormat ) );
		}

		out.append("</table>").append( CR )
			.append( getScrollBarEndDiv() );	// 3.8.0.3 (2005/07/15)

		return out.toString();
	}

	/**
	 * このビーに対する特別な初期化を行う。
	 *
	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
	 * @og.rev 3.5.5.9 (2004/06/07) ヘッダーの日付け表示に、Locale を加味できるように変更
	 * @og.rev 3.6.1.0 (2005/01/05) startDay,endDay,useSeqDay 属性追加
	 */
	private void paramInit() {

		final String strGroupCols	= getParam( ViewGanttTableParam.GROUP_COLUMNS_KEY	,ViewGanttTableParam.GROUP_COLUMNS_VALUE	);
		final String strColDuration	= getParam( ViewGanttTableParam.DURATION_COLUMN_KEY	,null					);
		final String strColDystart	= getParam( ViewGanttTableParam.DYSTART_COLUMN_KEY	,ViewGanttTableParam.DYSTART_COLUMN_VALUE	);
		final String strMinDuration	= getParam( ViewGanttTableParam.MIN_DURATION_KEY	,ViewGanttTableParam.MIN_DURATION_VALUE		);
//		final String strHeadDuration= getParam( ViewGanttTableParam.HEADER_DURATION_KEY	,strMinDuration			);

		formatDystart	= getParam( ViewGanttTableParam.DYSTART_FORMAT_KEY	,ViewGanttTableParam.DYSTART_FORMAT_VALUE	);
		headerLocale	= getParam( ViewGanttTableParam.HEADER_LOCALE_KEY	,ViewGanttTableParam.HEADER_LOCALE_VALUE	);
		startDay		= getParam( ViewGanttTableParam.START_DAY_KEY		,null					);	// 3.6.1.0 (2005/01/05)
		endDay			= getParam( ViewGanttTableParam.END_DAY_KEY			,null					);	// 3.6.1.0 (2005/01/05)

		final String seqDay	= getParam( ViewGanttTableParam.USE_SEQ_DAY_KEY  ,ViewGanttTableParam.USE_SEQ_DAY_VALUE  );	// 3.6.1.0 (2005/01/05)
		useSeqDay	= Boolean.parseBoolean( seqDay );		// 6.1.0.0 (2014/12/26) refactoring

		final DBTableModel table = getDBTableModel();

		// 3.5.5.9 (2004/06/07) durationColumn を指定しない場合の処理を追加
		if( strColDuration != null ) {
			posDuration = table.getColumnNo( strColDuration );
		}

		posDystart  = table.getColumnNo( strColDystart );

		final String[] groupKeys = StringUtil.csv2Array(strGroupCols);
		groupCols = new int[groupKeys.length];
		for( int nIndex=0; nIndex<groupCols.length ; nIndex++ ) {
			groupCols[nIndex] = table.getColumnNo( groupKeys[nIndex] );
		}

		minDuration = StringUtil.parseDouble( strMinDuration );
		if( minDuration <= 0.0d ) {
			final String errMsg = "最小期間単位(minDuration)が、０ かそれ以下です。";
			throw new HybsSystemException( errMsg );
		}

		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
		final String strHeadDuration= getParam( ViewGanttTableParam.HEADER_DURATION_KEY	,strMinDuration			);
		headerDuration = StringUtil.parseDouble( strHeadDuration );
		if( headerDuration <= 0.0d ) {
			final String errMsg = "ヘッダーの表示期間(headerDuration)が、０ かそれ以下です。";
			throw new HybsSystemException( errMsg );
		}

		// 3.5.5.9 (2004/06/07) エラーチェックの強化
		// 4.0.0 (2005/01/31) Equality checks with floating point numbers can lead to unexpected behavior.
		if( posDuration < 0 && (
				Double.compare( minDuration,1.0d ) != 0 ||
				Double.compare( headerDuration,1.0d ) != 0 ) ) {

			final String errMsg = "期間カラム(durationColumn)を指定しない場合は、"
					+ "最小期間単位(minDuration)および、"
					+ "ヘッダーの表示期間(headerDuration)を '1' 以外に設定できません。";
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 上下行のデータが同じグルプかどうかをチェックする。
	 *
	 * @param   nRowIndex テーブルモデルの行番号
	 * @param   astrOldValues 古いグループデータ配列
	 *
	 * @return  使用可能(true)/ 使用不可能 (false)
	 */
	private boolean isSameGroup(final int nRowIndex, final String[] astrOldValues) {
		boolean bRet = groupCols.length > 0 ;
		if( bRet ) {
			for( int nIndex=0; bRet && nIndex<groupCols.length ; nIndex++ ) {
				bRet = astrOldValues[nIndex].equals( getValue( nRowIndex, groupCols[nIndex] ) );
			}

			// 不一致時に astrOldValues に 新しい値を設定しておきます。
			if( !bRet ) {
				for( int nIndex=0; nIndex<groupCols.length; nIndex++ ) {
					astrOldValues[nIndex] = getValue(nRowIndex, groupCols[nIndex]);
				}
			}
		}

		return bRet;
	}

	/**
	 * DBTableModel から テーブルのタグ文字列を作成して返します。
	 *
	 * @og.rev 3.5.0.0 (2003/09/17) &lt;tr&gt;属性は、元のフォーマットのまま使用します。
	 * @og.rev 3.5.1.0 (2003/10/03) Noカラムに、numberType 属性を追加
	 * @og.rev 3.5.2.0 (2003/10/20) ヘッダー繰り返し部をgetHeadLine()へ移動
	 * @og.rev 3.5.3.1 (2003/10/31) VERCHAR2 を VARCHAR2 に修正。
	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
	 * @og.rev 3.5.6.5 (2004/08/09) thead に、id="header" を追加
	 * @og.rev 4.0.0.0 (2005/01/31) DBColumn の 属性(CLS_NM)から、DBTYPEに変更
	 * @og.rev 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
	 * @og.rev 5.9.1.2 (2015/10/23) 自己終了警告対応
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getTableHead() {
		headerFormat.makeFormat( getDBTableModel() );
		// 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
		setFormatNoDisplay( headerFormat );

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
		if( isNumberDisplay() ) {
			// 5.9.1.2 (2015/10/23) 自己終了警告対応
//			buf.append("<colgroup class=\"X\" /><colgroup class=\"BIT\" /><colgroup class=\"S9\" />")
			buf.append("<colgroup class=\"X\" ><!-- --></colgroup><colgroup class=\"BIT\" ><!-- --></colgroup><colgroup class=\"S9\" ><!-- --></colgroup>")
				.append(CR);
		}

		// 3.5.2.0 (2003/10/20) ヘッダー繰り返し部をgetHeadLine()へ移動
		buf.append("<thead id=\"header\">").append( CR )	// 3.5.6.5 (2004/08/09)
			.append( getHeadLine() )
			.append("</thead>").append( CR );

		return buf.toString();
	}

	/**
	 * ヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 *
	 * @og.rev 6.1.2.0 (2015/01/24) キャッシュを返すのを、#getHeadLine() に移動。
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getHeadLine() {
		if( headerLine == null ) {					// キャッシュになければ、設定する。
			headerLine = getHeadLine( "<th" + headerFormat.getRowspan() ) ;
		}

		return headerLine ;
	}

	/**
	 * ヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 *
	 * @og.rev 3.5.2.0 (2003/10/20) 新規作成
	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
	 * @og.rev 3.5.4.3 (2004/01/05) useCheckControl 属性の機能を追加
	 * @og.rev 3.5.4.6 (2004/01/30) numberType="none" 時の処理を追加(Noラベルを出さない)
	 * @og.rev 3.5.4.7 (2004/02/06) ヘッダーにソート機能用のリンクを追加します。
	 * @og.rev 3.5.5.0 (2004/03/12) systemFormat(例：[KEY.カラム名]形式等)の対応
	 * @og.rev 3.7.0.1 (2005/01/31) 全件チェックコントロール処理変更
	 * @og.rev 5.0.0.3 (2009/09/22) itdの有無を取得します。
	 * @og.rev 6.1.2.0 (2015/01/24) キャッシュを返すのを、#getHeadLine() に移動。
	 *
	 * @param	thTag タグの文字列
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	@Override
	protected String getHeadLine( final String thTag ) {

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( headerFormat.getTrTag() ).append( CR );

		// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
		if( isNumberDisplay() ) {
			// 6.1.2.0 (2015/01/24) thTag に、headerFormat.getRowspan() を含ませて受け取る。
			// 3.5.4.3 (2004/01/05) 追加分
			if( isUseCheckControl() && "checkbox".equals( getSelectedType() ) ) {
				buf.append( thTag ).append( "></th>" )
					.append( thTag ).append( '>' ).append( getAllCheckControl() ).append( "</th>" )		// 6.0.2.5 (2014/10/31) char を append する。
					.append( thTag ).append( '>' ).append( getNumberHeader()    ).append( "</th>" );	// 6.0.2.5 (2014/10/31) char を append する。
			}
			else {
				buf.append( thTag ).append(" colspan=\"3\">").append( getNumberHeader() ).append( "</th>" );	// 6.0.2.5 (2014/10/31) char を append する。
			}
		}

		int cl = 0;
		for( ; cl<headerFormat.getLocationSize(); cl++ ) {
			buf.append( headerFormat.getFormat(cl) );
			final int loc = headerFormat.getLocation(cl);
			if( loc >= 0 ) { buf.append( getSortedColumnLabel(loc) ); }
		}
		buf.append( headerFormat.getFormat(cl) ).append( CR );

		// 5.0.0.3 (2009/09/22) ITD_MARKERの条件判断追加
		if( buf.indexOf( TableFormatter.HYBS_ITD_MARKER ) >= 0 ){
			useItd = true;
		}

		return StringUtil.replace( buf.toString(), TableFormatter.HYBS_ITD_MARKER, ganttHeadLine );
	}

	/**
	 * ガントチャートヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 * このメソッドは、durationColumn を利用して、連続日付けデータを作成します。
	 * データは、開始日と期間データを持ち、ヘッダーは連続日付けになります。
	 * ヘッダーの期間(headerDuration)とデータの最小期間(minDuration)が異なる為、
	 * 最初の行データより、最終日を求め、headerDuration で割り算した個数の連続日数を
	 * 表示させています。
	 *
	 * @og.rev 3.5.5.9 (2004/06/07) ヘッダーの日付け表示に、Locale を加味できるように変更
	 * @og.rev 3.5.6.0 (2004/06/18) ithFormat 変数削除
	 *
	 * @param	startNo	開始行番号
	 * @param	lastNo	最終行番号
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	private String getGanttHead(final int startNo, final int lastNo) {
		String[] astrOldGroupKeys = new String[groupCols.length];
		for( int nIndex =0; nIndex<astrOldGroupKeys.length; nIndex++ ) {
			astrOldGroupKeys[nIndex] = "";
		}

		// 3.5.5.9 (2004/06/07) ヘッダーの日付け表示に、Locale を加味できるように変更
		final Locale local = new Locale( headerLocale );
		final SimpleDateFormat fmtDate = new SimpleDateFormat( formatDystart,local );

		double nSumDuration = 0.0d ;
		Date dFirst = null ;

		for( int nRowUp=startNo; nRowUp<lastNo; nRowUp++ ) {
			// ガントでは、データのスキップは行いません。

			// 最初の行 または、ブレイクするまで
			if( isSameGroup(nRowUp, astrOldGroupKeys) || nRowUp == startNo ) {
				nSumDuration += StringUtil.parseDouble( getValue(nRowUp, posDuration) );
				try {
					if( dFirst == null ) {
						dFirst = fmtDate.parse(getValue(nRowUp, posDystart));
					}
				}
				catch ( ParseException ex) {
					final String errMsg = "DYSTARTに指定した日付形式と異なるデータが存在しています。"
							+ "[" + getValue(nRowUp  , posDystart) + "] => ["
							+ fmtDate.toPattern() + "]" ;
					throw new HybsSystemException( errMsg,ex );
				}
			}
			else { break; }
		}

		String thVal	= ""     ;		// <td ･･･> の <td 以下の ･･･部分の属性文字列。
		String ymdForm	= "MM/dd" ;		// td タグの BODY 部 の 日付けフォーマット

		if( headerFormat != null ) {
			final String format = headerFormat.getItdBody().trim() ;
			final Matcher matcher = TDTH_PTN.matcher(format);
			if( matcher.find() ) {
				thVal   = matcher.group(2);
				ymdForm = matcher.group(3);
			}
		}

		try {
			fmtDate.applyPattern(ymdForm);
		}
		catch(IllegalArgumentException eArg) {
			final String errMsg = "theadの内のitdの内側に指定された日付の形式が不正です。(" + ymdForm + ")";
			throw new HybsSystemException( errMsg,eArg );
		}

		final int colspan = (int)Math.round( headerDuration / minDuration );
		final String th ;
		if( colspan == 1 ) { th = "<th " + thVal + ">"; }
		else { th = "<th colspan=\"" + colspan + "\" " + thVal + ">" ; }

		final Calendar cal = Calendar.getInstance() ;
		cal.setTime( dFirst );

		maxDayCnt = (int)Math.round(nSumDuration / headerDuration) ;		// 3.5.5.9 (2004/06/07)
		int addDate ;
		int field   ;
		if( headerDuration >= 1.0d ) {
			addDate = (int)Math.round( headerDuration );
			field   = Calendar.DATE ;
		}
		else {
			addDate = (int)Math.round( 24.0d * headerDuration );
			field   = Calendar.HOUR_OF_DAY ;
		}

		// 端数を指定すると、積算誤差がでます。
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		for( int nIndex=0; nIndex<maxDayCnt; nIndex++ ) {
			buf.append( th );
			buf.append( fmtDate.format( cal.getTime() ) );
			buf.append( "</th>" );
			cal.add( field ,addDate );
		}

		return buf.toString();
	}

	/**
	 * ガントチャートヘッダー繰り返し部を、getTableHead()メソッドから分離。
	 * このメソッドは、durationColumn が指定されていない場合の処理を行います。
	 * データは、すべての行に関して、同じ日付けのデータとして扱われます。
	 * よって、行間で、日付け違いの並び順になっているとずれることがあります。
	 * ヘッダーは、最初の行の日付けをそのまま表示させます。よって、データと
	 * 日付けが同期されていれば、不連続な日付けのヘッダーを表示させることも可能です。
	 * ヘッダーの期間(headerDuration)とデータの最小期間(minDuration)は、
	 * ともに、'1' であることが前提です。
	 * useSeqDay 属性に、"true" を設定すると、開始日(startDay)と終了日(endDay)
	 * の日付けを連続した日付けとします。開始日(startDay)や終了日(endDay)が指定
	 * されていない場合は、dystartColumn 属性で指定されたカラムの値の最大値、最小値を
	 * 自動セットします。
	 *
	 * @og.rev 3.5.5.9 (2004/06/07) 新規作成
	 * @og.rev 3.5.6.0 (2004/06/18) ithFormat 変数削除
	 * @og.rev 3.6.1.0 (2005/01/05) startDay,endDay,useSeqDay 属性追加
	 *
	 * @param	startNo	開始行番号
	 * @param	lastNo	最終行番号
	 *
	 * @return	テーブルのタグ文字列
	 * @og.rtnNotNull
	 */
	private String getGanttHeadNoDuration(final int startNo, final int lastNo) {
		String[] astrOldGroupKeys = new String[groupCols.length];
		for( int nIndex=0; nIndex<astrOldGroupKeys.length; nIndex++ ) {
			astrOldGroupKeys[nIndex] = "";
		}

		String thVal	= ""     ;		// <td ･･･> の <td 以下の ･･･部分の属性文字列。
		String ymdForm	= "MM/dd" ;		// td タグの BODY 部 の 日付けフォーマット

		if( headerFormat != null ) {
			final String format = headerFormat.getItdBody().trim() ;
			final Matcher matcher = TDTH_PTN.matcher(format);
			if( matcher.find() ) {
				thVal   = matcher.group(2);
				ymdForm = matcher.group(3);
			}
		}

		// 3.5.5.9 (2004/06/07) ヘッダーの日付け表示に、Locale を加味できるように変更
		final Locale local = new Locale( headerLocale );
		final SimpleDateFormat dataFmt = new SimpleDateFormat( formatDystart,local );
//		final SimpleDateFormat headFmt = new SimpleDateFormat( ymdForm,Locale.JAPAN );
//		final Calendar cal = Calendar.getInstance() ;

		final TreeSet<String> daySet = new TreeSet<>();
		for( int nRowUp=startNo; nRowUp<lastNo; nRowUp++ ) {
			// ガントでは、データのスキップは行いません。

			final String day = getValue(nRowUp, posDystart);
			daySet.add( day );
		}
		// 3.6.1.0 (2005/01/05)
		if( useSeqDay ) {
			if( startDay == null ) { startDay = daySet.first() ; }
			if( endDay   == null ) { endDay   = daySet.last()  ; }

			try {
				final Calendar startCal = Calendar.getInstance() ;
				final Date dStart = dataFmt.parse( startDay );
				startCal.setTime( dStart );

				final Calendar endCal = Calendar.getInstance() ;
				final Date dEnd = dataFmt.parse( endDay );
				endCal.setTime( dEnd );
				endCal.set( Calendar.HOUR_OF_DAY,12 );	// 日付け比較する為、１２時間進めておく。

				while( startCal.before( endCal ) ) {
					daySet.add( dataFmt.format( startCal.getTime() ) );
					startCal.add( Calendar.DATE ,1 );
				}
			}
			catch ( ParseException ex) {
				final String errMsg = "startDay,endDayに指定した日付形式と異なるデータが存在しています。"
						+ "[" + startDay + "],["
						+ "[" + endDay + "] => ["
						+ dataFmt.toPattern() + "]" ;
				throw new HybsSystemException( errMsg,ex );
			}
		}

		headDays = daySet.toArray( new String[daySet.size()] );	// 4.0.0 (2005/01/31)

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		final String th = "<th " + thVal + ">";					// 6.1.0.0 (2014/12/26) ソースの統一のため、一旦
		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
		final SimpleDateFormat headFmt = new SimpleDateFormat( ymdForm,Locale.JAPAN );
		final Calendar cal = Calendar.getInstance() ;
		for( int i=0; i<headDays.length; i++ ) {
			try {
				cal.setTime( dataFmt.parse(headDays[i]) );
				buf.append( th )
					.append( headFmt.format( cal.getTime() ) )
					.append( "</th>" );
			}
			catch ( ParseException ex) {
				final String errMsg = "DYSTARTに指定した日付形式と異なるデータが存在しています。"
						+ "[" + headDays[i] + "] => ["
						+ dataFmt.toPattern() + "]" ;
				throw new HybsSystemException( errMsg,ex );
			}
		}

		return buf.toString();
	}

//	/**
//	 * DBTableModel から テーブルのタグ文字列を作成して返します。
//	 *
//	 * @og.rev 3.5.0.0 (2003/09/17) &lt;tr&gt;属性は、元のフォーマットのまま使用します。
//	 * @og.rev 3.5.1.0 (2003/10/03) Noカラムに、numberType 属性を追加
//	 * @og.rev 3.5.4.0 (2003/11/25) TableFormatter クラスを使用するように変更。
//	 * @og.rev 3.5.4.7 (2004/02/06) ヘッダーにソート機能用のリンクを追加します。
//	 * @og.rev 3.5.5.0 (2004/03/12) systemFormat(例：[KEY.カラム名]形式等)の対応
//	 * @og.rev 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
//	 * @og.rev 6.3.9.0 (2015/11/06) 引数にTableFormatterを渡して、処理の共有化を図る。
//	 *
//	 * @return	テーブルのタグ文字列
//	 * @og.rtnNotNull
//	 */
//	protected String getTableFoot() {
//		footerFormat.makeFormat( getDBTableModel() );
//		// 6.2.0.0 (2015/02/27) フォーマット系の noDisplay 対応
//		setFormatNoDisplay( footerFormat );
//
//		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
//			.append("<tfoot>").append( CR )
//			.append( footerFormat.getTrTag() ).append( CR );
//
//		// 3.5.5.0 (2004/03/12) No 欄そのものの作成判断追加
//		if( isNumberDisplay() ) {
//			buf.append("<th colspan=\"3\"")
//				.append( footerFormat.getRowspan() )
//				.append("></th>");
//		}
//
//		int cl = 0;
//		for( ; cl < footerFormat.getLocationSize(); cl++ ) {
//			final int loc = footerFormat.getLocation(cl);
//			if( loc >= 0 ) { buf.append( getSortedColumnLabel(loc) ); }
//		}
//		buf.append( footerFormat.getFormat(cl) ).append( CR )
//			.append("</tfoot>").append( CR );
//
//		return buf.toString();
//	}

	/**
	 * itaタグの中身を形式化する。
	 *
	 * @og.rev 3.5.5.0 (2004/03/12) systemFormat(例：[KEY.カラム名]形式等)の対応
	 * @og.rev 3.5.5.9 (2004/06/07) durationColumn を指定しない場合の処理を追加
	 * @og.rev 3.5.6.0 (2004/06/18) itdタグの[$xx] , [#xx]対応
	 * @og.rev 3.5.6.0 (2004/06/18) '!' 値のみ 追加 既存の '$' は、レンデラー
	 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、なし → private に変更。
	 *
	 * @param   nTblRow		テーブルモデルの行番号
	 * @param   myIteFormat	TableFormatteオブジェクト
	 * @param   strBuf 		出力データバーファ(not null , 同じオブジェクトを return する)
	 *
	 */
//	void formatItd( final int nTblRow, final TableFormatter myIteFormat, final StringBuilder strBuf ) {
	private void formatItd( final int nTblRow, final TableFormatter myIteFormat, final StringBuilder strBuf ) {
		if( myIteFormat == null ) { strBuf.setLength(0); }

		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//		int colspan = 1;
		final int colspan ;
		if( posDuration >= 0 ) {
			// 3.7.0.0 (2005/01/18) 小数点の桁落ち対策
			colspan = (int)Math.round( StringUtil.parseDouble( getValue(nTblRow, posDuration) ) / minDuration );
		}
		else {	// 日付けヘッダー未満は、空タグを出力しておく
			final String today = getValue(nTblRow, posDystart);
			int comp = headDays[headDaysCnt].compareTo( today );
			headDaysCnt++ ;
			while( comp < 0 && headDaysCnt < headDays.length ) {
				strBuf.append( "<td></td>" );
				comp = headDays[headDaysCnt].compareTo( today );
				headDaysCnt++ ;
			}
			if( comp != 0 ) {	// 見つからなかった(先に日付けが無くなった)
				final String errMsg = "日付けヘッダーと日付けデータに矛盾が発生しています。"
						+ CR
						+ "groupColumns で日付けがユニークになっていない可能性があります。"
						+ "row=[" + (nTblRow +1) + "] , today=[" + today + "]" ;
				throw new HybsSystemException( errMsg );
			}
			colspan = 1;		// 6.3.9.1 (2015/11/27)
		}

		int cl = 0;
		for( ; cl<myIteFormat.getLocationSize(); cl++ ) {
			String fmt = myIteFormat.getFormat(cl) ;
			final int loc = myIteFormat.getLocation(cl);	// 3.5.6.0
			if( cl == 0 && colspan != 1 ) {
				fmt = StringUtil.replace( fmt , "<td", "<td colspan=\"" + colspan + "\"" );
			}
			strBuf.append( fmt );			// 3.5.6.0
			if( loc >= 0 ) {
				switch( myIteFormat.getType(cl) ) {
					case '#' : strBuf.append( getColumnLabel(loc) );			break;
					case '$' : strBuf.append( getRendererValue(nTblRow,loc) );	break;
					case '!' : strBuf.append( getValue(nTblRow,loc) );			break;
					default  : strBuf.append( getValueLabel(nTblRow,loc) );		break;
				}
			}
			else {
				strBuf.append( myIteFormat.getSystemFormat(nTblRow,loc) );
			}
		}
		strBuf.append( myIteFormat.getFormat(cl) );

	}

	/**
	 * フォーマットを設定します。
	 *
	 * @og.rev 3.5.4.0 (2003/11/25) 新規作成
	 * @og.rev 3.5.4.4 (2004/01/16) 配列の最大数を変更
	 * @og.rev 3.5.6.0 (2004/06/18) ithFormat ,  itdFormat 変数削除
	 *
	 * @param	list	TableFormatterのリスト
	 */
	@Override
	public void setFormatterList( final List<TableFormatter> list ) {		// 4.3.3.6 (2008/11/15) Generics警告対応
		bodyFormats = new TableFormatter[BODYFORMAT_MAX_COUNT];

		bodyFormatsCount = 0;
		for( int i=0; i<list.size(); i++ ) {
			final TableFormatter format = list.get( i );		// 4.3.3.6 (2008/11/15) Generics警告対応

			switch( format.getFormatType() ) {
				case TYPE_HEAD : headerFormat = format; break;
				case TYPE_BODY : bodyFormats[bodyFormatsCount++] = format; break;
				case TYPE_FOOT : footerFormat = format; break;
				default : final String errMsg = "FormatterType の定義外の値が指定されました。";
				// 4.3.4.4 (2009/01/01)
						  throw new HybsSystemException( errMsg );
			}
			// 3.5.6.0 (2004/06/18) 廃止
		}

		// 3.5.6.0 (2004/06/18) itdFormats 処理追加
		// tbody 配列分だけ設定しておきます。
		itdFormats = new TableFormatter[bodyFormatsCount];
		for( int i=0; i<bodyFormatsCount; i++ ) {
			final String format = bodyFormats[i].getItdBody().trim();
			itdFormats[i] = new TableFormatter();
			itdFormats[i].setFormat( format );
		}
	}

	/**
	 * フォーマットメソッドを使用できるかどうかを問い合わせます。
	 *
	 * @return  使用可能(true)/ 使用不可能 (false)
	 */
	@Override
	public boolean canUseFormat() {
		return true;
	}

	/**
	 * 表示項目の編集(並び替え)が可能かどうかを返します。
	 *
	 * @og.rev 5.1.6.0 (2010/05/01) 新規追加
	 *
	 * @return	表示項目の編集(並び替え)が可能かどうか(false:不可能)
	 */
	@Override
	public boolean isEditable() {
		return false;
	}
}
