/*
 * 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 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_STDDEV2 は、TableFilter インターフェースを継承した、DBTableModel 処理用の
 * 実装クラスです。
 * 標準偏差等の対象カラムは、横持で、CLMNO属性で指定したカラム以降に指定します。
 * よって、対象カラム以降に、自由にカラムを配置することはできません。
 *
 * 横持のカラムを、縦に再セットします。その際、キーワードを、CLMNAME で指定のカラムに
 * セットします。CLMNAME のカラムは、予め、DBTableModel に用意しておいてください。
 * CLMNO が未指定の場合は、CLMNAMEの次からと認識されます。
 *
 * ここではグループ単位に、平均、標準偏差等を求め、データの分布を示すデータを作成します。
 * グループキーとなるカラムは、あらかじめソーティングしておく必要があります。(キーブレイク判断するため)
 * グループキー以外の値は、参考情報として残し、CLMS属性に指定したカラムを削除し、カラムの最後に、
 * CNT,SUM,AVG,(STDEVS or STDEVP),COEFF,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S カラムを追加します。
 *
 * CNT(個数),SUM(合計),AVG(平均),
 * STDEVS(標本標準偏差:n-1) または、STDEVP(母標準偏差:n) を、USE_TYPE で選択します。
 * COEFF(変動係数) は、標準偏差(σ)を算術平均で、割ったものの百分率
 * M3S(～-3σ),M2S(-3σ～-2σ),M1S(-2σ～-σ),M0S(-σ～0),P0S(0～σ),P1S(σ～2σ),P2S(2σ～3σ),P3S(3σ～)
 * FILTERは、1:(-2σ～-σ or σ～2σ) , 2:(-3σ～-2σ or 2σ～3σ) , 3:(～-3σ or 3σ～) のみピックアップします。
 * 初期値の 0 は、フィルターなしです。
 *
 * 6.9.9.2 (2018/09/18)
 *   COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加します。
 *   これは、単位(%)で、指定の値以下の変動係数のﾚｺｰﾄﾞを出力しません。
 *
 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
 * 【パラメータ】
 *  {
 *       GROUP_KEY  : グループカラム          (複数指定可)
 *       CLMNAME    : 縦持キーとなるカラム名  (必須)
 *       CLMNO      : 対象カラムの最初の番号
 *       USE_TYPE   : P(母) or S(標本)        (初期値:P(母標準偏差))
 *       FORMAT     : 数値のフォーマット      (初期値:%.3f ･･･ 小数第３位以下を、四捨五入する)
 *       FILTER     : 1 , 2 , 3               (初期値:0)
 *       MIN_CV     : 変動係数の最小除外値(%指定)  例:2.0
 *  }
 *
 * @og.formSample
 * ●形式：
 *      ① &lt;og:tableFilter classId="STDDEV2" selectedAll="true"
 *                   keys="GROUP_KEY,CLMNO" vals='"GOKI,SID",7' /&gt;
 *
 *      ② &lt;og:tableFilter classId="STDDEV2"  selectedAll="true" &gt;
 *               {
 *                   GROUP_KEY : GOKI,SID ;
 *                   CLMNO     : 7 ;
 *               }
 *         &lt;/og:tableFilter&gt;
 *
 * @og.rev 6.7.1.0 (2017/01/05) 新規追加
 * @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_STDDEV2 extends AbstractTableFilter {
	/** このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "7.3.0.0 (2021/01/06)" ;

	private DBTableModel	table	;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
	 */
	public TableFilter_STDDEV2() {
		super();
		initSet( "GROUP_KEY"   	, "グループカラム           (複数指定可)"		);
		initSet( "CLMNAME"		, "縦持キーとなるカラム名   (必須)"				);
		initSet( "CLMNO"		, "対象カラムの最初の番号"						);
		initSet( "USE_TYPE"		, "P(母) or S(標本)         (初期値:P)"			);
		initSet( "FORMAT"		, "数値のフォーマット       (初期値:%.3f ･･･ 小数代３位以下を、四捨五入する)"	);
		initSet( "FILTER"		, "1 , 2 , 3                (初期値:0)"			);
		initSet( "MIN_CV"		, "変動係数の最小除外値(%)"						);		// 6.9.9.2 (2018/09/18)
	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 6.7.2.0 (2017/01/16) FILTERパラメータ追加。
	 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
	 * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する
	 *
	 * @return 処理結果のDBTableModel
	 */
	public DBTableModel execute() {
		table	= getDBTableModel();
		final ResourceManager	resource = getResource();

		final String[]	grpClm	= StringUtil.csv2Array(	getValue( "GROUP_KEY" ) );
		final String	devType	= getValue( "USE_TYPE" );
		final String	fmt		= getValue( "FORMAT" );
		final int		ftype	= StringUtil.nval( getValue( "FILTER" ) , 0 );			// 6.7.2.0 (2017/01/16)
		final String	clmName	= getValue( "CLMNAME" );
		final int		nameNo	= table.getColumnNo( clmName );							// 必須なので、無ければ、エラーにします。
		final int		minNo	= StringUtil.nval( getValue( "CLMNO" ) , nameNo+1 );	// CLMNOが未指定の場合は、CLMNAME の次のカラムになります。
		final String	minCV	= getValue( "MIN_CV" );									// 6.9.9.2 (2018/09/18)

		final boolean	useDEVP	= devType == null || devType.isEmpty() || "P".equals( devType ) ;	// 初期値が、"P" (母標準偏差)
		final String	format	= fmt == null || fmt.isEmpty() ? "%.3f" : fmt ;						// 初期値が、"%.3f"

		// グループカラムのカラム番号を求めます。
		final int[] grpNos = new int[grpClm.length];
		for( int i=0; i<grpNos.length; i++ ) {
			grpNos[i] = table.getColumnNo( grpClm[i] );			// 無ければ、エラーにします。
		}

		final DBColumn[] orgClms = table.getDBColumns() ;		// 検索時のオリジナルのカラム

		// 計算対象のカラムのカラム番号を求めます。
		final int nSize = orgClms.length - minNo;				// 全体カラム数から、対象カラム番号を引けば、残りが対象カラム数
		final StandardDeviation[] stdDevs = new StandardDeviation[nSize];		// 追加カラム分
		for( int i=0; i<nSize; i++ ) {
//			stdDevs[i] = new StandardDeviation( ftype,useDEVP,format );
			stdDevs[i] = new StandardDeviation( ftype,useDEVP,format,minCV );	// 6.9.9.2 (2018/09/18)
		}

		// 元のカラムの最小番号以降を、統計カラムに差し替えます。
		final int ADD_CLM_LEN = ADD_CLMS.length;
		final String names[] = new String[minNo + ADD_CLM_LEN];

		final DBTableModel nTable = DBTableModelUtil.newDBTable();
		nTable.init( names.length );
		int no = 0;
		for( ; no<minNo; no++ ) {
			nTable.setDBColumn( no, orgClms[no] );				// 0 ～ minNo まで、順番にセット
		}
		for( int j=0; j<ADD_CLM_LEN; j++ ) {
			nTable.setDBColumn( no++, resource.makeDBColumn( ADD_CLMS[j] ) );
		}

		final int ROW_CNT = table.getRowCount();
		String bkKey = getSeparatedValue( 0, grpNos );			// ブレイクキー
		String[] old = table.getValues( 0 );

		// 後で、row==0で統合する。
		for( int j=0; j<nSize; j++ ) {
			stdDevs[j].addData( old[j+minNo] );					// 集計対象カラム
		}

		// １回目は初期設定しておく(row=1)。最後はキーブレイクしないので、１回余分に回す(row<=ROW_CNT)。
		for( int row=1; row<=ROW_CNT; row++ ) {
			final String rowKey = row==ROW_CNT ? "" : getSeparatedValue( row, grpNos );	// 余分なループ時にブレイクさせる。
			if( bkKey.equals( rowKey ) ) {					// 前と同じ(継続)
				old = table.getValues( row );
				for( int j=0; j<nSize; j++ ) {
					stdDevs[j].addData( old[j+minNo] );		// 集計対象カラム
				}
			}
			else {											// キーブレイク
				for( int j=0; j<nSize; j++ ) {
					final String[] rtnVals = stdDevs[j].getData();
					// 値が戻ってきた場合のみ、テーブルに追加します。
//					if( rtnVals != null ) {
					if( rtnVals.length > 0 ) {				// 7.3.0.0 (2021/01/06) null ではなく長さゼロの配列
						final String vals[] = new String[names.length];
						no = 0;
						for( ; no<minNo; no++ ) {
							vals[no] = old[no];
						}
						for( int k=0; k<ADD_CLM_LEN; k++ ) {
							vals[no++] = rtnVals[k];
						}

						vals[nameNo] = orgClms[j+minNo].getName();		// nameNo のカラムを置き換えます。

						nTable.addColumnValues( vals );
					}
					stdDevs[j].clear();						// データを取り出した後、初期化します。
				}

				if( row==ROW_CNT ) { break; }				// 最後のデータは強制終了

				old = table.getValues( row );
				for( int j=0; j<nSize; j++ ) {
					stdDevs[j].addData( old[j+minNo] );		// 集計対象カラム
				}
				bkKey = rowKey;
			}
		}

		return nTable;
	}

	/**
	 * 各行のキーとなるキーカラムの値を連結した値を返します。
	 *
	 * @param	row		行番号
	 * @param	clmNo	カラム番号配列
	 *
	 * @return	各行のキーとなるキーカラムの値を連結した値
	 * @og.rtnNotNull
	 */
	private String getSeparatedValue( final int row, final int[] clmNo ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		// 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop
		for( final int clm : clmNo ) {
			if( clm >= 0 ) {
				final String val = table.getValue( row, clm );
//		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();
	}
}
