
# Provides cheap advice mechanism for Ruby.
class CheapAdvice
  attr_accessor :before, :after, :around, :advised

  @@advice_id = 0

  NULL_PROC = lambda { | ar | }
  NULL_AROUND_PROC = lambda { | ar, result | result.call }
  EMPTY_HASH = { }.freeze

  # options:
  #   :before
  #   :after
  #   :around
  def initialize opts, &blk
    @advised = [ ]

    opts_hash = EMPTY_HASH
    opts_key = nil
    
    case opts
      # advice :before => lambda ...
    when Hash 
      opts_hash = opts
      # advice :method, :before do ... end
    when Symbol 
      opts_key = opts
    end

    @before = (opts_key == :before ? blk : opts_hash[:before]) || 
      NULL_PROC
    @after  = (opts_key == :after  ? blk : opts_hash[:after])  || 
      NULL_PROC
    @around = (opts_key == :around ? blk : opts_hash[:around]) || 
      NULL_AROUND_PROC

    @blk = blk
  end


  # Apply advice to class and method.
  def advise cls, method
    return cls.map { | x | advise x, method } if 
      cls.kind_of?(Enumerable)
    return method.map { | x | advise cls, x } if 
      method.kind_of?(Enumerable)

    old_method = "__advice_#{@@advice_id += 1}_#{method}"
    new_method = "__advice_#{@@advice_id += 1}_#{method}"
    
    before_method = "__advice_before_#{@@advice_id}_#{method}"
    after_method  = "__advice_after_#{@@advice_id}_#{method}"
    around_method = "__advice_around_#{@@advice_id}_#{method}"
    
    advice = self

    advised = Advised.new(
                          self, cls, 
                          method, old_method, new_method,
                          before_method, after_method, around_method
                          )

    advised.apply_advice_methods

    cls.class_eval do
      define_method advised.new_method do | *args |
        ar = ActivationRecord.new(self, advised.method, args)
        
        do_result = Proc.new do
          self.send(advised.before_method, ar)
          begin
            ar.result = self.send(advised.old_method, *ar.args)
            ar.previous_called    
          rescue Exception => err
            ar.error = err
          ensure
            self.send(advised.after_method, ar)
          end

          ar.result
        end
        
        self.send advised.around_method, ar, do_result
        
        raise ar.error if ar.error
        
        ar.result
      end
    end
    
    advised.advise

    @advised << advised

    advised
  end


  def unadvise
    @advised.each { | x | x.unadvise }
  end


  def readvise
    @advised.each { | x | x.advise }
  end


  # Represents the application of advice to a class and method.
  class Advised
    attr_reader :advice, :cls
    attr_reader :method, :old_method, :new_method
    attr_reader :before_method, :after_method, :around_method

    def initialize *args
      @advice, @cls, 
      @method, @old_method, @new_method,
      @before_method, @after_method, @around_method = *args
    end


    def apply_advice_methods
      this = self
      @cls.instance_eval do 
        define_method(this.before_method, &this.advice.before)
        define_method(this.after_method,  &this.advice.after)
        define_method(this.around_method, &this.advice.around)  
      end
    end


    def advise
      this = self
      @cls.instance_eval do
        alias_method this.old_method, this.method if 
          method_defined? this.method and 
          ! method_defined? this.old_method

        alias_method this.method, this.new_method
      end
    end

    def unadvise
      this = self
      @cls.instance_eval do
        alias_method this.method, this.old_method if 
          method_defined? this.old_method
      end
    end
  end


  # Represents the activation record of a method invocation.
  class ActivationRecord
    attr_reader :rcvr, :method, :args
    attr_accessor :result, :error, :body
    attr_accessor :previous_called
    
    def initialize *args
      @rcvr, @method, @args = *args
    end
  end


  ######################################
  # Testing
  #

  class Foo
    attr_accessor :foo
    
    def baz(arg)
      puts "in baz"
      5 + arg
    end
  end


  class Bar
    attr_accessor :bar
    
    def baz(arg)
      puts "in baz"
      7 + arg
    end
  end


  def self.test_me

    tracing = CheapAdvice.new(:around) do | ar, result |
      puts "  TRACE: before #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")})"
      puts "         foo = #{@foo.inspect}"
      puts "         bar = #{@bar.inspect}"
      result = result.call
      puts "  TRACE: after  #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")}) => #{result.inspect}"
      ar.result = "yo!"
      puts "  TRACE: return #{ar.result.inspect}"
      "boy!"
    end
    
    puts "\n With tracing advice:\n"

    tracing.advise( [Foo, Bar], [ :bar, :bar=, :baz ])
    f = Foo.new
    b = Bar.new
    test_do_f(f, b)
    
    tracing.unadvise
    puts "\n Without tracing advice:\n"
    test_do_f(f, b)
    
    tracing
  end
  
  def self.test_do_f(f, b)
    puts "f.foo = 10 => #{(f.foo = 10).inspect}"
    puts "f.foo => #{(f.foo).inspect}"
    puts "f.baz(10) => #{(f.baz(10)).inspect}"

    puts "b.bar = 101 => #{(b.bar = 101).inspect}"
    puts "b.bar => #{(b.bar).inspect}"
    puts "b.baz(10) => #{(b.baz(10)).inspect}"
  end

end

