require 'singleton'
require 'forwardable'

module Exchange
  # The cache module. All Classes handling caching for this gem have to be placed here. Allows easy extension with own caching solutions
  # as shown in the example below.
  # @example Write your own caching module
  #   module Cache
  #     class MyCustomCache < Base
  #       # A cache class has to have the method "cached".
  #       # The cache Base is a singleton and forwards the method "cached"
  #       # to the instance
  #       #
  #       def cached api, opts={}, &block
  #         # generate the storage with key(api, opts[:at]) and you will get a 
  #         # unique key to store in your cache
  #         #
  #         # Your code goes here
  #       end
  #     end
  #   end
  #
  #   # Now, you can configure your Caching solution in the configuration. The Symbol will get camelcased and constantized
  #   #
  #   Exchange.configuration.cache.subclass = :my_custom_cache
  #
  #   # Have fun, and don't forget to write tests.
  
  module Cache
    
    # The base Class for all Caching operations. Essentially generates a helper function for all cache classes to generate a key
    # @author Beat Richartz
    # @version 0.6
    # @since 0.1
    #
    class Base
      include Singleton
      extend SingleForwardable
      
      # returns The result of the block called
      # This method has to be the same in all the cache classes in order for the configuration binding to work
      # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
      # @param [Hash] opts the options to cache with
      # @option opts [Time] :at is ignored for filecache, other than that is the time of the cached rate
      # @option opts [Symbol] :cache_period The period to cache for
      # @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
      # @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
      #
      def cached api, opts={}, &block
        raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?

        block.call
      end
      
      # Forwards the cached method to the instance using singleforwardable
      #
      def_delegator :instance, :cached
                    
      protected
      
      # A Cache Key generator for the API Classes and the time
      # Generates a key which can handle expiration by itself
      # @param [Exchange::ExternalAPI::Subclass] api_class The API to store the data for
      # @param [optional, Time] time The time for which the data is valid
      # @return [String] A string that can be used as cache key
      # @example
      #   Exchange::Cache::Base.key(Exchange::ExternalAPI::CurrencyBot, Time.gm(2012,1,1)) #=> "Exchange_ExternalAPI_CurrencyBot_2012_1"
      #
      def key api, opts={}
        time          = Exchange::Helper.assure_time(opts[:at], :default => :now)
        [ 'exchange',
          api.to_s, 
          time.year.to_s, 
          time.yday.to_s, 
          Exchange.configuration.cache.expire == :hourly ? time.hour.to_s : nil,
          *(opts[:key_for] || [])
        ].compact.join('_')
      end
        
    end
    
    # The error getting thrown if caching is attempted without a given block
    # @since 0.1
    # @version 0.1
    #
    CachingWithoutBlockError = Class.new(ArgumentError)
  end
end