/*
 * Copyright (c) 2017 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.io.IOException;
import java.io.Reader;
import java.sql.Struct;												// 6.3.3.0 (2015/07/25)
import java.sql.Clob;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.Locale;
import java.util.List;												// 6.3.3.0 (2015/07/25)
import java.util.ArrayList;											// 6.3.3.0 (2015/07/25)

import oracle.jdbc.OracleStruct;									// 6.3.8.0 (2015/09/11)
import oracle.jdbc.OracleTypeMetaData;								// 6.3.8.0 (2015/09/11)

/**
 * ResultSet のデータ処理をまとめたクラスです。
 * ここでは、ResultSetMetaData から、カラム数、カラム名(NAME列)、
 * Type属性を取得し、ResultSet で、値を求める時に、Object型の
 * 処理を行います。
 * Object型としては、CLOB、ROWID、TIMESTAMP 型のみ取り扱っています。
 * STRUCTタイプもサポートしますが、1レベルのみとします。(6.3.3.0 (2015/07/25))
 *
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public class ResultSetValue implements AutoCloseable {
	private static final int BUFFER_MIDDLE = 10000;		// 6.3.3.0 (2015/07/25)

	/** システム依存の改行記号(String)。	*/
	public static final String CR = System.getProperty("line.separator");

	private final ResultSet	resultSet ;			// 内部で管理する ResultSet オブジェクト
	private final List<ColumnInfo> clmInfos	;

	private boolean	skipNext  ;					// STRUCT 使用時に、next() してデータ構造を取得するため。
	private boolean	firstNext ;					// STRUCT 使用時に、next() してデータ構造を取得するため。

	/**
	 * ResultSet を引数にとるコンストラクタ
	 *
	 * ここで、カラムサイズ、カラム名、java.sql.Types の定数定義 を取得します。
	 * STRUCTタイプもサポートしますが、1レベルのみとします。
	 * つまり、Object型のカラムに、Object型を定義した場合、ここでは取り出すことができません。
	 * また、Object型は、継承関係を構築できるため、個々のオブジェクトの要素数は異なります。
	 * 一番最初のレコードのオブジェクト数を元に、算出しますので、ご注意ください。
	 *
	 * @param	res 内部で管理するResultSetオブジェクト
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合
	 */
	public ResultSetValue( final ResultSet res ) throws SQLException {
		resultSet = res;

		final ResultSetMetaData metaData  = resultSet.getMetaData();
		final int clmSize = metaData.getColumnCount();

		clmInfos = new ArrayList<>();

		for( int i=0; i<clmSize; i++ ) {
			final int clmNo = i+1;
			final int type = metaData.getColumnType( clmNo );
			final String  name = metaData.getColumnLabel( clmNo ).toUpperCase(Locale.JAPAN) ;
			if( type == Types.STRUCT && DBUtil.isOracle() ) {
				if( !skipNext ) {						// オブジェクト型を取得する為、データを取る必要がある。
					skipNext  = true;
					firstNext = resultSet.next();		// 初めての next() の結果を保持(falseなら、データなし)
				}
				if( firstNext ) {
					// 最初のオブジェクトのタイプを基準にする。
					final Object obj = resultSet.getObject( clmNo );
					if( obj != null ) {
						// 6.3.8.0 (2015/09/11) Oracle Database 12cリリース1 (12.1)以降、StructDescriptor は非推奨
						final OracleTypeMetaData omd  = ((OracleStruct)obj).getOracleMetaData();
						final ResultSetMetaData md    = ((OracleTypeMetaData.Struct)omd).getMetaData();

						final int mdsize = md.getColumnCount();
						for( int j=0; j<mdsize; j++ ) {
							final int objNo = j+1;
							// カラム名.オブジェクトカラム名
							final String  name2   = name + '.' + md.getColumnLabel(objNo).toUpperCase(Locale.JAPAN);
							final int     type2   = md.getColumnType( objNo );
							final int     size2   = md.getColumnDisplaySize( objNo );
							final boolean isWrit2 = md.isWritable( objNo );
							clmInfos.add( new ColumnInfo( name2,type2,size2,isWrit2,clmNo,j ) );	// ※ objNo でなく、「j」
						}
					}
				}
			}
			else {
				final int     size   = metaData.getColumnDisplaySize( clmNo );
				final boolean isWrit = metaData.isWritable( clmNo );
				clmInfos.add( new ColumnInfo( name,type,size,isWrit,clmNo,-1 ) );			// ※ objNo でなく、「-1」
			}
		}
	}

	/**
	 * ResultSetMetaData で求めた、カラム数を返します。
	 *
	 * @return  カラム数(データの列数)
	 */
	public int getColumnCount() {
		return clmInfos.size();
	}

	/**
	 * カラム名配列を返します。
	 *
	 * 配列は、0から始まり、カラム数-1 までの文字型配列に設定されます。
	 * カラム名は、ResultSetMetaData#getColumnLabel(int) を toUpperCase した
	 * 大文字が返されます。
	 *
	 * @return	カラム名配列
	 * @og.rtnNotNull
	 */
	public String[] getNames() {
		return clmInfos.stream().map( info -> info.getName() ).toArray( String[]::new );
	}

	/**
	 * 指定のカラム番号のカラム名を返します。
	 *
	 * カラム名を取得する、カラム番号は、0から始まり、カラム数-1 までの数字で指定します。
	 * データベース上の、1から始まる番号とは、異なります。
	 * カラム名は、ResultSetMetaData#getColumnLabel(int) を toUpperCase した
	 * 大文字が返されます。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return  指定のカラム番号のカラム名
	 */
	public String getColumnName( final int clmNo ) {
		return clmInfos.get( clmNo ).name ;
	}

	/**
	 * 指定のカラム番号のサイズを返します。
	 *
	 * カラムのサイズは、ResultSetMetaData#getColumnDisplaySize(int) の値です。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return	指定のカラム番号のサイズ
	 */
	public int getColumnDisplaySize( final int clmNo ) {
		return clmInfos.get( clmNo ).size ;
	}

	/**
	 * 指定の書き込み可能かどうかを返します。
	 *
	 * カラムの書き込み可能かどうかは、ResultSetMetaData#isWritable(int) の値です。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return	書き込み可能かどうか
	 */
	public boolean isWritable( final int clmNo ) {
		return clmInfos.get( clmNo ).isWrit ;
	}

	/**
	 * カーソルを現在の位置から順方向に1行移動します。
	 *
	 * ResultSet#next() を呼び出しています。
	 * 結果は,すべて文字列に変換されて格納されます。
	 *
	 * @return  新しい現在の行が有効である場合はtrue、行がそれ以上存在しない場合はfalse
	 * @see		java.sql.ResultSet#next()
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合、またはこのメソッドがクローズされた結果セットで呼び出された場合
	 */
	public boolean next() throws SQLException {
		if( skipNext ) { skipNext = false; return firstNext; }		// STRUCTタイプ取得時に、一度 next() している。
		return resultSet.next();
	}

	/**
	 * 現在のカーソル位置にあるレコードのカラム番号のデータを取得します。
	 *
	 * ResultSet#getObject( clmNo+1 ) を呼び出しています。
	 * 引数のカラム番号は、0から始まりますが、ResultSet のカラム順は、1から始まります。
	 * 指定は、0から始まるカラム番号です。
	 * 結果は,すべて文字列に変換されて返されます。
	 * また、null オブジェクトの場合も、ゼロ文字列に変換されて返されます。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return  現在行のカラム番号のデータ(文字列)
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合
	 * @og.rtnNotNull
	 */
	public String getValue( final int clmNo ) throws SQLException {
		final ColumnInfo clmInfo = clmInfos.get( clmNo ) ;	// 内部カラム番号に対応したObject
		final int dbClmNo = clmInfo.clmNo ;					// データベース上のカラム番号(+1済み)

		final String val ;
		final Object obj = resultSet.getObject( dbClmNo );

		if( obj == null ) {
			val = "";
		}
		else if( clmInfo.isStruct ) {
			final Object[] attrs = ((Struct)obj).getAttributes();
			final int no = clmInfo.objNo;
			val = no < attrs.length ? String.valueOf( attrs[no] ) : "" ;	// 配列オーバーする場合は、""(ゼロ文字列)
		}
		else if( clmInfo.isObject ) {
			switch( clmInfo.type ) {
				case Types.CLOB : 		val = getClobData( (Clob)obj ) ;
										break;
				case Types.ROWID: 		val = resultSet.getString( dbClmNo );
										break;
				case Types.TIMESTAMP : 	val = StringUtil.getTimeFormat( ((Timestamp)obj).getTime() , "yyyyMMddHHmmss" );
										break;
				default : 				val = String.valueOf( obj );
										break;
			}
		}
		else {
			val = String.valueOf( obj );
		}

		return val ;
	}

	/**
	 * 現在のカーソル位置にあるレコードの全カラムデータを取得します。
	 *
	 * 個々のカラムの値も、null を含みません。(ゼロ文字列になっています)
	 *
	 * #getValue( clmNo ) を、0から、カラム数-1 まで呼び出して求めた文字列配列を返します。
	 *
	 * @return  現在行の全カラムデータの文字列配列
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合
	 * @og.rtnNotNull
	 */
	public String[] getValues() throws SQLException {
		final String[] vals = new String[clmInfos.size()];

		for( int i=0; i<vals.length; i++ ) {
			vals[i] = getValue( i );
		}

		return vals ;
	}

	/**
	 * タイプに応じて変換された、Numberオブジェクトを返します。
	 *
	 * 条件に当てはまらない場合は、null を返します。
	 * org.opengion.hayabusa.io.HybsJDBCCategoryDataset2 から移動してきました。
	 * これは、検索結果をグラフ化する為の 値を取得する為のメソッドですので、
	 * 数値に変換できない場合は、エラーになります。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return	Numberオブジェクト(条件に当てはまらない場合は、null)
	 * @see		java.sql.Types
	 * @throws	java.sql.SQLException データベース・アクセス・エラーが発生した場合
	 * @throws	RuntimeException 数字変換できなかった場合。
	 */
	public Number getNumber( final int clmNo ) throws SQLException {
		final ColumnInfo clmInfo = clmInfos.get( clmNo ) ;	// 内部カラム番号に対応したObject
		final int dbClmNo = clmInfo.clmNo ;					// データベース上のカラム番号(+1済み)

		Number value = null;

		Object obj = resultSet.getObject( dbClmNo );
		if( obj != null ) {
			if( clmInfo.isStruct ) {
				final Object[] attrs = ((Struct)obj).getAttributes();
				final int no = clmInfo.objNo;
				obj = no < attrs.length ? attrs[no] : null ;	// 配列オーバーする場合は、null
				if( obj == null ) { return value; }				// 配列外 or 取出した結果が null の場合、処理を中止。
			}

			switch( clmInfo.type ) {
				case Types.TINYINT:
				case Types.SMALLINT:
				case Types.INTEGER:
				case Types.BIGINT:
				case Types.FLOAT:
				case Types.DOUBLE:
				case Types.DECIMAL:
				case Types.NUMERIC:
				case Types.REAL: {
					value = (Number)obj;
					break;
				}
				case Types.DATE:
				case Types.TIME:  {
					value = Long.valueOf( ((Date)obj).getTime() );
					break;
				}
				// 5.6.2.1 (2013/03/08) Types.DATE と Types.TIMESTAMP で処理を分けます。
				case Types.TIMESTAMP: {
					value = Long.valueOf( ((Timestamp)obj).getTime() );
					break;
				}
				case Types.CHAR:
				case Types.VARCHAR:
				case Types.LONGVARCHAR: {
					final String str = (String)obj;
					try {
						value = Double.valueOf(str);
					}
					catch ( final NumberFormatException ex ) {
						final String errMsg = "数字変換できませんでした。in=" + str
										+ CR + ex.getMessage() ;
						throw new RuntimeException( errMsg,ex );
						// suppress (value defaults to null)
					}
					break;
				}
				default:
					// not a value, can't use it (defaults to null)
					break;
			}
		}
		return value;
	}

	/**
	 * カラムのタイプを表現する文字列値を返します。
	 *
	 * この文字列を用いて、CCSファイルでタイプごとの表示方法を
	 * 指定することができます。
	 * 現時点では、VARCHAR2,LONG,NUMBER,DATE,CLOB,NONE のどれかにあてはめます。
	 *
	 * @param   clmNo  カラム番号 (0から始まり、カラム数-1までの数字)
	 * @return	カラムのタイプを表現する文字列値
	 * @see		java.sql.Types
	 * @og.rtnNotNull
	 */
	public String getClassName( final int clmNo ) {
		final String rtn ;

		switch( clmInfos.get( clmNo ).type ) {
			case Types.CHAR:
			case Types.VARCHAR:
			case Types.BIT:
				rtn = "VARCHAR2"; break;
			case Types.LONGVARCHAR:
				rtn = "LONG"; break;
			case Types.TINYINT:
			case Types.SMALLINT:
			case Types.INTEGER:
			case Types.NUMERIC:
			case Types.BIGINT:
			case Types.FLOAT:
			case Types.DOUBLE:
			case Types.REAL:
			case Types.DECIMAL:
				rtn = "NUMBER"; break;
			case Types.DATE:
			case Types.TIME:
			case Types.TIMESTAMP:
				rtn = "DATE"; break;
			case Types.CLOB:
				rtn = "CLOB"; break;
			case Types.STRUCT:						// 6.3.3.0 (2015/07/25) 内部分解されない２レベル以上の場合のみ
				rtn = "STRUCT"; break;
			default:
				rtn = "NONE"; break;
		}

		return rtn;
	}

	/**
	 * try-with-resourcesブロックで、自動的に呼ばれる AutoCloseable の実装。
	 *
	 * コンストラクタで渡された ResultSet を close() します。
	 *
	 * @og.rev 6.4.2.1 (2016/02/05) 新規作成。try-with-resourcesブロックで、自動的に呼ばれる AutoCloseable の実装。
	 *
	 * @see		java.lang.AutoCloseable#close()
	 */
	@Override
	public void close() {
		try {
			if( resultSet != null ) { resultSet.close(); }
		}
		catch( final SQLException ex ) {
			// MSG0020 = ResultSet を close することが出来ませんでした。{0} : {1}
			MsgUtil.errPrintln( ex , "MSG0020" , ex.getSQLState() , ex.getMessage() );
		}
		catch( final RuntimeException ex ) {
			// MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
			MsgUtil.errPrintln( ex , "MSG0021" , ex.getMessage() );
		}
	}

	/**
	 * Clob オブジェクトから文字列を取り出します。
	 *
	 * @og.rev 6.0.4.0 (2014/11/28) 新規作成: org.opengion.hayabusa.db.DBUtil#getClobData( Clob ) から移動
	 *
	 * @param	clobData Clobオブジェクト
	 * @return	Clobオブジェクトから取り出した文字列
	 * @throws	SQLException データベースアクセスエラー
	 * @throws	RuntimeException 入出力エラーが発生した場合
	 * @og.rtnNotNull
	 */
	private String getClobData( final Clob clobData ) throws SQLException {
		if( clobData == null ) { return ""; }

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		Reader reader = null;
		try {
			reader = clobData.getCharacterStream();
			final char[] ch = new char[BUFFER_MIDDLE];			// char配列とBuilderの初期値は無関係。
			int  len ;
			while( (len = reader.read( ch )) >= 0 ) {
				buf.append( ch,0,len );
			}
		}
		catch( final IOException ex ) {
			// MSG0022 = CLOBデータの読み込みに失敗しました。ﾒｯｾｰｼﾞ=[{0}]
			throw MsgUtil.throwException( ex , "MSG0022" , ex.getMessage() );
		}
		finally {
			try {
				if( reader != null ) { reader.close(); }
			}
			catch( final IOException ex ) {
				// MSG0023 = ストリーム close 処理でエラーが発生しました。ﾒｯｾｰｼﾞ=[{0}]
				MsgUtil.errPrintln( ex , "MSG0023" , ex.getMessage() );
			}
			catch( final RuntimeException ex ) {
				// MSG0021 = 予期せぬエラーが発生しました。\n\tﾒｯｾｰｼﾞ=[{0}]
				MsgUtil.errPrintln( ex , "MSG0021" , ex.getMessage() );
			}
		}

		return buf.toString();
	}

	/**
	 * 各種カラム属性の管理を、クラスで行うようにします。
	 *
	 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応
	 *
	 * @param	clobData Clobオブジェクト
	 * @return	Clobオブジェクトから取り出した文字列
	 */
	private static final class ColumnInfo {
		private final String	name ;				// カラム名(ResultSetMetaData#getColumnLabel(int) の toUpperCase)
		private final int		type ;				// java.sql.Types の定数定義
		private final int		size ;				// カラムサイズ(ResultSetMetaData#getColumnDisplaySize(int))
		private final boolean	isWrit ;			// 書き込み許可(ResultSetMetaData#isWritable(int))
		private final int		clmNo ;				// ResultSet での元のカラムNo( 1から始まる番号 )
		private final int		objNo ;				// STRUCT での配列番号( 0から始まる番号 )
		private final boolean	isStruct ;			// オリジナルのタイプが、Struct型 かどうか。
		private final boolean	isObject ;			// タイプが、CLOB,ROWID,TIMESTAMP かどうか。

		/**
		 * 引数付コンストラクター
		 *
		 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応
		 *
		 * @param	name		カラム名(ResultSetMetaData#getColumnLabel(int) の toUpperCase)
		 * @param	type		java.sql.Types の定数定義
		 * @param	size		カラムサイズ(ResultSetMetaData#getColumnDisplaySize(int))
		 * @param	isWrit		書き込み許可(ResultSetMetaData#isWritable(int))
		 * @param	clmNo		ResultSet での元のカラムNo( 1から始まる番号 )
		 * @param	objNo		STRUCT での配列番号( 0から始まる番号 )
		 */
		ColumnInfo( final String name , final int type , final int size , final boolean isWrit , final int clmNo , final int objNo ) {
			this.name	= name	;
			this.type	= type	;
			this.size	= size	;
			this.isWrit	= isWrit;
			this.clmNo	= clmNo;
			this.objNo	= objNo;
			isStruct	= objNo >= 0;				// Struct型かどうかは、配列番号で判定する。
			isObject	= type == Types.CLOB || type == Types.ROWID || type == Types.TIMESTAMP ;
		}

		/**
		 * カラム名を返します。
		 *
		 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応
		 *
		 * @return	カラム名
		 */
		public String getName() { return name; }
	}
}
