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

import org.opengion.fukurou.system.OgRuntimeException ;
import org.opengion.fukurou.util.HybsDateUtil;
import static org.opengion.fukurou.system.HybsConst.DB_BATCH_SIZE;	// 6.9.4.1 (2018/04/09)

import java.sql.PreparedStatement;
import java.sql.ParameterMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.ResultSet;											// 7.4.1.0 (2021/04/23)

/**
 * PreparedStatementを利用した更新処理を行う、簡易的なクラスです。
 *
 * ParameterMetaDataの使用有無を指定することで、ﾊﾟﾗﾒｰﾀを処理する際に、
 * sqlType を使用するかどうかを指定します。
 * また、データ登録時のバッチサイズに基づいた処理を行っています。
 * execute(String[]) で、行ごとのパラメータデータを渡します。
 * 一番最後に、execEnd() を呼ぶことで、更新件数を返します。
 * 更新件数を取得しない場合でも、このメソッドを呼んでください。
 *
 * このクラスは、マルチスレッドに対応していません。
 *
 * @version  6.9
 * @author   Kazuhiko Hasegawa
 * @since    JDK9.0,
 */
public final class DBUpdater {
	private final PreparedStatement	pstmt ;
	private final boolean			usePMeta ;
	private final int[]				types ;
	private final boolean[]			isTime;			// 7.2.9.1 (2020/10/23) メソッドを統合します。
	private final boolean			useSelect;		// 7.4.1.0 (2021/04/23)

	private int		rowCnt;
	private int		updCnt;

	/**
	 * PreparedStatement を指定して、インスタンスを作成します。
	 *
	 * 内部で、ParameterMetaData を作成して、sqlType を使用します。
	 *
	 * @param	prmSize	パラメータの個数
	 * @param	pstmt	PreparedStatementオブジェクト
	 */
	public DBUpdater( final int prmSize , final PreparedStatement pstmt ) {
		this( prmSize , pstmt , true );
	}

	/**
	 * PreparedStatement を指定して、インスタンスを作成します。
	 *
	 * 内部で、ParameterMetaData を作成して、sqlType を使用します。
	 *
	 * @param	prmSize	パラメータの個数
	 * @param	pstmt	PreparedStatementオブジェクト
	 * @param	usePMeta	sqlType を使用するかどうか [true:使用する/false:使用しない]
	 */
	public DBUpdater( final int prmSize , final PreparedStatement pstmt , final boolean usePMeta ) {
		this( prmSize , pstmt , usePMeta, null );
	}

	/**
	 * PreparedStatementと、sqlTypeの使用有無を指定して、インスタンスを作成します。
	 *
	 * usePMetaは、内部で、ParameterMetaData を作成して、sqlType を使用するかどうかを
	 * 指定します。ORACLEのようなタイプの
	 *
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 *
	 * @param	prmSize	パラメータの個数
	 * @param	pstmt		PreparedStatementオブジェクト
	 * @param	usePMeta	sqlType を使用するかどうか [true:使用する/false:使用しない]
	 * @param	isTime		sqlType を使用するかどうか [true:使用する/false:使用しない]
	 */
	public DBUpdater( final int prmSize , final PreparedStatement pstmt , final boolean usePMeta , final boolean[] isTime ) {
		this( prmSize , pstmt , usePMeta, isTime , false );
	}

	/**
	 * PreparedStatementと、sqlTypeの使用有無を指定して、インスタンスを作成します。
	 *
	 * usePMetaは、内部で、ParameterMetaData を作成して、sqlType を使用するかどうかを
	 * 指定します。ORACLEのようなタイプの
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) isTimeのメソッドを統合します。
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 *
	 * @param	prmSize	パラメータの個数
	 * @param	pstmt		PreparedStatementオブジェクト
	 * @param	usePMeta	sqlType を使用するかどうか [true:使用する/false:使用しない]
	 * @param	isTime		sqlType を使用するかどうか [true:使用する/false:使用しない]
	 * @param	useSelect	true の場合は、UPDATE/INSERTではなく、SELECT します。
	 */
//	public DBUpdater( final int prmSize , final PreparedStatement pstmt , final boolean usePMeta , final boolean[] isTime ) {
	public DBUpdater( final int prmSize , final PreparedStatement pstmt , final boolean usePMeta ,
						final boolean[] isTime , final boolean useSelect ) {
		this.usePMeta	= usePMeta;
		this.pstmt		= pstmt;
		this.isTime		= isTime;				// 7.2.9.1 (2020/10/23) メソッドを統合します。
		this.useSelect	= useSelect;			// 7.4.1.0 (2021/04/23)

		if( usePMeta ) {
			types = new int[prmSize];

			try {
				final ParameterMetaData pMeta = pstmt.getParameterMetaData();
				for( int j=0; j<prmSize; j++ ) {
					types[j] = pMeta.getParameterType( j+1 );	// ややこしいが配列の個数と添え字の関係から、j と j+1 での処理となる。
				}
			}
			catch( final SQLException ex ) {
				final String errMsg = "ParameterMetaData の取得に失敗しました。" ;
				throw new OgRuntimeException( errMsg,ex );
			}
		}
		else {
			types = null;
		}
	}

	/**
	 * データ配列を渡してPreparedStatementの引数に、値をセットします。
	 *
	 * オラクル系の場合は、そのまま、setObject を行えば、自動変換しますが、
	 * それ以外のDBでは、java.sql.Types を渡す必要があります。さらに、null 値も、setNullを使用します。
	 * 今は、pMeta が、null かどうかで、オラクル系か、どうかを判定するようにしています。
	 *
	 * ※ このメソッドでは、useSelect は使いません。
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) isTimeのメソッドを統合します。
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 *
	 * @param	values	？に割り当てる設定値
	 *
	 * @throws SQLException DB処理の実行に失敗した場合
	 */
	public void execute( final String[] values ) throws SQLException {
		if( values != null && values.length > 0 ) {
			rowCnt++;				// 行番号(処理行数)

			// ORACLE では、ParameterMetaDataは、使わない。
			if( usePMeta ) {
				for( int j=0; j<values.length; j++ ) {
					final String val = values[j];
					if( val == null || val.isEmpty() ) {
						pstmt.setNull( j+1, types[j] );			// JDBC のカラム番号は、１から始まる。
					}
					else {
						pstmt.setObject( j+1,val,types[j] );
					}
				}
			}
			else {
				if( isTime == null ) {
					for( int j=0; j<values.length; j++ ) {
						final String val = values[j];				// JDBC のカラム番号は、１から始まる。
						pstmt.setObject( j+1,val );
					}
				}
				else {
					// Timestamp オブジェクトを登録する場合の特別版です。
					// 過去のコーディングとの互換性の関係で、ParameterMetaData を使用しません。
					for( int j=0; j<values.length; j++ ) {
						final String val = values[j];
						if( isTime[j] && val != null && !val.isEmpty() ) {
							// val は、yyyy-mm-dd hh:mm:ss[.f...] 形式でなければならない。
							final Timestamp time = Timestamp.valueOf( HybsDateUtil.parseTimestamp( val ) );
							pstmt.setObject( j+1,time );
						}
						else {
							pstmt.setObject( j+1,val );
						}
					}
				}
			}
			pstmt.addBatch();

			if( rowCnt % DB_BATCH_SIZE == 0 ) {
				final int[] execCnt = pstmt.executeBatch();
				// 6.9.4.1 (2018/04/09) 更新件数は、暫定的に、データ処理件数と同じとする。
				updCnt += execCnt.length;
			}
		}
	}

//	/**
//	 * データ配列を渡してPreparedStatementの引数に、値をセットします。
//	 *
//	 * Timestamp オブジェクトを登録する場合の特別版です。
//	 * 過去のコーディングとの互換性の関係で、ParameterMetaData を使用しません。
//	 *
//	 * @og.rev 7.2.9.1 (2020/10/23) isTimeのメソッドを統合します。
//	 *
//	 * @param	values	？に割り当てる設定値
//	 * @param	isTime	Timestampを設定するカラムの場合は、true
//	 *
//	 * @throws SQLException DB処理の実行に失敗した場合
//	 */
//	public void execute( final String[] values , final boolean[] isTime ) throws SQLException {
//		if( values != null && values.length > 0 ) {
//			rowCnt++;				// 行番号(処理行数)
//
//			for( int j=0; j<values.length; j++ ) {
//				final String val = values[j];
//				if( isTime[j] && val != null && !val.isEmpty() ) {
//					// val は、yyyy-mm-dd hh:mm:ss[.f...] 形式でなければならない。
//					final Timestamp time = Timestamp.valueOf( HybsDateUtil.parseTimestamp( val ) );
//					pstmt.setObject( j+1,time );
//				}
//				else {
//					pstmt.setObject( j+1,val );
//				}
//			}
//
//			pstmt.addBatch();
//
//			if( rowCnt % DB_BATCH_SIZE == 0 ) {
//				final int[] execCnt = pstmt.executeBatch();
//
//				// 6.9.4.1 (2018/04/09) 更新件数は、暫定的に、データ処理件数と同じとする。
//				updCnt += execCnt.length;
//			}
//		}
//	}

	/**
	 * データ配列を渡してPreparedStatementの引数に、値をセットします。
	 *
	 * useSelect=true の場合は、更新系ではなく検索系の処理を行います。
	 * ここでは、マージ処理を考慮しているため、検索結果が、0件か、それ以上かのみ判定します。
	 *
	 * @og.rev 7.2.9.1 (2020/10/23) TableUpdateParamTag のマージ(UPDATE,INSERT)対応
	 * @og.rev 7.4.1.0 (2021/04/23) sqlType="MERGE" 時のみ有効で、where 条件で存在すれば何もしない
	 *
	 * @param	values	？に割り当てる設定値
	 * @return	更新件数
	 *
	 * @throws SQLException DB処理の実行に失敗した場合
	 */
	public int update( final String[] values ) throws SQLException {
		int rtnCnt = 0 ;

		if( values != null && values.length > 0 ) {
			rowCnt++;				// 行番号(処理行数)

			// ORACLE では、ParameterMetaDataは、使わない。
			if( usePMeta ) {
				for( int j=0; j<values.length; j++ ) {
					final String val = values[j];
					if( val == null || val.isEmpty() ) {
						pstmt.setNull( j+1, types[j] );			// JDBC のカラム番号は、１から始まる。
					}
					else {
						pstmt.setObject( j+1,val,types[j] );
					}
				}
			}
			else {
				if( isTime == null ) {
					for( int j=0; j<values.length; j++ ) {
						final String val = values[j];				// JDBC のカラム番号は、１から始まる。
						pstmt.setObject( j+1,val );
					}
				}
				else {
					// Timestamp オブジェクトを登録する場合の特別版です。
					// 過去のコーディングとの互換性の関係で、ParameterMetaData を使用しません。
					for( int j=0; j<values.length; j++ ) {
						final String val = values[j];
						if( isTime[j] && val != null && !val.isEmpty() ) {
							// val は、yyyy-mm-dd hh:mm:ss[.f...] 形式でなければならない。
							final Timestamp time = Timestamp.valueOf( HybsDateUtil.parseTimestamp( val ) );
							pstmt.setObject( j+1,time );
						}
						else {
							pstmt.setObject( j+1,val );
						}
					}
				}
			}
//			return pstmt.executeUpdate();
			if( useSelect ) {							// 7.4.1.0 (2021/04/23) 検索します。
				final ResultSet resultSet = pstmt.executeQuery();

				if( resultSet.next() ) {
					rtnCnt = resultSet.getInt(1);		// select count(*) なら…
				}
			}
			else {
				rtnCnt = pstmt.executeUpdate();
			}
		}
//		return 0;
		return rtnCnt;			// 7.4.1.0 (2021/04/23)
	}

	/**
	 * データの最後の処理を行います。
	 *
	 * 具体的には、executeBatch() で、所定のバッチ数に届いていない場合の処理です。
	 *
	 * @return	更新件数
	 * @throws	SQLException ﾃﾞｰﾀﾍﾞｰｽ処理で例外が発生した場合。
	 */
	public int execEnd() throws SQLException {
		final int[] execCnt = pstmt.executeBatch();
		// 6.9.4.1 (2018/04/09) 更新件数は、暫定的に、データ処理件数と同じとする。
		updCnt += execCnt.length;

		return updCnt;
	}
}
