package org.maachang.dao ;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Blob;
import java.sql.ResultSet;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.maachang.dao.dbms.DbUtil;
import org.maachang.dao.dbms.MetaColumn;
import org.maachang.dao.dbms.Record;
import org.maachang.dao.dbms.ResultUtil;
import org.maachang.dao.dbms.kind.SupportKind;
import org.maachang.util.StringUtil;

/**
 * Dao実行オブジェクト.
 * 
 * @version 2007/10/18
 * @author masahito suzuki
 * @since MaachangDao 1.00
 */
public class ExecutionDao {
    
    /**
     * SQL文[select].
     */
    private static final String RESULT_BY_SQL = "select" ;
    
    /**
     * SQL文[insert].
     */
    private static final String INSERT_BY_SQL = "insert" ;
    
    /**
     * シーケンス付加対象パラメータ.
     */
    public static final String SEQ_COLUMN = "id" ;
    
    /**
     * テーブルカラム生成日付付加パラメータ.
     */
    public static final String CREATE_TIME = "create_time" ;
    
    /**
     * テーブルカラム更新日付付加パラメータ.
     */
    public static final String UPDATE_TIME = "update_time" ;
    
    /**
     * 楽観的ロック対象バージョンカラム.
     */
    public static final String OPTIMISTIC_LOCK_COLUMN = "optimistic_lock" ;
    
    /**
     * SQL実行.
     * <BR><BR>
     * 指定SQLを実行します.
     * <BR>
     * @param out select文の実行結果が返されます.
     * @param outId insert文の実行時のシーケンスIDが返されます.
     * @param record SQL実行レコードを設定します.
     * @param model テーブル名を設定します.
     * @param meta このテーブルのメタデータを設定します.
     * @param sql 実行対象のSQL文を設定します.
     * @param offset 取得オフセット値を設定します.
     * @param limit 取得リミット値を設定します.
     * @param params パラメータ群を設定します.
     * @return int 実行件数が返されます.
     * @exception Exception 例外.
     */
    public static final int execution( ArrayList<Map<String,Object>> out,Long[] outId,
        Record record,String model,MetaColumn meta,
        String sql,int offset,int limit,ArrayList<Object> params )
        throws Exception {
        Object[] o = null ;
        if( params != null && params.size() > 0 ) {
            int len = params.size() ;
            o = new Object[ len ] ;
            for( int i = 0 ; i < len ; i ++ ) {
                o[ i ] = params.get( i ) ;
            }
        }
        return execution( out,outId,record,model,meta,sql,offset,limit,o ) ;
    }
    
    /**
     * SQL実行.
     * <BR><BR>
     * 指定SQLを実行します.
     * <BR>
     * @param out select文の実行結果が返されます.
     * @param outId insert文の実行時のシーケンスIDが返されます.
     * @param record SQL実行レコードを設定します.
     * @param model テーブル名を設定します.
     * @param meta このテーブルのメタデータを設定します.
     * @param sql 実行対象のSQL文を設定します.
     * @param offset 取得オフセット値を設定します.
     * @param limit 取得リミット値を設定します.
     * @param params パラメータ群を設定します.
     * @return int 実行件数が返されます.
     * @exception Exception 例外.
     */
    public static final int execution( ArrayList<Map<String,Object>> out,Long[] outId,
        Record record,String model,MetaColumn meta,
        String sql,int offset,int limit,Object[] params )
        throws Exception {
        if( record == null ) {
            return 0 ;
        }
        if( sql == null || ( sql = sql.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "SQL文は不正です" ) ;
        }
        SupportKind kind = record.getSupportKind() ;
        if( kind == null ) {
            throw new IOException( "サポート外のアダプタです" ) ;
        }
        String lower = sql.toLowerCase() ;
        boolean selectFlag = lower.startsWith( RESULT_BY_SQL ) ;
        boolean insertFlag = lower.startsWith( INSERT_BY_SQL ) ;
        
        // select実行.
        if( selectFlag == true ) {
            ResultSet rs = null ;
            try {
                // SQL文実行.
                if( params == null || params.length <= 0 ) {
                    rs = record.executeQuery( sql ) ;
                }
                else {
                    rs = record.executeQuery( sql,params ) ;
                }
                if( lower.startsWith( "select count(*)" ) ) {
                    int count = 0 ;
                    for( ;; ) {
                        if( rs.next() == false ) {
                            break ;
                        }
                        count = rs.getInt( 1 ) ;
                        break ;
                    }
                    rs.close() ;
                    rs = null ;
                    return count ;
                }
                else {
                    out.clear() ;
                    if( limit <= 0 ) {
                        limit = Integer.MAX_VALUE ;
                    }
                    if( offset <= 0 ) {
                        offset = 0 ;
                    }
                    pushResult( out,model,rs,offset,limit ) ;
                    rs.close() ;
                    rs = null ;
                    return out.size() ;
                }
            } finally {
                if( rs != null ) {
                    try {
                        rs.close() ;
                    } catch( Exception e ) {
                    }
                }
                rs = null ;
            }
        }
        // select以外で実行.
        else {
            boolean seqFlag = isSequence( meta ) ;
            Long seq = null ;
            // 最初にシーケンスIDを取得.
            if( outId != null && outId.length >= 1 &&
                seqFlag == true && insertFlag == true ) {
                String s = kind.getSequenceId( model ) ;
                if( s != null ) {
                    seq = getSequenceId( record,s ) ;
                    if( seq != null ) {
                        // シーケンスIDが取得できた場合は、
                        // パラメータ0にセット.
                        //(insert文生成時に、カラムのはじめを[id]に必ず設定し、
                        //params[0] == nullにする必要がある).
                        params[ 0 ] = seq ;
                    }
                }
            }
            // SQL文実行.
            int ret = 0 ;
            if( params == null || params.length <= 0 ) {
                ret = record.executeUpdate( sql ) ;
            }
            else {
                ret = record.executeUpdate( sql,params ) ;
            }
            // SQL実行後にシーケンスIDを取得.
            if( outId != null && outId.length >= 1 &&
                seqFlag == true && insertFlag == true && seq != null ) {
                String s = kind.getInsertIdBySQL() ;
                if( s != null ) {
                    seq = getInsertId( record,s ) ;
                }
            }
            // シーケンスが取得できた場合は、outIdに設定.
            if( outId != null ) {
                outId[ 0 ] = seq ;
            }
            return ret ;
        }
    }
    
    /**
     * SQL実行.
     * <BR><BR>
     * 指定SQLを実行します.
     * <BR>
     * @param record SQL実行レコードを設定します.
     * @param sql 実行対象のSQL文を設定します.
     * @return Object 実行結果が返されます.
     * @exception Exception 例外.
     */
    public static final Object executionStatement( Record record,String sql )
        throws Exception {
        if( record == null ) {
            return null ;
        }
        if( sql == null || ( sql = sql.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "SQL文は不正です" ) ;
        }
        SupportKind kind = record.getSupportKind() ;
        if( kind == null ) {
            throw new IOException( "サポート外のアダプタです" ) ;
        }
        String lower = sql.toLowerCase() ;
        boolean selectFlag = lower.startsWith( RESULT_BY_SQL ) ;
        
        // select実行.
        if( selectFlag == true ) {
            ResultSet rs = null ;
            try {
                // SQL文実行.
                rs = record.executeQuery( sql ) ;
                if( lower.startsWith( "select count(*)" ) ) {
                    int count = 0 ;
                    for( ;; ) {
                        if( rs.next() == false ) {
                            break ;
                        }
                        count = rs.getInt( 1 ) ;
                        break ;
                    }
                    rs.close() ;
                    rs = null ;
                    return count ;
                }
                else {
                    ArrayList<Map<String,Object>> ret = new ArrayList<Map<String,Object>>() ;
                    pushResult( ret,"dummy",rs,0,Integer.MAX_VALUE ) ;
                    rs.close() ;
                    rs = null ;
                    return ret ;
                }
            } finally {
                if( rs != null ) {
                    try {
                        rs.close() ;
                    } catch( Exception e ) {
                    }
                }
                rs = null ;
            }
        }
        // select以外で実行.
        else {
            return new Integer( record.executeUpdate( sql ) ) ;
        }
    }
    
    /**
     * シーケンス付与対象のパラメータが存在するかチェック.
     * <BR><BR>
     * シーケンス付与対象のパラメータが存在するかチェックします.
     * <BR>
     * @param meta 対象のメタデータを設定します.
     * @return boolean [true]の場合、シーケンス付与条件です.
     */
    public static final boolean isSequence( MetaColumn meta )
        throws Exception {
        int len = meta.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            if( SEQ_COLUMN.equals( meta.getColumnName( i ).toLowerCase() ) &&
                meta.getColumnType( i ) == Types.BIGINT ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * 楽観的ロック対象のパラメータが存在するかチェック.
     * <BR><BR>
     * 楽観的ロック対象のパラメータが存在するかチェックします.
     * <BR>
     * @param meta 対象のメタデータを設定します.
     * @return boolean [true]の場合、シーケンス付与条件です.
     */
    public static final boolean isOptimisticLock( MetaColumn meta )
        throws Exception {
        int len = meta.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            if( OPTIMISTIC_LOCK_COLUMN.equals( meta.getColumnName( i ).toLowerCase() ) &&
                meta.getColumnType( i ) == Types.BIGINT ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * 生成時間のパラメータが存在するかチェック.
     * <BR><BR>
     * 生成時間のパラメータが存在するかチェックします.
     * <BR>
     * @param meta 対象のメタデータを設定します.
     * @return boolean [true]の場合、生成時間パラメータが存在します.
     */
    public static final boolean isCreateTime( MetaColumn meta )
        throws Exception {
        int len = meta.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            if( CREATE_TIME.equals( meta.getColumnName( i ).toLowerCase() ) ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * 更新時間のパラメータが存在するかチェック.
     * <BR><BR>
     * 更新時間のパラメータが存在するかチェックします.
     * <BR>
     * @param meta 対象のメタデータを設定します.
     * @return boolean [true]の場合、更新時間パラメータが存在します.
     */
    public static final boolean isUpdateTime( MetaColumn meta )
        throws Exception {
        int len = meta.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            if( UPDATE_TIME.equals( meta.getColumnName( i ).toLowerCase() ) ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * シーケンスIDが利用できる場合は取得.
     */
    private static final Long getSequenceId( Record record,String sql )
        throws Exception {
        ResultSet result = null ;
        try {
            // シーケンスが利用できる場合.
            result = record.executeQuery( sql ) ;
            if( result != null ) {
                result.next() ;
                Long ret = result.getLong( 1 ) ;
                return ret ;
            }
            return null ;
        } catch( Exception e ) {
            throw e ;
        } finally {
            if( result != null ) {
                try {
                    result.close() ;
                } catch( Exception e ) {
                }
            }
        }
    }
    
    /**
     * Inset後にシーケンスIDを取得できる場合.
     */
    private static final Long getInsertId( Record record,String sql )
        throws Exception {
        ResultSet result = null ;
        try {
            // シーケンスが利用できる場合.
            result = record.executeQuery( sql ) ;
            if( result != null ) {
                result.next() ;
                Long ret = result.getLong( 1 ) ;
                return ret ;
            }
            return null ;
        } catch( Exception e ) {
            throw e ;
        } finally {
            if( result != null ) {
                try {
                    result.close() ;
                } catch( Exception e ) {
                }
            }
        }
    }
    
    /**
     * 実行結果を情報に設定.
     */
    private static final void pushResult( ArrayList<Map<String,Object>> out,String model,ResultSet result,int offset,int limit )
        throws Exception {
        ResultUtil.setPosition( result,offset ) ;
        MetaColumn meta = null ;
        for( int i = 0 ; i < limit ; i ++ ) {
            if( result.next() == false ) {
                break ;
            }
            if( meta == null ) {
                meta = new MetaColumn(model,result.getMetaData());
            }
            Map<String,Object> o = new HashMap<String,Object>() ;
            convertResultByObject( o,meta,result ) ;
            out.add( o ) ;
        }
    }
    
    /**
     * 指定ResultSetの内容をオブジェクトに割り当てる.
     */
    public static final void convertResultByObject(Map<String,Object>out,MetaColumn meta,ResultSet result)
            throws Exception {
        if (out == null || meta == null || result == null) {
            return ;
        }
        int len = meta.size();
        for (int i = 0; i < len; i++) {
            int type = meta.getColumnType( i ) ;
            String name = meta.getColumnName(i);
            //Object value = result.getObject(name);
            Object value = getParam( result,type,name ) ;
            String javaName = DbUtil.convertDBNameByJavaName(false, name);
            javaName = StringUtil.changeString( javaName,"?","" ) ;
            if (value != null) {
                // char(1)の条件は、Booleanと同様の扱いとする.
                if( type == Types.CHAR ) {
                    String c = ( String )value ;
                    if( c.length() == 1 ) {
                        if( c.equals( "0" ) ) {
                            value = new Boolean( false ) ;
                        }
                        else {
                            value = new Boolean( true ) ;
                        }
                    }
                }
                out.put( javaName,value ) ;
            }
            else {
                out.put( javaName,null ) ;
            }
        }
    }
    
    /**
     * 指定パラメータの要素を取得.
     */
    private static final Object getParam( ResultSet result,int type,String key )
        throws Exception {
        if( result.getObject( key ) == null ) {
            return null ;
        }
        Object data = null ;
        switch( type ){
            case Types.BIT :
                data = new Boolean( result.getBoolean( key ) ) ;
                break ;
            case Types.BOOLEAN :
                data = new Boolean( result.getBoolean( key ) ) ;
                break ;
            case Types.TINYINT :
                data = new Byte( result.getByte( key ) ) ;
                if( data != null ) {
                    byte b = ( ( Byte )data ).byteValue() ;
                    if( b == 1 ) {
                        data =  new Boolean( true ) ;
                    }
                    data = new Boolean( false ) ;
                }
                break ;
            case Types.SMALLINT :
                data = new Integer( result.getInt( key ) ) ;
                break ;
            case Types.INTEGER :
                data = new Integer( result.getInt( key ) ) ;
                break ;
            case Types.BIGINT :
                data = new Long( result.getLong( key ) ) ;
                break ;
            case Types.FLOAT :
                data = new Float( result.getFloat( key ) ) ;
                break ;
            case Types.REAL :
                data = new Float( result.getFloat( key ) ) ;
                break ;
            case Types.DOUBLE :
                data = new Double( result.getDouble( key ) ) ;
                break ;
            case Types.NUMERIC :
                data = result.getBigDecimal( key ) ;
                break ;
            case Types.DECIMAL :
                data = result.getBigDecimal( key ) ;
                break ;
            case Types.CHAR :
                data = result.getString( key ) ;
                break ;
            case Types.VARCHAR :
                data = result.getString( key ) ;
                break ;
            case Types.LONGVARCHAR :
                data = result.getString( key ) ;
                break ;
            case Types.DATE :
                data = result.getDate( key ) ;
                break ;
            case Types.TIME :
                data = result.getTime( key ) ;
                break ;
            case Types.TIMESTAMP :
                data = result.getTimestamp( key ) ;
                break ;
            case Types.BINARY :
                data = result.getBytes( key ) ;
                break ;
            case Types.VARBINARY :
                data = result.getBytes( key ) ;
                break ;
            case Types.LONGVARBINARY :
                data = result.getBytes( key ) ;
                break ;
            case Types.BLOB :
                data = result.getBlob( key ) ;
                break ;
            case Types.STRUCT :// 未サポート.
            case Types.CLOB :// 未サポート.
            case Types.NCLOB :// 未サポート.
            case Types.REF :// 未サポート.
            case Types.DATALINK :
                data = result.getString( key ) ;
                break ;
            default :
                data = null ;
        }
        if( data == null ) {
            return null ;
        }
        // blob.
        if( data instanceof Blob ) {
            InputStream b = new BufferedInputStream(
                ( ( Blob )data ).getBinaryStream() ) ;
            ByteArrayOutputStream bo = new ByteArrayOutputStream() ;
            byte[] bin = new byte[ 4096 ] ;
            for( ;; ) {
                int len = b.read( bin ) ;
                if( len <= -1 ) {
                    break ;
                }
                if( len != 0 ) {
                    bo.write( bin,0,len ) ;
                }
            }
            b.close() ;
            b = null ;
            data = bo.toByteArray() ;
            bo.close() ;
            bo = null ;
        }
        return data ;
    }
    
}

