module Tool
  # A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
  # Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been
  # garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to
  # be idempotent.
  #
  # @example
  #   class ExpensiveComputation
  #     @map = Tool::EqualityMap.new
  #
  #     def self.new(*args)
  #       @map.fetch(*args) { super }
  #     end
  #   end
  #
  # @see #fetch
  class EqualityMap
    attr_reader :map

    def self.new
      defined?(ObjectSpace::WeakMap) ? super : {}
    end

    def initialize
      @keys = {}
      @map  = ObjectSpace::WeakMap.new
    end

    # @param [Array<#hash>] key for caching
    # @yield block that will be called to populate entry if missing (has to be idempotent)
    # @return value stored in map or result of block
    def fetch(*key)
      identity = @keys[key.hash]
      key      = identity == key ? identity : key

      # it is ok that this is not thread-safe, worst case it has double cost in
      # generating, object equality is not guaranteed anyways
      @map[key] ||= track(key, yield)
    end

    # @param [#hash] key for identifying the object
    # @param [Object] object to be stored
    # @return [Object] same as the second parameter
    def track(key, object)
      ObjectSpace.define_finalizer(object, finalizer(key.hash))
      @keys[key.hash] = key
      object
    end

    # Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
    #
    # @param [Fixnum] hash for key
    # @return [Proc] finalizer callback
    def finalizer(hash)
      proc { @keys.delete(hash) }
    end

    private :track, :finalizer
  end
end