package org.onetwo.boot.module.redis;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.onetwo.common.exception.BaseException;
import org.onetwo.common.log.JFishLoggerFactory;
import org.onetwo.common.spring.SpringUtils;
import org.onetwo.common.utils.LangUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.integration.redis.util.RedisLockRegistry;

import lombok.Setter;

/**
 * @author wayshall
 * <br/>
 */
public class SimpleRedisOperationService implements InitializingBean, RedisOperationService {
	public static final String LOCK_KEY = "LOCKER:";

	protected final Logger logger = JFishLoggerFactory.getLogger(this.getClass());
	
	@Autowired
	private JedisConnectionFactory jedisConnectionFactory;
	private RedisTemplate<Object, Object> redisTemplate;
    private StringRedisTemplate stringRedisTemplate;
	@Autowired
	private RedisLockRegistry redisLockRegistry;
	private String cacheKeyPrefix = DEFAUTL_CACHE_PREFIX;
	private String lockerKey = LOCK_KEY;
	@Setter
    private long waitLockInSeconds = 60;
	@Setter
    private Map<String, Long> expires;
	@Autowired(required=false)
	private RedisCacheManager redisCacheManager;
	final private CacheStatis cacheStatis = new CacheStatis();
    
    @SuppressWarnings("unchecked")
	@Override
	public void afterPropertiesSet() throws Exception {
		redisTemplate = createReidsTemplate(jedisConnectionFactory);
		
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(jedisConnectionFactory);
		template.afterPropertiesSet();
		this.stringRedisTemplate = template;
		
		if (redisCacheManager!=null) {
			expires = (Map<String, Long>) SpringUtils.newPropertyAccessor(redisCacheManager, true).getPropertyValue("expires");
		} else {
			expires = Collections.emptyMap();
		}
	}
    
    protected RedisTemplate<Object, Object> createReidsTemplate(JedisConnectionFactory jedisConnectionFactory) {
    	JsonRedisTemplate redisTemplate = new JsonRedisTemplate();
		redisTemplate.setConnectionFactory(jedisConnectionFactory);
		redisTemplate.afterPropertiesSet();
		return redisTemplate;
    }

	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getRedisLockRunnerByKey(java.lang.String)
	 */
	@Override
	public RedisLockRunner getRedisLockRunnerByKey(String key){
		RedisLockRunner redisLockRunner = RedisLockRunner.builder()
														 .lockKey(lockerKey+key)
														 .time(waitLockInSeconds)
														 .unit(TimeUnit.SECONDS)
														 .errorHandler(e->{
															 throw new BaseException("no error hanlder!", e);
														 })
														 .redisLockRegistry(redisLockRegistry)
														 .build();
		return redisLockRunner;
	}
	
	protected String getCacheKey(String key) {
		return getCacheKeyPrefix() + key;
	}
	
	protected BoundValueOperations<Object, Object> boundValueOperations(String key) {
		return this.redisTemplate.boundValueOps(key);
	}
	

	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getCacheIfPresent(java.lang.String, java.lang.Class)
	 */
	@Override
	public <T> Optional<T> getCacheIfPresent(String key, Class<T> clazz) {
		T value = getCache(key, null);
		return Optional.ofNullable(clazz.cast(value));
	}
	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getCache(java.lang.String, java.util.function.Supplier)
	 */
	@Override
	public <T> T getCache(String key, Supplier<CacheData<T>> cacheLoader) {
		String cacheKey = getCacheKey(key);
		BoundValueOperations<Object, Object> ops = boundValueOperations(cacheKey);
		Object value = ops.get();
		if (value==null && cacheLoader!=null) {
			CacheData<T> cacheData = getCacheData(ops, cacheKey, cacheLoader);
			if (cacheData==null) {
				throw new BaseException("can not load cache data.").put("key", key);
			}
			value = cacheData.getValue();
		} else {
			cacheStatis.addHit(1);
		}
		return (T)value;
	}
	
	
	protected <T> CacheData<T> getCacheData(BoundValueOperations<Object, Object> ops, String cacheKey, Supplier<CacheData<T>> cacheLoader) {
		return this.getRedisLockRunnerByKey(cacheKey).tryLock(() -> {
			//double check...
			Object value = ops.get();
			if (value!=null) {
				cacheStatis.addHit(1);
				return CacheData.<T>builder().value((T)value).build();
			}
			cacheStatis.addMiss(1);
			CacheData<T> cacheData = cacheLoader.get();
			if(logger.isInfoEnabled()){
				logger.info("run cacheLoader for key: {}", cacheKey);
			}
			if (cacheData.getExpire()!=null && cacheData.getExpire()>0) {
//				ops.expire(cacheData.getExpire(), cacheData.getTimeUnit());
				ops.set(cacheData.getValue(), cacheData.getExpire(), cacheData.getTimeUnit());
			} else if (this.expires.containsKey(cacheKey)) {
				Long expireInSeconds = this.expires.get(cacheKey);
				ops.set(cacheData.getValue(), expireInSeconds, TimeUnit.SECONDS);
			} else {
				ops.set(cacheData.getValue());
			}
			
			return cacheData;
		}/*, ()->{
			//如果锁定失败,则休息1秒,然后递归……
			int retryLockInSeconds = 1;
			if(logger.isWarnEnabled()){
				logger.warn("obtain redis lock error, sleep {} seconds and retry...", retryLockInSeconds);
			}
			LangUtils.await(retryLockInSeconds);
			return getCacheData(ops, cacheKey, cacheLoader);
		}*/);
	}
	

	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getAndDel(java.lang.String)
	 */
	@Override
	@SuppressWarnings("unchecked")
	public String getAndDelString(String key){
//    	ValueOperations<String, Object> ops = redisTemplate.opsForValue();
		final String cacheKey = getCacheKey(key);
		RedisSerializer<String> stringSerializer = (RedisSerializer<String>)stringRedisTemplate.getKeySerializer();
//		RedisSerializer<Object> keySerializer = (RedisSerializer<Object>)redisTemplate.getKeySerializer();
    	String value = stringRedisTemplate.execute(new RedisCallback<String>() {

    		public String doInRedis(RedisConnection connection) throws DataAccessException {
    			byte[] rawKey = stringSerializer.serialize(cacheKey);
    			connection.multi();
    			connection.get(rawKey);
    			connection.del(rawKey);
    			List<Object> results = connection.exec();
    			if(LangUtils.isEmpty(results)){
    				return null;
    			}
//    			Object result = results.get(0);
    			byte[] rawValue = (byte[])results.get(0);
    			RedisSerializer<String> valueSerializer = (RedisSerializer<String>)getStringRedisTemplate().getValueSerializer();
    			return valueSerializer.deserialize(rawValue);
    		}
    		
		});
    	return value;
    }
	
	@Override
	public <T> T getAndDel(String key){
//    	ValueOperations<String, Object> ops = redisTemplate.opsForValue();
		final String cacheKey = getCacheKey(key);
//		RedisSerializer<String> stringSerializer = (RedisSerializer<String>)stringRedisTemplate.getKeySerializer();
		RedisSerializer<Object> keySerializer = getKeySerializer();
    	T value = this.redisTemplate.execute(new RedisCallback<T>() {

    		public T doInRedis(RedisConnection connection) throws DataAccessException {
    			byte[] rawKey = keySerializer.serialize(cacheKey);
    			connection.multi();
    			connection.get(rawKey);
    			connection.del(rawKey);
    			List<Object> results = connection.exec();
    			if(LangUtils.isEmpty(results)){
    				return null;
    			}
    			// rawValue
    			byte[] rawValue = (byte[])results.get(0);
    			return (T) getValueSerializer().deserialize(rawValue);
    		}
    		
		});
    	return value;
    }
	
	/****
	 * 如果不存在,则设置
	 * @author weishao zeng
	 * @param key
	 * @param value
	 * @param seconds
	 * @return 设置成功,则返回所设置的值,否则返回已存在的值
	 */
	public boolean setNX(String key, Object value, int seconds) {
		final String cacheKey = getCacheKey(key);
		Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {

			public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
    			byte[] rawKey = getKeySerializer().serialize(cacheKey);
    			byte[] rawValue = getValueSerializer().serialize(value);
    			Boolean setOp = connection.setNX(rawKey, rawValue);
    			if (setOp!=null && setOp) {
    				connection.expire(rawKey, seconds);
    			}/* else {
    				rawValue = connection.get(rawKey);
    			}*/
//    			return (T)getValueSerializer().deserialize(rawValue);
    			return setOp;
    		}
    		
		});
    	return result;
    }
	
	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#clear(java.lang.String)
	 */
	@Override
	public Long clear(String key){
		final String cacheKey = getCacheKey(key);
    	Long value = redisTemplate.execute(new RedisCallback<Long>() {

    		public Long doInRedis(RedisConnection connection) throws DataAccessException {
    			byte[] rawKey = getKeySerializer().serialize(cacheKey);
    			Long delCount = connection.del(rawKey);
    			return delCount;
    		}
    		
		});
    	return value;
    }

	@SuppressWarnings("unchecked")
	protected final RedisSerializer<Object> getKeySerializer() {
		return (RedisSerializer<Object>)redisTemplate.getKeySerializer();
	}
	@SuppressWarnings("unchecked")
	protected final RedisSerializer<Object> getValueSerializer() {
		return (RedisSerializer<Object>)redisTemplate.getValueSerializer();
	}
	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getRedisTemplate()
	 */
	@Override
	public RedisTemplate<Object, Object> getRedisTemplate() {
		return redisTemplate;
	}

	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getStringRedisTemplate()
	 */
	@Override
	public StringRedisTemplate getStringRedisTemplate() {
		return stringRedisTemplate;
	}

	public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
		this.redisTemplate = redisTemplate;
	}

	/* (non-Javadoc)
	 * @see org.onetwo.boot.module.redis.RedisOperationService#getCacheStatis()
	 */
	@Override
	public CacheStatis getCacheStatis() {
		return cacheStatis;
	}

	public void setCacheKeyPrefix(String cacheKeyPrefix) {
		this.cacheKeyPrefix = cacheKeyPrefix;
	}

	public void setLockerKey(String lockerKey) {
		this.lockerKey = lockerKey;
	}

	public String getCacheKeyPrefix() {
		return cacheKeyPrefix;
	}
	
}