require 'super_callbacks/version'

module SuperCallbacks
  VALID_OPTION_KEYS = [:if].freeze

  def self.included(base)
    base.singleton_class.send :attr_accessor, *[:before_callbacks, :after_callbacks]
    base.send :attr_accessor, *[:before_callbacks, :after_callbacks]
    base.extend ClassMethods
    base.send :include, InstanceMethods
    base.extend ClassAndInstanceMethods
    base.send :include, ClassAndInstanceMethods
    base.send :prepend, Prepended.new
  end

  class Prepended < Module
  end

  module Helpers
    # (modified) File activesupport/lib/active_support/core_ext/hash/deep_merge.rb, line 18
    def self.deep_merge_hashes_and_combine_arrays(this_hash, other_hash, &block)
      self.deep_merge_hashes_and_combine_arrays!(this_hash.dup, other_hash, &block)
    end

    # (modified) File activesupport/lib/active_support/core_ext/hash/deep_merge.rb, line 23
    def self.deep_merge_hashes_and_combine_arrays!(this_hash, other_hash, &block)
      this_hash.merge!(other_hash) do |key, this_val, other_val|
        if this_val.is_a?(Hash) && other_val.is_a?(Hash)
          self.deep_merge_hashes(this_val, other_val, &block)
        elsif this_val.is_a?(Array) && other_val.is_a?(Array)
          this_val + other_val
        elsif block_given?
          block.call(key, this_val, other_val)
        else
          other_val
        end
      end
    end
  end

  module ClassAndInstanceMethods
    def before!(method_name, *remaining_args)
      raise ArgumentError, "`#{method_name}` is not or not yet defined for #{self}" unless method_defined? method_name
      before(method_name, *remaining_args)
    end

    def after!(method_name, *remaining_args)
      raise ArgumentError, "`#{method_name}` is not or not yet defined for #{self}" unless method_defined? method_name
      before(method_name, *remaining_args)
    end

    def before(method_name, callback_method_name = nil, options = {}, &callback_proc)
      callback_method_name_or_proc = callback_proc || callback_method_name
      unless [Symbol, String, Proc].any? { |klass| callback_method_name_or_proc.is_a? klass }
        raise ArgumentError, "Only `Symbol`, `String` or `Proc` allowed for `method_name`, but is #{callback_method_name_or_proc.class}"
      end

      invalid_option_keys = options.keys - VALID_OPTION_KEYS
      unless invalid_option_keys.empty?
        raise ArgumentError, "Invalid `options` keys: #{invalid_option_keys}. Valid are only: #{VALID_OPTION_KEYS}"
      end
      if options[:if] && !([Symbol, String, Proc].any? { |klass| callback_method_name_or_proc.is_a? klass })
        raise ArgumentError, "Only `Symbol`, `String` or `Proc` allowed for `options[:if]`, but is #{options[:if].class}"
      end

      self.before_callbacks ||= {}
      self.before_callbacks[method_name.to_sym] ||= []
      self.before_callbacks[method_name.to_sym] << [callback_method_name_or_proc, options[:if]]

      _callbacks_prepended_module_instance = callbacks_prepended_module_instance

      # dont redefine, to save cpu cycles
      unless _callbacks_prepended_module_instance.method_defined? method_name
        _callbacks_prepended_module_instance.send(:define_method, method_name) do |*args|
          run_before_callbacks(method_name, *args)
          super_value = super(*args)
          run_after_callbacks(method_name, *args)
          super_value
        end
      end
    end

    def after(method_name, callback_method_name = nil, options = {}, &callback_proc)
      callback_method_name_or_proc = callback_proc || callback_method_name
      unless [Symbol, String, Proc].include? callback_method_name_or_proc.class
        raise ArgumentError, "Only `Symbol`, `String` or `Proc` allowed for `method_name`, but is #{callback_method_name_or_proc.class}"
      end

      invalid_option_keys = options.keys - VALID_OPTION_KEYS
      unless invalid_option_keys.empty?
        raise ArgumentError, "Invalid `options` keys: #{invalid_option_keys}. Valid are only: #{VALID_OPTION_KEYS}"
      end
      if options[:if] && ![Symbol, String, Proc].include?(options[:if].class)
        raise ArgumentError, "Only `Symbol`, `String` or `Proc` allowed for `options[:if]`, but is #{options[:if].class}"
      end

      self.after_callbacks ||= {}
      self.after_callbacks[method_name.to_sym] ||= []
      self.after_callbacks[method_name.to_sym] << [callback_method_name_or_proc, options[:if]]

      _callbacks_prepended_module_instance = callbacks_prepended_module_instance

      # dont redefine, to save cpu cycles
      unless _callbacks_prepended_module_instance.method_defined? method_name
        _callbacks_prepended_module_instance.send(:define_method, method_name) do |*args|
          run_before_callbacks(method_name, *args)
          super_value = super(*args)
          run_after_callbacks(method_name, *args)
          super_value
        end
      end
    end

    # TODO
    # def around
    # end
  end

  module ClassMethods

    private

    def callbacks_prepended_module_instance
      ancestors.reverse.detect { |ancestor| ancestor.is_a? SuperCallbacks::Prepended }
    end
  end

  module InstanceMethods
    # TODO: optimize by instead of dynamically getting all_ancestral_after_callbacks on runtime
    # set them immediately when `include` is called on Base class
    def run_before_callbacks(method_name, *args)
      all_ancestral_before_callbacks = self.class.ancestors.reverse.each_with_object({}) do |ancestor, hash|
        SuperCallbacks::Helpers.deep_merge_hashes_and_combine_arrays!(
          hash,
          ancestor.instance_variable_get(:@before_callbacks) || {}
        )
      end

      singleton_class_before_callbacks = instance_variable_get(:@before_callbacks) || {}

      all_before_callbacks = SuperCallbacks::Helpers.deep_merge_hashes_and_combine_arrays(
        all_ancestral_before_callbacks,
        singleton_class_before_callbacks
      )

      all_before_callbacks_on_method = all_before_callbacks[method_name] || []

      all_before_callbacks_on_method.each do |before_callback, options_if|
        is_condition_truthy = true

        if options_if
          is_condition_truthy = instance_exec *args, &options_if
        end

        if is_condition_truthy
          if before_callback.is_a? Proc
            instance_exec *args, &before_callback
          else
            send before_callback
          end
        end
      end
    end

    # TODO: optimize by instead of dynamically getting all_ancestral_after_callbacks on runtime
    # set them immediately when `include` is called on Base class
    def run_after_callbacks(method_name, *args)
      all_ancestral_after_callbacks = self.class.ancestors.reverse.each_with_object({}) do |ancestor, hash|
        SuperCallbacks::Helpers.deep_merge_hashes_and_combine_arrays!(
          hash,
          ancestor.instance_variable_get(:@after_callbacks) || {}
        )
      end

      singleton_class_after_callbacks = instance_variable_get(:@after_callbacks) || {}

      all_after_callbacks = SuperCallbacks::Helpers.deep_merge_hashes_and_combine_arrays(
        all_ancestral_after_callbacks,
        singleton_class_after_callbacks
      )

      all_after_callbacks_on_method = all_after_callbacks[method_name] || []

      all_after_callbacks_on_method.each do |after_callback, options_if|
        is_condition_truthy = true

        if options_if
          is_condition_truthy = instance_exec *args, &options_if
        end

        if is_condition_truthy
          if after_callback.is_a? Proc
            instance_exec *args, &after_callback
          else
            send after_callback
          end
        end
      end
    end

    private

    def callbacks_prepended_module_instance
      _callbacks_prepended_module_instance = self.singleton_class.ancestors.reverse.detect { |ancestor| ancestor.is_a? SuperCallbacks::Prepended }

      if _callbacks_prepended_module_instance.nil?
        self.singleton_class.prepend SuperCallbacks::Prepended
        _callbacks_prepended_module_instance = self.singleton_class.ancestors.reverse.detect { |ancestor| ancestor.is_a? SuperCallbacks::Prepended }
      end

      _callbacks_prepended_module_instance
    end
  end
end