/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * 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 com.alibaba.druid.pool;

import com.alibaba.druid.VERSION;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.alibaba.druid.util.JdbcUtils;
import com.alibaba.druid.util.MySqlUtils;

import java.net.SocketTimeoutException;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author wenshao [szujobs@hotmail.com]
 */
public class DruidPooledStatement extends PoolableWrapper implements Statement {
    private static final Log LOG = LogFactory.getLog(DruidPooledStatement.class);

    private final Statement stmt;
    protected DruidPooledConnection conn;
    protected List<ResultSet> resultSetTrace;
    protected boolean closed;
    protected int fetchRowPeak = -1;
    protected int exceptionCount;

    public DruidPooledStatement(DruidPooledConnection conn, Statement stmt) {
        super(stmt);

        this.conn = conn;
        this.stmt = stmt;
    }

    protected void addResultSetTrace(ResultSet resultSet) {
        if (resultSetTrace == null) {
            resultSetTrace = new ArrayList<ResultSet>(1);
        } else if (resultSetTrace.size() > 0) {
            int lastIndex = resultSetTrace.size() - 1;
            ResultSet lastResultSet = resultSetTrace.get(lastIndex);
            try {
                if (lastResultSet.isClosed()) {
                    resultSetTrace.set(lastIndex, resultSet);
                    return;
                }
            } catch (SQLException ex) {
                // skip
            }
        }

        resultSetTrace.add(resultSet);
    }

    protected void recordFetchRowCount(int fetchRowCount) {
        if (fetchRowPeak < fetchRowCount) {
            fetchRowPeak = fetchRowCount;
        }
    }

    public int getFetchRowPeak() {
        return fetchRowPeak;
    }

    protected SQLException checkException(Throwable error) throws SQLException {
        String sql = null;
        if (this instanceof DruidPooledPreparedStatement) {
            sql = ((DruidPooledPreparedStatement) this).getSql();
        }

        handleSocketTimeout(error);

        exceptionCount++;
        return conn.handleException(error, sql);
    }

    protected SQLException checkException(Throwable error, String sql) throws SQLException {
        handleSocketTimeout(error);

        exceptionCount++;
        return conn.handleException(error, sql);
    }

    protected void handleSocketTimeout(Throwable error) throws SQLException {
        if (this.conn == null
                || this.conn.transactionInfo != null
                || this.conn.holder == null) {
            return;
        }

        DruidDataSource dataSource = null;

        final DruidConnectionHolder holder = this.conn.holder;
        if (holder.dataSource instanceof DruidDataSource) {
            dataSource = (DruidDataSource) holder.dataSource;
        }
        if (dataSource == null) {
            return;
        }

        if (!dataSource.killWhenSocketReadTimeout) {
            return;
        }

        SQLException sqlException = null;
        if (error instanceof SQLException) {
            sqlException = (SQLException) error;
        }

        if (sqlException == null) {
            return;
        }

        Throwable cause = error.getCause();
        boolean socketReadTimeout = cause instanceof SocketTimeoutException
                && "Read timed out".equals(cause.getMessage());
        if (!socketReadTimeout) {
            return;
        }

        if (!JdbcUtils.isMysqlDbType(dataSource.dbTypeName)) {
            return;
        }

        String killQuery = MySqlUtils.buildKillQuerySql(this.conn.getConnection(), (SQLException) error);

        if (killQuery == null) {
            return;
        }

        DruidPooledConnection killQueryConn = null;
        Statement killQueryStmt = null;

        try {
            killQueryConn = dataSource.getConnection(1000);
            if (killQueryConn == null) {
                return;
            }

            killQueryStmt = killQueryConn.createStatement();
            killQueryStmt.execute(killQuery);

            if (LOG.isDebugEnabled()) {
                LOG.debug(killQuery + " success.");
            }
        } catch (Exception ex) {
            LOG.warn(killQuery + " error.", ex);
        } finally {
            JdbcUtils.close(killQueryStmt);
            JdbcUtils.close(killQueryConn);
        }
    }

    public DruidPooledConnection getPoolableConnection() {
        return conn;
    }

    public Statement getStatement() {
        return stmt;
    }

    protected void checkOpen() throws SQLException {
        if (closed) {
            Throwable disableError = null;
            if (this.conn != null) {
                disableError = this.conn.getDisableError();
            }

            if (disableError != null) {
                throw new SQLException("statement is closed", disableError);
            } else {
                throw new SQLException("statement is closed");
            }
        }
    }

    protected void clearResultSet() {
        if (resultSetTrace == null) {
            return;
        }

        for (ResultSet rs : resultSetTrace) {
            try {
                if (!rs.isClosed()) {
                    rs.close();
                }
            } catch (SQLException ex) {
                LOG.error("clearResultSet error", ex);
            }
        }
        resultSetTrace.clear();
    }

    public void incrementExecuteCount() {
        final DruidPooledConnection conn = this.getPoolableConnection();
        if (conn == null) {
            return;
        }

        final DruidConnectionHolder holder = conn.getConnectionHolder();
        if (holder == null) {
            return;
        }

        final DruidAbstractDataSource dataSource = holder.getDataSource();
        if (dataSource == null) {
            return;
        }

        dataSource.incrementExecuteCount();
    }

    public void incrementExecuteBatchCount() {
        final DruidPooledConnection conn = this.getPoolableConnection();
        if (conn == null) {
            return;
        }

        final DruidConnectionHolder holder = conn.getConnectionHolder();
        if (holder == null) {
            return;
        }

        if (holder.getDataSource() == null) {
            return;
        }

        final DruidAbstractDataSource dataSource = holder.getDataSource();
        if (dataSource == null) {
            return;
        }

        dataSource.incrementExecuteBatchCount();
    }

    public void incrementExecuteUpdateCount() {
        final DruidPooledConnection conn = this.getPoolableConnection();
        if (conn == null) {
            return;
        }

        final DruidConnectionHolder holder = conn.getConnectionHolder();
        if (holder == null) {
            return;
        }

        final DruidAbstractDataSource dataSource = holder.getDataSource();
        if (dataSource == null) {
            return;
        }

        dataSource.incrementExecuteUpdateCount();
    }

    public void incrementExecuteQueryCount() {
        final DruidPooledConnection conn = this.conn;
        if (conn == null) {
            return;
        }

        final DruidConnectionHolder holder = conn.holder;
        if (holder == null) {
            return;
        }

        final DruidAbstractDataSource dataSource = holder.dataSource;
        if (dataSource == null) {
            return;
        }

        dataSource.executeQueryCount++;
    }

    protected void transactionRecord(String sql) throws SQLException {
        conn.transactionRecord(sql);
    }

    @Override
    public final ResultSet executeQuery(String sql) throws SQLException {
        checkOpen();

        incrementExecuteQueryCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            ResultSet rs = stmt.executeQuery(sql);

            if (rs == null) {
                return rs;
            }

            DruidPooledResultSet poolableResultSet = new DruidPooledResultSet(this, rs);
            addResultSetTrace(poolableResultSet);

            return poolableResultSet;
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final int executeUpdate(String sql) throws SQLException {
        checkOpen();

        incrementExecuteUpdateCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeUpdate(sql);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    protected final void errorCheck(Throwable t) {
        String errorClassName = t.getClass().getName();
        if (errorClassName.endsWith(".CommunicationsException")
                && conn.holder != null
                && conn.holder.dataSource.testWhileIdle) {
            DruidConnectionHolder holder = conn.holder;
            DruidAbstractDataSource dataSource = holder.dataSource;

            long currentTimeMillis = System.currentTimeMillis();
            long lastActiveTimeMillis = holder.lastActiveTimeMillis;

            if (lastActiveTimeMillis < holder.lastKeepTimeMillis) {
                lastActiveTimeMillis = holder.lastKeepTimeMillis;
            }

            long idleMillis = currentTimeMillis - lastActiveTimeMillis;
            long lastValidIdleMillis = currentTimeMillis - holder.lastActiveTimeMillis;

            String errorMsg = "CommunicationsException, druid version " + VERSION.getVersionNumber()
                    + ", jdbcUrl : " + dataSource.jdbcUrl
                    + ", testWhileIdle " + dataSource.testWhileIdle
                    + ", idle millis " + idleMillis
                    + ", minIdle " + dataSource.minIdle
                    + ", poolingCount " + dataSource.getPoolingCount()
                    + ", timeBetweenEvictionRunsMillis " + dataSource.timeBetweenEvictionRunsMillis
                    + ", lastValidIdleMillis " + lastValidIdleMillis
                    + ", driver " + dataSource.driver.getClass().getName();

            if (dataSource.exceptionSorter != null) {
                errorMsg += ", exceptionSorter " + dataSource.exceptionSorter.getClass().getName();
            }

            LOG.error(errorMsg);
        }
    }

    @Override
    public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
        checkOpen();

        incrementExecuteUpdateCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeUpdate(sql, autoGeneratedKeys);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
        checkOpen();

        incrementExecuteUpdateCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeUpdate(sql, columnIndexes);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final int executeUpdate(String sql, String[] columnNames) throws SQLException {
        checkOpen();

        incrementExecuteUpdateCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeUpdate(sql, columnNames);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
        checkOpen();

        incrementExecuteCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.execute(sql, autoGeneratedKeys);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final boolean execute(String sql, int[] columnIndexes) throws SQLException {
        checkOpen();

        incrementExecuteCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.execute(sql, columnIndexes);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final boolean execute(String sql, String[] columnNames) throws SQLException {
        checkOpen();

        incrementExecuteCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.execute(sql, columnNames);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public int getMaxFieldSize() throws SQLException {
        checkOpen();

        try {
            return stmt.getMaxFieldSize();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void close() throws SQLException {
        if (this.closed) {
            return;
        }

        clearResultSet();
        if (stmt != null) {
            stmt.close();
        }
        this.closed = true;

        DruidConnectionHolder connHolder = conn.getConnectionHolder();
        if (connHolder != null) {
            connHolder.removeTrace(this);
        }
    }

    @Override
    public void setMaxFieldSize(int max) throws SQLException {
        checkOpen();

        try {
            stmt.setMaxFieldSize(max);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getMaxRows() throws SQLException {
        checkOpen();

        try {
            return stmt.getMaxRows();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setMaxRows(int max) throws SQLException {
        checkOpen();

        try {
            stmt.setMaxRows(max);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final void setEscapeProcessing(boolean enable) throws SQLException {
        checkOpen();

        try {
            stmt.setEscapeProcessing(enable);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getQueryTimeout() throws SQLException {
        checkOpen();

        try {
            return stmt.getQueryTimeout();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setQueryTimeout(int seconds) throws SQLException {
        checkOpen();

        try {
            stmt.setQueryTimeout(seconds);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final void cancel() throws SQLException {
        checkOpen();

        try {
            stmt.cancel();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final SQLWarning getWarnings() throws SQLException {
        checkOpen();

        try {
            return stmt.getWarnings();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final void clearWarnings() throws SQLException {
        checkOpen();

        try {
            stmt.clearWarnings();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final void setCursorName(String name) throws SQLException {
        checkOpen();

        try {
            stmt.setCursorName(name);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final boolean execute(String sql) throws SQLException {
        checkOpen();

        incrementExecuteCount();
        transactionRecord(sql);

        try {
            return stmt.execute(sql);
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t, sql);
        }
    }

    @Override
    public final ResultSet getResultSet() throws SQLException {
        checkOpen();

        try {
            ResultSet rs = stmt.getResultSet();
            if (rs == null) {
                return null;
            }

            DruidPooledResultSet poolableResultSet = new DruidPooledResultSet(this, rs);
            addResultSetTrace(poolableResultSet);

            return poolableResultSet;
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getUpdateCount() throws SQLException {
        checkOpen();

        try {
            return stmt.getUpdateCount();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final boolean getMoreResults() throws SQLException {
        checkOpen();

        try {
            boolean moreResults = stmt.getMoreResults();

            if (resultSetTrace != null && resultSetTrace.size() > 0) {
                ResultSet lastResultSet = resultSetTrace.get(resultSetTrace.size() - 1);
                if (lastResultSet instanceof DruidPooledResultSet) {
                    DruidPooledResultSet pooledResultSet = ((DruidPooledResultSet) lastResultSet);
                    pooledResultSet.closed = true;
                }
            }

            return moreResults;
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setFetchDirection(int direction) throws SQLException {
        checkOpen();

        try {
            stmt.setFetchDirection(direction);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getFetchDirection() throws SQLException {
        checkOpen();

        try {
            return stmt.getFetchDirection();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setFetchSize(int rows) throws SQLException {
        checkOpen();

        try {
            stmt.setFetchSize(rows);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getFetchSize() throws SQLException {
        checkOpen();

        try {
            return stmt.getFetchSize();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getResultSetConcurrency() throws SQLException {
        checkOpen();

        try {
            return stmt.getResultSetConcurrency();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getResultSetType() throws SQLException {
        checkOpen();

        try {
            return stmt.getResultSetType();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final void addBatch(String sql) throws SQLException {
        checkOpen();

        transactionRecord(sql);

        try {
            stmt.addBatch(sql);
        } catch (Throwable t) {
            throw checkException(t, sql);
        }
    }

    @Override
    public final void clearBatch() throws SQLException {
        if (closed) {
            return;
        }

        try {
            stmt.clearBatch();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public int[] executeBatch() throws SQLException {
        checkOpen();

        incrementExecuteBatchCount();

        conn.beforeExecute();
        try {
            return stmt.executeBatch();
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public final Connection getConnection() throws SQLException {
        checkOpen();

        return conn;
    }

    @Override
    public final boolean getMoreResults(int current) throws SQLException {
        checkOpen();

        try {
            boolean results = stmt.getMoreResults(current);

            if (resultSetTrace != null && resultSetTrace.size() > 0) {
                ResultSet lastResultSet = resultSetTrace.get(resultSetTrace.size() - 1);
                if (lastResultSet instanceof DruidPooledResultSet) {
                    DruidPooledResultSet pooledResultSet = ((DruidPooledResultSet) lastResultSet);
                    pooledResultSet.closed = true;
                }
            }

            return results;
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final ResultSet getGeneratedKeys() throws SQLException {
        checkOpen();

        try {
            ResultSet rs = stmt.getGeneratedKeys();

            DruidPooledResultSet poolableResultSet = new DruidPooledResultSet(this, rs);

            addResultSetTrace(poolableResultSet);

            return poolableResultSet;
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final int getResultSetHoldability() throws SQLException {
        checkOpen();

        try {
            return stmt.getResultSetHoldability();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public final boolean isClosed() throws SQLException {
        return closed;
    }

    @Override
    public final void setPoolable(boolean poolable) throws SQLException {
        if (poolable) {
            return;
        }

        throw new SQLException("not support");
    }

    @Override
    public final boolean isPoolable() throws SQLException {
        return false;
    }

    public String toString() {
        return stmt.toString();
    }

    public void closeOnCompletion() throws SQLException {
        stmt.closeOnCompletion();
    }

    public boolean isCloseOnCompletion() throws SQLException {
        return stmt.isCloseOnCompletion();
    }
}
