package com.meipian.queues.redis;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

import org.hamcrest.core.IsEqual;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.meipian.queues.core.DelayQueue;
import com.meipian.queues.core.Message;

import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.Tuple;
import redis.clients.jedis.params.sortedset.ZAddParams;

public class RedisDelayQueue implements DelayQueue {
	private transient final ReentrantLock lock = new ReentrantLock();

	private final Condition available = lock.newCondition();

	private JedisCluster jedisCluster;

	private long MAX_TIMEOUT = 525600000; // 最大超时时间不能超过一年

	private ObjectMapper om;

	private int unackTime = 60 * 1000;

	private String queueName;

	private String redisKeyPrefix;

	private String messageStoreKey;

	private String realQueueName;

	private DelayQueueProcessListener delayQueueProcessListener;

	private volatile boolean isEmpty = true;

	public RedisDelayQueue(String redisKeyPrefix, String queueName, JedisCluster jedisCluster, int unackTime,
			DelayQueueProcessListener delayQueueProcessListener) {
		om = new ObjectMapper();
		om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		om.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
		om.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
		om.setSerializationInclusion(Include.NON_NULL);
		om.setSerializationInclusion(Include.NON_EMPTY);
		om.disable(SerializationFeature.INDENT_OUTPUT);
		this.redisKeyPrefix = redisKeyPrefix;
		this.messageStoreKey = redisKeyPrefix + ".MESSAGE." + queueName;
		this.unackTime = unackTime;
		this.jedisCluster = jedisCluster;
		realQueueName = redisKeyPrefix + ".QUEUE." + queueName;
		this.delayQueueProcessListener = delayQueueProcessListener;
	}

	public String getQueueName() {
		return queueName;
	}

	@Override
	public boolean push(Message message) {
		if (message.getTimeout() > MAX_TIMEOUT) {
			throw new RuntimeException("Maximum delay time should not be exceed one year");
		}
		try {
			String json = om.writeValueAsString(message);
			jedisCluster.hset(messageStoreKey, message.getId(), json);
			double priority = message.getPriority() / 100;
			double score = Long.valueOf(System.currentTimeMillis() + message.getTimeout()).doubleValue() + priority;
			jedisCluster.zadd(realQueueName, score, message.getId());
			delayQueueProcessListener.pushCallback(message);
			isEmpty = false;
			return true;
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		return false;

	}

	public void listen() {
		while (true) {
			String id = peekId();
			if (id == null) {
				continue;
			}
			String json = jedisCluster.hget(messageStoreKey, id);
			try {
				Message message = om.readValue(json, Message.class);
				if (message == null) {
					continue;
				}
				long delay = message.getCreateTime() + message.getTimeout() - System.currentTimeMillis();
				System.out.println(delay);
				if (delay <= 0) {
					delayQueueProcessListener.peekCallback(message);
				} else {
					LockSupport.parkNanos(this, TimeUnit.NANOSECONDS.convert(delay, TimeUnit.MILLISECONDS));
					delayQueueProcessListener.peekCallback(message);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}

		}
	}

	@Override
	public boolean ack(String messageId) {
		String unackQueueName = getUnackQueueName(queueName);
		jedisCluster.zrem(unackQueueName, messageId);
		Long removed = jedisCluster.zrem(realQueueName, messageId);
		Long msgRemoved = jedisCluster.hdel(messageStoreKey, messageId);
		if (removed > 0 && msgRemoved > 0) {
			return true;
		}
		return false;

	}

	@Override
	public boolean setUnackTimeout(String messageId, long timeout) {
		double unackScore = Long.valueOf(System.currentTimeMillis() + timeout).doubleValue();
		String unackQueueName = getUnackQueueName(queueName);
		Double score = jedisCluster.zscore(unackQueueName, messageId);
		if (score != null) {
			jedisCluster.zadd(unackQueueName, unackScore, messageId);
			return true;
		}
		return false;

	}

	@Override
	public boolean setTimeout(String messageId, long timeout) {
		try {
			String json = jedisCluster.hget(messageStoreKey, messageId);
			if (json == null) {
				return false;
			}
			Message message = om.readValue(json, Message.class);
			message.setTimeout(timeout);
			Double score = jedisCluster.zscore(realQueueName, messageId);
			if (score != null) {
				double priorityd = message.getPriority() / 100;
				double newScore = Long.valueOf(System.currentTimeMillis() + timeout).doubleValue() + priorityd;
				ZAddParams params = ZAddParams.zAddParams().xx();
				long added = jedisCluster.zadd(realQueueName, newScore, messageId, params);
				if (added == 1) {
					json = om.writeValueAsString(message);
					jedisCluster.hset(messageStoreKey, message.getId(), json);
					return true;
				}
				return false;
			}
			return false;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	@Override
	public Message get(String messageId) {
		String json = jedisCluster.hget(messageStoreKey, messageId);
		if (json == null) {
			return null;
		}
		Message msg;
		try {
			msg = om.readValue(json, Message.class);
			return msg;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}

	}

	@Override
	public long size() {
		return jedisCluster.zcard(realQueueName);
	}

	@Override
	public void clear() {
		String unackShard = getUnackQueueName(queueName);
		jedisCluster.del(realQueueName);
		jedisCluster.del(unackShard);
		jedisCluster.del(messageStoreKey);

	}

	private String peekId() {
		try {
			if (!isEmpty) {
				System.out.println("xxxxx");
				lock.lockInterruptibly();
				double max = Long.valueOf(System.currentTimeMillis() + MAX_TIMEOUT).doubleValue();
				Set<String> scanned = jedisCluster.zrangeByScore(realQueueName, 0, max, 0, 1);
				if (scanned.size() > 0) {
					String messageId = scanned.toArray()[0].toString();
					jedisCluster.zrem(realQueueName, messageId);
					setUnackTimeout(messageId, unackTime);
					if (size() == 0) {
						isEmpty = true;
					}
					available.signal();
					lock.unlock();
					return messageId;
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
			available.signal();
			lock.unlock();
		}
		return null;
	}

	public void processUnacks() {
		long queueDepth = size();
		int batchSize = 1_000;
		String unackQueueName = getUnackQueueName(queueName);
		double now = Long.valueOf(System.currentTimeMillis()).doubleValue();
		Set<Tuple> unacks = jedisCluster.zrangeByScoreWithScores(unackQueueName, 0, now, 0, batchSize);
		for (Tuple unack : unacks) {
			double score = unack.getScore();
			String member = unack.getElement();
			String payload = jedisCluster.hget(messageStoreKey, member);
			if (payload == null) {
				jedisCluster.zrem(unackQueueName, member);
				continue;
			}
			jedisCluster.zadd(realQueueName, score, member);
			jedisCluster.zrem(unackQueueName, member);
		}
	}

	private String getUnackQueueName(String queueName) {
		return redisKeyPrefix + ".UNACK." + queueName;
	}

	@Override
	public String getName() {
		return this.realQueueName;
	}

	@Override
	public int getUnackTime() {

		return this.unackTime;
	}

}