/*
 * 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.util.StringUtil;
import org.opengion.fukurou.util.ApplicationInfo;
import org.opengion.fukurou.util.Closer;
import org.opengion.fukurou.model.Formatter;
import org.opengion.fukurou.model.ArrayDataModel;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ParameterMetaData;
import java.sql.SQLException;

import java.util.Arrays;

/**
 * DBTableModel インターフェースを継承した TableModel の実装クラスです。
 * sql文を execute( query ) する事により,データベースを検索した結果を
 * DBTableModel に割り当てます。
 *
 * メソッドを宣言しています
 * DBTableModel インターフェースは，データベースの検索結果(Resultset)をラップする
 * インターフェースとして使用して下さい。
 *
 * @og.rev 5.2.2.0 (2010/11/01) パッケージ移動(hayabusa.db ⇒ fukurou.db)
 * @og.group ＤＢ/Shell制御
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class DBSimpleTable {

	/** システム依存の改行記号をセットします。	*/
	private static final String CR = System.getProperty("line.separator");

	private	final String[]	names ;				// データ配列に対応するカラム配列(names)
	private	String[]	keys 		= null;		// 登録に使用するカラムキー配列(keys)
	private	int[]		keysNo		= null;		// 登録に使用するカラムキー配列番号
	private	String		table		= null;		// 登録テーブル名
	private	String		where		= null;		// where 条件式[カラム名]を含む
	private	int[]		whereNo		= null;		// [カラム名]に対応するデータ配列番号
	private	String[]	constrain 	= null;		// key に対応した制約条件

	private String		connID		= null;		// 登録に使用するコネクションID
	private boolean		useWhere	= false;	// where が設定されると true にセットされます。

	private Connection	conn		= null;
	private PreparedStatement pstmt = null;
	private ParameterMetaData pMeta = null;		// 5.1.2.0 (2010/01/01) setObject に、Type を渡す。(PostgreSQL対応)
	private String		query		= null;		// エラーメッセージ用の変数
	private int			execCnt		= 0;
	private ApplicationInfo appInfo  = null;	// 3.8.7.0 (2006/12/15)
	private boolean useParamMetaData = false;	// 5.1.2.0 (2010/01/01) setObject に、Type を渡す。(PostgreSQL対応)

	/**
	 * データ配列のカラム名称配列を指定してオブジェクトを構築します。
	 *
	 * @param	nm	カラム名称配列
	 * @throws RuntimeException tbl が null の場合
	 */
	public DBSimpleTable( final String[] nm ) {
		if( nm == null ) {
			String errMsg = "データ配列のカラム名称に null は設定できません。";
			throw new RuntimeException( errMsg );
		}

		names = new String[nm.length];
		System.arraycopy( nm,0,names,0,names.length );
	}

	/**
	 * 登録に使用するカラムキー配列(keys)を登録します。
	 *
	 * 引数のkey配列が null の場合は、names と同じカラム名称配列(names)が使用されます。
	 * キー配列(keys)は、一度しか登録できません。また、addConstrain等のメソッド
	 * 呼び出しを先に実行すると、カラム名称配列(names)が設定されてしまう為、
	 * その後にこのメソッドを呼び出すとエラーが発生します。
	 *
	 * @param	key	登録カラム名称配列
	 * @see		#addConstrain( String ,String )
	 * @throws RuntimeException すでに キー配列(keys)が登録済み/作成済みの場合
	 */
	public void setKeys( final String[] key ) {
		if( keys != null ) {
			String errMsg = "すでに キー配列(keys)が登録済みです。";
			throw new RuntimeException( errMsg );
		}

		if( key != null ) {
			int size = key.length;
			keys = new String[size];
			System.arraycopy( key,0,keys,0,size );

			constrain = new String[size];
			Arrays.fill( constrain,"?" );

			keysNo = new int[size];
			for( int i=0; i<size; i++ ) {
				int address = findAddress( names,keys[i] );
				if( address >= 0 ) {
					keysNo[i] = address;
				}
				else {
					String errMsg = "指定の key は、カラム配列(names)に存在しません"
								+ " key[" + i + "]=" + key[i]
								+ " names=" + StringUtil.array2csv( names ) ;
					throw new RuntimeException( errMsg );
				}
			}
		}
	}

	/**
	 * カラム名称配列(names)と同じキー配列(keys)を作成します。
	 *
	 * これは、キー配列(keys) が作成されなかった場合の処理です。
	 * keys が null の場合のみ、処理を実行します。
	 *
	 * @see		#setKeys( String[] )
	 */
	private void makeKeys() {
		// キー配列(keys) が未設定の場合は、カラム名称配列(names)が設定されます。
		if( keys == null ) {
			keys = names;
			int size = keys.length;

			constrain = new String[size];
			Arrays.fill( constrain,"?" );

			keysNo = new int[size];
			for( int i=0; i<size; i++ ) {
				keysNo[i] = i;
			}
		}
	}

	/**
	 * Insert/Update/Delete 時の登録するテーブル名
	 *
	 * @param	tbl	テーブル名
	 * @throws RuntimeException tbl が null の場合
	 */
	public void setTable( final String tbl ) {
		if( tbl == null ) {
			String errMsg = "table に null は設定できません。";		// 5.1.8.0 (2010/07/01) errMsg 修正
			throw new RuntimeException( errMsg );
		}

		table = tbl;
	}

	/**
	 * データベースの接続先IDを設定します。
	 *
	 * @param	conn	接続先ID
	 */
	public void setConnectionID( final String conn ) {
		connID = conn;
	}

	/**
	 * アクセスログ取得の為,ApplicationInfoオブジェクトを設定します。
	 *
	 * @og.rev 3.8.7.0 (2006/12/15) 新規追加
	 *
	 * @param   appInfo アプリ情報オブジェクト
	 */
	public void setApplicationInfo( final ApplicationInfo appInfo ) {
		this.appInfo = appInfo;
	}

	/**
	 * Insert/Update/Delete 時の PreparedStatement の引数(?)制約
	 *
	 * 制約条件(val)は、そのまま引数に使用されます。通常、? で表される
	 * パラメータに、文字長を制限する場合、SUBSTRB( ?,1,100 ) という
	 * val 変数を与えます。
	 * また、キー一つに対して、値を複数登録したい場合にも、使用できます。
	 * 例えば、NVAL( ?,? ) のような場合、キー一つに値２つを割り当てます。
	 * 値配列の並び順は、キー配列(keys)に対する(?の個数)に対応します。
	 * 注意：カラム名称配列(names)ではありません。また、先にキー配列(keys)を登録
	 * しておかないと、キー配列登録時にエラーが発生します。
	 * 制約条件は、処理するQUERYに対して適用されますので、
	 * key または、val が null の場合は、RuntimeException を Throwします。
	 *
	 * @param	key	制約をかけるキー
	 * @param	val	制約条件式
	 * @see		#setKeys( String[] )
	 * @throws RuntimeException key または、val が null の場合
	 */
	public void addConstrain( final String key,final String val ) {
		if( key == null || val == null ) {
			String errMsg = "key または、val に null は設定できません。"
						+ " key=[" + key + "] , val=[" + val + "]" ;
			throw new RuntimeException( errMsg );
		}

		// キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。
		if( keys == null ) { makeKeys(); }

		// 制約条件のアドレスは、カラム名称配列(names)でなく、キー配列(keys)を使用します。
		int address = findAddress( keys,key );
		if( address >= 0 ) {
			constrain[address] = val;
		}
		else {
			String errMsg = "指定の key は、キー配列(keys)に存在しません"
						+ " key=[" + key + "] , val=[" + val + "]"
						+ " keys=" + StringUtil.array2csv( keys ) ;
			throw new RuntimeException( errMsg );
		}
	}

	/**
	 * Update/Delete 時のキーとなるWHERE 条件のカラム名を設定します。
	 *
	 * 通常の WHERE 句の書き方と同じで、カラム配列(names)に対応する設定値(values)の値を
	 * 割り当てたい箇所に[カラム名] を記述します。文字列の場合、設定値をセットする
	 * ときに、シングルコーテーションを使用しますが、[カラム名]で指定する場合は、
	 * その前後に、(')シングルコーテーションは、不要です。
	 * WHERE条件は、登録に使用するキー配列(keys)に現れない条件で行を特定することがありますので
	 * カラム名称配列(names)を元にカラム名のアドレスを求めます。
	 * [カラム名]は、? に置き換えて、PreparedStatement として、実行される形式に変換されます。
	 * 例：FGJ='1' and CLM=[CLM] and SYSTEM_ID in ([SYSID],'**')
	 *
	 * @og.rev 4.3.4.0 (2008/12/01) キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てる
	 * @og.rev 5.0.2.0 (2009/11/01) バグ修正(keysはデータセットのキーなので、where句のカラムに含まれて入いるわけではない)
	 *
	 * @param  wh WHERE条件のカラム名
	 * @throws RuntimeException [カラム名]がカラム配列(names)に存在しない場合
	 */
	public void setWhere( final String wh ) {

		if( wh != null ) {
			// キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。
//			if( keys == null ) { makeKeys(); }

//			ArrayDataModel data = new ArrayDataModel( keys );
			// 5.0.2.0 (2009/11/01)
			ArrayDataModel data = new ArrayDataModel( names );
			Formatter format = new Formatter( data );
			format.setFormat( wh );
			where = format.getQueryFormatString();
			whereNo = format.getClmNos();

//			StringBuilder buf = new StringBuilder( wh.length() );
//
//			// 注意：[ と ] が隣接したケースでは処理できません。
//			String wh2 = wh.replace( '[',']' );
//			CSVTokenizer token = new CSVTokenizer( wh2,']',false );
//			int cnt = token.countTokens() / 2 ;
////			String format = null;
//			whereNo = new int[ cnt ];
//			for( int i=0; i<cnt; i++ ) {
////				format = token.nextToken();
////				buf.append( format);
//				buf.append( token.nextToken() );	// format
//				String clm = token.nextToken();
//				// カラム名称配列(names)を元にカラム名のアドレスを求めます。
//				int address = findAddress( names,clm );
//				if( address >= 0 ) { whereNo[i] = address; }
//				else {
//					String errMsg = "[" + clm + "]が、カラム配列(names)に存在しません。" + CR
//								+ " names=" + StringUtil.array2csv( names );
//					throw new RuntimeException( errMsg );
//				}
//				buf.append( "?" );		// [カラム] を ? に置き換え
//			}
////			format = token.nextToken();
////			buf.append( format );
//			buf.append( token.nextToken() );	// format
//
//			where = buf.toString();
		}
		else {
			where = null;
		}
	}

	/**
	 * データをインサートする場合に使用するSQL文を作成します。
	 *
	 * @return  インサートSQL
	 */
	private String getInsertSQL() {
		// キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。
		if( keys == null ) { makeKeys(); }

		StringBuilder sql = new StringBuilder();
		sql.append( "INSERT INTO " ).append( table );
		sql.append( " ( " );
		sql.append( keys[0] );
		for( int i=1; i<keys.length; i++ ) {
			sql.append( "," ).append( keys[i] );
		}
		sql.append( " ) VALUES ( " );
		sql.append( constrain[0] );
		for( int i=1; i<keys.length; i++ ) {
			sql.append( "," ).append( constrain[i] );
		}
		sql.append( " )" );

		useWhere = false;

		return sql.toString();
	}

	/**
	 * データをアップデートする場合に使用するSQL文を作成します。
	 *
	 * @return  アップデートSQL
	 */
	private String getUpdateSQL() {
		// キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。
		if( keys == null ) { makeKeys(); }

		StringBuilder sql = new StringBuilder();
		sql.append( "UPDATE " ).append( table ).append( " SET " );
		sql.append( keys[0] ).append( " = " ).append( constrain[0] );

		for( int i=1; i<keys.length; i++ ) {
			sql.append( " , " );
			sql.append( keys[i] ).append( " = " ).append( constrain[i] );
		}

		if( where != null && where.length() > 0 ) {
			sql.append( " WHERE " ).append( where );
			useWhere = true;
		}
		else {
			useWhere = false;
		}

		return sql.toString();
	}

	/**
	 * データをデリートする場合に使用するSQL文を作成します。
	 *
	 * @og.rev 5.0.2.0 (2009/11/01) バグ修正(削除時はkeysは必要ない)
	 *
	 * @return  デリートSQL
	 */
	private String getDeleteSQL() {
		// キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。
//		if( keys == null ) { makeKeys(); }
		// 5.0.2.0 (2009/11/01)
		keys = new String[0];

		StringBuilder sql = new StringBuilder();
		sql.append( "DELETE FROM " ).append( table );

		if( where != null && where.length() > 0 ) {
			sql.append( " WHERE " ).append( where );
			useWhere = true;
		}
		else {
			useWhere = false;
		}

		return sql.toString();
	}

	/**
	 * Insert 処理の開始を宣言します。
	 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。
	 * このメソッドと、close() メソッドは必ずセットで処理してください。
	 *
	 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応)
	 *
	 * @throws SQLException Connection のオープンに失敗した場合
	 */
	public void startInsert() throws SQLException {
		execCnt = 0;
		query = getInsertSQL();
		conn  = ConnectionFactory.connection( connID,appInfo );
		pstmt = conn.prepareStatement( query );
		// 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
//		useParamMetaData = ApplicationInfo.useParameterMetaData( conn );
		useParamMetaData = ConnectionFactory.useParameterMetaData( connID );	// 5.3.8.0 (2011/08/01)
		if( useParamMetaData ) {
			pMeta = pstmt.getParameterMetaData();
		}
	}

	/**
	 * Update 処理の開始を宣言します。
	 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。
	 * このメソッドと、close() メソッドは必ずセットで処理してください。
	 *
	 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応)
	 *
	 * @throws SQLException Connection のオープンに失敗した場合
	 */
	public void startUpdate() throws SQLException {
		execCnt = 0;
		query = getUpdateSQL();
		conn  = ConnectionFactory.connection( connID,appInfo );
		pstmt = conn.prepareStatement( query );
		// 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
//		useParamMetaData = ApplicationInfo.useParameterMetaData( conn );
		useParamMetaData = ConnectionFactory.useParameterMetaData( connID );	// 5.3.8.0 (2011/08/01)
		if( useParamMetaData ) {
			pMeta = pstmt.getParameterMetaData();
		}
	}

	/**
	 * Delete 処理の開始を宣言します。
	 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。
	 * このメソッドと、close() メソッドは必ずセットで処理してください。
	 *
	 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応)
	 *
	 * @throws SQLException Connection のオープンに失敗した場合
	 */
	public void startDelete() throws SQLException {
		execCnt = 0;
		query = getDeleteSQL();
		conn  = ConnectionFactory.connection( connID,appInfo );
		pstmt = conn.prepareStatement( query );
		// 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
//		useParamMetaData = ApplicationInfo.useParameterMetaData( conn );
		useParamMetaData = ConnectionFactory.useParameterMetaData( connID );	// 5.3.8.0 (2011/08/01)
		if( useParamMetaData ) {
			pMeta = pstmt.getParameterMetaData();
		}
	}

	/**
	 * データ配列を渡して実際のDB処理を実行します。
	 *
	 * この処理の前に、startXXXX をコールしておき、INSER,UPDATE,DELETEのどの
	 * 処理を行うか、宣言しておく必要があります。
	 * 戻り値は、この処理での処理件数です。
	 * 最終件数は、close( boolean ) 時に取得します。
	 *
	 * @og.rev 4.0.0.0 (2007/11/28) SQLException をきちんと伝播させます。
	 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
	 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData 時の setNull 対応(PostgreSQL対応)
	 *
	 * @param	values	カラム配列(names) に対応する設定値配列
	 *
	 * @return	ここでの処理件数
	 *
	 * @see    #close( boolean )
	 * @throws SQLException Connection のクロースに失敗した場合
	 * @throws RuntimeException Connection DB処理の実行に失敗した場合
	 */
	public int execute( final String[] values ) throws SQLException {
		final int cnt;
		try {
			int clmNo = 1;	// JDBC のカラム番号は、１から始まる。

			// 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
			if( useParamMetaData ) {
				// keys に値を割り当てます。
				for( int i=0; i<keys.length; i++ ) {
					int type = pMeta.getParameterType( clmNo );
					// 5.3.8.0 (2011/08/01) setNull 対応
//					pstmt.setObject( clmNo++,values[keysNo[i]],type );
					String val = values[keysNo[i]];
					if( val == null || val.isEmpty() ) {
						pstmt.setNull( clmNo++, type );
					}
					else {
						pstmt.setObject( clmNo++,val,type );
					}
				}

				// where 条件を使用する場合は、値を割り当てます。
				if( useWhere ) {
					for( int i=0; i<whereNo.length; i++ ) {
						int type = pMeta.getParameterType( clmNo );
						// 5.3.8.0 (2011/08/01) setNull 対応
//						pstmt.setObject( clmNo++,values[whereNo[i]],type );
						String val = values[whereNo[i]];
						if( val == null || val.isEmpty() ) {
							pstmt.setNull( clmNo++, type );
						}
						else {
							pstmt.setObject( clmNo++,val,type );
						}
					}
				}
			}
			else {
				// keys に値を割り当てます。
				for( int i=0; i<keys.length; i++ ) {
					pstmt.setObject( clmNo++,values[keysNo[i]] );
				}

				// where 条件を使用する場合は、値を割り当てます。
				if( useWhere ) {
					for( int i=0; i<whereNo.length; i++ ) {
						pstmt.setObject( clmNo++,values[whereNo[i]] );
					}
				}
			}

			cnt = pstmt.executeUpdate();
			execCnt += cnt;
		}
		catch (SQLException ex) {
			Closer.stmtClose( pstmt );
			pMeta = null;		// 5.1.2.0 (2010/01/01)
			if( conn != null ) {
				conn.rollback();
				ConnectionFactory.remove( conn,connID );
				conn = null;
			}
			String errMsg = "DB処理の実行に失敗しました。" + CR
						+ " query=[" + query + "]" + CR
						+ " values=" + StringUtil.array2csv( values );
//			throw new RuntimeException( errMsg );
			throw new RuntimeException( errMsg ,ex );
		}
		return cnt;
	}

	/**
	 * DB処理をクロースします。
	 *
	 * 引数には、commit させる場合は、true を、rollback させる場合は、false をセットします。
	 * 戻り値は、今まで処理された合計データ件数です。
	 * この処理は、SQLException を内部で RuntimeException に変換している為、catch 節は
	 * 不要ですが、必ず finally 節で呼び出してください。そうしないと、リソースリークの
	 * 原因になります。
	 *
	 * @og.rev 5.1.2.0 (2010/01/01) pMeta のクリア
	 *
	 * @param  commitFlag コミットフラグ [true:commitする/false:rollbacする]
	 *
	 * @return	今までの合計処理件数
	 */
	public int close( final boolean commitFlag ) {
		if( conn != null ) {
			try {
				if( commitFlag ) {	conn.commit();  }
				else {				conn.rollback(); }
			}
			catch (SQLException ex) {
				ConnectionFactory.remove( conn,connID );
				conn = null;
				String errMsg = "DB処理を確定(COMMIT)できませんでした。" + CR
							+ " query=[" + query + "]" + CR ;
				throw new RuntimeException( errMsg,ex );
			}
			finally {
				Closer.stmtClose( pstmt );
				pMeta = null;		// 5.1.2.0 (2010/01/01)
				ConnectionFactory.close( conn,connID );
				conn = null;
			}
		}

		return execCnt;
	}

	/**
	 * 文字列配列中の値とマッチするアドレスを検索します。
	 * 文字列配列がソートされていない為、バイナリサーチが使えません。よって、
	 * 総当りでループ検索しています。
	 * 総数が多い場合は、遅くなる為、マップにセットして使用することを検討ください。
	 *
	 * @param	data  	ターゲットの文字列配列中
	 * @param	key   	検索する文字列
	 *
	 * @return  ターゲットの添え字(存在しない場合は、-1)
	 */
	private int findAddress( final String[] data,final String key ) {
		int address = -1;
		if( data != null && key != null ) {
			for( int i=0; i<data.length; i++ ) {
				if( key.equalsIgnoreCase( data[i] ) ) {
					address = i;
					break;
				}
			}
		}
		return address;
	}
}
