package org.maachang.dao.dbms;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.dao.dbms.kind.SupportKind;

/**
 * DBアクセスオブジェクト実装.
 * 
 * @version 2007/10/18
 * @author masahito suzuki
 * @since MaachangDao 1.00
 */
public class BaseRecord implements Record {
    
    /**
     * LOG.
     */
    private static final Log LOG = LogFactory.getLog( BaseRecord.class ) ;
    
    /**
     * サポートDBMS種類.
     */
    private SupportKind kind = null ;

    /**
     * Booleanサポートフラグ.
     */
    private boolean supportBoolean = false ;

    /**
     * コネクション.
     */
    private Connection connection = null;

    /**
     * コミットロールバックフラグ.
     */
    private boolean commitRollbackFlag = true;

    /**
     * シーケンス取得条件.
     */
    private boolean sequence = false ;

    /**
     * ステートメント管理.
     */
    private Statement statement = null ;

    /**
     * PreparedStatement管理.
     */
    private HashMap<String,PreparedStatement> pman = null ;
    
    /**
     * デバッグモード.
     */
    private boolean debug = false ;

    /**
     * コンストラクタ.
     */
    private BaseRecord() {

    }

    /**
     * コンストラクタ. <BR>
     * <BR>
     * コネクションオブジェクトを設定してオブジェクトを生成します. <BR>
     * 
     * @param adapter
     *            対象のアダプタ名を設定します.
     * @param connection
     *            コネクションオブジェクト.
     * @exception Exception
     *                例外.
     */
    public BaseRecord(SupportKind kind,boolean debug,Connection connection) throws Exception {
        if (connection == null) {
            throw new IllegalArgumentException("コネクションオブジェクトは存在しません");
        }
        this.kind = kind ;
        this.debug = debug ;
        this.supportBoolean = kind.isBoolean() ;
        this.sequence = ( kind.getSequenceId( null ) != null ) ? true : false ;
        this.connection = connection;
    }

    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        this.close();
    }

    /**
     * クローズ処理. <BR>
     * <BR>
     * オブジェクトをクローズします.
     */
    public void close() {
        if (commitRollbackFlag == false) {
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (Exception e) {
                }
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception e) {
            }
        }
        clearStatements() ;
        commitRollbackFlag = true;
        connection = null;
        statement = null;
        pman = null ;
    }

    /**
     * コネクションオブジェクトを取得. <BR>
     * <BR>
     * コネクションオブジェクトを取得します. <BR>
     * 
     * @return Connection コネクションオブジェクトが返されます.
     */
    public Connection getConnection() {
        return connection;
    }

    /**
     * コミット処理. <BR>
     * <BR>
     * コミット処理を実施します. <BR>
     * 
     * @return boolean [true]の場合、コミット処理が呼ばれました.
     * @exception Exception 例外.
     */
    public boolean commit() throws Exception {
        if (connection != null) {
            if( debug == true ) {
                if( LOG.isDebugEnabled() ) {
                    LOG.debug( "# sql:commit" ) ;
                }
            }
            if( commitRollbackFlag == false ) {
                connection.commit();
                commitRollbackFlag = true;
                clearStatements() ;
                return true ;
            }
            else {
                connection.rollback();
                commitRollbackFlag = true;
                clearStatements() ;
                return false ;
            }
        }
        
        clearStatements() ;
        return false ;
    }

    /**
     * ロールバック処理. <BR>
     * <BR>
     * ロールバック処理を実施します. <BR>
     * 
     * @return boolean [true]の場合、ロールバック処理が呼ばれました.
     * @exception Exception 例外.
     */
    public boolean rollback() throws Exception {
        if (connection != null) {
            if( debug == true ) {
                if( LOG.isDebugEnabled() ) {
                    LOG.debug( "# sql:rollback" ) ;
                }
            }
            connection.rollback();
            commitRollbackFlag = true;
            clearStatements() ;
            return true ;
        }
        clearStatements() ;
        return false ;
    }

    /**
     * 管理ステートメントをクリア.
     * <BR><BR>
     * 管理しているステートメントをクリアします.
     */
    public synchronized void clearStatements() {
        if( statement != null ) {
            try {
                statement.close() ;
            } catch( Exception e ) {
            }
            statement = null ;
        }
        if( pman != null ) {
            Object[] objs = pman.keySet().toArray() ;
            if( objs != null && objs.length > 0 ) {
                int len = objs.length ;
                for( int i = 0 ; i < len ; i ++ ) {
                    PreparedStatement ps = pman.get( ( String )objs[ i ] ) ;
                    if( ps != null ) {
                        try {
                            ps.close() ;
                        } catch( Exception e ) {
                        }
                    }
                    ps = null ;
                }
            }
            pman.clear() ;
        }
    }

    /**
     * メタカラムを取得. <BR>
     * <BR>
     * メタカラムを取得します. <BR>
     * 
     * @param table
     *            対象のテーブル名を設定します.
     * @return MetaColumn メタカラムが返されます.
     * @exception Exception
     *                例外.
     */
    public MetaColumn getMetaColumn(String table) throws Exception {
        ResultSet r = null ;
        try {
            r = getStatement().executeQuery("select * from " + table);
            ResultSetMetaData meta = r.getMetaData();
            MetaColumn ret = new MetaColumn(table, meta);
            r.close();
            r = null ;
            return ret;
        } finally {
            if( r != null ) {
                try {
                    r.close() ;
                } catch( Exception e ) {
                }
            }
            r = null ;
        }
    }

    /**
     * このオブジェクトが利用可能かチェック. <BR>
     * <BR>
     * このオブジェクトが利用可能かチェックします. <BR>
     * 
     * @return boolean [true]の場合、利用可能です.
     */
    public boolean isUse() {
        boolean ret = false;
        try {
            ret = (connection == null || connection.isClosed() == true) ? false
                    : true;
        } catch (Exception e) {
            ret = false;
        }
        return ret;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のSQLを設定します.
     * @return ResultSet 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public ResultSet executeQuery(String sql) throws Exception {
        if (connection == null) {
            throw new IOException("コネクション情報は存在しません");
        }
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql ) ;
            }
        }
        long time = System.currentTimeMillis() ;
        ResultSet ret = getStatement().executeQuery(sql);
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql + "["+( System.currentTimeMillis()-time )+"ms]" ) ;
            }
        }
        commitRollbackFlag = false;
        return ret;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のSQLを設定します.
     * @return int 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public int executeUpdate(String sql) throws Exception {
        if (connection == null) {
            throw new IOException("コネクション情報は存在しません");
        }
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql ) ;
            }
        }
        long time = System.currentTimeMillis() ;
        int ret = getStatement().executeUpdate(sql);
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql + "["+( System.currentTimeMillis()-time )+"ms]" ) ;
            }
        }
        commitRollbackFlag = false;
        return ret;
    }

    /**
     * ステートメントを取得.
     */
    private synchronized Statement getStatement()
        throws Exception {
        if( statement == null ) {
            statement = connection.createStatement() ;
        }
        return statement ;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のパースされたSQLを設定します.
     * @param meta
     *            対象のメタデータを設定します.
     * @param params
     *            対象のパラメータを設定します.
     * @return ResultSet 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public ResultSet executeQuery(String sql, 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 executeQuery( sql,o ) ;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のパースされたSQLを設定します.
     * @param meta
     *            対象のメタデータを設定します.
     * @param params
     *            対象のパラメータを設定します.
     * @return ResultSet 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public ResultSet executeQuery(String sql, Object[] params)
            throws Exception {
        if (connection == null) {
            throw new IOException("コネクション情報は存在しません");
        }
        if (sql == null || (sql = sql.trim()).length() <= 0) {
            throw new IllegalArgumentException("引数は不正です");
        }
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql ) ;
            }
        }
        long time = System.currentTimeMillis() ;
        PreparedStatement pre = getPreparedStatement(true,sql) ;
        putParams(supportBoolean,pre, params);
        ResultSet ret = pre.executeQuery();
        commitRollbackFlag = false;
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql + "["+( System.currentTimeMillis()-time )+"ms]" ) ;
            }
        }
        return ret;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のパースされたSQLを設定します.
     * @param meta
     *            対象のメタデータを設定します.
     * @param params
     *            対象のパラメータを設定します.
     * @return int 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public int executeUpdate(String sql, 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 executeUpdate( sql,o ) ;
    }

    /**
     * データ取得用ステートメントを実行. <BR>
     * <BR>
     * データ取得用のステートメントを実行します. <BR>
     * 
     * @param sql
     *            対象のパースされたSQLを設定します.
     * @param meta
     *            対象のメタデータを設定します.
     * @param params
     *            対象のパラメータを設定します.
     * @return int 対象の結果情報が返されます.
     * @exception Exception
     *                例外.
     */
    public int executeUpdate(String sql, Object[] params)
            throws Exception {
        if (connection == null) {
            throw new IOException("コネクション情報は存在しません");
        }
        if (sql == null || (sql = sql.trim()).length() <= 0) {
            throw new IllegalArgumentException("引数は不正です");
        }
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql ) ;
            }
        }
        long time = System.currentTimeMillis() ;
        PreparedStatement pre = getPreparedStatement(false,sql);
        putParams(supportBoolean,pre, params);
        int ret = pre.executeUpdate();
        commitRollbackFlag = false;
        if( debug == true ) {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "# sql:" + sql + "["+( System.currentTimeMillis()-time )+"ms]" ) ;
            }
        }
        return ret;
    }

    /**
     * PreparedStatementを取得.
     */
    private synchronized PreparedStatement getPreparedStatement( boolean readOnly,String sql )
        throws Exception {
        if( sql == null || ( sql = sql.trim() ).length() <= 0 ||
            connection == null ) {
            return null ;
        }
        if( pman == null ) {
            pman = new HashMap<String,PreparedStatement>() ;
        }
        PreparedStatement ret = pman.get( sql ) ;
        if( ret != null ) {
            return ret ;
        }
        if( readOnly == true ) {
            ret = connection.prepareStatement(sql,
                ResultSet.TYPE_SCROLL_INSENSITIVE,
                ResultSet.CONCUR_READ_ONLY) ;
        }
        else {
            ret = connection.prepareStatement(sql) ;
        }
        ret = connection.prepareStatement(sql) ;
        pman.put( sql,ret ) ;
        return ret ;
    }

    /**
     * サポートDBMS種類オブジェクトを取得.
     * <BR><BR>
     * サポートDBMS種類オブジェクトを取得します.
     * <BR>
     * @return SupportKind サポートDBMS種類が返されます.
     */
    public SupportKind getSupportKind() {
        return kind ;
    }

    /**
     * シーケンス取得条件を取得.
     * <BR><BR>
     * シーケンス取得条件を取得します.
     * <BR>
     * @return boolean [true]の場合は、前に取得できます.
     */
    public boolean isSequence() {
        return sequence ;
    }

    /**
     * デバッグモードを取得.
     * <BR>
     * @return [true]の場合、デバッグは有効です.
     */
    public boolean isDebug() {
        return debug ;
    }

    /**
     * 各パラメータを設定.
     */
    private static final void putParams(boolean supportBoolean,PreparedStatement pre,
            Object[] params) throws Exception {
        if( params != null && params.length > 0 ) {
            int len = params.length;
            for (int i = 0; i < len; i++) {
                putOneParam(supportBoolean,i, pre, params[i]);
            }
        }
    }

    /**
     * １つの条件に対して、条件を設定.
     */
    private static final void putOneParam(boolean supportBoolean,int no, PreparedStatement pre,
        Object one) throws Exception {
        if( one == null ) {
            pre.setObject( no + 1,null );
        }
        else if (one instanceof Boolean) {
            boolean b = ((Boolean) one).booleanValue() ;
            if( supportBoolean == false ) {
                if( b == true ) {
                    pre.setString(no + 1, "1");
                }
                else {
                    pre.setString(no + 1, "0");
                }
            }
            else {
                pre.setBoolean(no + 1, b);
            }
        } else if (one instanceof String) {
            pre.setString(no + 1, (String) one);
        } else if (one instanceof Integer) {
            pre.setInt(no + 1, ((Integer) one).intValue());
        } else if (one instanceof Long) {
            pre.setLong(no + 1, ((Long) one).longValue());
        } else if (one instanceof Float) {
            pre.setFloat(no + 1, ((Float) one).floatValue());
        } else if (one instanceof Double) {
            pre.setDouble(no + 1, ((Double) one).doubleValue());
        } else if (one instanceof java.util.Date) {
            if (one instanceof java.sql.Timestamp) {
                pre.setTimestamp(no + 1, (java.sql.Timestamp) one);
            } else if (one instanceof java.sql.Time) {
                pre.setTime(no + 1, (java.sql.Time) one);
            } else if (one instanceof java.sql.Date) {
                pre.setDate(no + 1, (java.sql.Date) one);
            } else {
                pre.setTimestamp(no + 1, new java.sql.Timestamp(
                        ((java.util.Date) one).getTime()));
            }
        } else if (one instanceof byte[]) {
            //pre.setBlob(no + 1, new ByteArrayInputStream((byte[]) one));
            pre.setBytes(no + 1,( byte[] )one ) ;
        } else {
            throw new IllegalArgumentException("指定パラメータ[" + (no + 1)
                    + "]の条件はオブジェクトが一致しません:" + one.getClass().getName());
        }
    }
}
