/*
 * 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.DbType;
import com.alibaba.druid.filter.FilterChainImpl;
import com.alibaba.druid.pool.DruidAbstractDataSource.PhysicalConnectionInfo;
import com.alibaba.druid.proxy.jdbc.WrapperProxy;
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.Utils;

import javax.sql.ConnectionEventListener;
import javax.sql.StatementEventListener;

import java.lang.reflect.Field;
import java.net.Socket;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;

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

    static volatile boolean ORACLE_SOCKET_FIELD_ERROR;
    static volatile Field ORACLE_FIELD_NET;
    static volatile Field ORACLE_FIELD_S_ATTS;
    static volatile Field ORACLE_FIELD_NT;
    static volatile Field ORACLE_FIELD_SOCKET;

    public static boolean holdabilityUnsupported;

    protected final DruidAbstractDataSource dataSource;
    protected final long connectionId;
    protected final Connection conn;
    protected final List<ConnectionEventListener> connectionEventListeners = new CopyOnWriteArrayList<ConnectionEventListener>();
    protected final List<StatementEventListener> statementEventListeners = new CopyOnWriteArrayList<StatementEventListener>();
    protected final long connectTimeMillis;
    protected volatile long lastActiveTimeMillis;
    protected volatile long lastExecTimeMillis;
    protected volatile long lastKeepTimeMillis;
    protected volatile long lastValidTimeMillis;
    protected long useCount;
    private long keepAliveCheckCount;
    private long lastNotEmptyWaitNanos;
    private final long createNanoSpan;
    protected PreparedStatementPool statementPool;
    protected final List<Statement> statementTrace = new ArrayList<Statement>(2);
    protected final boolean defaultReadOnly;
    protected final int defaultHoldability;
    protected final int defaultTransactionIsolation;
    protected final boolean defaultAutoCommit;
    protected boolean underlyingReadOnly;
    protected int underlyingHoldability;
    protected int underlyingTransactionIsolation;
    protected boolean underlyingAutoCommit;
    protected volatile boolean discard;
    protected volatile boolean active;
    protected final Map<String, Object> variables;
    protected final Map<String, Object> globalVariables;
    final ReentrantLock lock = new ReentrantLock();
    protected String initSchema;
    protected Socket socket;

    volatile FilterChainImpl filterChain;

    public DruidConnectionHolder(DruidAbstractDataSource dataSource, PhysicalConnectionInfo pyConnectInfo)
            throws SQLException {
        this(
                dataSource,
                pyConnectInfo.getPhysicalConnection(),
                pyConnectInfo.getConnectNanoSpan(),
                pyConnectInfo.getVairiables(),
                pyConnectInfo.getGlobalVairiables()
        );
    }

    public DruidConnectionHolder(DruidAbstractDataSource dataSource, Connection conn, long connectNanoSpan)
            throws SQLException {
        this(dataSource, conn, connectNanoSpan, null, null);
    }

    public DruidConnectionHolder(
            DruidAbstractDataSource dataSource,
            Connection conn,
            long connectNanoSpan,
            Map<String, Object> variables,
            Map<String, Object> globalVariables
    ) throws SQLException {
        this.dataSource = dataSource;
        this.conn = conn;
        this.createNanoSpan = connectNanoSpan;
        this.variables = variables;
        this.globalVariables = globalVariables;

        this.connectTimeMillis = System.currentTimeMillis();
        this.lastActiveTimeMillis = connectTimeMillis;
        this.lastExecTimeMillis = connectTimeMillis;

        this.underlyingAutoCommit = conn.getAutoCommit();

        if (conn instanceof WrapperProxy) {
            this.connectionId = ((WrapperProxy) conn).getId();
        } else {
            this.connectionId = dataSource.createConnectionId();
        }

        Class<? extends Connection> conClass = conn.getClass();
        String connClassName = conClass.getName();
        if ((!ORACLE_SOCKET_FIELD_ERROR) && connClassName.equals("oracle.jdbc.driver.T4CConnection")) {
            try {
                if (ORACLE_FIELD_NET == null) {
                    Field field = conClass.getDeclaredField("net");
                    field.setAccessible(true);
                    ORACLE_FIELD_NET = field;
                }
                Object net = ORACLE_FIELD_NET.get(conn);

                if (ORACLE_FIELD_S_ATTS == null) {
                    // NSProtocol
                    Field field = net.getClass().getSuperclass().getDeclaredField("sAtts");
                    field.setAccessible(true);
                    ORACLE_FIELD_S_ATTS = field;
                }

                Object sAtts = ORACLE_FIELD_S_ATTS.get(net);

                if (ORACLE_FIELD_NT == null) {
                    Field field = sAtts.getClass().getDeclaredField("nt");
                    field.setAccessible(true);
                    ORACLE_FIELD_NT = field;
                }

                Object nt = ORACLE_FIELD_NT.get(sAtts);

                if (ORACLE_FIELD_SOCKET == null) {
                    Field field = nt.getClass().getDeclaredField("socket");
                    field.setAccessible(true);
                    ORACLE_FIELD_SOCKET = field;
                }

                socket = (Socket) ORACLE_FIELD_SOCKET.get(nt);
            } catch (Throwable ignored) {
                ORACLE_SOCKET_FIELD_ERROR = true;
                // ignored
            }
        }

        {
            boolean initUnderlyHoldability = !holdabilityUnsupported;
            DbType dbType = DbType.of(dataSource.dbTypeName);
            if (dbType == DbType.sybase //
                    || dbType == DbType.db2 //
                    || dbType == DbType.hive //
                    || dbType == DbType.odps //
            ) {
                initUnderlyHoldability = false;
            }
            if (initUnderlyHoldability) {
                try {
                    this.underlyingHoldability = conn.getHoldability();
                } catch (UnsupportedOperationException e) {
                    holdabilityUnsupported = true;
                    LOG.warn("getHoldability unsupported", e);
                } catch (SQLFeatureNotSupportedException e) {
                    holdabilityUnsupported = true;
                    LOG.warn("getHoldability unsupported", e);
                } catch (SQLException e) {
                    // bug fixed for hive jdbc-driver
                    if ("Method not supported".equals(e.getMessage())) {
                        holdabilityUnsupported = true;
                    }
                    LOG.warn("getHoldability error", e);
                }
            }
        }

        this.underlyingReadOnly = conn.isReadOnly();
        try {
            this.underlyingTransactionIsolation = conn.getTransactionIsolation();
        } catch (SQLException e) {
            // compartible for alibaba corba
            if ("HY000".equals(e.getSQLState())
                    || "com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException".equals(e.getClass().getName())) {
                // skip
            } else {
                throw e;
            }
        }

        this.defaultHoldability = underlyingHoldability;
        this.defaultTransactionIsolation = underlyingTransactionIsolation;
        this.defaultAutoCommit = underlyingAutoCommit;
        this.defaultReadOnly = underlyingReadOnly;
    }

    protected FilterChainImpl createChain() {
        FilterChainImpl chain = this.filterChain;
        if (chain == null) {
            chain = new FilterChainImpl(dataSource);
        } else {
            this.filterChain = null;
        }

        return chain;
    }

    protected void recycleFilterChain(FilterChainImpl chain) {
        chain.reset();
        this.filterChain = chain;
    }

    public long getConnectTimeMillis() {
        return connectTimeMillis;
    }

    public boolean isUnderlyingReadOnly() {
        return underlyingReadOnly;
    }

    public void setUnderlyingReadOnly(boolean underlyingReadOnly) {
        this.underlyingReadOnly = underlyingReadOnly;
    }

    public int getUnderlyingHoldability() {
        return underlyingHoldability;
    }

    public void setUnderlyingHoldability(int underlyingHoldability) {
        this.underlyingHoldability = underlyingHoldability;
    }

    public int getUnderlyingTransactionIsolation() {
        return underlyingTransactionIsolation;
    }

    public void setUnderlyingTransactionIsolation(int underlyingTransactionIsolation) {
        this.underlyingTransactionIsolation = underlyingTransactionIsolation;
    }

    public boolean isUnderlyingAutoCommit() {
        return underlyingAutoCommit;
    }

    public void setUnderlyingAutoCommit(boolean underlyingAutoCommit) {
        this.underlyingAutoCommit = underlyingAutoCommit;
    }

    public long getLastActiveTimeMillis() {
        return lastActiveTimeMillis;
    }

    public void setLastActiveTimeMillis(long lastActiveMillis) {
        this.lastActiveTimeMillis = lastActiveMillis;
    }

    public long getLastExecTimeMillis() {
        return lastExecTimeMillis;
    }

    public void setLastExecTimeMillis(long lastExecTimeMillis) {
        this.lastExecTimeMillis = lastExecTimeMillis;
    }

    public void addTrace(DruidPooledStatement stmt) {
        lock.lock();
        try {
            statementTrace.add(stmt);
        } finally {
            lock.unlock();
        }
    }

    public void removeTrace(DruidPooledStatement stmt) {
        lock.lock();
        try {
            statementTrace.remove(stmt);
        } finally {
            lock.unlock();
        }
    }

    public List<ConnectionEventListener> getConnectionEventListeners() {
        return connectionEventListeners;
    }

    public List<StatementEventListener> getStatementEventListeners() {
        return statementEventListeners;
    }

    public PreparedStatementPool getStatementPool() {
        if (statementPool == null) {
            statementPool = new PreparedStatementPool(this);
        }
        return statementPool;
    }

    public PreparedStatementPool getStatementPoolDirect() {
        return statementPool;
    }

    public void clearStatementCache() {
        if (this.statementPool == null) {
            return;
        }
        this.statementPool.clear();
    }

    public DruidAbstractDataSource getDataSource() {
        return dataSource;
    }

    public boolean isPoolPreparedStatements() {
        return dataSource.isPoolPreparedStatements();
    }

    public Connection getConnection() {
        return conn;
    }

    public long getTimeMillis() {
        return connectTimeMillis;
    }

    public long getUseCount() {
        return useCount;
    }

    public long getConnectionId() {
        return connectionId;
    }

    public void incrementUseCount() {
        useCount++;
    }

    public long getKeepAliveCheckCount() {
        return keepAliveCheckCount;
    }

    public void incrementKeepAliveCheckCount() {
        keepAliveCheckCount++;
    }

    public void reset() throws SQLException {
        // reset default settings
        if (underlyingReadOnly != defaultReadOnly) {
            conn.setReadOnly(defaultReadOnly);
            underlyingReadOnly = defaultReadOnly;
        }

        if (underlyingHoldability != defaultHoldability) {
            conn.setHoldability(defaultHoldability);
            underlyingHoldability = defaultHoldability;
        }

        if (!dataSource.isKeepConnectionUnderlyingTransactionIsolation()
                && underlyingTransactionIsolation != defaultTransactionIsolation) {
            conn.setTransactionIsolation(defaultTransactionIsolation);
            underlyingTransactionIsolation = defaultTransactionIsolation;
        }

        if (underlyingAutoCommit != defaultAutoCommit) {
            conn.setAutoCommit(defaultAutoCommit);
            underlyingAutoCommit = defaultAutoCommit;
        }

        if (!connectionEventListeners.isEmpty()) {
            connectionEventListeners.clear();
        }
        if (!statementEventListeners.isEmpty()) {
            statementEventListeners.clear();
        }

        lock.lock();
        try {
            if (!statementTrace.isEmpty()) {
                Object[] items = statementTrace.toArray();
                for (int i = 0; i < items.length; i++) {
                    Object item = items[i];
                    Statement stmt = (Statement) item;
                    JdbcUtils.close(stmt);
                }

                statementTrace.clear();
            }
        } finally {
            lock.unlock();
        }

        conn.clearWarnings();
    }

    public boolean isDiscard() {
        return discard;
    }

    public void setDiscard(boolean discard) {
        this.discard = discard;
    }

    public long getCreateNanoSpan() {
        return createNanoSpan;
    }

    public long getLastNotEmptyWaitNanos() {
        return lastNotEmptyWaitNanos;
    }

    protected void setLastNotEmptyWaitNanos(long lastNotEmptyWaitNanos) {
        this.lastNotEmptyWaitNanos = lastNotEmptyWaitNanos;
    }

    public String toString() {
        StringBuilder buf = new StringBuilder();

        buf.append("{ID:");
        buf.append(System.identityHashCode(conn));
        buf.append(", ConnectTime:\"");
        buf.append(Utils.toString(new Date(this.connectTimeMillis)));

        buf.append("\", UseCount:");
        buf.append(useCount);

        if (lastActiveTimeMillis > 0) {
            buf.append(", LastActiveTime:\"");
            buf.append(Utils.toString(new Date(lastActiveTimeMillis)));
            buf.append("\"");
        }

        if (lastKeepTimeMillis > 0) {
            buf.append(", LastKeepTimeMillis:\"");
            buf.append(Utils.toString(new Date(lastKeepTimeMillis)));
            buf.append("\"");
        }

        if (statementPool != null && statementPool.getMap().size() > 0) {
            buf.append("\", CachedStatementCount:");
            buf.append(statementPool.getMap().size());
        }

        buf.append("}");

        return buf.toString();
    }

}
