require 'securerandom'

module EM::Hiredis
  # Cross-process re-entrant lock, backed by redis
  class Lock

    EM::Hiredis::Client.load_scripts_from(File.expand_path("../lock_lua", __FILE__))

    # Register a callback which will be called 1s before the lock expires
    # This is an informational callback, there is no hard guarantee on the timing
    # of its invocation because the callback firing and lock key expiry are handled
    # by different clocks (the client process and redis server respectively)
    def onexpire(&blk); @onexpire = blk; end

    def initialize(redis, key, timeout)
      unless timeout.kind_of?(Integer) && timeout >= 1
        raise "Timeout must be an integer and >= 1s"
      end
      @redis, @key, @timeout = redis, key, timeout
      @token = SecureRandom.hex
    end

    # Acquire the lock
    #
    # This is a re-entrant lock, re-acquiring will succeed and extend the timeout
    #
    # Returns a deferrable which either succeeds if the lock can be acquired, or fails if it cannot.
    def acquire
      df = EM::DefaultDeferrable.new
      @redis.lock_acquire([@key], [@token, @timeout]).callback { |success|
        if (success)
          EM::Hiredis.logger.debug "#{to_s} acquired"

          EM.cancel_timer(@expire_timer) if @expire_timer
          @expire_timer = EM.add_timer(@timeout - 1) {
            EM::Hiredis.logger.debug "#{to_s} Expires in 1s"
            @onexpire.call if @onexpire
          }

          df.succeed
        else
          EM::Hiredis.logger.debug "#{to_s} failed to acquire"
          df.fail("Lock is not available")
        end
      }.errback { |e|
        EM::Hiredis.logger.error "#{to_s} Error acquiring lock #{e}"
        df.fail(e)
      }
      df
    end

    # Release the lock
    #
    # Returns a deferrable
    def unlock
      EM.cancel_timer(@expire_timer) if @expire_timer

      df = EM::DefaultDeferrable.new
      @redis.lock_release([@key], [@token]).callback { |keys_removed|
        if keys_removed > 0
          EM::Hiredis.logger.debug "#{to_s} released"
          df.succeed
        else
          EM::Hiredis.logger.debug "#{to_s} could not release, not held"
          df.fail("Cannot release a lock we do not hold")
        end
      }.errback { |e|
        EM::Hiredis.logger.error "#{to_s} Error releasing lock #{e}"
        df.fail(e)
      }
      df
    end

    # This should not be used in normal operation.
    # Force clear without regard to who owns the lock.
    def clear
      EM::Hiredis.logger.warn "#{to_s} Force clearing lock (unsafe)"
      EM.cancel_timer(@expire_timer) if @expire_timer

      @redis.del(@key)
    end

    def to_s
      "[lock #{@key}]"
    end

  end
end
