module Sequel
  module Plugins
    # The AssociationDependencies plugin allows you do easily set up before and/or after destroy hooks
    # for destroying, deleting, or nullifying associated model objects.  The following
    # association types support the following dependency actions:
    # 
    # * :many_to_many - :nullify (removes all related entries in join table)
    # * :many_to_one - :delete, :destroy
    # * :one_to_many, one_to_one - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
    #
    # This plugin works directly with the association datasets and does not use any cached association values.
    # The :delete action will delete all associated objects from the database in a single SQL call.
    # The :destroy action will load each associated object from the database and call the destroy method on it.
    #
    # To set up an association dependency, you must provide a hash with association name symbols
    # and dependency action values.  You can provide the hash to the plugin call itself or
    # to the add_association_dependencies method:
    #
    #   Business.plugin :association_dependencies, :address=>:delete
    #   # or:
    #   Artist.plugin :association_dependencies
    #   Artist.add_association_dependencies :albums=>:destroy, :reviews=>:delete, :tags=>:nullify
    module AssociationDependencies
      # Mapping of association types to when the dependency calls should be made (either
      # :before for in before_destroy or :after for in after_destroy)
      ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before, :one_to_one=>:before}

      # The valid dependence actions
      DEPENDENCE_ACTIONS = [:delete, :destroy, :nullify]

      # Initialize the association_dependencies hash for this model.
      def self.apply(model, hash={})
        model.instance_eval{@association_dependencies = {:before_delete=>[], :before_destroy=>[], :before_nullify=>[], :after_delete=>[], :after_destroy=>[]}}
      end

      # Call add_association_dependencies with any dependencies given in the plugin call.
      def self.configure(model, hash={})
        model.add_association_dependencies(hash) unless hash.empty?
      end

      module ClassMethods
        # A hash specifying the association dependencies for each model.  The keys
        # are symbols indicating the type of action and when it should be executed
        # (e.g. :before_delete).  Values are an array of method symbols.
        # For before_nullify, the symbols are remove_all_association methods.  For other
        # types, the symbols are association_dataset methods, on which delete or
        # destroy is called.
        attr_reader :association_dependencies

        # Add association dependencies to this model.  The hash should have association name
        # symbol keys and dependency action symbol values (e.g. :albums=>:destroy).
        def add_association_dependencies(hash)
          hash.each do |association, action|
            raise(Error, "Nonexistent association: #{association}") unless r = association_reflection(association)
            type = r[:type]
            raise(Error, "Invalid dependence action type: association: #{association}, dependence action: #{action}") unless DEPENDENCE_ACTIONS.include?(action)
            raise(Error, "Invalid association type: association: #{association}, type: #{type}") unless time = ASSOCIATION_MAPPING[type]
            association_dependencies[:"#{time}_#{action}"] << if action == :nullify
              case type
              when :one_to_many , :many_to_many
                proc{send(r.remove_all_method)}
              when :one_to_one
                proc{send(r.setter_method, nil)}
              else
                raise(Error, "Can't nullify many_to_one associated objects: association: #{association}")
              end
            else
              raise(Error, "Can only nullify many_to_many associations: association: #{association}") if type == :many_to_many
              r.dataset_method
            end
          end
        end

        # Copy the current model object's association_dependencies into the subclass.
        def inherited(subclass)
          super
          ad = association_dependencies.dup
          ad.keys.each{|k| ad[k] = ad[k].dup}
          subclass.instance_variable_set(:@association_dependencies, ad)
        end
      end

      module InstanceMethods
        # Run the delete and destroy association dependency actions for
        # many_to_one associations.
        def after_destroy
          super
          model.association_dependencies[:after_delete].each{|m| send(m).delete}
          model.association_dependencies[:after_destroy].each{|m| send(m).destroy}
        end

        # Run the delete, destroy, and nullify association dependency actions for
        # *_to_many associations.
        def before_destroy
          model.association_dependencies[:before_delete].each{|m| send(m).delete}
          model.association_dependencies[:before_destroy].each{|m| send(m).destroy}
          model.association_dependencies[:before_nullify].each{|p| instance_eval(&p)}
          super
        end
      end
    end
  end
end
