package com.mogujie.trade.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.AutowireCandidateQualifier;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * @author by jiuru on 16/7/14.
 */
public class DataSourceScanner implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware {

    private static final String PROPERTY_FILE_NAME = "jdbc.properties";

    private static final String KEY_SEPARATOR = ".";

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private DataSourceFactory<? extends DataSource> dataSourceFactory;

    private ApplicationContext applicationContext;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // do nothing
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        final Map<String, ReadWriteSplittingDataSource> dataSources = new HashMap<>();

        InputStream in = this.getClass().getClassLoader().getResourceAsStream(PROPERTY_FILE_NAME);
        if (in != null) {
            Properties properties = new Properties();
            try {
                properties.load(in);
            } catch (IOException e) {
                throw new BeanInitializationException("read property file error!", e);
            }
            try {
                Map<String, Map<DataSourceType, DataSource>> dataSourcesMapping = this.getDataSources(properties);
                this.registerDataSources(registry, dataSourcesMapping);
                int transcactionManagerCount = 0;
                String transactionManagerBeanName = null;
                for (Map.Entry<String, Map<DataSourceType, DataSource>> entry : dataSourcesMapping.entrySet()) {
                    final String name = entry.getKey();

                    final DataSource masterDataSource = entry.getValue().get(DataSourceType.master);

                    final ReadWriteSplittingDataSource readWriteSplittingDataSource = new ReadWriteSplittingDataSource(
                            entry.getKey(), entry.getValue().get(DataSourceType.master), entry.getValue().get(
                            DataSourceType.slave));
                    logger.info("init dataSource {}", readWriteSplittingDataSource);
                    dataSources.put(name, readWriteSplittingDataSource);

                    // 若无可写的数据源则跳过创建事务管理器
                    if (masterDataSource == null) {
                        continue;
                    }

                    transactionManagerBeanName = name + "TransactionManager";

                    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                    beanDefinition.setBeanClass(RoutingDataSourceTransactionManager.class);

                    MutablePropertyValues propertyValues = new MutablePropertyValues();
                    propertyValues.add("dataSource", readWriteSplittingDataSource);
                    propertyValues.add("name", name);
                    beanDefinition.setPropertyValues(propertyValues);
                    AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(Qualifier.class);
                    qualifier.setAttribute(AutowireCandidateQualifier.VALUE_KEY, name);
                    beanDefinition.addQualifier(qualifier);
                    registry.registerBeanDefinition(transactionManagerBeanName, beanDefinition);

                    PlatformTransactionManager transactionManager = this.applicationContext.getBean(
                            transactionManagerBeanName, PlatformTransactionManager.class);
                    Assert.notNull(transactionManager, "register BeanDefinition of " + transactionManagerBeanName
                            + " error!");
                    transcactionManagerCount++;
                }

                // 兼容只有一个或无TransactionManager的情况
                if (transcactionManagerCount == 1) {// 若只有一个则添加别名,兼容默认情况
                    registry.registerAlias(transactionManagerBeanName, "transcationManager");
                } else if (transcactionManagerCount == 0) {
                    // add an empty transcationManager
                    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                    beanDefinition.setBeanClass(EmptyTransactionManager.class);

                    registry.registerBeanDefinition("transactionManager", beanDefinition);
                }
            } catch (SQLException e) {
                throw new BeanCreationException("initial dataSources error!", e);
            }
        }

        // register dataSourceLookup
        GenericBeanDefinition dataSourceLookupBeanDefinition = new GenericBeanDefinition();
        dataSourceLookupBeanDefinition.setBeanClass(DataSourceLookup.class);
        ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, dataSources);
        dataSourceLookupBeanDefinition.setConstructorArgumentValues(constructorArgumentValues);
        registry.registerBeanDefinition("dataSourceLookup", dataSourceLookupBeanDefinition);
    }

    /**
     * 根据Properties配置解析得到数据源
     *
     * @param properties
     * @return
     * @throws SQLException
     */
    private Map<String, Map<DataSourceType, DataSource>> getDataSources(Properties properties) throws SQLException {
        Map<String, Map<DataSourceType, DataSource>> dataSourcesMapping = new HashMap<>(2);
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            String[] parts = entry.getKey().toString().trim().split("\\" + KEY_SEPARATOR);
            if (parts.length == 3) {
                String name = parts[0];
                if (!dataSourcesMapping.containsKey(name)) {
                    for (DataSourceType dataSourceType : DataSourceType.values()) {

                        DataSource ds = this.dataSourceFactory.getDataSource(this.parseDataSourceConfig(name,
                                dataSourceType, properties));
                        Map<DataSourceType, DataSource> map = dataSourcesMapping.get(name);
                        if (map == null) {
                            map = new EnumMap<DataSourceType, DataSource>(DataSourceType.class);
                            dataSourcesMapping.put(name, map);
                        }
                        DataSource preValue = map.put(dataSourceType, ds);
                        if (preValue != null) {
                            throw new IllegalArgumentException("dupilicated DataSource of" + name + " "
                                    + dataSourceType);
                        }
                    }

                }
            } else {
                // It's illegal, ignore.
            }
        }
        return dataSourcesMapping;
    }

    private DataSourceConfig parseDataSourceConfig(String name, DataSourceType dataSourceType, Properties properties) {
        String keyPrefix = name + KEY_SEPARATOR + dataSourceType + KEY_SEPARATOR;

        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        String url = properties.getProperty(keyPrefix + "url");
        Assert.hasText(url, keyPrefix + "url is empty!");
        dataSourceConfig.setUrl(url);

        String username = properties.getProperty(keyPrefix + "username");
        Assert.hasText(username, keyPrefix + "username is empty!");
        dataSourceConfig.setUsername(username);

        String password = properties.getProperty(keyPrefix + "password");
        Assert.hasText(password, keyPrefix + "password is empty!");
        dataSourceConfig.setPassword(password);

        String initialPoolSizeStr = properties.getProperty(keyPrefix + "initialPoolSize");
        int initialPoolSize = initialPoolSizeStr == null ? DataSourceConfig.DEFAULT_INI_POOL_SIZE : Integer
                .parseInt(initialPoolSizeStr);
        dataSourceConfig.setInitialPoolSize(initialPoolSize);

        String minPoolSizeStr = properties.getProperty(keyPrefix + "minPoolSize");
        int minPoolSize = minPoolSizeStr == null ? DataSourceConfig.DEFAULT_MIN_POOL_SIZE : Integer
                .parseInt(minPoolSizeStr);
        dataSourceConfig.setMinPoolSize(minPoolSize);

        String maxPoolSizeStr = properties.getProperty(keyPrefix + "maxPoolSize");
        int maxPoolSize = maxPoolSizeStr == null ? DataSourceConfig.DEFAULT_MAX_POOL_SIZE : Integer
                .parseInt(maxPoolSizeStr);
        dataSourceConfig.setMaxPoolSize(maxPoolSize);

        return dataSourceConfig;
    }

    /**
     * 将数据源注入到Spring中
     *
     * @param registry
     * @param dataSourcesMapping
     */
    private void registerDataSources(BeanDefinitionRegistry registry,
                                     Map<String, Map<DataSourceType, DataSource>> dataSourcesMapping) {

        for (Map.Entry<String, Map<DataSourceType, DataSource>> entry : dataSourcesMapping.entrySet()) {
            final String name = entry.getKey();
            for (Map.Entry<DataSourceType, DataSource> subEntry : entry.getValue().entrySet()) {
                GenericBeanDefinition dataSourceBeanDefinition = new GenericBeanDefinition();
                dataSourceBeanDefinition.setBeanClass(DataSourceFactoryBean.class);
                ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
                constructorArgumentValues.addIndexedArgumentValue(0, subEntry.getValue());
                dataSourceBeanDefinition.setConstructorArgumentValues(constructorArgumentValues);
                String beanName = name + Character.toUpperCase(subEntry.getKey().name().charAt(0))
                        + subEntry.getKey().name().substring(1) + "DataSource";
                registry.registerBeanDefinition(beanName, dataSourceBeanDefinition);
            }
        }
    }

    // --------------------Setters---------------

    public void setDataSourceFactory(DataSourceFactory<? extends DataSource> dataSourceFactory) {
        this.dataSourceFactory = dataSourceFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static class EmptyTransactionManager implements PlatformTransactionManager {

        @Override
        public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void commit(TransactionStatus status) throws TransactionException {
            throw new UnsupportedOperationException();
        }

        @Override
        public void rollback(TransactionStatus status) throws TransactionException {
            throw new UnsupportedOperationException();
        }

    }

    public static class DataSourceFactoryBean implements FactoryBean<DataSource> {

        private final DataSource dataSource;

        public DataSourceFactoryBean(DataSource dataSource) {
            Assert.notNull(dataSource);
            this.dataSource = dataSource;
        }

        @Override
        public DataSource getObject() throws Exception {
            return this.dataSource;
        }

        @Override
        public Class<?> getObjectType() {
            return this.dataSource.getClass();
        }

        @Override
        public boolean isSingleton() {
            return true;
        }

    }
}