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

import java.util.Arrays;

import org.opengion.fukurou.util.StringUtil;
import org.opengion.hayabusa.db.AbstractTableFilter;
// import org.opengion.hayabusa.db.DBColumn;
import org.opengion.hayabusa.db.DBTableModel;
// import org.opengion.hayabusa.db.DBTableModelUtil;
// import org.opengion.hayabusa.resource.ResourceManager;

// import static org.opengion.plugin.table.StandardDeviation.ADD_CLMS;

/**
 * TableFilter_SKIPROW は、TableFilter インターフェースを継承した、DBTableModel 処理用の
 * 実装クラスです。
 * グラフ等で使用する、数字カラムをもつﾃﾞｰﾀを、間引きます。
 *
 * DT_SKIP は、間引き方を指定します。
 *     マイナス：自動的に間引き率を設定します。(初期値)
 *               SKIP_MIN_COUNT 以上のﾃﾞｰﾀ件数の場合、SKIP_SIZE に近い数字に
 *               なるように間引きます。
 *     0       ：間引き処理を行いません。
 *     整数    ：間引く数を指定します。例えば、10 を指定すると、10行が、1行になるように、間引きます。
 *               正確には、20行ごとに、min行とmax行の2行を出力します。
 *
 * GROUP_KEY は、数値とは関係の無い、固定値のカラムをCSV形式で指定します。
 * このキーワード群を固まり(ｸﾞﾙｰﾌﾟ)として、間引き処理の初期化を行います。
 * キーブレイクごとに、間引きの開始を行います。
 * なお、先の間引きの間隔は、キーブレイクに関係なく、全体の件数に対して判定されます。
 * キーのｸﾞﾙｰﾌﾟの件数が少ない場合は、最小２件のﾃﾞｰﾀのみ出力されます。
 *
 * VAL_CLMS は、数値カラムで、グラフ等の値を持っているカラムをCSV形式で指定します。
 * これは、間引き処理で、最小値と最大値のﾚｺｰﾄﾞを作り直します。
 * グラフ等で間引くと、最大値や最小値などが、消えてなくなる可能性がありますが、
 * このフィルターでは、各カラムのの中で、最大値だけを集めたレコードと、最小値だけを集めた
 * ﾚｺｰﾄﾞを作ります。
 *
 * SKIP_MIN_COUNT は、自動設定時(DT_SKIPをマイナス)にセットした場合の、ﾃﾞｰﾀの最小単位です。
 * この件数以下の場合は、自動間引きの場合は、間引き処理を行いません。
 * 初期値は、{@og.value #AUTO_SKIP_MIN_COUNT} です。
 *
 * SKIP_SIZE は、自動設定時(DT_SKIPをマイナス)の間引き後のサイズを指定します。
 * サイズは、大体の目安です。例えば、1300件のﾃﾞｰﾀの場合、1300/SKIP_SIZE ごとに、
 * 間引くことになります。
 * 初期値は、{@og.value #AUTO_SKIP_SIZE} です。
 *
 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
 * 【パラメータ】
 *  {
 *       DT_SKIP        : まとめ数(-1:自動(初期値)、0:まとめなし、数値:まとめ数
 *       GROUP_KEY      : グループカラム           (複数指定可)
 *       VAL_CLMS       : まとめるに当たって、最大、最小判定を行うカラム列
 *       SKIP_MIN_COUNT : 自動設定時にセットした場合の、ﾃﾞｰﾀの最小単位
 *       SKIP_SIZE      : 自動設定時の間引き後のサイズ(の目安)
 *  }
 *
 * @og.formSample
 * ●形式：
 *      ① &lt;og:tableFilter classId="SKIPROW" selectedAll="true"
 *                       keys="GROUP_KEY,DT_SKIP,VAL_CLMS"
 *                       vals='"SYSTEM_ID,USERID",36,"USED_TIME,CNT_ACCESS,CNT_READ,TM_TOTAL_QUERY"' /&gt;
 *
 *      ② &lt;og:tableFilter classId="SKIPROW"  selectedAll="true" &gt;
 *               {
 *                   GROUP_KEY : SYSTEM_ID,USERID ;
 *                   DT_SKIP   : 36 ;
 *                   VAL_CLMS  : USED_TIME,CNT_ACCESS,CNT_READ,TM_TOTAL_QUERY ;
 *               }
 *         &lt;/og:tableFilter&gt;
 *
 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
 *
 * @version  0.9.0  2000/10/17
 * @author   Hiroki Nakamura
 * @since    JDK1.1,
 */
public class TableFilter_SKIPROW extends AbstractTableFilter {
	// * このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "6.9.3.0 (2018/03/26)" ;

	/** skipDataNum の自動設定時の、最小設定行数 {@value} **/
	public static final int	AUTO_SKIP_MIN_COUNT		= 1000;
	/** skipDataNum の自動設定時の、間引き後のサイズ {@value} **/
	public static final int	AUTO_SKIP_SIZE			= 500;

	private DBTableModel	table	;

	/**
	 * デフォルトコンストラクター
	 */
	public TableFilter_SKIPROW() {
		super();
		initSet( "DT_SKIP"			, "まとめ数(-1:自動(初期値)、0:まとめなし、数値:まとめ数"	);
		initSet( "GROUP_KEY"   		, "グループカラム           (複数指定可)"					);
		initSet( "VAL_CLMS"			, "まとめるに当たって、最大、最小判定を行うカラム列"		);
		initSet( "SKIP_MIN_COUNT"	, "自動設定時にセットした場合の、ﾃﾞｰﾀの最小単位"			);
		initSet( "SKIP_SIZE"		, "自動設定時の間引き後のサイズ(の目安)"					);
	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
	 *
	 * @return 処理結果のDBTableModel
	 */
	public DBTableModel execute() {
		table = getDBTableModel();

		int       skipDataNum	= StringUtil.nval( getValue( "DT_SKIP"        ) , -1 );
		final int skipMinCnt	= StringUtil.nval( getValue( "SKIP_MIN_COUNT" ) , AUTO_SKIP_MIN_COUNT );
		final int skipSize		= StringUtil.nval( getValue( "SKIP_SIZE"      ) , AUTO_SKIP_SIZE      );

		final int ROW_CNT = table.getRowCount();
		if( skipDataNum < 0 ) {								// < 0 は、自動設定。
			skipDataNum = ROW_CNT < skipMinCnt
								? 0							// (AUTO_SKIP_SIZE)件以下の場合は、間引き処理を行わない。
								: ROW_CNT / skipSize ;		// (AUTO_SKIP_SIZE)件を目安に数を減らします。
		}
		if( skipDataNum == 0 ) { return table; }			// 0 は、まとめなし

		final String[] grpClm = StringUtil.csv2Array( getValue( "GROUP_KEY" ) );
		// グループカラムのカラム番号を求めます。
		final int[] grpNos = getTableColumnNo( grpClm );

		final String[] valClms = StringUtil.csv2Array( getValue( "VAL_CLMS" ) );
		// 最大、最小判定を行うカラム番号を求めます。
		final int[] clmNos = getTableColumnNo( valClms );

		// まとめ処理を行った結果を登録するﾃｰﾌﾞﾙﾓﾃﾞﾙ

		final OmitTable omitTbl = new OmitTable( table , clmNos , skipDataNum );

		String bkKey = getSeparatedValue( 0, grpNos );	// row==0 ブレイクキー
		omitTbl.check( 0 , false );						// row==0 最初の行は、初期値設定

		// １回目は初期設定しておく(row=1)。
		for( int row=1; row<ROW_CNT; row++ ) {
			final String rowKey = (row+1)==ROW_CNT ? "" : getSeparatedValue( row, grpNos );	// 最後の列は、ブレイクさせる。
			if( bkKey.equals( rowKey ) ) {				// 前と同じ(継続)
				omitTbl.check( row , false );
			}
			else {										// キーブレイク
				omitTbl.check( row , true );
				bkKey = rowKey;
			}
		}

		return omitTbl.getTable();
	}

	/**
	 * 各行のキーとなるキーカラムの値を連結した値を返します。
	 *
	 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
	 *
	 * @param	row		行番号
	 * @param	clmNo	カラム番号配列
	 *
	 * @return	各行のキーとなるキーカラムの値を連結した値
	 * @og.rtnNotNull
	 */
	private String getSeparatedValue( final int row, final int[] clmNo ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		for( int i=0; i<clmNo.length; i++ ) {
			if( clmNo[i] >= 0 ) {
				final String val = table.getValue( row, clmNo[i] );
				if( val != null && val.length() > 0 ) {
					buf.append( val ).append( '_' );
				}
			}
		}
		return buf.toString();
	}

	/**
	 * 実際の間引き処理を行うクラス。
	 *
	 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
	 */
	private static final class OmitTable {
		private final DBTableModel	orgTable ;
		private final DBTableModel	rtnTable ;
		private final int[]			clmNos ;
		private final int			clmSize;
		private final int			skipNum;	// まとめ数。min/maxの２行出すので、間引くのは２の倍数となる。
		private final double[]		minVals;
		private final double[]		maxVals;
		private int   minRow;	// 最小値の置換があった最後の行番号
		private int   maxRow;	// 最大値の置換があった最後の行番号

		private int   count;	// 内部のカウンタ

		/**
		 * 実際の間引き処理を行うクラスのコンストラクター
		 *
		 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
		 *
		 * @param	table		間引き処理を行う、元のDBTableModel
		 * @param	clmNos		最大最小処理を行うカラム番号配列
		 * @param	skipDataNum	間引き数(すでに、0,マイナスは処理済)
		 *
		 */
		public OmitTable( final DBTableModel table , final int[] clmNos , final int skipDataNum ) {
			this.orgTable 	= table;
			this.rtnTable 	= table.newModel();
			this.clmNos 	= clmNos;
			this.clmSize	= clmNos.length;		// 値部分のみの配列番号
			this.skipNum	= skipDataNum * 2;		// まとめ数(min,max 2行作るので

			minVals = new double[clmSize];
			maxVals = new double[clmSize];
			dataInit();								// データの初期化
		}

		/**
		 * 最大最小のデータの初期化を行います。
		 *
		 * 最小値に、doubleの最大値を、最大値に、daoubleの最小値を設定しておくことで、
		 * 最初の処理で、必ず、置換が発生するようにしています。
		 * データがnullのような場合には、置換が最後まで発生しないので、
		 * 行番号を、-1 に設定しておきます。
		 *
		 * @og.rev 6.9.4.1 (2018/04/09) 新規作成
		 */
		private void dataInit() {
			Arrays.fill( minVals,Double.POSITIVE_INFINITY );	// 最小値の初期値は、正の無限大
			Arrays.fill( maxVals,Double.NEGATIVE_INFINITY );	// 最大値の初期値は、負の無限大
			minRow  = -1;										// 初期化：置換が無い場合のﾌﾗｸﾞを兼ねています。
			maxRow  = -1;										// 初期化：置換が無い場合のﾌﾗｸﾞを兼ねています。
		}

		/**
		 * 間引き処理のための最大最小のデータ交換処理。
		 *
		 * 対象ｶﾗﾑは複数あるので、どれかのｶﾗﾑが最小・最大値を持つ場合に、置換されます。
		 * そういう意味では、最大･最小の行番号は、最後に置換された行番号になります。
		 * 置換ｶﾗﾑが、一つだけの場合は、正確ですが、そうでない場合は、大体の目安程度です。
		 *
		 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
		 *
		 * @param	row		処理する行番号
		 */
		private void change( final int row ) {
			final String[] vals = orgTable.getValues( row );
			for( int i=0; i<clmSize; i++ ) {
				final String val = vals[clmNos[i]];
				if( !StringUtil.isNull( val ) ) {										// データが null の場合は、置換が発生しません。
					final double dval = Double.parseDouble( val );
					if( minVals[i] > dval ) { minVals[i] = dval; minRow = row; }		// 最小値の入れ替え
					if( maxVals[i] < dval ) { maxVals[i] = dval; maxRow = row; }		// 最大値の入れ替え
				}
			}
		}

		/**
		 * 間引き処理のための最大最小のデータチェック。
		 *
		 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
		 * @og.rev 6.9.4.1 (2018/04/09) 最大、最小の行の追加は、行番号の順番に行います。
		 *
		 * @param	row			処理する行番号
		 * @param	isBreak		ブレイク行かどうか
		 */
		public void check( final int row , final boolean isBreak ) {
			change( row );

			count++ ;			// 先に、カウントアップしておくのは、 == 0 のときに、出力処理を行わないようにするため。

			if( isBreak || ( count % skipNum == 0 ) ) {					// 間引き処理(出力処理)
				final String[] old1 = orgTable.getValues( minRow < 0 ? row : minRow );		// 最小値
				final String[] old2 = orgTable.getValues( maxRow < 0 ? row : maxRow );		// 最大値
				for( int i=0; i<clmSize; i++ ) {						// 指定のｶﾗﾑだけ、置き換えます。
					final int cno = clmNos[i];
					if( minVals[i] != Double.POSITIVE_INFINITY ) {		// 置換があったｶﾗﾑのみ置き換える。
						old1[cno] = String.valueOf( minVals[i] );
					}
					if( maxVals[i] != Double.NEGATIVE_INFINITY ) {		// 置換があったｶﾗﾑのみ置き換える。
						old2[cno] = String.valueOf( maxVals[i] );
					}
				}

				// 行番号の順番どおりに挿入します。
				if( minRow < maxRow ) {
					rtnTable.addColumnValues( old1 );
					rtnTable.addColumnValues( old2 );
				}
				else {
					rtnTable.addColumnValues( old2 );
					rtnTable.addColumnValues( old1 );
				}

				dataInit();		// データの初期化
			}

			if( isBreak ) { count = 0; }
		}

		/**
		 * 間引き処理の結果のDBTableModelを返します。
		 *
		 * @og.rev 6.9.3.0 (2018/03/26) 新規作成
		 *
		 * @return	間引き処理を行った、DBTableModel
		 */
		public DBTableModel getTable() { return rtnTable; }
	}
}
