/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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.seasar.extension.jdbc.manager;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.seasar.extension.jdbc.JdbcContext;
import org.seasar.extension.jdbc.StatementHandler;
import org.seasar.extension.jdbc.util.ConnectionUtil;
import org.seasar.extension.jdbc.util.StatementCache;
import org.seasar.framework.log.Logger;

/**
 * {@link JdbcContext}の実装クラスです。
 * 
 * @author higa
 * 
 */
public class JdbcContextImpl implements JdbcContext {

    private static final Logger logger = Logger
            .getLogger(JdbcContextImpl.class);

    private Connection connection;

    private Statement statement;

    private boolean transactional;

    private int preparedStatementCacheSize = 10;

    private int cursorPreparedStatementCacheSize = 5;

    private int callableStatementCacheSize = 5;

    private StatementCache preparedStatementCache = new StatementCache(
            preparedStatementCacheSize);

    private StatementCache cursorPreparedStatementCache = new StatementCache(
            cursorPreparedStatementCacheSize);

    private StatementCache callableStatementCache = new StatementCache(
            callableStatementCacheSize);

    /**
     * {@link JdbcContextImpl}を作成します。
     * 
     * @param connection
     *            コネクション
     * @param transactional
     *            トランザクション中に作成されたかどうか
     */
    public JdbcContextImpl(Connection connection, boolean transactional) {
        this.connection = connection;
        this.transactional = transactional;
    }

    public void destroy() {
        if (connection == null) {
            return;
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                logger.log(e);
            }
            statement = null;
        }
        try {
            preparedStatementCache.destroy();
        } catch (SQLException e) {
            logger.log(e);
        }
        try {
            cursorPreparedStatementCache.destroy();
        } catch (SQLException e) {
            logger.log(e);
        }
        try {
            callableStatementCache.destroy();
        } catch (SQLException e) {
            logger.log(e);
        }
        try {
            connection.close();
        } catch (SQLException e) {
            logger.log(e);
        }
        connection = null;
    }

    public boolean isTransactional() {
        return transactional;
    }

    public Statement getStatement() {
        if (statement != null) {
            return statement;
        }
        statement = ConnectionUtil.createStatement(connection);
        return statement;
    }

    @SuppressWarnings("unchecked")
    public PreparedStatement getPreparedStatement(String sql) {
        PreparedStatement ps = (PreparedStatement) preparedStatementCache
                .get(sql);
        if (ps != null) {
            return ps;
        }
        ps = ConnectionUtil.prepareStatement(connection, sql);
        preparedStatementCache.put(sql, ps);
        return ps;
    }

    @SuppressWarnings("unchecked")
    public <T> T usingPreparedStatement(String sql,
            StatementHandler<T, ? super PreparedStatement> handler) {
        PreparedStatement ps = (PreparedStatement) preparedStatementCache
                .remove(sql);
        if (ps == null) {
            ps = ConnectionUtil.prepareStatement(connection, sql);
        }
        try {
            return handler.handle(ps);
        } finally {
            preparedStatementCache.put(sql, ps);
        }
    }

    @SuppressWarnings("unchecked")
    public PreparedStatement getPreparedStatement(String sql,
            int autoGeneratedKeys) {
        final String key = new String(new StringBuilder(sql.length() + 12)
                .append('[').append(autoGeneratedKeys).append(']').append(sql));
        PreparedStatement ps = (PreparedStatement) preparedStatementCache
                .get(key);
        if (ps != null) {
            return ps;
        }
        ps = ConnectionUtil
                .prepareStatement(connection, sql, autoGeneratedKeys);
        preparedStatementCache.put(key, ps);
        return ps;
    }

    @SuppressWarnings("unchecked")
    public <T> T usingPreparedStatement(String sql, int autoGeneratedKeys,
            StatementHandler<T, ? super PreparedStatement> handler) {
        final String key = new String(new StringBuilder(sql.length() + 12)
                .append('[').append(autoGeneratedKeys).append(']').append(sql));
        PreparedStatement ps = (PreparedStatement) preparedStatementCache
                .remove(key);
        if (ps == null) {
            ps = ConnectionUtil.prepareStatement(connection, sql,
                    autoGeneratedKeys);
        }
        try {
            return handler.handle(ps);
        } finally {
            preparedStatementCache.put(key, ps);
        }
    }

    @SuppressWarnings("unchecked")
    public PreparedStatement getCursorPreparedStatement(String sql) {
        PreparedStatement ps = (PreparedStatement) cursorPreparedStatementCache
                .get(sql);
        if (ps != null) {
            return ps;
        }
        ps = ConnectionUtil.prepareStatement(connection, sql,
                ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
        cursorPreparedStatementCache.put(sql, ps);
        return ps;
    }

    @SuppressWarnings("unchecked")
    public <T> T usingCursorPreparedStatement(String sql,
            StatementHandler<T, ? super PreparedStatement> handler) {
        PreparedStatement ps = (PreparedStatement) cursorPreparedStatementCache
                .remove(sql);
        if (ps == null) {
            ps = ConnectionUtil.prepareStatement(connection, sql,
                    ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
        }
        try {
            return handler.handle(ps);
        } finally {
            cursorPreparedStatementCache.put(sql, ps);
        }
    }

    @SuppressWarnings("unchecked")
    public CallableStatement getCallableStatement(String sql) {
        CallableStatement cs = (CallableStatement) callableStatementCache
                .get(sql);
        if (cs != null) {
            return cs;
        }
        cs = ConnectionUtil.prepareCall(connection, sql);
        callableStatementCache.put(sql, cs);
        return cs;
    }

    @SuppressWarnings("unchecked")
    public <T> T usingCallableStatement(String sql,
            StatementHandler<T, ? super CallableStatement> handler) {
        CallableStatement cs = (CallableStatement) callableStatementCache
                .remove(sql);
        if (cs == null) {
            cs = ConnectionUtil.prepareCall(connection, sql);
        }
        try {
            return handler.handle(cs);
        } finally {
            callableStatementCache.put(sql, cs);
        }
    }

    /**
     * JDBCコンテキストが破棄されたかどうかを返します。
     * 
     * @return JDBCコンテキストが破棄されたかどうか
     */
    public boolean idDestroyed() {
        return isConnectionNull();
    }

    /**
     * コネクションが<code>null</code>かどうかを返します。
     * 
     * @return コネクションが<code>null</code>かどうか
     */
    public boolean isConnectionNull() {
        return connection == null;
    }

    /**
     * ステートメントが<code>null</code>かどうかを返します。
     * 
     * @return ステートメントが<code>null</code>かどうか
     */
    public boolean isStatementNull() {
        return statement == null;
    }

    /**
     * 準備されたステートメントのキャッシュが空かどうかを返します。
     * 
     * @return 準備されたステートメントのキャッシュが空かどうか
     */
    public boolean isPreparedStatementCacheEmpty() {
        return preparedStatementCache.isEmpty();
    }

    /**
     * カーソルつきの準備されたステートメントのキャッシュが空かどうかを返します。
     * 
     * @return カーソルつきの準備されたステートメントのキャッシュが空かどうか
     */
    public boolean isCursorPreparedStatementCacheEmpty() {
        return cursorPreparedStatementCache.isEmpty();
    }

    /**
     * 呼び出し可能なステートメントのキャッシュが空かどうかを返します。
     * 
     * @return 呼び出し可能なステートメントのキャッシュが空かどうか
     */
    public boolean isCallableStatementCacheEmpty() {
        return callableStatementCache.isEmpty();
    }

    /**
     * 準備されたステートメントをキャッシュする数を返します。
     * 
     * @return 準備されたステートメントをキャッシュする数
     */
    public int getPreparedStatementCacheSize() {
        return preparedStatementCacheSize;
    }

    /**
     * 準備されたステートメントをキャッシュする数を設定します。
     * 
     * @param preparedStatementCacheSize
     *            準備されたステートメントをキャッシュする数
     */
    public void setPreparedStatementCacheSize(int preparedStatementCacheSize) {
        this.preparedStatementCacheSize = preparedStatementCacheSize;
        preparedStatementCache = new StatementCache(preparedStatementCacheSize);
    }

    /**
     * カーソルつきの準備されたステートメントをキャッシュする数を返します。
     * 
     * @return カーソルつきの準備されたステートメントをキャッシュする数
     */
    public int getCursorPreparedStatementCacheSize() {
        return cursorPreparedStatementCacheSize;
    }

    /**
     * カーソルつきの準備されたステートメントをキャッシュする数を設定します。
     * 
     * @param cursorPreparedStatementCacheSize
     *            カーソルつきの準備されたステートメントをキャッシュする数
     */
    public void setCursorPreparedStatementCacheSize(
            int cursorPreparedStatementCacheSize) {
        this.cursorPreparedStatementCacheSize = cursorPreparedStatementCacheSize;
        cursorPreparedStatementCache = new StatementCache(
                cursorPreparedStatementCacheSize);
    }

    /**
     * 呼び出し可能なステートメントをキャッシュする数を返します。
     * 
     * @return 呼び出し可能なステートメントをキャッシュする数
     */
    public int getCallableStatementCacheSize() {
        return callableStatementCacheSize;
    }

    /**
     * 呼び出し可能なステートメントをキャッシュする数を設定します。
     * 
     * @param callableStatementCacheSize
     *            呼び出し可能なステートメントをキャッシュする数
     */
    public void setCallableStatementCacheSize(int callableStatementCacheSize) {
        this.callableStatementCacheSize = callableStatementCacheSize;
        callableStatementCache = new StatementCache(callableStatementCacheSize);
    }

}
