#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

module Callbacks
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def define_callbacks(*names, &block)
      names.each do |name|
        unless method_defined?("#{name}_with_callback")
          define_method(:"#{name}_with_callback") do |*args, &block|
            _run_callbacks(:"#{name}", [:before, :around], *args, &block)
            result = send(:"#{name}_without_callback", *args, &block)
            _run_callbacks(:"#{name}", [:around, :after], *args, &block)
            result
          end
          alias_method :"#{name}_without_callback", :"#{name}"
          alias_method :"#{name}", :"#{name}_with_callback"
        end
      end
    end
  end

  def _callbacks
    @_callbacks ||= {}
  end
  private :_callbacks

  def _run_callbacks(name, types, *args, &block)
    named_callbacks = _callbacks[name] || {}
    types.each do |type|
      (named_callbacks[type] || []).each do |callback|
        callback.call(*args, &block)
      end
    end
  end
  private :_run_callbacks

  def _insert_callbacks(names, type, block, options = {})
    prepend = options[:prepend]
    names.each do |name|
      named_callbacks = (_callbacks[:"#{name}"] ||= {})
      callbacks = (named_callbacks[type] ||= [])
      if prepend
        callbacks.unshift block
      else
        callbacks.push block
      end
    end
  end
  private :_insert_callbacks

  def _remove_callbacks(names, type, block)
    names.each do |name|
      named_callbacks = (_callbacks[:"#{name}"] ||= {})
      callbacks = (named_callbacks[type] ||= [])
      callbacks.delete block
    end
  end
  private :_remove_callbacks

  [:before, :after, :around].each do |type|
    define_method(:"#{type}_filter") do |*names, &block|
      _insert_callbacks(names, type, block)
    end

    define_method(:"prepend_#{type}_filter") do |*names, &block|
      _insert_callbacks(names, type, block, :prepend => true)
    end

    define_method(:"remove_#{type}_filter") do |*names, &block|
      _remove_callbacks(names, type, block)
    end

    alias_method :"append_#{type}_filter", :"#{type}_filter"
  end
end


class Record
  include Callbacks

  def save(v)
    puts "#{v}: - save"
  end
  define_callbacks :save
end

record = Record.new
record.before_filter :save do |v|
  puts "#{v}: saving..."
end
record.after_filter :save do |v|
  puts "#{v}: saved"
end

record.save 123

