/*
 * 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.VERSION;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.sql.visitor.SQLEvalVisitorUtils;
import com.alibaba.druid.support.http.stat.WebAppStatManager;
import com.alibaba.druid.support.spring.stat.SpringStatManager;
import com.alibaba.druid.util.DruidDataSourceUtils;
import com.alibaba.druid.util.JdbcSqlStatUtils;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.druid.util.Utils;

import java.sql.Driver;
import java.sql.DriverManager;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 监控相关的对外数据暴露
 * <p>
 * 1. 为了支持jndi数据源本类内部调用druid相关对象均需要反射调用,返回值也应该是Object,List&lt;Object&gt;,Map&lt;String,Object&gt;等无关于druid的类型
 * 2. 对外暴露的public方法都应该先调用init()，应该有更好的方式，暂时没想到
 *
 * @author sandzhang[sandzhangtoo@gmail.com]
 */
public final class DruidStatManagerFacade {
    private static final DruidStatManagerFacade instance = new DruidStatManagerFacade();
    private boolean resetEnable = true;
    private final AtomicLong resetCount = new AtomicLong();

    private DruidStatManagerFacade() {
    }

    public static DruidStatManagerFacade getInstance() {
        return instance;
    }

    private Set<Object> getDruidDataSourceInstances() {
        return DruidDataSourceStatManager.getInstances().keySet();
    }

    public Object getDruidDataSourceByName(String name) {
        for (Object o : this.getDruidDataSourceInstances()) {
            String itemName = DruidDataSourceUtils.getName(o);
            if (StringUtils.equals(name, itemName)) {
                return o;
            }
        }

        return null;
    }

    public void resetDataSourceStat() {
        DruidDataSourceStatManager.getInstance().reset();
    }

    public void resetSqlStat() {
        JdbcStatManager.getInstance().reset();
    }

    public void resetAll() {
        if (!isResetEnable()) {
            return;
        }

        SpringStatManager.getInstance().resetStat();
        WebAppStatManager.getInstance().resetStat();
        resetSqlStat();
        resetDataSourceStat();
        resetCount.incrementAndGet();
    }

    public void logAndResetDataSource() {
        if (!isResetEnable()) {
            return;
        }
        DruidDataSourceStatManager.getInstance().logAndResetDataSource();
    }

    public boolean isResetEnable() {
        return resetEnable;
    }

    public void setResetEnable(boolean resetEnable) {
        this.resetEnable = resetEnable;
    }

    public Object getSqlStatById(Integer id) {
        for (Object ds : getDruidDataSourceInstances()) {
            Object sqlStat = DruidDataSourceUtils.getSqlStat(ds, id);
            if (sqlStat != null) {
                return sqlStat;
            }
        }
        return null;
    }

    public Map<String, Object> getDataSourceStatData(Integer id) {
        if (id == null) {
            return null;
        }

        Object datasource = getDruidDataSourceById(id);
        return datasource == null ? null : dataSourceToMapData(datasource, false);
    }

    public Object getDruidDataSourceById(Integer identity) {
        if (identity == null) {
            return null;
        }

        for (Object datasource : getDruidDataSourceInstances()) {
            if (System.identityHashCode(datasource) == identity) {
                return datasource;
            }
        }
        return null;
    }

    public List<Map<String, Object>> getSqlStatDataList(Integer dataSourceId) {
        Set<Object> dataSources = getDruidDataSourceInstances();

        if (dataSourceId == null) {
            JdbcDataSourceStat globalStat = JdbcDataSourceStat.getGlobal();

            List<Map<String, Object>> sqlList = new ArrayList<Map<String, Object>>();

            DruidDataSource globalStatDataSource = null;
            for (Object datasource : dataSources) {
                if (datasource instanceof DruidDataSource) {
                    if (((DruidDataSource) datasource).getDataSourceStat() == globalStat) {
                        if (globalStatDataSource == null) {
                            globalStatDataSource = (DruidDataSource) datasource;
                        } else {
                            continue;
                        }
                    }
                }
                sqlList.addAll(getSqlStatDataList(datasource));
            }

            return sqlList;
        }

        for (Object datasource : dataSources) {
            if (dataSourceId != null && dataSourceId.intValue() != System.identityHashCode(datasource)) {
                continue;
            }

            return getSqlStatDataList(datasource);
        }

        return new ArrayList<Map<String, Object>>();
    }

    @SuppressWarnings("unchecked")
    public Map<String, Object> getWallStatMap(Integer dataSourceId) {
        Set<Object> dataSources = getDruidDataSourceInstances();

        if (dataSourceId == null) {
            Map<String, Object> map = new HashMap<String, Object>();

            for (Object datasource : dataSources) {
                Map<String, Object> wallStat = DruidDataSourceUtils.getWallStatMap(datasource);
                map = mergeWallStat(map, wallStat);
            }

            return map;
        }

        for (Object datasource : dataSources) {
            if (dataSourceId != null && dataSourceId.intValue() != System.identityHashCode(datasource)) {
                continue;
            }

            return DruidDataSourceUtils.getWallStatMap(datasource);
        }

        return new HashMap<String, Object>();
        //
    }

    /**
     * Merges two wall stat maps into a single map.
     *
     * @param mapA the first wall stat map to be merged
     * @param mapB the second wall stat map to be merged
     * @return a new map that contains the merged wall stats from mapA and mapB
     * @deprecated
     */
    public static Map mergWallStat(Map mapA, Map mapB) {
        return mergeWallStat(mapA, mapB);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public static Map mergeWallStat(Map mapA, Map mapB) {
        if (mapA == null || mapA.size() == 0) {
            return mapB;
        }

        if (mapB == null || mapB.size() == 0) {
            return mapA;
        }

        Map<String, Object> newMap = new LinkedHashMap<String, Object>();
        for (Object item : mapB.entrySet()) {
            Map.Entry entry = (Map.Entry) item;
            String key = (String) entry.getKey();
            Object valueB = entry.getValue();
            Object valueA = mapA.get(key);

            if (valueA == null) {
                newMap.put(key, valueB);
            } else if (valueB == null) {
                newMap.put(key, valueA);
            } else if ("blackList".equals(key)) {
                Map<String, Map<String, Object>> newSet = new HashMap<String, Map<String, Object>>();

                Collection<Map<String, Object>> collectionA = (Collection<Map<String, Object>>) valueA;
                for (Map<String, Object> blackItem : collectionA) {
                    if (newSet.size() >= 1000) {
                        break;
                    }

                    String sql = (String) blackItem.get("sql");
                    Map<String, Object> oldItem = newSet.get(sql);
                    newSet.put(sql, mergeWallStat(oldItem, blackItem));
                }

                Collection<Map<String, Object>> collectionB = (Collection<Map<String, Object>>) valueB;
                for (Map<String, Object> blackItem : collectionB) {
                    if (newSet.size() >= 1000) {
                        break;
                    }

                    String sql = (String) blackItem.get("sql");
                    Map<String, Object> oldItem = newSet.get(sql);
                    newSet.put(sql, mergeWallStat(oldItem, blackItem));
                }
                newMap.put(key, newSet.values());
            } else {
                if (valueA instanceof Map && valueB instanceof Map) {
                    Object newValue = mergeWallStat((Map) valueA, (Map) valueB);
                    newMap.put(key, newValue);
                } else if (valueA instanceof Set && valueB instanceof Set) {
                    Set<Object> set = new HashSet<Object>();
                    set.addAll((Set) valueA);
                    set.addAll((Set) valueB);
                    newMap.put(key, set);
                } else if (valueA instanceof List && valueB instanceof List) {
                    List<Map<String, Object>> mergedList = mergeNamedList((List) valueA, (List) valueB);
                    newMap.put(key, mergedList);
                } else if (valueA instanceof long[] && valueB instanceof long[]) {
                    long[] arrayA = (long[]) valueA;
                    long[] arrayB = (long[]) valueB;

                    int len = arrayA.length >= arrayB.length ? arrayA.length : arrayB.length;
                    long[] sum = new long[len];

                    for (int i = 0; i < sum.length; ++i) {
                        if (i < arrayA.length) {
                            sum[i] += arrayA.length;
                        }
                        if (i < arrayB.length) {
                            sum[i] += arrayB.length;
                        }
                    }
                    newMap.put(key, sum);
                } else if (valueA instanceof String && valueB instanceof String) {
                    newMap.put(key, valueA);
                } else {
                    Object sum = SQLEvalVisitorUtils.add(valueA, valueB);
                    newMap.put(key, sum);
                }
            }
        }

        return newMap;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private static List<Map<String, Object>> mergeNamedList(List listA, List listB) {
        Map<String, Map<String, Object>> mapped = new HashMap<String, Map<String, Object>>();
        for (Object item : (List) listA) {
            Map<String, Object> map = (Map<String, Object>) item;
            String name = (String) map.get("name");
            mapped.put(name, map);
        }

        List<Map<String, Object>> mergedList = new ArrayList<Map<String, Object>>();
        for (Object item : (List) listB) {
            Map<String, Object> mapB = (Map<String, Object>) item;
            String name = (String) mapB.get("name");
            Map<String, Object> mapA = mapped.get(name);

            Map<String, Object> mergedMap = mergeWallStat(mapA, mapB);
            mergedList.add(mergedMap);
        }

        return mergedList;
    }

    public List<Map<String, Object>> getSqlStatDataList(Object datasource) {
        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        Map<?, ?> sqlStatMap = DruidDataSourceUtils.getSqlStatMap(datasource);
        for (Object sqlStat : sqlStatMap.values()) {
            Map<String, Object> data = JdbcSqlStatUtils.getData(sqlStat);

            long executeCount = (Long) data.get("ExecuteCount");
            long runningCount = (Long) data.get("RunningCount");

            if (executeCount == 0 && runningCount == 0) {
                continue;
            }

            result.add(data);
        }

        return result;
    }

    public Map<String, Object> getSqlStatData(Integer id) {
        if (id == null) {
            return null;
        }

        Object sqlStat = getSqlStatById(id);

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

        return JdbcSqlStatUtils.getData(sqlStat);
    }

    public List<Map<String, Object>> getDataSourceStatDataList() {
        return getDataSourceStatDataList(false);
    }

    public List<Map<String, Object>> getDataSourceStatDataList(boolean includeSqlList) {
        List<Map<String, Object>> datasourceList = new ArrayList<Map<String, Object>>();
        for (Object dataSource : getDruidDataSourceInstances()) {
            datasourceList.add(dataSourceToMapData(dataSource, includeSqlList));
        }
        return datasourceList;
    }

    public List<List<String>> getActiveConnStackTraceList() {
        List<List<String>> traceList = new ArrayList<List<String>>();
        for (Object dataSource : getDruidDataSourceInstances()) {
            List<String> stacks = ((DruidDataSource) dataSource).getActiveConnectionStackTrace();
            if (stacks.size() > 0) {
                traceList.add(stacks);
            }
        }
        return traceList;
    }

    public Map<String, Object> returnJSONBasicStat() {
        Map<String, Object> dataMap = new LinkedHashMap<String, Object>();
        dataMap.put("Version", VERSION.getVersionNumber());
        dataMap.put("Drivers", getDriversData());
        dataMap.put("ResetEnable", isResetEnable());
        dataMap.put("ResetCount", getResetCount());
        dataMap.put("JavaVMName", System.getProperty("java.vm.name"));
        dataMap.put("JavaVersion", System.getProperty("java.version"));
        dataMap.put("JavaClassPath", System.getProperty("java.class.path"));
        dataMap.put("StartTime", Utils.getStartTime());
        return dataMap;
    }

    public long getResetCount() {
        return resetCount.get();
    }

    private List<String> getDriversData() {
        List<String> drivers = new ArrayList<String>();
        for (Enumeration<Driver> e = DriverManager.getDrivers(); e.hasMoreElements(); ) {
            Driver driver = e.nextElement();
            drivers.add(driver.getClass().getName());
        }
        return drivers;
    }

    public List<Map<String, Object>> getPoolingConnectionInfoByDataSourceId(Integer id) {
        Object datasource = getDruidDataSourceById(id);

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

        return DruidDataSourceUtils.getPoolingConnectionInfo(datasource);
    }

    public List<String> getActiveConnectionStackTraceByDataSourceId(Integer id) {
        Object datasource = getDruidDataSourceById(id);

        if (datasource == null || !DruidDataSourceUtils.isRemoveAbandoned(datasource)) {
            return null;
        }

        return DruidDataSourceUtils.getActiveConnectionStackTrace(datasource);
    }

    private Map<String, Object> dataSourceToMapData(Object dataSource, boolean includeSql) {
        Map<String, Object> map = DruidDataSourceUtils.getStatData(dataSource);

        if (includeSql) {
            List<Map<String, Object>> sqlList = getSqlStatDataList(dataSource);
            map.put("SQL", sqlList);
        }

        return map;
    }
}
