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

import java.sql.Connection;
import java.sql.ResultSet;
// import java.sql.Statement;
// import java.sql.ResultSetMetaData;
import java.sql.PreparedStatement;
import java.sql.ParameterMetaData;
import java.sql.SQLException;

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.PoolProperties;

/**
 * データベース処理を行う、簡易的なユーティリティークラスです。
 * staticメソッドしか持っていません。
 * sql文を execute( query ) する事により,データベースに書き込みます。
 *
 * このクラスは、マルチスレッドに対して、安全です。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class DBUtil {
	private static final XLogger LOGGER= XLogger.getLogger( DBUtil.class.getName() );	// ログ出力

	/** ﾃﾞｰﾀﾍﾞｰｽのｷｰﾜｰﾄﾞ {@value}	*/	
	public static final String DATABASE_KEY	= "DATABASE";

	/** 接続先URL	{@value}	*/	
	public static final String URL_KEY		= "REALM_URL";
	/** ﾄﾞﾗｲﾊﾞｰ 	{@value}	*/	
	public static final String DRIVER_KEY	= "REALM_DRIVER";
	/** ﾕｰｻﾞｰID 	{@value}	*/	
	public static final String NAME_KEY		= "REALM_NAME";
	/** ﾊﾟｽﾜｰﾄﾞ 	{@value}	*/	
	public static final String PASSWORD_KEY	= "REALM_PASSWORD";

	/** データベースリトライの待ち時間(ミリ秒) {@value} */
	public static final int CONN_SLEEP_TIME  = 2000 ;	// 6.8.2.2 (2017/11/02) コネクションの獲得まで、２秒待つ
	/** データベースリトライ回数 {@value} */
	public static final int CONN_RETRY_COUNT = 10 ;		// 6.8.2.2 (2017/11/02) コネクションの獲得まで、１０回リトライする。
	/** データベースValid タイムアウト時間(秒) {@value} */
	public static final int CONN_VALID_TIMEOUT = 10 ;	// 6.8.2.2 (2017/11/02) コネクションのValidチェックのタイムアウト時間。

	private static final DataSource DATA_SOURCE = new DataSource();

	private static boolean readyFlag ;		// 準備が出来た場合は、true

	private static boolean oracleFlag ;		// 接続先がORACLEの場合は、true

	private static final int	BUFFER_MIDDLE = 200 ;

	/**
	 * インスタンスを作成させないため、private 化します。
	 */
	private DBUtil() {}
	/**
	 * 引数を指定せず、オブジェクトを作成します。
	 *
	 * System.getProperty より取得し、さらに、そこから取得できなかった
	 * 場合は、環境変数から、取得します。
	 *
	 * @see		#URL_KEY
	 */
	public static void init() {
		init(	System.getProperty( URL_KEY			, System.getenv( URL_KEY		) ) ,
				System.getProperty( DRIVER_KEY		, System.getenv( DRIVER_KEY		) ) ,
				System.getProperty( NAME_KEY		, System.getenv( NAME_KEY		) ) ,
				System.getProperty( PASSWORD_KEY	, System.getenv( PASSWORD_KEY	) )
		);
	}

	/**
	 * 接続先URL、ドライバー、ユーザーID、パスワードなどを含んだMapを指定して、オブジェクトを作成します。
	 *
	 * Mapに指定のキーが含まれない場合は、System.getProperty より取得し、さらに、そこから取得できなかった
	 * 場合は、環境変数から、取得します。
	 *
	 * @param	prmMap	必要情報を含んだMapオブジェクト
	 * @see		#URL_KEY
	 */
	public static void init( final Map<String,String> prmMap ) {
		init(	prmMap.getOrDefault( URL_KEY		, System.getProperty( URL_KEY			, System.getenv( URL_KEY		) ) ) ,
				prmMap.getOrDefault( DRIVER_KEY		, System.getProperty( DRIVER_KEY		, System.getenv( DRIVER_KEY		) ) ) ,
				prmMap.getOrDefault( NAME_KEY		, System.getProperty( NAME_KEY			, System.getenv( NAME_KEY		) ) ) ,
				prmMap.getOrDefault( PASSWORD_KEY	, System.getProperty( PASSWORD_KEY		, System.getenv( PASSWORD_KEY	) ) )
		);
	}

	/**
	 * 接続先URL、ドライバー、ユーザーID、パスワードを指定して、オブジェクトを作成します。
	 *
	 * params は、必ず、４つ必要です。
	 *
	 * @param	params		接続先URL、ドライバー、ユーザーID、パスワード
	 * @see		#isReady()
	 */
	public static void init( final String... params ) {
		if( readyFlag ) {
			// MSG0024 = すでに、接続先設定は完了しています。[{0}]
			throw MsgUtil.throwException( "MSG0024" , DATA_SOURCE );
		}

		if( params == null || params.length != 4 ) {
			// MSG0027 = 接続先設定情報が不足しています。[{0}]
			throw MsgUtil.throwException( "MSG0027" , Arrays.toString( params ) );
		}

		final PoolProperties pp = new PoolProperties();
		pp.setUrl(				params[0] );
		pp.setDriverClassName(	params[1] );
		pp.setUsername(			params[2] );
		pp.setPassword(			params[3] );

		DATA_SOURCE.setPoolProperties( pp );
		readyFlag = true;

		oracleFlag = params[0] != null && params[0].startsWith( "jdbc:oracle" );
	}

	/**
	 * DataSourceの初期化が完了していれば、true を返します。
	 *
	 * 初期化は、#init(String...) メソッドの呼び出して、完了します。
	 * #crear() で、未完了に戻ります。
	 *
	 * @return	初期化が完了しているかどうか
	 * @see		#init(String...)
	 */
	public static boolean isReady() { return readyFlag; }

	/**
	 * 接続先がORACLEかどうかを返します。
	 *
	 * ORACLE の場合は、true を返します。
	 *
	 * @return	接続先がORACLEかどうか[true:ORACLE false:その他]
	 */
	public static boolean isOracle() { return oracleFlag; }

	/**
	 * DataSource から、Connectionを取得して、返します。
	 *
	 * @og.rev 6.8.2.2 (2017/11/02) コネクションの再取得をリトライします。
	 *
	 * @return	DataSourceから、Connectionを取得して、返します。
	 * @throws	SQLException SQLエラーが発生した場合
	 */
	public static Connection getConnection() throws SQLException {
		if( !readyFlag ) {
	//		// MSG0025 = 接続先設定が完了していません。
	//		throw MsgUtil.throwException( "MSG0025" , "getConnection() Error!!" );
			init();
		}

//		final Connection conn = DATA_SOURCE.getConnection();
//		conn.setAutoCommit( false );
//	//	conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);  // 初期値

//		return conn;

		SQLException errEX = null;
		for( int i=0; i<CONN_RETRY_COUNT; i++ ) {
			try {
				final Connection conn = DATA_SOURCE.getConnection();
				conn.setAutoCommit( false );

				if( conn.isValid( CONN_VALID_TIMEOUT ) ) { return conn; }
			}
			catch( SQLException ex ) {
				// MSG0019 = DB処理の実行に失敗しました。ﾒｯｾｰｼﾞ=[{0}]。\n\tquery=[{1}]\n\tvalues={2}
				MsgUtil.errPrintln( "MSG0019" , ex.getMessage() );

				errEX = ex ;
				try{ Thread.sleep( CONN_SLEEP_TIME ); } catch( final InterruptedException ex2 ){}
			}
		}

		final String errMsg = errEX == null ? "COUNT Over" : errEX.getMessage() ;
		// MSG0019 = DB処理の実行に失敗しました。ﾒｯｾｰｼﾞ=[{0}]。\n\tquery=[{1}]\n\tvalues={2}
		throw MsgUtil.throwException( errEX , "MSG0019" , errMsg , "getConnection" , "" );
	}

	/**
	 * データ配列を渡して実際のDB処理を実行します。
	 *
	 * ここでは、１行だけ処理するための簡易メソッドを提供します。
	 *
	 * @param	query	実行するSQL文
	 * @param	values	？に割り当てる設定値
	 * @return	ここでの処理件数
	 *
	 * @throws RuntimeException Connection DB処理の実行に失敗した場合
	 */
	public static int execute( final String query , final String... values ) {
		final List<String[]> list = new ArrayList<>();
		list.add( values );

		return execute( query,list );
	}

	/**
	 * データ配列のListを渡して実際のDB処理を実行します。
	 *
	 * データ配列は、１行分のデータに対する設定値の配列です。
	 * これは、keys で指定した並び順と一致している必要があります。
	 *
	 * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
	 *
	 * @param	query	実行するSQL文
	 * @param	list	？に割り当てる設定値
	 * @return	ここでの処理件数
	 *
	 * @throws RuntimeException Connection DB処理の実行に失敗した場合
	 */
	public static int execute( final String query , final List<String[]> list ) {
		LOGGER.debug( () -> "execute query=" + query );

		String[] debugLine = null;
		int	execCnt = 0;

		// try-with-resources 文 (AutoCloseable)
		try( final Connection conn = getConnection();
			 final PreparedStatement pstmt = conn.prepareStatement( query ) ) {

			// ORACLE では、ParameterMetaDataは、使わない。
			final ParameterMetaData pMeta = oracleFlag ? null : pstmt.getParameterMetaData();

			for( final String[] values : list ) {
				debugLine = values;
				LOGGER.debug( () -> "execute values=" + Arrays.toString( values ) );
				setObject( pstmt , values , pMeta );
				execCnt += pstmt.executeUpdate();
			}

			conn.commit();
		}
		catch( final SQLException ex ) {
			// MSG0019 = DB処理の実行に失敗しました。ﾒｯｾｰｼﾞ=[{0}]。\n\tquery=[{1}]\n\tvalues={2}
			throw MsgUtil.throwException( ex , "MSG0019" , ex.getMessage() , query , Arrays.toString( debugLine ) );
		}

		return execCnt;
	}

	/**
	 * データ配列を渡してPreparedStatementの引数に、値をセットします。
	 *
	 * オラクル系の場合は、そのまま、setObject を行えば、自動変換しますが、
	 * それ以外のDBでは、java.sql.Types を渡す必要があります。さらに、null 値も、setNullを使用します。
	 * 今は、pMeta が、null かどうかで、オラクル系か、どうかを判定するようにしています。
	 *
	 * @param	pstmt	PreparedStatementオブジェクト
	 * @param	values	？に割り当てる設定値
	 * @param	pMeta	オラクル系以外のDBに対して、type指定する場合に使用する ParameterMetaDataオブジェクト
	 *
	 * @throws SQLException DB処理の実行に失敗した場合
	 */
	private static void setObject( final PreparedStatement pstmt , final String[] values , final ParameterMetaData pMeta ) throws SQLException {
		if( values != null && values.length > 0 ) {
			// ORACLE では、ParameterMetaDataは、使わない。
			if( pMeta == null ) {
				int clmNo = 1;	// JDBC のカラム番号は、１から始まる。
				for( int i=0; i<values.length; i++ ) {
					final String val = values[i];
					pstmt.setObject( clmNo++,val );
				}
			}
			else {
				int clmNo = 1;	// JDBC のカラム番号は、１から始まる。
				for( int i=0; i<values.length; i++ ) {
					final int type = pMeta.getParameterType( clmNo );
					final String val = values[i];
					if( val == null || val.isEmpty() ) {
						pstmt.setNull( clmNo++, type );
					}
					else {
						pstmt.setObject( clmNo++,val,type );
					}
				}
			}
		}
	}

	/**
	 * データ配列のListを渡して実際のDB処理を実行します。(暫定メソッド)
	 *
	 * これは、updQueryで、更新してみて、０件の場合、insQuery で追加処理を行います。
	 *
	 * データ配列は、１行分のデータに対する設定値の配列です。
	 * これは、keys で指定した並び順と一致している必要があります。
	 *
	 * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
	 *
	 * @param	insQuery	追加するSQL文
	 * @param	updQuery	更新するSQL文
	 * @param	insList	？に割り当てる設定値
	 * @param	updList	？に割り当てる設定値
	 * @return	ここでの処理件数
	 *
	 * @throws RuntimeException Connection DB処理の実行に失敗した場合
	 */
	public static int execute( final String insQuery , final String updQuery , final List<String[]> insList , final List<String[]> updList ) {
		LOGGER.debug( () -> "execute insQuery=" + insQuery + " , updQuery=" + updQuery );

		String[] debugLine = null;
		String   query     = null;

		int	execCnt = 0;

		// try-with-resources 文 (AutoCloseable)
		try( final Connection conn = getConnection();
			 final PreparedStatement inPstmt = conn.prepareStatement( insQuery );
			 final PreparedStatement upPstmt = conn.prepareStatement( updQuery ) ) {

			// ORACLE では、ParameterMetaDataは、使わない。
			final ParameterMetaData	inpMeta = oracleFlag ? null : inPstmt.getParameterMetaData();
			final ParameterMetaData	uppMeta = oracleFlag ? null : upPstmt.getParameterMetaData();

			for( int i=0; i<updList.size(); i++ ) {			// 更新処理と、挿入処理は、同じ数のListを用意する。
				query = updQuery;
				// 更新処理を行う。
				final String[] upVals = updList.get(i);
				debugLine = upVals;
				setObject( upPstmt , upVals , uppMeta );

				int cnt = upPstmt.executeUpdate();

				if( cnt <= 0 ) {	// 更新が無い、つまり、追加対象
					query = insQuery;
					// 挿入処理を行う。
					final String[] inVals = insList.get(i);
					debugLine = inVals;
					setObject( inPstmt , inVals , inpMeta );

					LOGGER.debug( () -> "execute INSERT=" + Arrays.toString( inVals ) );

					cnt = inPstmt.executeUpdate();
				}
				else {		// 元々、このelse は必要ない。UPDATE は、先に処理済
					LOGGER.debug( () -> "execute UPDATE=" + Arrays.toString( upVals ) );
				}

				execCnt += cnt;
			}
			conn.commit();
		}
		catch( final SQLException ex ) {
			// try-with-resources文では、すでに、close() 処理済で、例外に渡される。
			// JDBCの仕様上、close時に内部でrollbackされる約束になっているので、書かなくても大丈夫。

			// MSG0019 = DB処理の実行に失敗しました。ﾒｯｾｰｼﾞ=[{0}]。\n\tquery=[{1}]\n\tvalues={2}
			throw MsgUtil.throwException( ex , "MSG0019" , ex.getMessage() , query , Arrays.toString( debugLine ) );
		}

		return execCnt;
	}

	/**
	 * 検索するデータベースを指定して、Queryを実行します(Transaction 対応)。
	 *
	 * ステートメントと引数により、Prepared クエリーの検索のみ実行します。
	 * 結果は,すべて文字列に変換されて格納されます。
	 *
	 * @param   query ステートメント文字列
	 * @param   args オブジェクトの引数配列
	 *
	 * @return  検索結果のリスト配列(結果が無ければ、サイズゼロのリスト)
	 * @throws RuntimeException DB検索処理の実行に失敗した場合
	 * @og.rtnNotNull
	 */
	public static List<String[]> dbQuery( final String query , final String... args ) {
		// try-with-resources 文 (AutoCloseable)
		try( final Connection conn = getConnection();
			 final PreparedStatement pstmt = conn.prepareStatement( query ) ) {

			// ORACLE では、ParameterMetaDataは、使わない。
			final ParameterMetaData pMeta = oracleFlag ? null : pstmt.getParameterMetaData();
			// 6.4.3.2 (2016/02/19) args が null でなく、length==0 でない場合のみ、処理する。
			setObject( pstmt , args , pMeta );

			if( pstmt.execute() ) {
				try( final ResultSet resultSet = pstmt.getResultSet() ) {
					return resultToArray( resultSet );
				}
			}
			conn.commit();
		}
		catch ( final SQLException ex ) {
			// try-with-resources文では、すでに、close() 処理済で、例外に渡される。
			// JDBCの仕様上、close時に内部でrollbackされる約束になっているので、書かなくても大丈夫。

			// MSG0019 = DB処理の実行に失敗しました。ﾒｯｾｰｼﾞ=[{0}]。\n\tquery=[{1}]\n\tvalues={2}
			throw MsgUtil.throwException( ex , "MSG0019" , ex.getMessage() , query , Arrays.toString( args ) );
		}

		return new ArrayList<String[]>();
	}

	/**
	 * ResultSet より、結果の文字列配列を作成します。
	 *
	 * 結果は,すべて文字列に変換されて格納されます。
	 * 移動したメソッドで使われているのでこれも移動
	 *
	 * @param   resultSet ResultSetオブジェクト
	 *
	 * @return  ResultSetの検索結果リスト配列
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合
	 * @og.rtnNotNull
	 */
	public static List<String[]> resultToArray( final ResultSet resultSet ) throws SQLException {
		final ArrayList<String[]> data = new ArrayList<>();

		final ResultSetValue rsv = new ResultSetValue( resultSet );

		while( rsv.next() ) {
			data.add( rsv.getValues() );
		}

		return data;
	}

	/**
	 * データをインサートする場合に使用するSQL文を作成します。
	 *
	 * これは、key に対応した ？ 文字列で、SQL文を作成します。
	 * 実際の値設定は、この、キーの並び順に応じた値を設定することになります。
	 * conKeysとconValsは、固定値のキーと値です。
	 * conKeys,conVals がnullの場合は、これらの値を使用しません。
	 *
	 * @param	table	テーブルID
	 * @param	keys	設定値に対応するキー配列
	 * @param	conKeys	固定値の設定値に対応するキー配列
	 * @param	conVals	固定値に対応する値配列
	 * @return  インサートSQL
	 * @og.rtnNotNull
	 */
	public static String getInsertSQL( final String table , final String[] keys , final String[] conKeys , final String[] conVals ) {
		final String[] vals = new String[keys.length];
		Arrays.fill( vals , "?" );

		final boolean useConst = conKeys != null && conVals != null && conKeys.length == conVals.length && conKeys.length > 0 ;

		final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE )
			.append( "INSERT INTO " ).append( table )
			.append( " ( " )
			.append( String.join( "," , keys ) );

		if( useConst ) {
			sql.append( ',' ).append( String.join( "," , conKeys ) );
		}

		sql.append( " ) VALUES ( " )
			.append( String.join( "," , vals ) );

		if( useConst ) {
			sql.append( ",'" ).append( String.join( "','" , conVals ) ).append( '\'' );
		}

		return sql.append( " )" ).toString();
	}

	/**
	 * データをアップデートする場合に使用するSQL文を作成します。
	 *
	 * これは、key に対応した ？ 文字列で、SQL文を作成します。
	 * 実際の値設定は、この、キーの並び順に応じた値を設定することになります。
	 * WHERE 文字列は、この、？ も含めたWHERE条件の文字列を渡します。
	 * WHERE条件の場合は、この、？に、関数を設定したり、条件を指定したり、
	 * 色々なケースがあるため、単純にキーだけ指定する方法では、対応範囲が
	 * 限られるためです。
	 * conKeysとconValsは、固定値のキーと値です。
	 * conKeys,conVals,where がnullの場合は、これらの値を使用しません。
	 *
	 * @param	table	テーブルID
	 * @param	keys	設定値に対応するキー配列
	 * @param	conKeys	固定値の設定値に対応するキー配列
	 * @param	conVals	固定値に対応する値配列(VARCHARのみ)
	 * @param	where	WHERE条件式
	 * @return  アップデートSQL
	 * @og.rtnNotNull
	 */
	public static String getUpdateSQL( final String table , final String[] keys , final String[] conKeys , final String[] conVals , final String where ) {
		final boolean useConst = conKeys != null && conVals != null && conKeys.length == conVals.length && conKeys.length > 0 ;

		final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE )
			.append( "UPDATE " ).append( table ).append( " SET " )
			.append( String.join( " = ? ," , keys ) )					// key[0] = ? , ･･･  = ? , key[n-1] という文字列が作成されます。
			.append( " = ? " );											// 最後の key[n-1] の後ろに、 = ? という文字列を追加します。

		if( useConst ) {
			for( int i=0; i<conKeys.length; i++ ) {
				sql.append( ',' ).append( conKeys[i] ).append( " = '" ).append( conVals[i] ).append( "' " );
			}
		}

		if( where != null && !where.isEmpty() ) {
			sql.append( " WHERE " ).append( where );			// WHERE条件は、? に関数が入ったりするため、予め文字列を作成しておいてもらう。
		}

		return sql.toString();
	}

	/**
	 * データをデリートする場合に使用するSQL文を作成します。
	 *
	 * これは、key に対応した ？ 文字列で、SQL文を作成します。
	 * 実際の値設定は、この、キーの並び順に応じた値を設定することになります。
	 * WHERE 文字列は、この、？ も含めたWHERE条件の文字列を渡します。
	 * WHERE条件の場合は、この、？に、関数を設定したり、条件を指定したり、
	 * 色々なケースがあるため、単純にキーだけ指定する方法では、対応範囲が
	 * 限られるためです。
	 *
	 * @param	table	テーブルID
	 * @param	where	設定値に対応するキー配列(可変長引数)
	 * @return  デリートSQL
	 * @og.rtnNotNull
	 */
	public static String getDeleteSQL( final String table , final String where ) {
		final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE )
			.append( "DELETE FROM " ).append( table );

		if( where != null && !where.isEmpty() ) {
			sql.append( " WHERE " ).append( where );			// WHERE条件は、? に関数が入ったりするため、予め文字列を作成しておいてもらう。
		}

		return sql.toString();
	}
}
