require 'digest'

module PuppetForge
  # Implements a simple LRU cache. This is used internally by the
  # {PuppetForge::V3::Base} class to cache API responses.
  class LruCache
    # Takes a list of strings (or objects that respond to #to_s) and
    # returns a SHA256 hash of the strings joined with colons. This is
    # a convenience method for generating cache keys. Cache keys do not
    # have to be SHA256 hashes, but they must be unique.
    def self.new_key(*string_args)
      Digest(:SHA256).hexdigest(string_args.map(&:to_s).join(':'))
    end

    # @return [Integer] the maximum number of items to cache.
    attr_reader :max_size

    # @param max_size [Integer] the maximum number of items to cache. This can
    #   be overridden by setting the PUPPET_FORGE_MAX_CACHE_SIZE environment
    #   variable.
    def initialize(max_size = 30)
      raise ArgumentError, "max_size must be a positive integer" unless max_size.is_a?(Integer) && max_size > 0

      @max_size = ENV['PUPPET_FORGE_MAX_CACHE_SIZE'] ? ENV['PUPPET_FORGE_MAX_CACHE_SIZE'].to_i : max_size
      @cache = {}
      @lru = []
      @semaphore = Mutex.new
    end

    # Retrieves a value from the cache.
    # @param key [Object] the key to look up in the cache
    # @return [Object] the cached value for the given key, or nil if
    #   the key is not present in the cache.
    def get(key)
      if cache.key?(key)
        semaphore.synchronize do
          # If the key is present, move it to the front of the LRU
          # list.
          lru.delete(key)
          lru.unshift(key)
        end
        cache[key]
      end
    end

    # Adds a value to the cache.
    # @param key [Object] the key to add to the cache
    # @param value [Object] the value to add to the cache
    def put(key, value)
      semaphore.synchronize do
        if cache.key?(key)
          # If the key is already present, delete it from the LRU list.
          lru.delete(key)
        elsif cache.size >= max_size
          # If the cache is full, remove the least recently used item.
          cache.delete(lru.pop)
        end
        # Add the key to the front of the LRU list and add the value
        # to the cache.
        lru.unshift(key)
        cache[key] = value
      end
    end

    # Clears the cache.
    def clear
      semaphore.synchronize do
        cache.clear
        lru.clear
      end
    end

    private

    # Makes testing easier as these can be accessed directly with #send.
    attr_reader :cache, :lru, :semaphore
  end
end
