namespace ThingsGateway.NewLife.Caching;

/// <summary>分布式锁</summary>
public class CacheLock : DisposeBase
{
    private ICache Client { get; set; }

    /// <summary>
    /// 是否持有锁
    /// </summary>
    private Boolean _hasLock;

    /// <summary>键</summary>
    public String Key { get; set; }

    /// <summary>实例化</summary>
    /// <param name="client"></param>
    /// <param name="key"></param>
    public CacheLock(ICache client, String key)
    {
        if (client == null) throw new ArgumentNullException(nameof(client));
        if (key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(key));

        Client = client;
        Key = key;
    }

    /// <summary>申请锁</summary>
    /// <param name="msTimeout">锁等待时间，申请加锁时如果遇到冲突则等待的最大时间，单位毫秒</param>
    /// <param name="msExpire">锁过期时间，超过该时间如果没有主动释放则自动释放锁，必须整数秒，单位毫秒</param>
    /// <returns></returns>
    public Boolean Acquire(Int32 msTimeout, Int32 msExpire)
    {
        var ch = Client;
        var now = Runtime.TickCount64;

        // 循环等待
        var end = now + msTimeout;
        while (now < end)
        {
            // 申请加锁。没有冲突时可以直接返回
            var rs = ch.Add(Key, now + msExpire, msExpire / 1000);
            if (rs) return _hasLock = true;

            // 死锁超期检测
            var dt = ch.Get<Int64>(Key);
            if (dt <= now)
            {
                // 开抢死锁。所有竞争者都会修改该锁的时间戳，但是只有一个能拿到旧的超时的值
                var old = ch.Replace(Key, now + msExpire);
                // 如果拿到超时值，说明抢到了锁。其它线程会抢到一个为超时的值
                if (old <= dt)
                {
                    ch.SetExpire(Key, TimeSpan.FromMilliseconds(msExpire));
                    return _hasLock = true;
                }
            }

            // 没抢到，继续
            Thread.Sleep(200);

            now = Runtime.TickCount64;
        }

        return false;
    }

    /// <summary>销毁</summary>
    /// <param name="disposing"></param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        // 如果客户端已释放，则不删除
        if (Client is DisposeBase db && db.Disposed)
        {
        }
        else
        {
            if (_hasLock)
            {
                Client.Remove(Key);
            }
        }
    }
}