package com.starcharge.base.redis;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis工具包
 * 依赖Spring自带的RedisTemplate,可根据配置更换Redis客户端Lettuce/Jedis
 *
 * @author kevin
 */
@Component
public class RedisClient {

	@Resource
	private RedisTemplate<String, Object> redisTemplate;

	private static final Logger logger = LoggerFactory.getLogger(RedisClient.class);

	private static final String REDIS_LOCK_PREFIX = "tmp:lock:";

	private static final String LOCK_LUA = "if redis.call('get',KEYS[1]) then return 0 else " +
			"if redis.call('set',KEYS[1],ARGV[1]) then if redis.call('expire',KEYS[1],ARGV[2]) " +
			"then return 1 else return 0 end else return 0 end end";

	private static final String RELEASE_LOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

	@SuppressWarnings("unchecked")
	public <T> RedisTemplate<String, T> getRedisTemplate(Class<T> clz) {
		return (RedisTemplate<String, T>) redisTemplate;
	}

	/*********************************key-value*************************************/
	
	public void put(final String key, final Object value) {
		put(key, value, null);
	}

	/**
	 * 移除key后重新放值
	 * 
	 * @param key
	 * @param value
	 */
	public void removePut(final String key, final Object value) {

		remove(key);

		put(key, value, null);
	}

	/**
	 * 塞入string
	 * 
	 * @param key
	 * @param value
	 * @param expire
	 */
	public void put(final String key, final Object value, final Integer expire) {
		redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
	}

	public <T> T get(final String key, Class<T> clz) {
		return get(key, clz, null);
	}

	public String get(final String key) {
		return get(key, String.class, null);
	}

	@SuppressWarnings("unchecked")
	public <T> T get(final String key, Class<T> clz, final Integer expire) {

		if (expire != null) {
			redisTemplate.expire(key, expire, TimeUnit.SECONDS);
		}

		return (T) redisTemplate.opsForValue().get(key);
	}
	
	@SuppressWarnings("unchecked")
	public <T> List<T> multiGet(final List<String> keys, Class<T> clz) {
		return (List<T>)redisTemplate.opsForValue().multiGet(keys);
	}
	
	/*********************************key-hash*************************************/
	
	public void putHash(final String key, final Object hashKey, final Object value) {
		putHash(key, hashKey, value, null);
	}
	
	public void putHash(final String key, final Object hashKey, final Object value, final Integer expire) {
		redisTemplate.opsForHash().put(key, hashKey, value);

		if (expire != null) {
			redisTemplate.expire(key, expire, TimeUnit.SECONDS);
		}
	}
	
	public void putHash(final String key, Map<String, Object> mapValue) {
		putHash(key, mapValue, null);
	}
	
	public void putHash(final String key, Map<String, Object> mapValue, final Integer expire) {
		redisTemplate.opsForHash().putAll(key, mapValue);

		if (expire != null) {
			redisTemplate.expire(key, expire, TimeUnit.SECONDS);
		}
	}
	
	public void deleteHashKey(final String key, Object... hashKeys) {
		redisTemplate.opsForHash().delete(key, hashKeys);
	}
	
	
	/*********************************key-set (无序,不重复)*************************************/
	public void putSet(final String key, final Object value) {
		if(value instanceof Set) {
			putSet(key, ((Set)value).toArray());
		}else {
			putSet(key, new Object[] {value});
		}
	}
	
	public void putSet(final String key, final Object... values) {
		redisTemplate.opsForSet().add(key, values);
	}

	@SuppressWarnings("unchecked")
	public <T> T getSet(final String key, Class<T> clz) {
		return (T)redisTemplate.opsForSet().pop(key);
	}
	

	@SuppressWarnings("unchecked")
	public <T> List<T> members(final String key, Class<T> clz, final Integer expire) {

		if (expire != null) {
			redisTemplate.expire(key, expire, TimeUnit.SECONDS);
		}

		return (List<T>)redisTemplate.opsForSet().members(key);
	}
	
	/*********************************key-list (有序,可重复)*************************************/
	
	public void rightPush(final String key, final Object value) {
		redisTemplate.opsForList().rightPush(key, value);
	}
	
	public void rightPushAll(final String key, final List values) {
		redisTemplate.opsForList().rightPushAll(key, values);
	}
	
	public void rightPushAll(final String key, final Object... values) {
		redisTemplate.opsForList().rightPushAll(key, values);
	}
	
	public void leftPush(final String key, final Object value) {
		redisTemplate.opsForList().leftPush(key, value);
	}
	
	@SuppressWarnings("unchecked")
	public <T> T leftPop(final String key, Class<T> clz) {
		return (T)redisTemplate.opsForList().leftPop(key);
	}
	
	@SuppressWarnings("unchecked")
	public <T> T rightPop(final String key, Class<T> clz) {
		return (T)redisTemplate.opsForList().rightPop(key);
	}
	

	/**
	 * 得到key数据结构未list的数据,带分页支持
	 */
	public <T> List<T> findList(String key, Class<T> clz, Integer startRow, Long endRow) {
		ListOperations<String, T> operations = getRedisTemplate(clz).opsForList();
		return operations.range(key, startRow - 1, endRow - 1);

	}
	
	public Long getListTotal(String key) {
		return getRedisTemplate(Long.class).opsForList().size(key);
	}

	public void expire(String key, long expire) {
		redisTemplate.expire(key, expire, TimeUnit.SECONDS);
	}
	
	/**
	 * 删除某个key
	 * 
	 * @param key
	 */
	public void remove(String key) {
		redisTemplate.delete(key);
	}

	public void del(String key) {
		remove(key);
	}

	/**
	 * 模糊删除key
	 * 
	 * @param key
	 */
	public void clear(String key) {

		Set<String> keys = redisTemplate.keys(key);
		if (!keys.isEmpty()) {
			redisTemplate.delete(keys);
		}
	}

	public Long getAtomicLong(String key) {
		return getAtomicLong(key, 1);
	}
	
	public Long getAtomicLong(String key, long delta) {
		return getRedisTemplate(Long.class).boundValueOps(key).increment(delta);
	}

	public Long getAtomicLong(String key, String hashKey) {
		return getAtomicLong(key, hashKey, 1);
	}
	
	public Long getAtomicLong(String key, String hashKey, long delta) {
		return getRedisTemplate(Long.class).boundHashOps(key).increment(hashKey, delta);
	}

	/**
	 * 获取redis锁,默认重试1s
	 * @param lockId
	 * @param requestId
	 * @param expire
	 * @return
	 */
	public Boolean getLockRetry(String lockId, String requestId, long expire) {
		return getLockRetry(lockId, requestId, expire, 1000);
	}

	/**
	 * 获取redis锁,获取失败则等待200~300毫秒进行重试,可配置超时时间
	 * @param lockId
	 * @param requestId
	 * @param expire
	 * @param retryTimeout 重试超时时间,根据资源的消耗进行配置
	 * @return
	 */
	public Boolean getLockRetry(String lockId, String requestId, long expire, long retryTimeout) {
		return getLockRetry(lockId, requestId, expire, retryTimeout, 200+ RandomUtils.nextInt(0, 100));
	}

	/**
	 * 重试获取锁
	 * @param lockId
	 * @param requestId
	 * @param expire
	 * @param retryTimeout	重试时长
	 * @param retryInterval	重试时间间隔
	 * @return
	 */
	public Boolean getLockRetry(String lockId, String requestId, long expire, long retryTimeout, long retryInterval) {
		//请求时间
		long requestTime=System.currentTimeMillis();
		while(System.currentTimeMillis()-requestTime < retryTimeout){
			if(lock(lockId, requestId, expire)) {
				return Boolean.TRUE;
			}

			try {
				TimeUnit.MILLISECONDS.sleep(retryInterval);
			} catch (InterruptedException e) {
				e.printStackTrace();
				logger.error("线程等待出现异常"+e.getMessage(), e);
			}
		}
		return Boolean.FALSE;
	}

	/**
	 * 利用redis进行锁
	 * boolean result = redisTemplate.boundValueOps(key).setIfAbsent(0);
	 * if (result) {
	 *		redisTemplate.expire(key, expire, TimeUnit.SECONDS);
	 * }
	 * 上面的方式不具有原子性,系统异常时会导致加锁了但是未设置失效时间,导致死锁
	 *
	 * 在没有锁值传入的情况下,最好返回定义的锁值,便于后期释放锁
	 * 推荐{@link RedisClient#lock(String, String, long)} 或者 {@link RedisClient#lock(String, long)}
	 */
	public boolean getLock(String lockId, long expire) {
		return redisTemplate.boundValueOps(getLockKey(lockId)).setIfAbsent(0, expire, TimeUnit.SECONDS);
	}

	/**
	 * 当前方法仅适用自动释放锁的情况
	 * @param lockId
	 * @param expire
	 * @return
	 */
	public boolean getLockAutoRelease(String lockId, long expire) {
		return StringUtils.isNotBlank(lock(lockId, expire));
	}

	public String getLockKey(String lockId) {
		return REDIS_LOCK_PREFIX + lockId;
	}

	/**
	 * 获取锁,返回锁值
	 * @param lockId
	 * @param expire
	 * @return
	 */
	public String lock(String lockId, final long expire) {
		String value=UUID.randomUUID().toString();
		if(lock(lockId, value, expire)){
			return value;
		}
		return null;
	}

	/**
	 * 获取锁
	 * @param lockId	锁的key
	 * @param requestId UUID,用于释放当前线程的锁
	 * @param expire
	 * @return
	 */
	public Boolean lock(String lockId, final String requestId, final long expire) {
		final String key = getLockKey(lockId);

		return redisTemplate.execute(new RedisCallback<Boolean>() {
			@Override
			public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
				try {
					return connection.set(key.getBytes("UTF-8"), requestId.getBytes("UTF-8"), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
				}catch (Exception e) {
					logger.error("获取redis锁出现异常", e);
				}
				return false;
			}
		});
	}

	/**
	 * 如果当前线程的锁已经释放/失效,拿到的锁值可能不是当前线程存储的值
	 * @param lockId
	 * @return
	 */
	public String getLockValue(String lockId) {
		final String key = getLockKey(lockId);
		return redisTemplate.execute(new RedisCallback<String>() {
			@Override
			public String doInRedis(RedisConnection connection) throws DataAccessException {
				try {
					return new String(connection.get(key.getBytes("UTF-8")),"UTF-8");
				}catch (Exception e) {
					logger.error("获取redis锁requestId出现异常", e);
				}
				return null;
			}
		});
	}

	/**
	 * 释放锁,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
	 * 需要判断加锁的requestId与当前锁的requestId是否一致,一致则释放
	 * @param lockId
	 * @param requestId 获取锁成功后调用getLockRequestId获取requestId,释放锁时传入requestId
	 * @return
	 */
	public boolean releaseLock(String lockId, String requestId) {
		if (StringUtils.isBlank(lockId) || StringUtils.isBlank(requestId)) {
			return false;
		}
		final String key = getLockKey(lockId);
        try {
            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
        	RedisCallback<Boolean> callback = (connection) -> {
        		try {
        			return connection.eval(RELEASE_LOCK_LUA.getBytes("UTF-8"), ReturnType.BOOLEAN, 1, key.getBytes("UTF-8"),  requestId.getBytes("UTF-8"));
        		}catch (Exception e) {
        			logger.error("执行释放锁出现异常", e);
					return Boolean.FALSE;
				}
        	};
        	return redisTemplate.execute(callback);
            
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        }
        return false;
    }
	
	public boolean isLock(String lockId) {
		final String key = getLockKey(lockId);
		return redisTemplate.execute(new RedisCallback<Boolean>() {
			@Override
			public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
				try {
					return connection.get(key.getBytes("UTF-8"))==null;
				}catch (Exception e) {

				}
				return false;
			}
		});
	}
}