## signal-system.rb --- simple intra-process signal system
# Copyright (C) 2005  Daniel Brockman

# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation;
# either version 2 of the License, or (at your option) any
# later version.

# This file 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.
# See the GNU General Public License for more details.

# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the Free
# Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.

require "puppet/external/event-loop/better-definers"

module SignalEmitterModule
  def self.extended (object)
    if object.kind_of? Module and not object < SignalEmitter
      if object.respond_to? :fcall
        # This is the way to call private methods
        # in Ruby 1.9 as of November 16.
        object.fcall :include, SignalEmitter
      else
        object.__send__ :include, SignalEmitter
      end
    end
  end

  def define_signal (name, slot=:before, &body)
    # Can't use `define_method' and take a block pre-1.9.
    class_eval %{ def on_#{name} &block
      add_signal_handler(:#{name}, &block) end }
    define_signal_handler(name, :before, &lambda {|*a|})
    define_signal_handler(name, :after, &lambda {|*a|})
    define_signal_handler(name, slot, &body) if block_given?
  end

  def define_signals (*names, &body)
    names.each { |x| define_signal(x, &body) }
  end

  def define_signal_handler (name, slot=:before, &body)
    case slot
    when :before
      define_protected_method "handle_#{name}", &body
    when :after
      define_protected_method "after_handle_#{name}", &body
    else
      raise ArgumentError, "invalid slot `#{slot.inspect}'; " +
        "should be `:before' or `:after'", caller(1)
    end
  end
end

# This is an old name for the same thing.
SignalEmitterClass = SignalEmitterModule

module SignalEmitter
  def self.included (includer)
    includer.extend SignalEmitterClass if not includer.kind_of? SignalEmitterClass
  end

  def __maybe_initialize_signal_emitter
    @signal_handlers ||= Hash.new { |h, k| h[k] = Array.new }
    @allow_dynamic_signals ||= false
  end

  define_accessors :allow_dynamic_signals?

  def add_signal_handler (name, &handler)
    __maybe_initialize_signal_emitter
    @signal_handlers[name] << handler
    handler
  end

  define_soft_aliases [:on, :on_signal] => :add_signal_handler

  def remove_signal_handler (name, handler)
    __maybe_initialize_signal_emitter
    @signal_handlers[name].delete(handler)
  end

  def __signal__ (name, *args, &block)
    __maybe_initialize_signal_emitter
    respond_to? "on_#{name}" or allow_dynamic_signals? or
    fail "undefined signal `#{name}' for #{self}:#{self.class}"
    __send__("handle_#{name}", *args, &block) if
      respond_to? "handle_#{name}"
    @signal_handlers[name].each { |x| x.call(*args, &block) }
    __send__("after_handle_#{name}", *args, &block) if
      respond_to? "after_handle_#{name}"
  end

  define_soft_alias :signal => :__signal__
end

# This module is indended to be a convenience mixin to be used by
# classes whose objects need to observe foreign signals.  That is,
# if you want to observe some signals coming from an object, *you*
# should mix in this module.
#
# You cannot use this module at two different places of the same
# inheritance chain to observe signals coming from the same object.
#
# XXX: This has not seen much use, and I'd like to provide a
#      better solution for the problem in the future.
module SignalObserver
  def __maybe_initialize_signal_observer
    @observed_signals ||= Hash.new do |signals, object|
      signals[object] = Hash.new do |handlers, name|
        handlers[name] = Array.new
      end
    end
  end

  def observe_signal (subject, name, &handler)
    __maybe_initialize_signal_observer
    @observed_signals[subject][name] << handler
    subject.add_signal_handler(name, &handler)
  end

  def map_signals (source, pairs={})
    pairs.each do |src_name, dst_name|
      observe_signal(source, src_name) do |*args|
        __signal__(dst_name, *args)
      end
    end
  end

  def absorb_signals (subject, *names)
    names.each do |name|
      observe_signal(subject, name) do |*args|
        __signal__(name, *args)
      end
    end
  end

  define_soft_aliases \
    :map_signal    => :map_signals,
    :absorb_signal => :absorb_signals

  def ignore_signal (subject, name)
    __maybe_initialize_signal_observer
    __ignore_signal_1(subject, name)
    @observed_signals.delete(subject) if
      @observed_signals[subject].empty?
  end

  def ignore_signals (subject, *names)
    __maybe_initialize_signal_observer
    names = @observed_signals[subject] if names.empty?
    names.each { |x| __ignore_signal_1(subject, x) }
  end

  private

  def __ignore_signal_1(subject, name)
    @observed_signals[subject][name].each do |handler|
      subject.remove_signal_handler(name, handler) end
    @observed_signals[subject].delete(name)
  end
end

if __FILE__ == $0
  require "test/unit"
  class SignalEmitterTest < Test::Unit::TestCase
    class X
      include SignalEmitter
      define_signal :foo
    end

    def setup
      @x = X.new
    end

    def test_on_signal
      moomin = 0
      @x.on_signal(:foo) { moomin = 1 }
      @x.signal :foo
      assert moomin == 1
    end

    def test_on_foo
      moomin = 0
      @x.on_foo { moomin = 1 }
      @x.signal :foo
      assert moomin == 1
    end

    def test_multiple_on_signal
      moomin = 0
      @x.on_signal(:foo) { moomin += 1 }
      @x.on_signal(:foo) { moomin += 2 }
      @x.on_signal(:foo) { moomin += 4 }
      @x.on_signal(:foo) { moomin += 8 }
      @x.signal :foo
      assert moomin == 15
    end

    def test_multiple_on_foo
      moomin = 0
      @x.on_foo { moomin += 1 }
      @x.on_foo { moomin += 2 }
      @x.on_foo { moomin += 4 }
      @x.on_foo { moomin += 8 }
      @x.signal :foo
      assert moomin == 15
    end
  end
end

## application-signals.rb ends here.
