/*
 * 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_ROTATE は、TableFilter インターフェースを継承した、DBTableModel 処理用の
 * 実装クラスです。
 *
 * ここではテーブルの回転、及びその逆回転を行います。
 *
 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
 * 【パラメータ】
 *  {
 *       KEY_CLM    : キーカラム(複数指定可)
 *       ROTATE_CLM : 回転するカラム
 *       VALUE_CLM  : 回転カラムの値
 *       REVERSE    : 回転(false)・逆回転(true) (任意指定 初期値:false)
 *       MUST_CLM   : 必須属性を定義するカラム  (任意指定 初期値:false)
 *       DEF_CLM    : 初期値を定義するカラム    (任意指定)
 *  }
 *
 *  ※ それぞれに指定されたカラム名が存在しない場合は、処理されませんのでご注意下さい。
 *
 * ①回転
 *  キーカラムに指定された値が同じN行を1行として回転します。
 *  (キーカラムの値がブレイクしたタイミングで、行を変更します)
 *  このN行に含まれる回転カラムの値がカラム名に、回転カラム値が各カラムの値になります。
 *  キーカラムは、CSV形式で複数指定可能です。
 *
 *  生成されたテーブルモデルのカラムは、始めのMカラムがキーカラムに、その後ろのNカラムが
 *  回転されたカラムになります。
 *
 *  また、元テーブルにMUST_CLMにより、各カラムの必須属性を定義することが
 *  できます。(MUST属性は、'1'又は'true'の場合に必須になります。)
 *
 * ②逆回転
 *  回転時の逆の挙動になります。
 *  "キーカラムに指定されたカラム以外"を回転カラムで指定されたカラムの値として分解します。
 *  各回転カラムの値は、回転カラム値に指定されたカラムに格納されます。
 *
 *  分解後のカラム数は、キーカラム数 + 2 (回転カラム、回転カラム値)になります。
 *  また、行数は、(分解前の行数) x (回転カラム数)になります。
 *
 * @og.formSample
 * ●形式：
 *      ① &lt;og:tableFilter classId="ROTATE" keys="KEY_CLM,REVERSE" vals='"AA,BB,CC",true' /&gt;
 *
 *      ② &lt;og:tableFilter classId="ROTATE" &gt;
 *               {
 *                   KEY_CLM    : キーカラム(複数指定可)
 *                   ROTATE_CLM : 回転するカラム
 *                   VALUE_CLM  : 回転カラムの値
 *                   REVERSE    : 回転(false)・逆回転(true) (任意指定 初期値:false)
 *                   MUST_CLM   : 必須属性を定義するカラム  (任意指定 初期値:false)
 *                   DEF_CLM    : 初期値を定義するカラム    (任意指定)
 *               }
 *         &lt;/og:tableFilter&gt;
 *
 * @og.rev 5.6.6.0 (2013/07/05) keys の整合性チェックを追加
 *
 * @version  0.9.0  2000/10/17
 * @author   Hiroki Nakamura
 * @since    JDK1.1,
 */
public class TableFilter_ROTATE extends AbstractTableFilter {
	// * このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "6.4.3.4 (2016/03/11)" ;

	private DBTableModel	table	 ;			// 5.5.2.6 (2012/05/25) 共通に使うため、変数定義
	private ResourceManager	resource ;			// 5.5.2.6 (2012/05/25) 共通に使うため、変数定義

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.1.1 (2016/01/16) keysMap を、サブクラスから設定させるように変更。
	 */
	public TableFilter_ROTATE() {
		super();
		initSet( "KEY_CLM"   	, "キーカラム(複数指定可)"					);
		initSet( "ROTATE_CLM"	, "回転するカラム"							);
		initSet( "VALUE_CLM"	, "回転カラムの値"							);
		initSet( "REVERSE"		, "回転(false)/逆回転(true) (初期値:false)"	);
		initSet( "MUST_CLM"		, "必須属性を定義するカラム (初期値:false)"	);
		initSet( "DEF_CLM"		, "初期値を定義するカラム"					);
	}

//	/**
//	 * keys の整合性チェックを行うための初期設定を行います。
//	 *
//	 * @og.rev 5.6.6.1 (2013/07/12) keys の整合性チェック対応
//	 * @og.rev 6.4.1.1 (2016/01/16) keysMap を、サブクラスから設定させるため、廃止。
//	 *
//	 * @param	keysMap keys の整合性チェックを行うための Map
//	 */
//	@Override
//	protected void init( final Map<String,String> keysMap ) {
//		keysMap.put( "KEY_CLM"   	, "キーカラム(複数指定可)"					);
//		keysMap.put( "ROTATE_CLM"	, "回転するカラム"							);
//		keysMap.put( "VALUE_CLM"	, "回転カラムの値"							);
//		keysMap.put( "REVERSE"		, "回転(false)/逆回転(true) (初期値:false)"	);
//		keysMap.put( "MUST_CLM"		, "必須属性を定義するカラム (初期値:false)"	);
//		keysMap.put( "DEF_CLM"		, "初期値を定義するカラム"					);
//	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 4.3.7.4 (2009/07/01) 新規追加
	 * @og.rev 5.5.2.6 (2012/05/25) protected変数を、private化したため、getterメソッドで取得するように変更
	 *
	 * @return 処理結果のDBTableModel
	 */
	public DBTableModel execute() {
//		final boolean reverse = StringUtil.nval( getValue( "REVERSE" ), false );

		table    = getDBTableModel();		// 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加
		resource = getResource();			// 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加

		// 6.3.9.1 (2015/11/27) A method should have only one exit point, and that should be the last statement in the method.(PMD)
		final boolean reverse = StringUtil.nval( getValue( "REVERSE" ), false );

		//				 逆回転				  回転
		return reverse ? getRecoverdTable() : getRotateTable();

//		// 逆回転
//		if( reverse ) {
//			return getRecoverdTable();
//		}
//		// 回転
//		else {
//			return getRotateTable();
//		}
	}

	/**
	 * 回転後のDBTableModelを返します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) メソッド名変更(setDefValue ⇒ setDefault)
	 * @og.rev 6.4.3.4 (2016/03/11) forループを、forEach メソッドに置き換えます。
	 *
	 * @return 回転後のDBTableModel
	 */
	private DBTableModel getRotateTable() {
		final String[] keyClm = StringUtil.csv2Array( getValue( "KEY_CLM" ) );
		final int rotateNo = table.getColumnNo( getValue( "ROTATE_CLM" ), false );
		final int valNo = table.getColumnNo( getValue( "VALUE_CLM" ), false );

		if( keyClm == null || keyClm.length == 0 || rotateNo < 0 || valNo < 0 ) {
			return table;
		}

		int clmCount = 0; // 回転後のカラム数
		// キーカラムのカラム番号を求め、カラム数としてカウントします。
		final Map<String, Integer> clmMap = new HashMap<>();
		int[] keyNos = new int[keyClm.length];
		for( int i=0; i<keyNos.length; i++ ) {
			keyNos[i] = table.getColumnNo( keyClm[i], false );
			if( keyNos[i] < 0 ) {
				return table;
			}
			clmMap.put( keyClm[i], clmCount );
			clmCount++;
		}

		int rowCount = 0; // 回転後の行数
		// 回転カラムの値から回転後のカラム数を求めます。
		// また同時に、キーカラムの値のブレイク数により行数を求めます。
		final Map<String, Integer> rowMap		= new HashMap<>();
		final Map<String, Boolean> mustMap		= new HashMap<>();
		final Map<String, String>  defaultMap	= new HashMap<>();
		final int mustNo = table.getColumnNo( getValue( "MUST_CLM"), false );
		final int defNo  = table.getColumnNo( getValue( "DEF_CLM"), false );
		for( int i=0; i<table.getRowCount(); i++ ) {
			final String clmKey = table.getValue( i, rotateNo );
			if( clmMap.get( clmKey ) == null ) {
				clmMap.put( clmKey, clmCount );
				clmCount++;
			}
			// 必須カラム抜き出し
			if( mustNo > -1 && StringUtil.nval( table.getValue( i, mustNo ), false ) ) {
				mustMap.put( clmKey, true );
			}
			// デフォルト値を書き換えるカラムの抜き出し
			if( defNo > -1 && table.getValue( i, defNo ) != null && table.getValue( i, defNo ).length() > 0 ) {
				defaultMap.put( clmKey, table.getValue( i, defNo ) );
			}

			final String rowKey = getSeparatedValue( i, keyNos );
			// 6.0.0.1 (2014/04/25) These nested if statements could be combined
			if( rowKey != null && rowKey.length() > 0 && rowMap.get( rowKey ) == null ) {
				rowMap.put( rowKey, rowCount );
				rowCount++;
			}
		}

		// 回転後のカラム一覧よりDBTableModelを初期化します。
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//		String names[] = new String[clmMap.size()];
		final String names[] = new String[clmMap.size()];
		// 6.4.3.4 (2016/03/11) forループを、forEach メソッドに置き換えます。
		clmMap.forEach( (k,v) -> names[v] = k );
//		for( final Map.Entry<String, Integer> entry : clmMap.entrySet() ) {
//			names[entry.getValue()] = entry.getKey();
//		}
		final DBTableModel nTable = DBTableModelUtil.newDBTable();
		nTable.init( names.length );
		for( int i=0; i<names.length; i++ ) {
			if( mustMap.get( names[i] ) != null ) {
				table.addMustType( i, "must" );
			}
			DBColumn column = resource.makeDBColumn( names[i] );
			if( defaultMap.get( names[i] ) != null ) {
				final DBColumnConfig dbConfig = column.getConfig();
				dbConfig.setDefault( defaultMap.get( names[i] ) );		// 5.1.8.0 (2010/07/01)
				column = new DBColumn( dbConfig );
			}
			nTable.setDBColumn( i, column );							// 5.1.8.0 (2010/07/01)
		}

		// 値の一覧を作成し、DBTableModelに値をセットします。
		if( rowCount > 0 ) {
			// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//			String[][] vals = new String[rowCount][names.length];
			final String[][] vals = new String[rowCount][names.length];
			for( int i=0; i<table.getRowCount(); i++ ) {
				final int row = rowMap.get( getSeparatedValue( i, keyNos ) );
				final int clm = clmMap.get( table.getValue( i, rotateNo ) );

				for( int j=0; j<keyNos.length; j++ ) {
					vals[row][j] = table.getValue( i, keyNos[j] );
				}
				vals[row][clm] = table.getValue( i, valNo );
			}
			for( int i=0; i<vals.length; i++ ) {
				nTable.addColumnValues( vals[i] );
			}
		}

		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++ ) {
			final String val = table.getValue( row, clmNo[i] );
			if( val != null && val.length() > 0 ) {
				if( i > 0 ) {
					buf.append( "__" );
				}
				buf.append( val );
			}
		}
		return buf.toString();
	}

	/**
	 * 逆回転後のDBTableModelを返します。
	 *
	 * @return 逆回転後のDBTableModel
	 */
	private DBTableModel getRecoverdTable() {
		final String[] keyClm = StringUtil.csv2Array( getValue( "KEY_CLM" ) );
		final String rotateClm = getValue( "ROTATE_CLM" );
		final String valClm = getValue( "VALUE_CLM" );

		if( keyClm == null || keyClm.length == 0
				|| rotateClm == null || rotateClm.isEmpty()
				|| valClm    == null || valClm.isEmpty() ) {		// 6.1.0.0 (2014/12/26) refactoring
			return table;
		}

		// キーカラムのカラム番号を求めます。
		int[] keyNos = new int[keyClm.length];
		for( int i=0; i<keyNos.length; i++ ) {
			keyNos[i] = table.getColumnNo( keyClm[i], false );
			if( keyNos[i] < 0 ) {
				return table;
			}
		}

		// キーカラム以外(回転カラム以外)のカラム番号を求めます。
		int clmIdx = 0;
		// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//		int[] clmNos = new int[table.getColumnCount() - keyNos.length];
		final int[] clmNos = new int[table.getColumnCount() - keyNos.length];
		for( int i=0; i<table.getColumnCount(); i++ ) {
			boolean isClm = true;
			for( int j=0; j<keyNos.length; j++ ) {
				if( i == keyNos[j] ) {
					isClm = false;
				}
			}
			if( isClm ) {
				clmNos[clmIdx] = i;
				clmIdx++;
			}
		}

		// テーブルモデルを初期化します。
		final DBTableModel nTable = DBTableModelUtil.newDBTable();
		nTable.init( keyNos.length + 2 );
		for( int i=0; i<keyNos.length; i++ ) {
			nTable.setDBColumn( i, resource.makeDBColumn( keyClm[i] ) );
		}
		nTable.setDBColumn( keyNos.length, resource.makeDBColumn( rotateClm ) );
		nTable.setDBColumn( keyNos.length + 1, resource.makeDBColumn( valClm ) );

		// 各行を作成し、DBTableModelに登録します。
		for( int i=0; i<table.getRowCount(); i++ ) {
			for( int j=0; j<clmNos.length; j++ ) {
				// 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
//				String[] vals = new String[keyNos.length + 2];
				final String[] vals = new String[keyNos.length + 2];
				for( int k=0; k<keyNos.length; k++ ) {
					vals[k] = table.getValue( i, keyNos[k] );
				}
				vals[keyNos.length] = table.getColumnName( clmNos[j] );
				vals[keyNos.length + 1] = table.getValue( i, clmNos[j] );
				nTable.addColumnValues( vals );
			}
		}

		return nTable;
	}

}
