require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'

module ActiveSupport
  module Cache
    module Strategy
      # Caches that implement LocalCache will be backed by an in memory cache for the
      # duration of a block. Repeated calls to the cache for the same key will hit the
      # in memory cache for faster access.
      module LocalCache
        # Simple memory backed cache. This cache is not thread safe but is intended only
        # for serving as a temporary memory cache for a single thread.
        class LocalStore < Store
          def initialize
            super
            @data = {}
          end

          # Since it isn't thread safe, don't allow synchronizing.
          def synchronize # :nodoc:
            yield
          end

          def clear(options = nil)
            @data.clear
          end

          def read_entry(key, options)
            @data[key]
          end

          def write_entry(key, value, options)
            @data[key] = value
            true
          end

          def delete_entry(key, options)
            !!@data.delete(key)
          end
        end

        # Use a local cache to front for the cache for the duration of a block.
        def with_local_cache
          save_val = Thread.current[thread_local_key]
          begin
            Thread.current[thread_local_key] = LocalStore.new
            yield
          ensure
            Thread.current[thread_local_key] = save_val
          end
        end

        # Middleware class can be inserted as a Rack handler to use a local cache for the
        # duration of a request.
        def middleware
          @middleware ||= begin
            klass = Class.new
            klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
              class << self
                def name
                  "ActiveSupport::Cache::Strategy::LocalCache"
                end
                alias :to_s :name
              end

              def initialize(app)
                @app = app
              end

              def call(env)
                Thread.current[:#{thread_local_key}] = LocalStore.new
                @app.call(env)
              ensure
                Thread.current[:#{thread_local_key}] = nil
              end
            EOS
            klass
          end
        end

        def clear(options = nil) # :nodoc:
          local_cache.clear(options) if local_cache
          super
        end

        def cleanup(options = nil) # :nodoc:
          local_cache.clear(options) if local_cache
          super
        end

        def increment(name, amount = 1, options = nil) # :nodoc:
          value = bypass_local_cache{super}
          if local_cache
            local_cache.mute do
              if value
                local_cache.write(name, value, options)
              else
                local_cache.delete(name, options)
              end
            end
          end
          value
        end

        def decrement(name, amount = 1, options = nil) # :nodoc:
          value = bypass_local_cache{super}
          if local_cache
            local_cache.mute do
              if value
                local_cache.write(name, value, options)
              else
                local_cache.delete(name, options)
              end
            end
          end
          value
        end

        protected
          def read_entry(key, options) # :nodoc:
            if local_cache
              entry = local_cache.read_entry(key, options)
              unless entry
                entry = super
                local_cache.write_entry(key, entry, options)
              end
              entry
            else
              super
            end
          end

          def write_entry(key, entry, options) # :nodoc:
            local_cache.write_entry(key, entry, options) if local_cache
            super
          end

          def delete_entry(key, options) # :nodoc:
            local_cache.delete_entry(key, options) if local_cache
            super
          end

        private
          def thread_local_key
            @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
          end

          def local_cache
            Thread.current[thread_local_key]
          end

          def bypass_local_cache
            save_cache = Thread.current[thread_local_key]
            begin
              Thread.current[thread_local_key] = nil
              yield
            ensure
              Thread.current[thread_local_key] = save_cache
            end
          end
      end
    end
  end
end
