# = Advisable
#
# Add before, after and around advice in dynamic fashion.
#
# == Authors
#
# * TransAspect
#
# == Copying
#
# Copyright (c) 2007 Thomas Sawyer
#
# Ruby License
#
# This module is free software. You may use, modify, and/or redistribute this
# software under the same terms as Ruby.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.

require 'facets/1stclassmethod'
require 'facets/unboundmethod/arguments'

# = Advisable
#
# Advisable provides a means of using before, after and
# around adivce.

module Advisable

  def advice_before
    @advice_before ||= {} #Hash.new{|h,k| h[k] = []}
  end

  def advice_after
    @advice_after ||= {} #Hash.new{|h,k| h[k] = []}
  end

  def advice_around
    @advice_around ||= {} #Hash.new{|h,k| h[k] = []}
  end

  def before(meth, &block)
    name = "#{meth}:before#{block.object_id}"
    define_method(name, &block)
    (advice_before[meth.to_sym] ||= []) << name
  end

  def after(meth, &block)
    name = "#{meth}:after#{block.object_id}"
    define_method(name, &block)
    (advice_after[meth.to_sym] ||= []) << name
  end

  def around(meth, &block)
    name = "#{meth}:around#{block.object_id}"
    define_method(name, &block)
    (advice_around[meth.to_sym] ||= []) << name
  end

  # Advise a method.

  def advise(meth)
    #return false if defined?(meth_origin)
    args = instance_method(meth).arguments

    module_eval(<<-END, __FILE__, __LINE__)
      alias_method '#{meth}_origin', '#{meth}'
      def #{meth}(#{args})
        target = method!('#{meth}_origin')

        unless target.advised
          ancs = self.class.ancestors.select{ |anc| anc.respond_to?(:advice_before) }
          target.advice_before = ancs.collect{ |anc| anc.advice_before[:'#{meth}'] }
          target.advice_after  = ancs.collect{ |anc| anc.advice_after[:'#{meth}'] }
          target.advice_around = ancs.collect{ |anc| anc.advice_around[:'#{meth}'] }
          target.advised = true
        end

        target.call_with_advice(self, *[#{args}])
      end
    END
  end

  #advise(:method_added)

  #after :method_added do |meth|
  #  advise(meth) unless defined?("#{meth}_orig")
  #end

  def method_added(meth)
    return if meth == :method_added
    @added_stack ||= []
    return if @added_stack.last == meth
    return if /_(origin)$/ =~ meth.to_s
    return if /:(before|after|around)/ =~ meth.to_s
    @added_stack << meth
    #return if instance_methods(false).include?("#{meth}_orig")
    advise(meth)
    @added_stack.pop
  end

  # Extensions for Method class.

  module Method

    attr_accessor :advised

    attr_reader :advice_before
    attr_reader :advice_after
    attr_reader :advice_around

    def advised?
      @advised
    end

    def advice_before=(set); @advice_before = set.flatten.compact; end
    def advice_after=(set) ;  @advice_after = set.flatten.compact; end
    def advice_around=(set); @advice_around = set.flatten.compact; end

    # Call with advice.

    def call_with_advice(obj, *args, &blk)
      advice_before.each do |name|
        #advice.call(*args, &blk)
        obj.send(name, *args, &blk)
      end

      target = lambda{ call(*args, &blk) }
      advice_around.each do |name|
        target = lambda_target(obj, name, target, *args, &blk)
      end
      ret = target.call

      advice_after.reverse_each do |name|
        #advice.call(*args, &blk)
        obj.send(name, *args, &blk)
      end

      return ret
    end

    private

    # Using separate method for this prevents infinite loop.

    def lambda_target(obj, name, target, *args, &blk)
      lambda do
        #advice.call(target, *args, &blk)
        obj.send(name, target, *args, &blk)
      end
    end

  end

end

class Method #:nodoc:
  include Advisable::Method
end

