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

import static org.opengion.fukurou.system.HybsConst.CR ;		// 6.1.0.0 (2014/12/26)
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.util.ColorMap;			// 6.0.2.2 (2014/10/03)
import org.opengion.fukurou.db.ResultSetValue;		// 6.0.4.0 (2014/11/28)
import org.opengion.hayabusa.db.DBTableModel;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;

import java.awt.Color;								// 6.0.2.2 (2014/10/03)

import org.jfree.data.Range;
import org.jfree.data.category.DefaultCategoryDataset;

/**
 * HybsCategoryDataset は、org.jfree.data.category.DefaultCategoryDataset を継承したサブクラスで、
 * HybsDataset インターフェースの実装クラスになっています。
 * これは、JDBCCategoryDatasetの データベース機能と、DBTableModel から Dataset を作成する機能を
 * 兼ね備えています。
 * HybsDataset インターフェースは、シリーズのラベル指定、カテゴリカラーバー、パレート図用積上げ
 * 計算などの処理を行うための、インターフェースで、それらの処理も、HybsCategoryDataset に実装します。
 * 
 * このクラスでは、検索結果を内部で持っておき、getValue(int row, int column)
 * メソッドで直接値を返します。
 * 
 * select category,series1,series2,series3,･･･ from ･･･
 * series の横持ち(標準と同じ) 対応です。
 * category カラムの値は、カテゴリのラベルになり、series1,2,3 のラベルがシリーズラベル、値が
 * seriesの値になります。
 *
 * カテゴリのカラー名の指定を行う場合、最後のカラムが、カラー名の文字列になります。
 * select category,series1,series2,series3,･･･,color from ･･･
 * color文字列の検索結果は、Dataset には含まれません。
 *
 * その場合、color カラムがシリーズとして認識されない様に、ChartDatasetTag で、useCategoryColor="true"
 * を指定しておく必要があります。このフラグは、HybsCategoryDataset を使う処理以外では効果が
 * ありません（シリーズとして使用されてしまう）のでご注意ください。
 * このフラグは、カテゴリカラーバーを使う場合には必要ですが、カテゴリカラーバーと（例えばパレート図）
 * を合成する場合に、パレート図側にも useCategoryColor="true" を設定しておけば、同じSQL または、
 * DBTableModel を使う事ができるというためのフラグです。
 *
 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。
 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に
 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース)
 * このRenderer で、getItemPaint( int  , int )メソッドをオーバーライドすることで、カテゴリごとの
 * 色を返します。
 *
 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
 *
 * @version  6.0.2.2 (2014/10/03)
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.6,
 */
public class HybsCategoryDataset extends DefaultCategoryDataset implements HybsDataset {
	private static final long serialVersionUID = 602220141003L ;

	private final Set<String> cateCheck	= new HashSet<>();		// category の重複チェック
	private final int	hsCode	= Long.valueOf( System.nanoTime() ).hashCode() ;	// 5.1.9.0 (2010/08/01) equals,hashCode

	private String[]	seriesLabels		;
	private boolean		isColorCategory		;				// 6.0.2.2 (2014/10/03)
	private boolean		isParetoData		;				// 6.0.2.2 (2014/10/03)

	private Number[][]	numdata				;
	private Color[]		categoryColor		;
	private Range		range				;

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

	/**
	 * CategoryDataset を構築するに当たり、初期パラメータを設定します。
	 *
	 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
	 *
	 * @param lbls  シリーズのラベル名配列
	 * @param isColCate  カテゴリのカラー名の指定有無(true:使用する)
	 * @param isPareto   パレート図用のDatasetとして処理するかどうか(true:処理する)
	 */
	public void initParam( final String[] lbls , final boolean isColCate , final boolean isPareto ) {
		// 6.0.2.5 (2014/10/31) refactoring
		if( lbls != null ) { seriesLabels = lbls.clone(); }
		isColorCategory	= isColCate;
		isParetoData	= isPareto;
	}

	/**
	 * コネクションと、SQL文字列から、CategoryDataset のデータを作成します。
	 * 元となる処理は、org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String ) です。
	 *
	 * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して
	 * 検索した結果のデータを加工、処理します。
	 * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。
	 *
	 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
	 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
	 * @og.rev 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
	 * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
	 *
	 * @param conn  コネクション
	 * @param query  SQL文字列
	 *
	 * @throws SQLException データベースアクセス時のエラー
	 * @see		org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String )
	 * @see		org.opengion.fukurou.db.ResultSetValue
	 */
	public void execute( final Connection conn, final String query ) throws SQLException {

		// Range を予め求めておきます。
		double minimum = Double.POSITIVE_INFINITY;
		double maximum = Double.NEGATIVE_INFINITY;
		double sum     = 0.0d;					// 6.0.2.3 (2014/10/19) パレート図用合計

		List<Color> colorList = null;			// 6.0.2.2 (2014/10/03) カテゴリカラー

		// 6.4.2.1 (2016/02/05) try-with-resources 文
		try( final Statement statement = conn.createStatement();
			 final ResultSet resultSet = statement.executeQuery(query) ) {

			// 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
			final ResultSetValue rsv = new ResultSetValue( resultSet );

			int dataSize = rsv.getColumnCount() -1;			// series の個数は、category 分を引いた数。
			if( isColorCategory ) {							// ColorCategory使用時
				colorList	= new ArrayList<>();		// カテゴリカラー
				dataSize--;									// 最終カラムが Colorコードなので、マイナスする。
			}

			if( dataSize<1 ) {
				final String errMsg = "JDBCCategoryDataset.executeQuery() : insufficient columns "
							+ "returned from the database. \n"
							+ " SQL=" + query ;
				throw new SQLException( errMsg );
			}

			// 6.0.2.0 (2014/09/19) シリーズのラベル名配列を使うときは、シリーズ数必要。
			if( seriesLabels != null && seriesLabels.length < dataSize ) {
				final String errMsg = "seriesLabels を使用する場合は、必ずシリーズ数以上指定してください。"
								+ CR
								+ " seriesLabels=" + Arrays.toString( seriesLabels )
								+ CR
								+ " seriesLabels.length=" + seriesLabels.length
								+ " dataSize=" + dataSize
								+ CR ;
				throw new IllegalArgumentException( errMsg );
			}

			String[] series  = new String[dataSize];
			// 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
			final String[] names   = rsv.getNames();
			// ORACLEの引数は、配列＋１から始まるので、metaDataはi＋2から取得。series と、seriesLabels は０から始まる。
			for( int i=0; i<dataSize; i++ ) {
				// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
				series[i] = seriesLabels == null || seriesLabels[i] == null
									? names[i+1]
									: seriesLabels[i] ;
			}

			final List<Number[]> rowList = new ArrayList<>();
			// 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
			while( rsv.next() ) {
				Number[] clmList = new Number[dataSize];
				// first column contains the row key...
	 			// 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。
				final String category = uniqCategory( resultSet.getString(1) );	// 6.0.2.3 (2014/10/10) categoryの重複回避

				for( int i=0; i<dataSize; i++ ) {					// 6.0.2.2 (2014/10/03) dataSize 分回す。
					Number value = null;
					// 6.0.2.1 (2014/09/26) org.opengion.fukurou.db.DBUtil に、移動
					try {
						// JDBCのアドレス指定は、+2 する。(category 分と、ｱﾄﾞﾚｽが１から始まる為。)
						// ResultSetValueのカラム番号は、+1 する。(category 分があるため)
						value = rsv.getNumber( i+1 );
					}
					catch( final SQLException ex ) {		// 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。
						LogWriter.log( ex );
					}
					catch( final RuntimeException ex ) {
						LogWriter.log( ex );
					}

					clmList[i] = value;
					addValue(value, series[i], category);		// 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。
					// Range 求め
					if( value != null ) {
						final double dbl = value.doubleValue();
						if( isParetoData ) {					// 6.0.2.3 (2014/10/19) パレート図用合計
							sum += dbl ;
						} else {
							if( dbl     < minimum ) { minimum = dbl; }
							if( maximum < dbl     ) { maximum = dbl; }
						}
					}
				}
				rowList.add( clmList );
				// 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム
				if( isColorCategory ) {
					// 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
					final String colStr = rsv.getValue(dataSize+1);				// 最後のカラム
					final Color color   = ColorMap.getColorInstance( colStr );	// 6.0.2.1 (2014/09/26) StringUtil → ColorMap
					colorList.add( color );
				}
			}
			numdata = rowList.toArray( new Number[dataSize][rowList.size()] );
		}

		// colorList が null でないかどうかで判定。
		if( isColorCategory && colorList != null ) {
			categoryColor = colorList.toArray( new Color[colorList.size()] );
		}

		// 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
		if( isParetoData ) {
			changeParetoData( sum );
			minimum = 0.0;
			maximum = 100.0;
		}

		range = new Range( minimum, maximum );
	}

	/**
	 * DBTableModelオブジェクトから、CategoryDataset のデータを作成します。
	 * openGionの独自処理メソッドです。
	 *
	 * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して
	 * 検索した結果のデータを加工、処理します。
	 * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。
	 *
	 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
	 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
	 *
	 * @param table DBTableModelオブジェクト
	 * @see		#execute( Connection,String )
	 */
	public void execute( final DBTableModel table ) {
 		final int clmNo = table.getColumnCount();
		final int rowNo = table.getRowCount();

		// Range を予め求めておきます。
		double minimum = Double.POSITIVE_INFINITY;
		double maximum = Double.NEGATIVE_INFINITY;
		double sum     = 0.0d;					// 6.0.2.3 (2014/10/19) パレート図用合計

		int dataSize = clmNo -1;						// series の個数は、category 分を引いた数。
		List<Color> colorList = null;			// 6.0.2.2 (2014/10/03) カテゴリカラー
		if( isColorCategory ) {							// ColorCategory使用時
			colorList	= new ArrayList<>();		// カテゴリカラー
			dataSize--;									// 最終カラムが Colorコードなので、マイナスする。
		}

		numdata = new Number[rowNo][clmNo];

		// ※ DBTableModel の row,col と、Dataset の row,col は、逆になっています。
		for( int row=0; row<rowNo; row++ ) {
			final String   category = uniqCategory( table.getValue( row,0 ) );	// 6.0.2.3 (2014/10/10) categoryの重複回避
			final String[] vals     = table.getValues( row );
			for( int clm=0; clm<dataSize; clm++ ) {
				final String sval = vals[clm+1];				// ２番目(アドレス=1)からカラムデータを取得
				final double val  = sval == null || sval.isEmpty() ? 0.0d : Double.parseDouble( sval ) ;		// 6.4.2.1 (2016/02/05) PMD refactoring. Useless parentheses.

				addValue( val , seriesLabels[clm] , category );		// val,row,clm
				numdata[row][clm] = Double.valueOf( val );			// 6.0.2.4 (2014/10/17) 効率の悪いメソッド
				// Range 求め
				if( isParetoData ) {					// 6.0.2.3 (2014/10/19) パレート図用合計
					sum += val ;
				} else {
					if( val     < minimum ) { minimum = val; }
					if( maximum < val     ) { maximum = val; }
				}
			}

			// 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム
			if( isColorCategory ) {
				final String colStr = vals[dataSize+1];			// 最後のカラム
				final Color color   = ColorMap.getColorInstance( colStr );	// 6.0.2.1 (2014/09/26) StringUtil → ColorMap
				colorList.add( color );
			}
		}

		// colorList が null でないかどうかで判定。
		if( isColorCategory && colorList != null ) {
			categoryColor = colorList.toArray( new Color[colorList.size()] );
		}

		// 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
		if( isParetoData ) {
			changeParetoData( sum );
			minimum = 0.0;
			maximum = 100.0;
		}

		range = new Range( minimum, maximum );
	}

	/**
	 * 指定された行列から、数字オブジェクトを取得します。
	 *
	 * @param	row 	行番号(シリーズ：横持=clm相当)
	 * @param	column	カラム番号(カテゴリ：縦持ち=row相当)
	 *
	 * @return	指定の行列の値
	 */
	@Override
	public Number getValue( final int row, final int column ) {
		// 注意：行列の順序が逆です。
		return numdata[column][row];
	}

	/**
	 * レンジオブジェクトを取得します。(独自メソッド)
	 *
	 * @return	レンジオブジェクト
	 */
	public Range getRange() {
		return range;
	}

	/**
	 * パレート図用のDatasetに値を書き換えます。(独自メソッド)
	 *
	 * 色々と方法はあると思いますが、簡易的に、内部の Number配列を
	 * 積上げ計算して、パレート図用のデータを作成します。
	 * レンジオブジェクト も変更します。
	 *
	 * ※ 注意：親クラスの内部に持っている実データは変更されていないので、
	 * 場合によっては、おかしな動きをするかもしれません。
	 * その場合は、上位にもデータをセットするように変更する必要があります。
	 *
	 * なお、行列の順序が、イメージと異なりますので、注意願います。
	 * (columnは、series , row は、category で、シリーズを積み上げます)
	 *
	 * @og.rev 6.0.2.1 (2014/09/26) 新規追加
	 * @og.rev 6.0.2.2 (2014/10/03) HybsDataset i/f
	 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
	 *
	 * @param  sum データの合計
	 */
	private void changeParetoData( final double sum ) {
		if( numdata == null || numdata.length == 0 || numdata[0].length == 0 || sum == 0.0 ) { return ; }

		final int rowCnt = numdata[0].length ;
		final int clmCnt = numdata.length ;

		for( int rowNo=0; rowNo<rowCnt; rowNo++ ) {			// 行列が逆。
			double val = 0.0;		// 初期値
			for( int clmNo=0; clmNo<clmCnt; clmNo++ ) {		// 積上げ計算するカラムでループを回す。
				final Number v1Num = numdata[clmNo][rowNo];
				if( v1Num != null ) {
					val += v1Num.doubleValue();				// 積上げ計算は、元の値のままにしておきます。
				}
				// データをセットするときに、１００分率にします。
				numdata[clmNo][rowNo] = Double.valueOf( Math.round( val * 1000.0 / sum ) / 10.0 );
	// きちんと計算するなら、BigDecimal で、スケールを指定して四捨五入すべき・・・かも
	//			java.math.BigDecimal bd = new BigDecimal( val * 100.0 / sum );
	//			numdata[clmNo][rowNo] = bd.setScale( 1, java.math.RoundingMode.HALF_UP );
			}
		}

	}

	/**
	 * categoryカラー配列を取得します。(独自メソッド)
	 *
	 * このクラスは、一番最後のカラムを、色文字列として処理し、categoryにColorを指定できます。
	 * select文で指定されていなかった場合は、null を返します。
	 *
	 * select category,series1,series2,series3,･･･,color from ･･･
	 *
	 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
	 *
	 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。
	 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に
	 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース)
	 * このRenderer で、getItemPaint( int  , int )メソッドをオーバーライドすることで、カテゴリごとの
	 * 色を返します。
	 * この設定を行うと、シリーズは、カテゴリと同一色になります。
	 *
	 * @return	categoryカラー配列(なければ null)
	 */
	public Color[] getCategoryColor() {
		// 6.0.2.5 (2014/10/31) refactoring
		return ( categoryColor == null ) ? null : categoryColor.clone();
	}

	/**
	 * category の重複をさけて、必要であれば、新しいカテゴリ名を作成します。
	 *
	 * カテゴリが同じ場合、JFreeChartでは、表示されません。これは、同じカテゴリと認識され
	 * 値が上書きされるためです。
	 * この問題は、なかなか気づきにくく、デバッグ等に時間がかかってしまいます。
	 * 重複チェックを行い、警告してもよいのですが、ここでは、新しいカテゴリ名を作成することで
	 * エラーを回避しつつ、とりあえずグラフ表示をするようにします。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規追加
	 *
	 * @param	category	元のカテゴリ名
	 * @return	新しい元のカテゴリ名
	 */
	private String uniqCategory( final String category ) {
		String newCate = category ;
		int i = 0;
		while( !cateCheck.add( newCate ) ) {	// すでに存在している場合。
			newCate = category + "(" + (i++ ) + ")" ;
		}

		return newCate ;
	}

	/**
	 * この文字列と指定されたオブジェクトを比較します。
	 *
	 * 親クラスで、equals メソッドが実装されているため、警告がでます。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) findbug対応
	 * @og.rev 5.1.9.0 (2010/08/01) findbug対応
	 *
	 * @param	object	比較するオブジェクト
	 *
	 * @return	Objectが等しい場合は true、そうでない場合は false
	 */
	@Override
	public boolean equals( final Object object ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		return super.equals( object ) && hsCode == ((HybsCategoryDataset)object).hsCode;

	}

	/**
	 * このオブジェクトのハッシュコードを取得します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) findbug対応
	 * @og.rev 5.1.9.0 (2010/08/01) findbug対応
	 *
	 * @return	ハッシュコード
	 */
	@Override
	public int hashCode() { return hsCode ; }
}
