module InfoparkComponentCache
  # @author Tomasz Przedmojski <tomasz.przedmojski@infopark.de>
  # This class provides user-level access to component cache.
  #
  # @example Do some expensive computation on a page
  #   InfoparkComponentCache::ComponentCache.new(@obj, 'processed_body', :page => 14) do
  #     expensive_process_body @obj.body
  #   end
  #
  # By default ComponentCache comes with a set of Guards, i.e.
  # classes that check the consistency of cache and invalidate it,
  # should it be neccesary.
  # @see ComponentCache#initialize
  class ComponentCache
    attr_reader :component, :guards

    # First three parameters are used to construct
    # the component
    # @see Component
    #
    # @param [Array<Class>, Array<Hash>] guards list of guard
    #   classes used when deciding whether cache is valid
    #   when left empty the default set is used:
    #   @see Guard::ValuePresent
    #   @see Guard::LastChanged
    #   @see Guard::ObjCount
    def initialize(obj, name, params = {}, guards = [])
      @component = Component.new(obj, name, params)
      @guards = if guards.empty?
                  [
                    Guards::ValuePresent.new(@component),
                    Guards::LastChanged.new(@component),
                    Guards::ObjCount.new(@component),
                    Guards::ValidFrom.new(@component),
                    Guards::ValidUntil.new(@component)
                  ]
                else
                  guards.map do |klass_or_hash|
                    if klass_or_hash.kind_of?(Hash)
                      klass = klass_or_hash.delete(:guard)
                      klass.new(@component, klass_or_hash)
                    else
                      klass = klass_or_hash
                      klass.new(@component)
                    end
                  end
                end
    end

    # Checks if cache is valid (in consistent state).
    # It delegates work to specified #guards
    #
    # For any unexpected case it returns true.
    #
    # @return true if cache is valid and in consistent state
    def expired?
      return true unless cache.enabled?

      !guards.all?(&:consistent?)
    rescue StandardError => e
      raise e if Rails.env.test?

      true
    end

    # Checks if the cache is in consistent state and cached value
    # can be returned.
    # In such case it returns cached value.
    # Otherwise it evaluates passed block and updates the cache.
    #
    # @see {#expired?}
    # @yieldreturn value to be used in case of cache miss
    # @return cached value or the return value of the block
    def fetch(&_block)
      if expired?
        value = yield
        begin
          if cache.enabled?
            cache.write(component.cache_key, value)
            ensure_consistency!
          end
          value
        rescue StandardError => e
          raise e if Rails.env.test?

          value
        end
      else
        cache.read(component.cache_key)
      end
    end

    # @private
    # @return [CacheStorage] instance of CacheStorage to use
    def cache
      CacheStorage.instance
    end

    # @private
    def ensure_consistency!
      guards.each(&:guard!)
    end
  end
end