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

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidAbstractDataSource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.WrapperAdapter;
import com.alibaba.druid.pool.ha.node.FileNodeListener;
import com.alibaba.druid.pool.ha.node.NodeListener;
import com.alibaba.druid.pool.ha.node.PoolUpdater;
import com.alibaba.druid.pool.ha.selector.DataSourceSelector;
import com.alibaba.druid.pool.ha.selector.DataSourceSelectorEnum;
import com.alibaba.druid.pool.ha.selector.DataSourceSelectorFactory;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;

import javax.sql.DataSource;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;

/**
 * DataSource class which contains multiple DataSource objects.
 *
 * @author DigitalSonic
 */
public class HighAvailableDataSource extends WrapperAdapter implements DataSource {
    private static final Log LOG = LogFactory.getLog(HighAvailableDataSource.class);
    private static final String DEFAULT_DATA_SOURCE_FILE = "ha-datasource.properties";

    // Properties copied from DruidAbstractDataSource BEGIN
    private String driverClassName;
    private Properties connectProperties = new Properties();
    private String connectionProperties;

    private int initialSize = DruidAbstractDataSource.DEFAULT_INITIAL_SIZE;
    private int maxActive = DruidAbstractDataSource.DEFAULT_MAX_ACTIVE_SIZE;
    private int minIdle = DruidAbstractDataSource.DEFAULT_MIN_IDLE;
    private long maxWait = DruidAbstractDataSource.DEFAULT_MAX_WAIT;

    private String validationQuery = DruidAbstractDataSource.DEFAULT_VALIDATION_QUERY;
    private int validationQueryTimeout = -1;
    private boolean testOnBorrow = DruidAbstractDataSource.DEFAULT_TEST_ON_BORROW;
    private boolean testOnReturn = DruidAbstractDataSource.DEFAULT_TEST_ON_RETURN;
    private boolean testWhileIdle = DruidAbstractDataSource.DEFAULT_WHILE_IDLE;

    private boolean poolPreparedStatements;
    private boolean sharePreparedStatements;
    private int maxPoolPreparedStatementPerConnectionSize = 10;

    private int queryTimeout;
    private int transactionQueryTimeout;

    private long timeBetweenEvictionRunsMillis = DruidAbstractDataSource.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
    private long minEvictableIdleTimeMillis = DruidAbstractDataSource.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
    private long maxEvictableIdleTimeMillis = DruidAbstractDataSource.DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS;
    private long phyTimeoutMillis = DruidAbstractDataSource.DEFAULT_PHY_TIMEOUT_MILLIS;
    private long timeBetweenConnectErrorMillis = DruidAbstractDataSource.DEFAULT_TIME_BETWEEN_CONNECT_ERROR_MILLIS;

    private boolean removeAbandoned;
    private long removeAbandonedTimeoutMillis = 300 * 1000;
    private boolean logAbandoned;

    private String filters;
    private List<Filter> proxyFilters = new CopyOnWriteArrayList<Filter>();
    private PrintWriter logWriter = new PrintWriter(System.out);
    // Properties copied from DruidAbstractDataSource END

    private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<String, DataSource>();
    private Set<String> blacklist = new CopyOnWriteArraySet<String>();
    private DataSourceSelector selector;
    private String dataSourceFile = DEFAULT_DATA_SOURCE_FILE;
    private String propertyPrefix = "";
    private int poolPurgeIntervalSeconds = PoolUpdater.DEFAULT_INTERVAL;
    private boolean allowEmptyPoolWhenUpdate;

    private volatile boolean inited;

    private PoolUpdater poolUpdater = new PoolUpdater(this);
    private NodeListener nodeListener;

    public void init() {
        if (inited) {
            return;
        }
        synchronized (this) {
            if (inited) {
                return;
            }
            if (dataSourceMap == null || dataSourceMap.isEmpty()) {
                poolUpdater.setIntervalSeconds(poolPurgeIntervalSeconds);
                poolUpdater.setAllowEmptyPool(allowEmptyPoolWhenUpdate);
                poolUpdater.init();
                createNodeMap();
            }
            if (selector == null) {
                setSelector(DataSourceSelectorEnum.RANDOM.getName());
            }
            if (dataSourceMap == null || dataSourceMap.isEmpty()) {
                LOG.warn("There is NO DataSource available!!! Please check your configuration.");
            }
            inited = true;
        }
    }

    public void close() {
        destroy();
    }

    public void destroy() {
        if (nodeListener != null) {
            nodeListener.destroy();
        }
        if (poolUpdater != null) {
            poolUpdater.destroy();
        }
        if (selector != null) {
            selector.destroy();
        }
        if (dataSourceMap == null || dataSourceMap.isEmpty()) {
            return;
        }
        for (DataSource dataSource : dataSourceMap.values()) {
            if (dataSource instanceof DruidDataSource) {
                ((DruidDataSource) dataSource).close();
            }
        }
    }

    public void setTargetDataSource(String targetName) {
        selector.setTarget(targetName);
    }

    @Override
    public Connection getConnection() throws SQLException {
        init();
        DataSource dataSource = selector.get();
        if (dataSource == null) {
            LOG.warn("Can NOT obtain DataSource, return null.");
            return null;
        }
        return dataSource.getConnection();
    }

    public String getDataSourceFile() {
        return dataSourceFile;
    }

    public void setDataSourceFile(String dataSourceFile) {
        this.dataSourceFile = dataSourceFile;
    }

    public String getPropertyPrefix() {
        return propertyPrefix;
    }

    public void setPropertyPrefix(String propertyPrefix) {
        this.propertyPrefix = propertyPrefix;
    }

    public void setDataSourceMap(Map<String, DataSource> dataSourceMap) {
        if (dataSourceMap != null) {
            this.dataSourceMap = dataSourceMap;
        }
    }

    public Map<String, DataSource> getDataSourceMap() {
        return dataSourceMap;
    }

    public Map<String, DataSource> getAvailableDataSourceMap() {
        Map<String, DataSource> map = new ConcurrentHashMap<String, DataSource>(this.dataSourceMap);
        for (String n : blacklist) {
            map.remove(n);
        }
        return map;
    }

    public void addBlackList(String name) {
        if (dataSourceMap.containsKey(name)) {
            blacklist.add(name);
        } else {
            LOG.info("Key " + name + " is NOT existed, ignore it.");
        }
    }

    public void removeBlackList(String name) {
        blacklist.remove(name);
    }

    public boolean isInBlackList(String name) {
        return blacklist.contains(name);
    }

    public void setSelector(String name) {
        DataSourceSelector selector = DataSourceSelectorFactory.getSelector(name, this);
        if (selector != null) {
            selector.init();
            setDataSourceSelector(selector);
        }
    }

    public String getSelector() {
        return selector == null ? null : selector.getName();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        throw new UnsupportedOperationException("Not supported by HighAvailableDataSource.");
    }

    @Override
    public void setLoginTimeout(int seconds) {
        DriverManager.setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() {
        return DriverManager.getLoginTimeout();
    }

    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }

    public void setConnectionProperties(String connectionProperties) {
        this.connectionProperties = connectionProperties;

        // COPIED FROM DruidAbstractDataSource.setConnectionProperties()

        if (connectionProperties == null || connectionProperties.trim().length() == 0) {
            setConnectProperties(null);
            return;
        }

        String[] entries = connectionProperties.split(";");
        Properties properties = new Properties();
        for (int i = 0; i < entries.length; i++) {
            String entry = entries[i];
            if (entry.length() > 0) {
                int index = entry.indexOf('=');
                if (index > 0) {
                    String name = entry.substring(0, index);
                    String value = entry.substring(index + 1);
                    properties.setProperty(name, value);
                } else {
                    // no value is empty string which is how java.util.Properties works
                    properties.setProperty(entry, "");
                }
            }
        }

        setConnectProperties(properties);
    }

    public void setConnectProperties(Properties connectProperties) {
        if (connectProperties == null) {
            connectProperties = new Properties();
        }

        if (this.connectProperties != null) {
            this.connectProperties.putAll(connectProperties);
        } else {
            this.connectProperties = connectProperties;
        }
    }

    private void createNodeMap() {
        if (nodeListener == null) {
            // Compatible with the old version.
            // Create a FileNodeListener to watch the dataSourceFile.
            FileNodeListener listener = new FileNodeListener();
            listener.setFile(dataSourceFile);
            listener.setPrefix(propertyPrefix);
            nodeListener = listener;
        }
        nodeListener.setObserver(poolUpdater);
        nodeListener.init();
        nodeListener.update(); // Do update in the current Thread at the startup
    }

    public boolean isAllowEmptyPoolWhenUpdate() {
        return allowEmptyPoolWhenUpdate;
    }

    public void setAllowEmptyPoolWhenUpdate(boolean allowEmptyPoolWhenUpdate) {
        this.allowEmptyPoolWhenUpdate = allowEmptyPoolWhenUpdate;
    }

    // Getters & Setters

    public int getPoolPurgeIntervalSeconds() {
        return poolPurgeIntervalSeconds;
    }

    public void setPoolPurgeIntervalSeconds(int poolPurgeIntervalSeconds) {
        this.poolPurgeIntervalSeconds = poolPurgeIntervalSeconds;
    }

    public NodeListener getNodeListener() {
        return nodeListener;
    }

    public void setNodeListener(NodeListener nodeListener) {
        this.nodeListener = nodeListener;
    }

    public DataSourceSelector getDataSourceSelector() {
        return this.selector;
    }

    public void setDataSourceSelector(DataSourceSelector dataSourceSelector) {
        this.selector = dataSourceSelector;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return logWriter;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        this.logWriter = out;
    }

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public Properties getConnectProperties() {
        return connectProperties;
    }

    public int getInitialSize() {
        return initialSize;
    }

    public void setInitialSize(int initialSize) {
        this.initialSize = initialSize;
    }

    public int getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(int maxActive) {
        this.maxActive = maxActive;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }

    public long getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(long maxWait) {
        this.maxWait = maxWait;
    }

    public String getValidationQuery() {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery) {
        this.validationQuery = validationQuery;
    }

    public int getValidationQueryTimeout() {
        return validationQueryTimeout;
    }

    public void setValidationQueryTimeout(int validationQueryTimeout) {
        this.validationQueryTimeout = validationQueryTimeout;
    }

    public boolean isTestOnBorrow() {
        return testOnBorrow;
    }

    public void setTestOnBorrow(boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }

    public boolean isTestOnReturn() {
        return testOnReturn;
    }

    public void setTestOnReturn(boolean testOnReturn) {
        this.testOnReturn = testOnReturn;
    }

    public boolean isTestWhileIdle() {
        return testWhileIdle;
    }

    public void setTestWhileIdle(boolean testWhileIdle) {
        this.testWhileIdle = testWhileIdle;
    }

    public boolean isPoolPreparedStatements() {
        return poolPreparedStatements;
    }

    public void setPoolPreparedStatements(boolean poolPreparedStatements) {
        this.poolPreparedStatements = poolPreparedStatements;
    }

    public boolean isSharePreparedStatements() {
        return sharePreparedStatements;
    }

    public void setSharePreparedStatements(boolean sharePreparedStatements) {
        this.sharePreparedStatements = sharePreparedStatements;
    }

    public int getMaxPoolPreparedStatementPerConnectionSize() {
        return maxPoolPreparedStatementPerConnectionSize;
    }

    public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
        this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
    }

    public int getQueryTimeout() {
        return queryTimeout;
    }

    public void setQueryTimeout(int queryTimeout) {
        this.queryTimeout = queryTimeout;
    }

    public int getTransactionQueryTimeout() {
        return transactionQueryTimeout;
    }

    public void setTransactionQueryTimeout(int transactionQueryTimeout) {
        this.transactionQueryTimeout = transactionQueryTimeout;
    }

    public long getTimeBetweenEvictionRunsMillis() {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public long getMinEvictableIdleTimeMillis() {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public long getMaxEvictableIdleTimeMillis() {
        return maxEvictableIdleTimeMillis;
    }

    public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
        this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
    }

    public long getPhyTimeoutMillis() {
        return phyTimeoutMillis;
    }

    public void setPhyTimeoutMillis(long phyTimeoutMillis) {
        this.phyTimeoutMillis = phyTimeoutMillis;
    }

    public long getTimeBetweenConnectErrorMillis() {
        return timeBetweenConnectErrorMillis;
    }

    public void setTimeBetweenConnectErrorMillis(long timeBetweenConnectErrorMillis) {
        this.timeBetweenConnectErrorMillis = timeBetweenConnectErrorMillis;
    }

    public boolean isRemoveAbandoned() {
        return removeAbandoned;
    }

    public void setRemoveAbandoned(boolean removeAbandoned) {
        this.removeAbandoned = removeAbandoned;
    }

    public long getRemoveAbandonedTimeoutMillis() {
        return removeAbandonedTimeoutMillis;
    }

    public void setRemoveAbandonedTimeoutMillis(long removeAbandonedTimeoutMillis) {
        this.removeAbandonedTimeoutMillis = removeAbandonedTimeoutMillis;
    }

    public boolean isLogAbandoned() {
        return logAbandoned;
    }

    public void setLogAbandoned(boolean logAbandoned) {
        this.logAbandoned = logAbandoned;
    }

    public String getConnectionProperties() {
        return connectionProperties;
    }

    public String getFilters() {
        return filters;
    }

    public void setFilters(String filters) {
        this.filters = filters;
    }

    public List<Filter> getProxyFilters() {
        return proxyFilters;
    }

    public void setProxyFilters(List<Filter> proxyFilters) {
        this.proxyFilters = proxyFilters;
    }
}
