/*
 * 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.util.Histogram;
import com.alibaba.druid.util.JMXUtils;

import javax.management.JMException;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author wenshao [szujobs@hotmail.com]
 */
public class JdbcConnectionStat implements JdbcConnectionStatMBean {
    private final AtomicInteger activeCount = new AtomicInteger();
    private final AtomicInteger activeCountMax = new AtomicInteger();

    private final AtomicInteger connectingCount = new AtomicInteger();
    private final AtomicInteger connectingMax = new AtomicInteger();

    private final AtomicLong connectCount = new AtomicLong();
    private final AtomicLong connectErrorCount = new AtomicLong();
    private Throwable connectErrorLast;
    private final AtomicLong connectNanoTotal = new AtomicLong(0);                       // 连接建立消耗时间总和（纳秒）
    private final AtomicLong connectNanoMax = new AtomicLong(0);                       // 连接建立消耗最大时间（纳秒）

    private final AtomicLong errorCount = new AtomicLong();

    private final AtomicLong aliveNanoTotal = new AtomicLong();
    private Throwable lastError;
    private long lastErrorTime;

    private long connectLastTime;

    private final AtomicLong closeCount = new AtomicLong(0);                       // 执行Connection.close的计数
    private final AtomicLong transactionStartCount = new AtomicLong(0);
    private final AtomicLong commitCount = new AtomicLong(0);                       // 执行commit的计数
    private final AtomicLong rollbackCount = new AtomicLong(0);                       // 执行rollback的计数

    private final AtomicLong aliveNanoMin = new AtomicLong();
    private final AtomicLong aliveNanoMax = new AtomicLong();

    private final Histogram histogram = new Histogram(TimeUnit.SECONDS, new long[]{ //
            1, 5, 15, 60, 300, 1800});

    public JdbcConnectionStat() {
    }

    public void reset() {
        connectingMax.set(0);
        connectErrorCount.set(0);
        errorCount.set(0);
        aliveNanoTotal.set(0);
        aliveNanoMin.set(0);
        aliveNanoMax.set(0);
        lastError = null;
        lastErrorTime = 0;
        connectLastTime = 0;

        connectCount.set(0);
        closeCount.set(0);
        transactionStartCount.set(0);
        commitCount.set(0);
        rollbackCount.set(0);
        connectNanoTotal.set(0);
        connectNanoMax.set(0);

        histogram.reset();
    }

    public void beforeConnect() {
        int invoking = connectingCount.incrementAndGet();

        for (; ; ) {
            int max = connectingMax.get();
            if (invoking > max) {
                if (connectingMax.compareAndSet(max, invoking)) {
                    break;
                } else {
                    continue;
                }
            } else {
                break;
            }
        }

        connectCount.incrementAndGet();
        connectLastTime = System.currentTimeMillis();
    }

    public void afterConnected(long delta) {
        connectingCount.decrementAndGet();
        connectNanoTotal.addAndGet(delta);
        for (; ; ) {
            // connectNanoMax
            long max = connectNanoMax.get();
            if (delta > max) {
                if (connectNanoMax.compareAndSet(max, delta)) {
                    break;
                } else {
                    continue;
                }
            } else {
                break;
            }
        }

        activeCount.incrementAndGet();
    }

    public long getConnectNanoMax() {
        return this.connectNanoMax.get();
    }

    public long getConnectMillisMax() {
        return this.connectNanoMax.get() / (1000 * 1000);
    }

    public void setActiveCount(int activeCount) {
        this.activeCount.set(activeCount);

        for (; ; ) {
            int max = activeCountMax.get();
            if (activeCount > max) {
                if (activeCountMax.compareAndSet(max, activeCount)) {
                    break;
                } else {
                    continue;
                }
            } else {
                break;
            }
        }
    }

    public int getActiveCount() {
        return activeCount.get();
    }

    public int getAtiveCountMax() {
        return this.activeCount.get();
    }

    public long getErrorCount() {
        return errorCount.get();
    }

    public int getConnectingCount() {
        return connectingCount.get();
    }

    public int getConnectingMax() {
        return connectingMax.get();
    }

    public long getAliveTotal() {
        return aliveNanoTotal.get();
    }

    public long getAliveNanoMin() {
        return aliveNanoMin.get();
    }

    public long getAliveMillisMin() {
        return aliveNanoMin.get() / (1000 * 1000);
    }

    public long getAliveNanoMax() {
        return aliveNanoMax.get();
    }

    public long getAliveMillisMax() {
        return aliveNanoMax.get() / (1000 * 1000);
    }

    public void afterClose(long aliveNano) {
        activeCount.decrementAndGet();
        aliveNanoTotal.addAndGet(aliveNano);

        for (; ; ) {
            long max = aliveNanoMax.get();
            if (aliveNano > max) {
                if (aliveNanoMax.compareAndSet(max, aliveNano)) {
                    break;
                } else {
                    continue;
                }
            } else {
                break;
            }
        }

        for (; ; ) {
            long min = aliveNanoMin.get();
            if (min == 0 || aliveNano < min) {
                if (aliveNanoMin.compareAndSet(min, aliveNano)) {
                    break;
                } else {
                    continue;
                }
            } else {
                break;
            }
        }

        long aliveMillis = aliveNano / (1000 * 1000);
        histogram.record(aliveMillis);
    }

    public Throwable getErrorLast() {
        return lastError;
    }

    public Throwable getConnectErrorLast() {
        return this.connectErrorLast;
    }

    public Date getErrorLastTime() {
        if (lastErrorTime <= 0) {
            return null;
        }

        return new Date(lastErrorTime);
    }

    public void connectError(Throwable error) {
        connectErrorCount.incrementAndGet();
        connectErrorLast = error;

        errorCount.incrementAndGet();
        lastError = error;
        lastErrorTime = System.currentTimeMillis();
    }

    public void error(Throwable error) {
        errorCount.incrementAndGet();
        lastError = error;
        lastErrorTime = System.currentTimeMillis();
    }

    @Override
    public long getCloseCount() {
        return this.closeCount.get();
    }

    @Override
    public long getCommitCount() {
        return this.commitCount.get();
    }

    @Override
    public long getConnectCount() {
        return connectCount.get();
    }

    @Override
    public long getConnectMillis() {
        return connectNanoTotal.get() / (1000 * 1000);
    }

    @Override
    public int getActiveMax() {
        return this.activeCountMax.get();
    }

    @Override
    public long getRollbackCount() {
        return rollbackCount.get();
    }

    @Override
    public long getConnectErrorCount() {
        return connectErrorCount.get();
    }

    @Override
    public Date getConnectLastTime() {
        if (connectLastTime == 0) {
            return null;
        }

        return new Date(connectLastTime);
    }

    public void incrementConnectionCloseCount() {
        closeCount.incrementAndGet();
    }

    public void incrementConnectionCommitCount() {
        commitCount.incrementAndGet();
    }

    public void incrementConnectionRollbackCount() {
        rollbackCount.incrementAndGet();
    }

    public void incrementTransactionStartCount() {
        transactionStartCount.incrementAndGet();
    }

    public long getTransactionStartCount() {
        return transactionStartCount.get();
    }

    public static class Entry implements EntryMBean {
        private long id;
        private long establishTime;
        private long establishNano;
        private Date connectTime;
        private long connectTimespanNano;
        private Exception connectStackTraceException;

        private String lastSql;
        private Exception lastStatementStatckTraceException;
        protected Throwable lastError;
        protected long lastErrorTime;
        private final String dataSource;

        public Entry(String dataSource, long connectionId) {
            this.id = connectionId;
            this.dataSource = dataSource;
        }

        public void reset() {
            this.lastSql = null;
            this.lastStatementStatckTraceException = null;
            this.lastError = null;
            this.lastErrorTime = 0;
        }

        public Date getEstablishTime() {
            if (establishTime <= 0) {
                return null;
            }
            return new Date(establishTime);
        }

        public void setEstablishTime(long establishTime) {
            this.establishTime = establishTime;
        }

        public long getEstablishNano() {
            return establishNano;
        }

        public void setEstablishNano(long establishNano) {
            this.establishNano = establishNano;
        }

        public Date getConnectTime() {
            return connectTime;
        }

        public void setConnectTime(Date connectTime) {
            this.connectTime = connectTime;
        }

        public long getConnectTimespanNano() {
            return connectTimespanNano;
        }

        public void setConnectTimespanNano(long connectTimespanNano) {
            this.connectTimespanNano = connectTimespanNano;
        }

        public String getLastSql() {
            return lastSql;
        }

        public void setLastSql(String lastSql) {
            this.lastSql = lastSql;
        }

        public String getConnectStackTrace() {
            if (connectStackTraceException == null) {
                return null;
            }

            StringWriter buf = new StringWriter();
            connectStackTraceException.printStackTrace(new PrintWriter(buf));
            return buf.toString();
        }

        public void setConnectStackTrace(Exception connectStackTraceException) {
            this.connectStackTraceException = connectStackTraceException;
        }

        public String getLastStatementStatckTrace() {
            if (lastStatementStatckTraceException == null) {
                return null;
            }

            StringWriter buf = new StringWriter();
            lastStatementStatckTraceException.printStackTrace(new PrintWriter(buf));
            return buf.toString();
        }

        public void setLastStatementStatckTrace(Exception lastStatementStatckTrace) {
            this.lastStatementStatckTraceException = lastStatementStatckTrace;
        }

        public void error(Throwable lastError) {
            this.lastError = lastError;
            this.lastErrorTime = System.currentTimeMillis();
        }

        public Date getLastErrorTime() {
            if (lastErrorTime <= 0) {
                return null;
            }
            return new Date(lastErrorTime);
        }

        private static String[] indexNames = {"ID", "ConnectTime", "ConnectTimespan", "EstablishTime",
                "AliveTimespan", "LastSql", "LastError", "LastErrorTime",
                "ConnectStatckTrace", "LastStatementStackTrace", "DataSource"};
        private static String[] indexDescriptions = indexNames;

        public static CompositeType getCompositeType() throws JMException {
            OpenType<?>[] indexTypes = new OpenType<?>[]{SimpleType.LONG, SimpleType.DATE, SimpleType.LONG,
                    SimpleType.DATE, SimpleType.LONG,

                    SimpleType.STRING, JMXUtils.getThrowableCompositeType(), SimpleType.DATE, SimpleType.STRING,
                    SimpleType.STRING,

                    SimpleType.STRING};

            return new CompositeType("ConnectionStatistic", "Connection Statistic", indexNames, indexDescriptions,
                    indexTypes);
        }

        public String getDataSource() {
            return this.dataSource;
        }

        public CompositeDataSupport getCompositeData() throws JMException {
            Map<String, Object> map = new HashMap<String, Object>();

            map.put("ID", id);
            map.put("ConnectTime", getConnectTime());
            map.put("ConnectTimespan", getConnectTimespanNano() / (1000 * 1000));
            map.put("EstablishTime", getEstablishTime());
            map.put("AliveTimespan", (System.nanoTime() - getEstablishNano()) / (1000 * 1000));

            map.put("LastSql", getLastSql());
            map.put("LastError", JMXUtils.getErrorCompositeData(this.lastError));
            map.put("LastErrorTime", getLastErrorTime());
            map.put("ConnectStatckTrace", getConnectStackTrace());
            map.put("LastStatementStackTrace", getLastStatementStatckTrace());

            map.put("DataSource", this.getDataSource());

            return new CompositeDataSupport(getCompositeType(), map);
        }
    }

    public interface EntryMBean {
        Date getEstablishTime();

        long getEstablishNano();

        Date getConnectTime();

        long getConnectTimespanNano();

        String getLastSql();

        String getConnectStackTrace();

        String getLastStatementStatckTrace();

        Date getLastErrorTime();

        void reset();
    }

    public long[] getHistorgramValues() {
        return this.histogram.toArray();
    }

    public long[] getHistogramRanges() {
        return this.histogram.getRanges();
    }
}
