/*
 * 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.HashMap;
// import java.util.Map;

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

/**
 * TableFilter_STDDEV は、TableFilter インターフェースを継承した、DBTableModel 処理用の
 * 実装クラスです。
 *
 * ここではグループ単位に、平均、標準偏差等を求め、データの分布を示すデータを作成します。
 * グループキーとなるカラムは、あらかじめソーティングしておく必要があります。(キーブレイク判断するため)
 * グループキー以外の値は、参考情報として残し、カラムの最後に、
 * CNT,SUM,AVG,STDEVS,STDEVP,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S カラムを追加します。
 *
 * CNT(個数),SUM(合計),AVG(平均),STDEVS(標本標準偏差:n-1),STDEVP(母標準偏差:n)
 * 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 は、フィルターなしです。
 *
 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
 * 【パラメータ】
 *  {
 *       GROUP_KEY  : グループカラム     (複数指定可)
 *       VAL_KEY    : 値のカラム         (必須)
 *       USE_TYPE   : P(母) or S(標本)   (初期値:P(母標準偏差))
 *       FORMAT     : 数値のフォーマット (初期値:%.3f ･･･ 小数第３位以下を、四捨五入する)
 *       FILTER     : 1 , 2 , 3          (初期値:0)
 *  }
 *
 * @og.formSample
 * ●形式：
 *      ① &lt;og:tableFilter classId="STDDEV" selectedAll="true"
 *                   keys="GROUP_KEY,VAL_KEY" vals='"GOKI,SID",ACT_VAL' /&gt;
 *
 *      ② &lt;og:tableFilter classId="STDDEV"  selectedAll="true" &gt;
 *               {
 *                   GROUP_KEY : GOKI,SID ;
 *                   VAL_KEY   : ACT_VAL ;
 *               }
 *         &lt;/og:tableFilter&gt;
 *
 * @og.rev 6.7.1.0 (2017/01/05) 新規追加
 *
 * @version  0.9.0  2000/10/17
 * @author   Hiroki Nakamura
 * @since    JDK1.1,
 */
public class TableFilter_STDDEV extends AbstractTableFilter {
	// * このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "6.7.2.0 (2017/01/16)" ;

	private static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEVS","STDEVP","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" };
	private static final int      HIST_SU  = 8;		// "M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" の個数

	private DBTableModel	table	;

	/**
	 * デフォルトコンストラクター
	 */
	public TableFilter_STDDEV() {
		super();
		initSet( "GROUP_KEY"   	, "グループカラム     (複数指定可)"			);
		initSet( "VAL_KEY"		, "値のカラム         (必須)"				);
		initSet( "USE_TYPE"		, "P(母) or S(標本)   (初期値:P)"			);
		initSet( "FORMAT"		, "数値のフォーマット (初期値:%.3f ･･･ 小数代３位以下を、四捨五入する)"	);
		initSet( "FILTER"		, "1 , 2 , 3          (初期値:0)"			);
	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 6.7.2.0 (2017/01/16) FILTERパラメータ追加。
	 *
	 * @return 処理結果のDBTableModel
	 */
	public DBTableModel execute() {
		table	= getDBTableModel();
		final ResourceManager	resource = getResource();

		final String[]	grpClm	= StringUtil.csv2Array(	getValue( "GROUP_KEY" ) );
		final int		valNo	= table.getColumnNo(	getValue( "VAL_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 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 String names[] = new String[orgClms.length + ADD_CLMS.length - 1];	// 元のカラムに、追加カラムを加えます。-1は、値カラムを削除するためです。

		final DBTableModel nTable = DBTableModelUtil.newDBTable();
		nTable.init( names.length );

		int orgNo = 0;
		for( int i=0; i<names.length; i++ ) {
			if( i == valNo ) {			// 値のカラム列に、追加カラムを、挿入します。
				for( int j=0; j<ADD_CLMS.length; j++ ) {
					nTable.setDBColumn( i++, resource.makeDBColumn( ADD_CLMS[j] ) );
				}
				i -= 1;		// for文で、++するので、ひとつ戻しておく。
				orgNo++;
			}
			else {
				nTable.setDBColumn( i, orgClms[orgNo++] );
			}
		}

		final int ROW_CNT = table.getRowCount();
		String bkKey = getSeparatedValue( 0, grpNos );		// ブレイクキー
		String[] old = table.getValues( 0 );
		int    cnt = 1;										// 件数
		double sum = parseDouble( old[valNo] );				// 合計
		int    stPos = 0;									// ｸﾞﾙｰﾌﾟの開始行番号
		// １回目は初期設定しておく(row=1)。最後はキーブレイクしないので、１回余分に回す(row<=ROW_CNT)。
		for( int row=1; row<=ROW_CNT; row++ ) {
			final String rowKey = row==ROW_CNT ? "" : getSeparatedValue( row, grpNos );		// 余分なループ時にブレイクさせる。
			final int edPos = row;								// ｸﾞﾙｰﾌﾟの終了行番号
			if( bkKey.equals( rowKey ) ) {					// 前と同じ(継続)
				old = table.getValues( row );
				cnt++ ;
				sum += parseDouble( old[valNo] );
			}
			else {											// キーブレイク
				final double avg = sum/cnt;

				double sa1 = 0d;
				for( int j=stPos; j<edPos; j++ ) {			// 標準偏差の計算のためにもう一度回す
					final double val = parseDouble( table.getValue( j,valNo ) );
					sa1 += Math.pow( val - avg , 2 ) ;
				}
				final double stdevs = cnt==1 ? 0d : Math.sqrt( sa1/(cnt-1) );		// 母集団の標本の標準偏差(標本標準偏差)
				final double stdevp = Math.sqrt( sa1/cnt );							// 母集団全ての標準偏差(母標準偏差)

				final int[] dtCnt = new int[HIST_SU];
				final double sa2 = useDEVP ? stdevp : stdevs ;			// useDEVP == true の場合、母標準偏差 を使用します。
				for( int j=stPos; j<edPos; j++ ) {			// 確率分布の合計グラフを作成するためにもう一度回す
					final double val2 = parseDouble( table.getValue( j,valNo ) ) - avg;

					if(        0.0d == val2 || cnt == 1      ) { dtCnt[4]++ ; }		//   0  ･･･データが１件の場合
					else if(                   val2 < -sa2*3 ) { dtCnt[0]++ ; }		// -3σ＜
					else if( -sa2*3 <= val2 && val2 < -sa2*2 ) { dtCnt[1]++ ; }		// -2σ＜
					else if( -sa2*2 <= val2 && val2 < -sa2*1 ) { dtCnt[2]++ ; }		// -1σ＜
					else if( -sa2*1 <= val2 && val2 <  0.0d  ) { dtCnt[3]++ ; }		//   0＜
					else if(   0.0d <= val2 && val2 <  sa2*1 ) { dtCnt[4]++ ; }		//   0≦
					else if(  sa2*1 <= val2 && val2 <  sa2*2 ) { dtCnt[5]++ ; }		//  1σ≦
					else if(  sa2*2 <= val2 && val2 <  sa2*3 ) { dtCnt[6]++ ; }		//  2σ≦
					else if(  sa2*3 <= val2                  ) { dtCnt[7]++ ; }		//  3σ≦
				}

				// 6.7.2.0 (2017/01/16) FILTERパラメータ追加。
				// ここで、フィルター処理を行います。
				final boolean useValue ;
				switch( ftype ) {
					case 1  : useValue = ( dtCnt[0] + dtCnt[1] + dtCnt[2] + dtCnt[5] + dtCnt[6] + dtCnt[7] ) > 0 ; break ;
					case 2  : useValue = ( dtCnt[0] + dtCnt[1] +                       dtCnt[6] + dtCnt[7] ) > 0 ; break ;
					case 3  : useValue = ( dtCnt[0] +                                             dtCnt[7] ) > 0 ; break ;
					default : useValue = true ;
				}

				if( useValue ) {
					final String[] vals = new String[names.length];
					orgNo = 0;
					for( int k=0; k<names.length; k++ ) {
						if( k == valNo ) {				// 値のカラム列に、追加カラムを挿入している。
							vals[k++] = String.valueOf( cnt );				// CNT
							vals[k++] = String.format( format , sum );		// SUM
							vals[k++] = String.format( format , avg );		// AVG
							vals[k++] = String.format( format , stdevs );	// STDEVS(標本標準偏差)
							vals[k++] = String.format( format , stdevp );	// STDEVP(母標準偏差)
							vals[k++] = String.valueOf( dtCnt[0] );			// M3S
							vals[k++] = String.valueOf( dtCnt[1] );			// M2S
							vals[k++] = String.valueOf( dtCnt[2] );			// M1S
							vals[k++] = String.valueOf( dtCnt[3] );			// M0S
							vals[k++] = String.valueOf( dtCnt[4] );			// P0S
							vals[k++] = String.valueOf( dtCnt[5] );			// P1S
							vals[k++] = String.valueOf( dtCnt[6] );			// P2S
							vals[k  ] = String.valueOf( dtCnt[7] );			// P3S			// for文で、++するので、最後は、++ しない。
							orgNo++;
						}
						else {
							vals[k] = old[orgNo++];		// ひとつ前の行の値
						}
					}

					nTable.addColumnValues( vals );
				}

				if( row==ROW_CNT ) { break; }					// 最後のデータは強制終了
				cnt = 1;
				old = table.getValues( row );
				sum = parseDouble( old[valNo] );
				stPos = row;
				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 );
		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();
	}

	/**
	 * 引数の文字列を、double に変換して返します。
	 *
	 * 処理が止まらないように、null や、変換ミスのの場合は、ゼロを返します。
	 *
	 * @param	val	変換する元の文字列
	 *
	 * @return	変換後のdouble
	 * @og.rtnNotNull
	 */
	private double parseDouble( final String val ) {
		double rtn = 0.0d;
		if( val != null && !val.trim().isEmpty() ) {
			try {
				rtn = Double.parseDouble( val.trim() );
			}
			catch( final NumberFormatException ex ) {
				final String errMsg = "文字列を数値に変換できません。val=[" + val + "]" + ex.getMessage(); ;
				System.out.println( errMsg );
			}
		}

		return rtn ;
	}
}
