# =============================================================================
# Extends module to inject interceptors                                        
# =============================================================================
require 'benchmark'
require 'active_support'

class Module              
  # intercepts a collection of features and wrap performance check based on the
  # specified :perf_threshold. The trigger defaults to 5 secs if not explicitly set.
  #
  # Example:   
  #
  #   MyClass.mole_perf do |context, action, elapsed_time, args|                            
  #     Mole::DbMole.perf_it( context.session[:user_id], 
  #                           :controller   => context.class.name,
  #                           :action       => action,                           
  #                           :elapsed_time => "%3.3f" % elapsed_time )
  #   end  
  #
  # This will trap all public methods on the MyClass that takes more than 
  # :perf_threshold to complete. You can override this default by using the option
  # :features => [m1,m2,...]. This is handy for controller moling rails
  # and merb context.
  # 
  # If you elect not to use the block form of the call, you can pass in the
  # following arguments to the option hash:
  # <tt>:interceptor</tt>::   The class name of your interceptor class
  # <tt>:method</tt>::        The name of the method to callback the interceptor on
  #                           once a perf condition has been trapped.
  def mole_perf( opts={}, &filter )    
    opts[:interceptor] ||= filter
    opts[:method]      ||= :call             
    opts[:features]    ||= instance_methods( false )
    opts[:features].each do |feature|  
      wrap feature
      perf_mole_filters[feature.to_s] << [opts[:interceptor], opts[:method]]
    end    
  end
         
  # monitors a collections of features and wrap rescue logic to trap unchecked 
  # exceptions. You can handle to trap differently by either logging the event
  # in the db or sending out email/IM notification.
  #
  # Example:   
  #
  #   MyClass.mole_unchecked do |context, action, boom, args|
  #     Mole::Moler.check_it( context.session[:user_id], 
  #                          :controller => context.class.name,  
  #                          :action     => action,
  #                          :boom       => boom )    
  #   end  
  #
  # This will wrap all public instance methods on MyClass. If any of these methods
  # raises an unchecked exception, the MOle will surface the condition. 
  # This call also takes in a :features option to specify a list of methods if the
  # default instance methods is not suitable, you can pass in a collection of methods
  # that you wish to mole. This is handy in the case of Rails/Merb where conveniences
  # are provided to gather controller actions
  # 
  # If you elect not to use the block form of the call, you can pass in the
  # following arguments to the option hash:
  # <tt>:interceptor</tt>::   The class name of your interceptor class
  # <tt>:method</tt>::        The name of the method to callback the interceptor on
  #                           once an exception condition has been trapped.   
  def mole_unchecked( opts={}, &filter )    
    opts[:interceptor] ||= filter
    opts[:method]      ||= :call             
    opts[:features]    ||= instance_methods( false )
    opts[:features].each do |feature|  
      wrap feature
      unchecked_mole_filters[feature.to_s] << [opts[:interceptor], opts[:method]]
    end
  end

  # intercepts a feature before the feature is called. During the callback
  # you will have access to the object ( context ) on which the call is intercepted,
  # as well as the arguments/block, the feature was issued with.
  #
  # Example:   
  #
  #   MyClass.mole_before( :feature => :blee ) do |context, feature, *args|
  #     Mole::Moler.mole_it( context, feature, context.session[:user_id], 
  #                         :args => args )
  #   end  
  #
  # This will wrap the method blee with a before interceptor. Before the blee call
  # is issued on MyClass, control will be handed to the before interceptor. 
  # 
  # Options:
  # <tt>:feature</tt>::       The name of the feature to be intercepted  
  # If you elect not to use the block form of the call, you can pass in the
  # following arguments to the option hash:  
  # <tt>:interceptor</tt>::   The class name of your interceptor class. If no interceptor block is specified
  # <tt>:method</tt>::        The name of the method to callback the interceptor on
  #                           once an exception condition has been trapped.     
  def mole_before(opts = {}, &filter)   
    raise "Missing :feature option" if opts[:feature].nil? or opts[:feature].to_s.empty?
    opts[:interceptor] ||= filter
    opts[:method] ||= :call
    feature = opts[:feature].to_s
    if before_mole_filters[feature].empty?
      wrap feature 
      before_mole_filters[feature] << [opts[:interceptor], opts[:method]]
    end
  end

  # intercepts a feature after the feature is called. During the callback
  # you will have access to the object ( context ) on which the call is intercepted,
  # as well as the arguments/block and return values the feature was issued with.
  #
  # Example:   
  #
  #   MyClass.mole_after( :feature => :blee ) do |context, feature, ret_val, *args|
  #     Mole::Moler.mole_it( context, feature, context.session[:user_id], 
  #                         :args => args )
  #   end  
  #
  # This will wrap the method blee with an after interceptor. After the blee call
  # is issued on MyClass, control will be handed to the after interceptor. 
  # 
  # Options:
  # <tt>:feature</tt>::       The name of the feature to be intercepted  
  # <tt>:interceptor</tt>::   The class name of your interceptor class. If no interceptor block is specified
  # <tt>:method</tt>::        The name of the method to callback the interceptor on
  #                           once an exception condition has been trapped.     
  def mole_after(opts = {}, &interceptor)
    raise "Missing :feature option" if opts[:feature].nil? or opts[:feature].to_s.empty?    
    opts[:interceptor] ||= interceptor
    opts[:method] ||= :call
    feature = opts[:feature].to_s
    if after_mole_filters[feature].empty?
      wrap feature 
      after_mole_filters[feature] << [opts[:interceptor], opts[:method]]
    end
  end
          
  # def dump
  #   puts "Filters for class <- #{self} ->"
  #   puts "Before filters"
  #   before_mole_filters.each_pair do |k,v|
  #     puts "#{k} --> #{v}"
  #   end
  #   puts "After filters"
  #   after_mole_filters.each_pair do |k,v|
  #     puts "#{k} --> #{v}"
  #   end
  #   puts "Unchecked filters"
  #   unchecked_mole_filters.each_pair do |k,v|
  #     puts "#{k} --> #{v}"
  #   end
  #   puts "Perf filters"
  #   perf_mole_filters.each_pair do |k,v|
  #     puts "#{k} --> #{v}"
  #   end    
  # end
      
  # ===========================================================================
  # protected
    
  # ---------------------------------------------------------------------------
  # Holds before filters
  def before_mole_filters #:nodoc:
    @before_mole_filters ||= Hash.new{ |h,k| h[k] = [] }
  end

  # ---------------------------------------------------------------------------
  # Holds after filters
  def after_mole_filters #:nodoc:
    @after_mole_filters ||= Hash.new{ |h,k| h[k] = [] }
  end
  
  # Holds perf around filters   
  def perf_mole_filters #:nodoc:
    @perf_mole_filters ||= Hash.new{ |h,k| h[k] = []} 
  end

  # Holds unchecked exception filters   
  def unchecked_mole_filters #:nodoc:
    @unchecked_mole_filters ||= Hash.new{ |h,k| h[k] = []} 
  end
        
  # Attempt to find singleton class method with given name 
  # TODO Figure out how to get method for static signature...
  # def find_public_class_method(method)                        
  #   singleton_methods.each { |name| puts "Looking for #{method}--#{method.class} -- #{name}#{name.class}";return name if name == method }
  #   nil
  # end

  # Wrap method call                                
  # TODO Add support for wrapping class methods ??
  def wrap( method ) #:nodoc:
    return if wrapped?( method )  
    begin
      between = instance_method( method )
    rescue
      # between = find_public_class_method( method )
      raise NameError unless(between)
    end
    code = <<-code
      def #{method}_with_mole (*a, &b)
        key                 = '#{method}'
        klass               = self.class
        between             = klass.wrapped[key]
        ret_val             = nil
        klass.apply_before_filters( klass.before_mole_filters[key], self, key, *a, &b )
        begin                                          
          elapsed = Benchmark::realtime do 
            ret_val = between.bind(self).call(*a) 
          end   
          klass.apply_perf_filters( elapsed, klass.perf_mole_filters[key], self, key, *a, &b )                              
        rescue => boom   
          klass.apply_unchecked_filters( boom, klass.unchecked_mole_filters[key], self, key, *a, &b )
          raise boom            
        end                                                                 
        klass.apply_after_filters( klass.after_mole_filters[key], self, key, *a, &b )
        ret_val
      end
    code
   
    module_eval                code               
    alias_method_chain method, "mole" 
    wrapped[method.to_s]       = between
  end    
   
  def apply_before_filters( filters, clazz, key, *a, &b )          #:nodoc:              
    begin
      filters.each { |r,m| r.send(m, clazz, key, *a, &b) }
    rescue => ca_boom
      ::Mole.logger.error ">>> Mole Error: Before-Filter -- " + ca_boom
      # ca_boom.backtrace.each { |l| ::Mole.logger.error l }           
    end    
  end
                       
  def apply_after_filters( filters, clazz, key, *a, &b )           #:nodoc:              
    begin  
      filters.each { |r,m| r.send(m, clazz, key, *a, &b) }
    rescue => ca_boom    
      ::Mole.logger.error ">>> Mole Error: After-Filter -- " + ca_boom
      # ca_boom.backtrace.each { |l| ::Mole.logger.error l }           
    end    
  end
                       
  def apply_perf_filters( elapsed, filters, clazz, key, *a )       #:nodoc:
    begin
      if ( elapsed >= Mole.perf_threshold  )
        filters.each { |r,m| r.send(m, clazz, key, elapsed, *a) }            
      end
    rescue => ca_boom
      ::Mole.logger.error ">>> Mole Error: Perf-Filter -- " + ca_boom
      # ca_boom.backtrace.each { |l| ::Mole.logger.error l }             
    end    
  end
                       
  def apply_unchecked_filters( boom, filters, clazz, key, *a, &b ) #:nodoc:
    begin
      filters.each { |r,m| r.send(m, clazz, key, boom, *a, &b ) }
    rescue => ca_boom                               
      ::Mole.logger.error ">>> Mole Error: Unchecked-Filter -- " + ca_boom
      # ca_boom.backtrace.each { |l| ::Mole.logger.error l }             
    end                                                                       
  end
                          
  # ---------------------------------------------------------------------------
  # Log wrapped class
  def wrapped #:nodoc:
    @wrapped ||= {}
  end

  # ---------------------------------------------------------------------------
  # Check if method has been wrapped
  def wrapped?(which) #:nodoc:
    wrapped.has_key?(which.to_s)
  end       
end