/*
 * 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.stat;

import com.alibaba.druid.Constants;
import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.proxy.jdbc.DataSourceProxy;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.alibaba.druid.util.Histogram;

import javax.management.JMException;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class JdbcDataSourceStat implements JdbcDataSourceStatMBean {
    private static final Log LOG = LogFactory.getLog(JdbcDataSourceStat.class);

    private final String name;
    private final String url;
    private String dbType;

    private final JdbcConnectionStat connectionStat = new JdbcConnectionStat();
    private final JdbcResultSetStat resultSetStat = new JdbcResultSetStat();
    private final JdbcStatementStat statementStat = new JdbcStatementStat();

    private int maxSqlSize = 1000;

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final LinkedHashMap<String, JdbcSqlStat> sqlStatMap;

    private final AtomicLong skipSqlCount = new AtomicLong();

    private final Histogram connectionHoldHistogram = new Histogram(new long[]{ //
            //
            1, 10, 100, 1000, 10 * 1000, //
            100 * 1000, 1000 * 1000
            //
    });

    private final ConcurrentMap<Long, JdbcConnectionStat.Entry> connections = new ConcurrentHashMap<Long, JdbcConnectionStat.Entry>(
            16,
            0.75f,
            1);

    private final AtomicLong clobOpenCount = new AtomicLong();
    private final AtomicLong blobOpenCount = new AtomicLong();
    private final AtomicLong keepAliveCheckCount = new AtomicLong();

    private boolean resetStatEnable = true;

    private static JdbcDataSourceStat global;

    static {
        String dbType = null;
        {
            String property = System.getProperty("druid.globalDbType");
            if (property != null && property.length() > 0) {
                dbType = property;
            }
        }
        global = new JdbcDataSourceStat("Global", "Global", dbType);
    }

    public static JdbcDataSourceStat getGlobal() {
        return global;
    }

    public static void setGlobal(JdbcDataSourceStat value) {
        global = value;
    }

    public void configFromProperties(Properties properties) {
    }

    public boolean isResetStatEnable() {
        return resetStatEnable;
    }

    public void setResetStatEnable(boolean resetStatEnable) {
        this.resetStatEnable = resetStatEnable;
    }

    public JdbcDataSourceStat(String name, String url) {
        this(name, url, null);
    }

    public JdbcDataSourceStat(String name, String url, String dbType) {
        this(name, url, dbType, null);
    }

    @SuppressWarnings("serial")
    public JdbcDataSourceStat(String name, String url, String dbType, Properties connectProperties) {
        this.name = name;
        this.url = url;
        this.dbType = dbType;

        if (connectProperties != null) {
            Object arg = connectProperties.get(Constants.DRUID_STAT_SQL_MAX_SIZE);

            if (arg == null) {
                arg = System.getProperty(Constants.DRUID_STAT_SQL_MAX_SIZE);
            }

            if (arg != null) {
                try {
                    maxSqlSize = Integer.parseInt(arg.toString());
                } catch (NumberFormatException ex) {
                    LOG.error("maxSize parse error", ex);
                }
            }
        }

        sqlStatMap = new LinkedHashMap<String, JdbcSqlStat>(16, 0.75f, false) {
            protected boolean removeEldestEntry(Map.Entry<String, JdbcSqlStat> eldest) {
                boolean remove = (size() > maxSqlSize);

                if (remove) {
                    JdbcSqlStat sqlStat = eldest.getValue();
                    if (sqlStat.getRunningCount() > 0 || sqlStat.getExecuteCount() > 0) {
                        skipSqlCount.incrementAndGet();
                    }
                }

                return remove;
            }
        };
    }

    public int getMaxSqlSize() {
        return this.maxSqlSize;
    }

    public void setMaxSqlSize(int value) {
        if (value == this.maxSqlSize) {
            return;
        }

        lock.writeLock().lock();
        try {
            if (value < this.maxSqlSize) {
                int removeCount = this.maxSqlSize - value;
                Iterator<Map.Entry<String, JdbcSqlStat>> iter = sqlStatMap.entrySet().iterator();
                while (iter.hasNext()) {
                    iter.next();
                    if (removeCount > 0) {
                        iter.remove();
                        removeCount--;
                    } else {
                        break;
                    }
                }
            }
            this.maxSqlSize = value;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public String getDbType() {
        return dbType;
    }

    public void setDbType(String dbType) {
        this.dbType = dbType;
    }

    public long getSkipSqlCount() {
        return skipSqlCount.get();
    }

    public long getSkipSqlCountAndReset() {
        return skipSqlCount.getAndSet(0);
    }

    public void reset() {
        if (!isResetStatEnable()) {
            return;
        }

        blobOpenCount.set(0);
        clobOpenCount.set(0);

        connectionStat.reset();
        statementStat.reset();
        resultSetStat.reset();
        connectionHoldHistogram.reset();
        skipSqlCount.set(0);

        lock.writeLock().lock();
        try {
            Iterator<Map.Entry<String, JdbcSqlStat>> iter = sqlStatMap.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, JdbcSqlStat> entry = iter.next();
                JdbcSqlStat stat = entry.getValue();
                if (stat.getExecuteCount() == 0 && stat.getRunningCount() == 0) {
                    stat.setRemoved(true);
                    iter.remove();
                } else {
                    stat.reset();
                }
            }
        } finally {
            lock.writeLock().unlock();
        }

        for (JdbcConnectionStat.Entry connectionStat : connections.values()) {
            connectionStat.reset();
        }
    }

    public Histogram getConnectionHoldHistogram() {
        return connectionHoldHistogram;
    }

    public JdbcConnectionStat getConnectionStat() {
        return connectionStat;
    }

    public JdbcResultSetStat getResultSetStat() {
        return resultSetStat;
    }

    public JdbcStatementStat getStatementStat() {
        return statementStat;
    }

    @Override
    public String getConnectionUrl() {
        return url;
    }

    @Override
    public TabularData getSqlList() throws JMException {
        Map<String, JdbcSqlStat> sqlStatMap = this.getSqlStatMap();
        CompositeType rowType = JdbcSqlStat.getCompositeType();
        String[] indexNames = rowType.keySet().toArray(new String[rowType.keySet().size()]);

        TabularType tabularType = new TabularType("SqlListStatistic", "SqlListStatistic", rowType, indexNames);
        TabularData data = new TabularDataSupport(tabularType);

        for (Map.Entry<String, JdbcSqlStat> entry : sqlStatMap.entrySet()) {
            data.put(entry.getValue().getCompositeData());
        }

        return data;
    }

    public static StatFilter getStatFilter(DataSourceProxy dataSource) {
        for (Filter filter : dataSource.getProxyFilters()) {
            if (filter instanceof StatFilter) {
                return (StatFilter) filter;
            }
        }

        return null;
    }

    public JdbcSqlStat getSqlStat(int id) {
        return getSqlStat((long) id);
    }

    public JdbcSqlStat getSqlStat(long id) {
        lock.readLock().lock();
        try {
            for (Map.Entry<String, JdbcSqlStat> entry : this.sqlStatMap.entrySet()) {
                if (entry.getValue().getId() == id) {
                    return entry.getValue();
                }
            }

            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

    public final ConcurrentMap<Long, JdbcConnectionStat.Entry> getConnections() {
        return connections;
    }

    @Override
    public TabularData getConnectionList() throws JMException {
        CompositeType rowType = JdbcConnectionStat.Entry.getCompositeType();
        String[] indexNames = rowType.keySet().toArray(new String[rowType.keySet().size()]);

        TabularType tabularType = new TabularType("ConnectionListStatistic", "ConnectionListStatistic", rowType,
                indexNames);
        TabularData data = new TabularDataSupport(tabularType);

        for (Map.Entry<Long, JdbcConnectionStat.Entry> entry : getConnections().entrySet()) {
            data.put(entry.getValue().getCompositeData());
        }

        return data;
    }

    public String getName() {
        return name;
    }

    public String getUrl() {
        return url;
    }

    public Map<String, JdbcSqlStat> getSqlStatMap() {
        Map<String, JdbcSqlStat> map = new LinkedHashMap<String, JdbcSqlStat>(sqlStatMap.size());
        lock.readLock().lock();
        try {
            map.putAll(sqlStatMap);
        } finally {
            lock.readLock().unlock();
        }
        return map;
    }

    public List<JdbcSqlStatValue> getSqlStatMapAndReset() {
        List<JdbcSqlStat> stats = new ArrayList<JdbcSqlStat>(sqlStatMap.size());
        lock.writeLock().lock();
        try {
            Iterator<Map.Entry<String, JdbcSqlStat>> iter = sqlStatMap.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, JdbcSqlStat> entry = iter.next();
                JdbcSqlStat stat = entry.getValue();
                if (stat.getExecuteCount() == 0 && stat.getRunningCount() == 0) {
                    stat.setRemoved(true);
                    iter.remove();
                } else {
                    stats.add(entry.getValue());
                }
            }
        } finally {
            lock.writeLock().unlock();
        }

        List<JdbcSqlStatValue> values = new ArrayList<JdbcSqlStatValue>(stats.size());
        for (JdbcSqlStat stat : stats) {
            JdbcSqlStatValue value = stat.getValueAndReset();
            if (value.getExecuteCount() == 0 && value.getRunningCount() == 0) {
                continue;
            }
            values.add(value);
        }
        return values;
    }

    public List<JdbcSqlStatValue> getRuningSqlList() {
        List<JdbcSqlStat> stats = new ArrayList<JdbcSqlStat>(sqlStatMap.size());
        lock.readLock().lock();
        try {
            for (Map.Entry<String, JdbcSqlStat> entry : sqlStatMap.entrySet()) {
                JdbcSqlStat stat = entry.getValue();
                if (stat.getRunningCount() >= 0) {
                    stats.add(entry.getValue());
                }
            }
        } finally {
            lock.readLock().unlock();
        }

        List<JdbcSqlStatValue> values = new ArrayList<JdbcSqlStatValue>(stats.size());
        for (JdbcSqlStat stat : stats) {
            JdbcSqlStatValue value = stat.getValue(false);
            if (value.getRunningCount() > 0) {
                values.add(value);
            }
        }
        return values;
    }

    public JdbcSqlStat getSqlStat(String sql) {
        lock.readLock().lock();
        try {
            return sqlStatMap.get(sql);
        } finally {
            lock.readLock().unlock();
        }
    }

    public JdbcSqlStat createSqlStat(String sql) {
        lock.writeLock().lock();
        try {
            JdbcSqlStat sqlStat = sqlStatMap.get(sql);
            if (sqlStat == null) {
                sqlStat = new JdbcSqlStat(sql);
                sqlStat.setDbType(this.dbType);
                sqlStat.setName(this.name);
                sqlStatMap.put(sql, sqlStat);
            }

            return sqlStat;
        } finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public long getConnectionActiveCount() {
        return this.connections.size();
    }

    @Override
    public long getConnectionConnectAliveMillis() {
        long nowNano = System.nanoTime();
        long aliveNanoSpan = this.getConnectionStat().getAliveTotal();

        for (JdbcConnectionStat.Entry connection : connections.values()) {
            aliveNanoSpan += nowNano - connection.getEstablishNano();
        }
        return aliveNanoSpan / (1000 * 1000);
    }

    public long getConnectionConnectAliveMillisMax() {
        long max = this.getConnectionStat().getAliveNanoMax();

        long nowNano = System.nanoTime();
        for (JdbcConnectionStat.Entry connection : connections.values()) {
            long connectionAliveNano = nowNano - connection.getEstablishNano();
            if (connectionAliveNano > max) {
                max = connectionAliveNano;
            }
        }
        return max / (1000 * 1000);
    }

    public long getConnectionConnectAliveMillisMin() {
        long min = this.getConnectionStat().getAliveNanoMin();

        long nowNano = System.nanoTime();
        for (JdbcConnectionStat.Entry connection : connections.values()) {
            long connectionAliveNano = nowNano - connection.getEstablishNano();
            if (connectionAliveNano < min || min == 0) {
                min = connectionAliveNano;
            }
        }
        return min / (1000 * 1000);
    }

    public long[] getConnectionHistogramRanges() {
        return connectionStat.getHistogramRanges();
    }

    public long[] getConnectionHistogramValues() {
        return connectionStat.getHistorgramValues();
    }

    public long getClobOpenCount() {
        return clobOpenCount.get();
    }

    public long getClobOpenCountAndReset() {
        return clobOpenCount.getAndSet(0);
    }

    public void incrementClobOpenCount() {
        clobOpenCount.incrementAndGet();
    }

    public long getBlobOpenCount() {
        return blobOpenCount.get();
    }

    public long getBlobOpenCountAndReset() {
        return blobOpenCount.getAndSet(0);
    }

    public void incrementBlobOpenCount() {
        blobOpenCount.incrementAndGet();
    }

    public long getKeepAliveCheckCount() {
        return keepAliveCheckCount.get();
    }

    public long getKeepAliveCheckCountAndReset() {
        return keepAliveCheckCount.getAndSet(0);
    }

    public void addKeepAliveCheckCount(long delta) {
        keepAliveCheckCount.addAndGet(delta);
    }
}
