# Component caching is very fine-grained - a component is cached based on all
# of its arguments. Any more or less would result in inaccurate cache hits.
#
# === Howto
#
# First, configure fragment caching on ActionController. That cache store will be used
# for components as well.
#
# Second, decide which component actions should be cached. Some components cache better
# than others due to the nature of the arguments passed.
#
# For each component you wish to cache, add `cache :my_action` _after_ you define the
# action itself. This is necessary because your action will be wrapped with another
# method - :my_action_with_caching. This enables component actions to be cached no matter
# how they are called.
#
# === Example
#
#   class UserComponent < Components::Base
#     def show(user_id)
#       @user = User.find(user_id)
#       render
#     end
#     cache :show
#   end
#
# === Expiration
#
# I know of three general methods to expire caches:
#
# * TTL: expires a cache after some number of seconds. This works well for content that
#   is hit frequently but can stand to be a bit stale at times.
# * Versioning: caches are never actually expired, rather, they are eventually ignored
#   when all new cache requests are for a new version. This only works with cache stores
#   that have some kind of limited cache space, otherwise cache consumption will go
#   through the metaphorical roof.
# * Direct Expiration: caches are expired by name. This does not work when cache keys
#   have variable elements, or when the complete list of cache keys is not available
#   or a brute-force regular expression approach.
#
# Of those three, direct expiration is not a viable option due to the variable nature of
# component cache keys. And since both of the remaining methods are best supported by some
# variation of memcache, that is the officially recommended cache store.
#
# ==== TTL Expiration
#
# If you are using Rails' :mem_cache_store for fragments, then you can set up TTL-style
# expiration by specifying an :expires_in option, like so:
#
#   class UserComponent < Components::Base
#     def show(user_id)
#       @user = User.find(user_id)
#       render
#     end
#     cache :show, :expires_in => 15.minutes
#   end
#
# ==== Versioned Expiration
#
# Maintaining and incrementing version numbers may be implemented any number of ways. To
# use the version numbers, though, you can specify a :version option, which may either name
# a method (use a Symbol) or provide a proc. In either case, the method or proc should
# receive all of the same arguments as the action itself, and should return the version
# string.
#
#   class UserComponent < Components::Base
#     def show(user_id)
#       @user = User.find(user_id)
#       render
#     end
#     cache :show, :version => :show_cache_version
#
#     protected
#
#     def show_cache_version(user_id)
#       # you may want to find your version from a model object, from memcache, or whereever.
#       Version.for("users/show", user_id)
#     end
#   end
#
module Components::Caching
  def self.included(base) #:nodoc:
    base.class_eval do
      extend ClassMethods
    end
  end

  module ClassMethods
    # Caches the named actions by wrapping them via alias_method_chain. May only
    # be called on actions (methods) that have already been defined.
    #
    # Cache options will be passed through to the cache store's read/write methods.
    def cache(action, cache_options = nil)
      return unless ActionController::Base.cache_configured?

      class_eval <<-EOL, __FILE__, __LINE__
        cattr_accessor :#{action}_cache_options

        def #{action}_with_caching(*args)
          with_caching(:#{action}, args) do
            #{action}_without_caching(*args)
          end
        end
        alias_method_chain :#{action}, :caching
      EOL
      self.send("#{action}_cache_options=", cache_options)
    end

    def cache_store #:nodoc:
      @cache_store ||= ActionController::Base.cache_store
    end
  end

  protected

  def with_caching(action, args, &block) #:nodoc:
    key = cache_key(action, args)
    cache_options = self.send("#{action}_cache_options") || {}
    passthrough_cache_options = cache_options.reject{|k, v| reserved_cache_option_keys.include? k}
    passthrough_cache_options = nil if cache_options.empty?

    # conditional caching: the prohibited case
    if cache_options[:if] and not call(cache_options[:if], args)
      fragment = block.call
    else
      fragment = read_fragment(key, passthrough_cache_options)
      unless fragment
        fragment = block.call
        write_fragment(key, fragment, passthrough_cache_options)
      end
    end

    return fragment
  end

  def read_fragment(key, cache_options = nil) #:nodoc:
    returning self.class.cache_store.read(key, cache_options) do |content|
      logger.debug "Component Cache hit: #{key}" unless content.blank?
    end
  end

  def write_fragment(key, content, cache_options = nil) #:nodoc:
    logger.debug "Component Cache miss: #{key}"
    self.class.cache_store.write(key, content, cache_options)
  end

  # generates the cache key for the given action/args
  def cache_key(action, args = []) #:nodoc:
    key_pieces = [self.class.path, action] + args

    if v = call(versioning(action), args)
      key_pieces << "v#{v}"
    end
    key = key_pieces.collect do |arg|
      case arg
        when ActiveRecord::Base
          "#{arg.class.to_s.underscore}#{arg.id}" # note: doesn't apply to record sets
        else
          arg.to_param
      end
    end.join('/')
    
    key = Digest::MD5.hexdigest(key) if key.length > 200

    ActiveSupport::Cache.expand_cache_key(key, :components)
  end

  # returns the versioning configuration for the given action, if any
  def versioning(action) #:nodoc:
    (self.send("#{action}_cache_options") || {})[:version]
  end

  private

  def call(method, args)
    case method
      when Proc
        method.call(*args)
      when Symbol
        send(method, *args)
    end
  end

  def reserved_cache_option_keys
    @reserved_cache_option_keys ||= [:if, :version]
  end
end