require "ostruct"

module Backup
  module Config
    module Helpers
      def self.included(klass)
        klass.extend ClassMethods
      end

      module ClassMethods
        def defaults
          @defaults ||= Config::Defaults.new

          if block_given?
            yield @defaults
          else
            @defaults
          end
        end

        # Used only within the specs
        def clear_defaults!
          defaults.reset!
        end

        def deprecations
          @deprecations ||= {}
        end

        def log_deprecation_warning(name, deprecation)
          msg = "#{self}##{name} has been deprecated as of " \
              "backup v.#{deprecation[:version]}"
          msg << "\n#{deprecation[:message]}" if deprecation[:message]
          Logger.warn Config::Error.new(<<-EOS)
            [DEPRECATION WARNING]
            #{msg}
          EOS
        end

        protected

        ##
        # Method to deprecate an attribute.
        #
        # :version
        #   Must be set to the backup version which will first
        #   introduce the deprecation.
        #
        # :action
        #   If set, this Proc will be called with a reference to the
        #   class instance and the value set on the deprecated accessor.
        #   e.g. deprecation[:action].call(klass, value)
        #   This should perform whatever action is neccessary, such as
        #   transferring the value to a new accessor.
        #
        # :message
        #   If set, this will be appended to #log_deprecation_warning
        #
        # Note that this replaces the `attr_accessor` method, or other
        # method previously used to set the accessor being deprecated.
        # #method_missing will handle any calls to `name=`.
        #
        def attr_deprecate(name, args = {})
          deprecations[name] = {
            version: nil,
            message: nil,
            action: nil
          }.merge(args)
        end
      end # ClassMethods

      private

      ##
      # Sets any pre-configured default values.
      # If a default value was set for an invalid accessor,
      # this will raise a NameError.
      def load_defaults!
        self.class.defaults._attributes.each do |name|
          val = self.class.defaults.send(name)
          val = val.dup rescue val
          send(:"#{ name }=", val)
        end
      end

      ##
      # Check missing methods for deprecated attribute accessors.
      #
      # If a value is set on an accessor that has been deprecated
      # using #attr_deprecate, a warning will be issued and any
      # :action (Proc) specified will be called with a reference to
      # the class instance and the value set on the deprecated accessor.
      # See #attr_deprecate and #log_deprecation_warning
      #
      # Note that OpenStruct (used for setting defaults) does not allow
      # multiple arguments when assigning values for members.
      # So, we won't allow it here either, even though an attr_accessor
      # will accept and convert them into an Array. Therefore, setting
      # an option value using multiple values, whether as a default or
      # directly on the class' accessor, should not be supported.
      # i.e. if an option will accept being set as an Array, then it
      # should be explicitly set as such. e.g. option = [val1, val2]
      #
      def method_missing(name, *args)
        deprecation = nil
        if method = name.to_s.chomp!("=")
          if (len = args.count) != 1
            raise ArgumentError,
              "wrong number of arguments (#{len} for 1)", caller(1)
          end
          deprecation = self.class.deprecations[method.to_sym]
        end

        if deprecation
          self.class.log_deprecation_warning(method, deprecation)
          deprecation[:action].call(self, args[0]) if deprecation[:action]
        else
          super
        end
      end
    end # Helpers

    # Store for pre-configured defaults.
    class Defaults < OpenStruct
      # Returns an Array of all attribute method names
      # that default values were set for.
      def _attributes
        @table.keys
      end

      # Used only within the specs
      def reset!
        @table.clear
      end
    end
  end
end
