require File.dirname(__FILE__) + '/base_object'

class Redis
  #
  # Class representing a lock.  This functions like a proxy class, in
  # that you can say @object.lock_name { block } to use the lock and also
  # @object.counter_name.clear to reset on it.  You can use this
  # directly, but it is better to use the lock :foo class method in your
  # class to define a lock.
  #
  class Lock < BaseObject
    class LockTimeout < StandardError; end #:nodoc:

    def initialize(key, *args)
      super(key, *args)
      @options[:timeout] ||= 5
      @options[:init] = false if @options[:init].nil? # default :init to false
      redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
    end

    def value
      nil
    end

    # Get the lock and execute the code block. Any other code that needs the lock
    # (on any server) will spin waiting for the lock up to the :timeout
    # that was specified when the lock was defined.
    def lock
      raise ArgumentError, 'Block not given' unless block_given?
      expiration_ms = generate_expiration
      expiration_s  = expiration_ms / 1000.0
      end_time = nil
      try_until_timeout do
        end_time = Time.now.to_i + expiration_s
        # Set a NX record and use the Redis expiration mechanism.
        # Empty value because the presence of it is enough to lock
        # `px` only except an Integer in millisecond
        break if redis.set(key, nil, px: expiration_ms, nx: true)

        # Backward compatibility code
        # TODO: remove at the next major release for performance
        unless @options[:expiration].nil?
          old_expiration = redis.get(key).to_f

          # Check it was not an empty string with `zero?` and
          # the expiration time is passed.
          if !old_expiration.zero? && old_expiration < Time.now.to_f
            expiration_ms = generate_expiration
            expiration_s  = expiration_ms / 1000.0
            end_time = Time.now.to_i + expiration_s
            break if redis.set(key, nil, px: expiration_ms)
          end
        end
      end
      begin
        yield
      ensure
        # We need to be careful when cleaning up the lock key.  If we took a really long
        # time for some reason, and the lock expired, someone else may have it, and
        # it's not safe for us to remove it.  Check how much time has passed since we
        # wrote the lock key and only delete it if it hasn't expired (or we're not using
        # lock expiration)
        if @options[:expiration].nil? || end_time > Time.now.to_f
          redis.del(key)
        end
      end
    end

    # Return expiration in milliseconds
    def generate_expiration
      ((@options[:expiration].nil? ? 1 : @options[:expiration].to_f) * 1000).to_i
    end

    private

    def try_until_timeout
      if @options[:timeout] == 0
        yield
      else
        start = Time.now
        while Time.now - start < @options[:timeout]
          yield
          sleep 0.1
        end
      end
      raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec"
    end
  end
end
