package com.github.trang.redisson.autoconfigure;

import static java.util.Collections.emptyList;

import java.util.List;
import java.util.Map;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.spring.cache.CacheConfig;
import org.redisson.spring.cache.RedissonSpringCacheManager;
import org.redisson.spring.transaction.RedissonTransactionManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheAspectSupport;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.github.trang.autoconfigure.Customizer;
import com.github.trang.redisson.autoconfigure.RedissonSpringProperties.RedissonCacheManagerProperties;

import lombok.extern.slf4j.Slf4j;

/**
 * Redisson Spring 自动配置
 *
 * @author trang
 */
@Configuration
@ConditionalOnClass(Redisson.class)
@ConditionalOnBean(RedissonClient.class)
@AutoConfigureAfter({CacheAutoConfiguration.class, TransactionAutoConfiguration.class})
@EnableConfigurationProperties(RedissonSpringProperties.class)
@Slf4j
public class RedissonSpringAutoConfiguration {

    private RedissonSpringProperties redissonSpringProperties;
    private List<Customizer<RedissonSpringCacheManager>> redissonSpringCacheManagerCustomizers;

    public RedissonSpringAutoConfiguration(RedissonSpringProperties redissonSpringProperties,
                                           ObjectProvider<List<Customizer<RedissonSpringCacheManager>>> customizersProvider) {
        this.redissonSpringProperties = redissonSpringProperties;
        this.redissonSpringCacheManagerCustomizers = customizersProvider.getIfAvailable();
        this.redissonSpringCacheManagerCustomizers = redissonSpringCacheManagerCustomizers != null
                ? redissonSpringCacheManagerCustomizers : emptyList();
    }

    /**
     * 声明 RedissonSpringCacheManager
     *
     * 由于 #{@link CacheAutoConfiguration} 的加载顺序在本类之前,并且其默认会注册一个 #{@link org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration},
     * 所以这里将 beanName 设置为 'cacheManager',目的是为了覆盖掉默认的 cacheManager(Spring 有一种机制来保证 bean 的优先级,详情请查看
     * #{@link DefaultListableBeanFactory#registerBeanDefinition(String, BeanDefinition)})
     *
     * 为什么不先加载本类呢?因为 CacheAutoConfiguration 中有一些功能是我们需要的,如果先加载本类,那么 RedissonSpringCacheManager注册成功后,
     * CacheAutoConfiguration 将不会加载,因为其加载条件是不存在 CacheManager
     *
     * @param redisson redisson 客户端
     * @return RedissonSpringCacheManager cacheManager
     */
    @Bean
    @ConditionalOnClass(CacheManager.class)
    @ConditionalOnBean(CacheAspectSupport.class)
    @ConditionalOnMissingBean(RedissonSpringCacheManager.class)
    @ConditionalOnProperty(prefix = "spring.redisson.cache-manager", name = "enabled", havingValue = "true", matchIfMissing = true)
    public RedissonSpringCacheManager cacheManager(RedissonClient redisson) {
        log.info("redisson cache-manager init...");
        RedissonCacheManagerProperties redissonCacheManagerProperties = redissonSpringProperties.getCacheManager();
        // 获取 ConfigMap
        // CacheConfig:
        //   ttl         过期时间,key 写入一定时间后删除,相当于 GuavaCache 的 expireAfterWrite
        //   maxIdleTime 最大空闲时间,key 一定时间内没有被访问后删除,相当于 GuavaCache 的 expireAfterAccess
        //   maxIdleTime 最大数量,达到一定数量后删除一部分 key,基于 LRU 算法
        Map<String, CacheConfig> config = redissonCacheManagerProperties.getConfigs();
        // 创建 CacheManager,ConfigMap 会转换为 Cache
        RedissonSpringCacheManager redissonSpringCacheManager = new RedissonSpringCacheManager(redisson, config);
        // RedissonSpringCacheManager 中的 dynamic 属性默认为 true,即获取不存在的 Cache 时,Redisson 创建一个永不过期的 Cache 以供使用
        // 个人认为这样不合理,会导致滥用缓存,所以 starter 中 dynamic 的默认值为 false,当获取不存在的 Cache 时会抛出异常
        // 当然,你也可以手动开启 dynamic 功能
        if (!redissonCacheManagerProperties.isDynamic()) {
            redissonSpringCacheManager.setCacheNames(redissonCacheManagerProperties.getConfigs().keySet());
        }
        if (redissonCacheManagerProperties.getCodec() != null) {
            redissonSpringCacheManager.setCodec(redissonCacheManagerProperties.getCodec().getInstance());
        }
        if (redissonCacheManagerProperties.getConfigLocation() != null && !redissonCacheManagerProperties.getConfigLocation().isEmpty()) {
            redissonSpringCacheManager.setConfigLocation(redissonCacheManagerProperties.getConfigLocation());
        }
        redissonSpringCacheManager.setAllowNullValues(redissonCacheManagerProperties.isAllowNullValues());
        // 用户自定义配置,拥有最高优先级
        redissonSpringCacheManagerCustomizers.forEach(customizer -> customizer.customize(redissonSpringCacheManager));
        return redissonSpringCacheManager;
    }

    /**
     * 声明 CompositeCacheManager
     *
     * 1. 因为上面已经有 RedissonSpringCacheManager 了,所以这里有 @Primary 修饰
     * 2. 注入 RedissonSpringCacheManager 而不是 CacheManager 是因为该方法只为 RedissonSpringCacheManager 服务 :)
     * 3. 当声明 RedissonSpringCacheManager 的 beanName 为 'cacheManager' 时,@ConditionalOnBean 的条件
     * 'value=RedissonSpringCacheManager.class' 并不能生效,但是别的 beanName 是没问题的,具体原因待查。
     * 另外根据实验得出 spring-boot 1.x 中各条件之间是或的关系,而 spring boot 2.x 中是且的关系。
     *
     * @return CompositeCacheManager cacheManager
     */
    @Bean
    @Primary
    @ConditionalOnBean(name = "cacheManager")
    @ConditionalOnMissingBean(CompositeCacheManager.class)
    @ConditionalOnProperty(prefix = "spring.redisson.cache-manager", name = {"enabled", "fallback-to-no-op-cache"},
            havingValue = "true", matchIfMissing = true)
    public CompositeCacheManager compositeCacheManager(RedissonSpringCacheManager cacheManager) {
        log.info("composite cache-manager init...");
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager(cacheManager);
        // 设置 NoOpCacheManager,当获取不存在的 Cache 时不会抛出异常,而是穿透缓存
        compositeCacheManager.setFallbackToNoOpCache(true);
        return compositeCacheManager;
    }

    /**
     * 声明 RedissonTransactionManager
     *
     * 加载顺序放到了 #{@link TransactionAutoConfiguration} 之后,避免影响
     * #{@link DataSourceTransactionManagerAutoConfiguration.DataSourceTransactionManagerConfiguration} 的加载
     *
     * @param redisson redisson 客户端
     * @return RedissonTransactionManager redissonTransactionManager
     */
    @Bean
    @ConditionalOnMissingBean(RedissonTransactionManager.class)
    @ConditionalOnClass(name = "org.springframework.transaction.PlatformTransactionManager")
    @ConditionalOnProperty(prefix = "spring.redisson.transaction", name = "enabled", havingValue = "true")
    public RedissonTransactionManager redissonTransactionManager(RedissonClient redisson) {
        return new RedissonTransactionManager(redisson);
    }

}