package com.sifang.modules.config;

import com.sifang.modules.sys.common.utils.IdGen;
import org.apache.log4j.Logger;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Collections;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class DLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final int LOCK_TIMEOUT = 10000; // MS
    private static final int PARK_INTERVAL = 100; // MS
    private static final int MAX_LOCK_TIME = 30 * 1000; // MS

    private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    private final JedisPool jedisPool;
    private final String key;
    private final String requestId;

    private static Logger logger = Logger.getLogger(DLock.class);

    public DLock(String key, JedisPool jedisPool) {
        this.jedisPool = jedisPool;
        this.key = key;
        this.requestId = IdGen.uuid();
    }

    public boolean lock() {
        return lock(LOCK_TIMEOUT, MAX_LOCK_TIME);
    }

    public boolean lock(int lockTimeOut, int maxLockTime) {
        long base = System.currentTimeMillis();
        boolean locked = false;
        while (System.currentTimeMillis() <= base + lockTimeOut) {
            locked = tryLock(this.key, this.requestId, maxLockTime);
            if (!locked) {
                LockSupport.parkNanos(Thread.currentThread(), PARK_INTERVAL);
            } else {
                break;
            }
        }

        return locked;
    }

    public boolean unlock() {
        return tryUnlockWithRetry(this.key, this.requestId);
    }

    private boolean tryLock(String lockKey, String requestId, int expireTime) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            return LOCK_SUCCESS.equals(result);
        }
    }

    private boolean tryUnlockWithRetry(final String lockKey, final String requestId) {
        boolean ret = tryUnlock(lockKey, requestId);
        if (ret) {
            return true;
        }

        executor.schedule(new Runnable() {
            @Override
            public void run() {
                boolean ret = tryUnlock(lockKey, requestId);
                if (!ret) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    tryUnlock(lockKey, requestId);
                }
            }
        }, 1, TimeUnit.SECONDS);

        return false;
    }

    private boolean tryUnlock(String lockKey, String requestId) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            return RELEASE_SUCCESS.equals(result);
        }
    }

    @Override
    public String toString() {
        return String.format("DLock(key=%s,request_id=%s)", this.key, this.requestId);
    }
}
