# frozen_string_literal: true

module Dry
  module Effects
    module Initializer
      # @api private
      module DefineWithHook
        # @api private
        def param(...)
          super.tap do
            @params_arity = nil
            __define_with__
          end
        end

        # @api private
        def option(...)
          super.tap do
            __define_with__ unless method_defined?(:with)
            @has_options = true
          end
        end

        # @api private
        def params_arity
          @params_arity ||= dry_initializer
            .definitions
            .reject { _2.option }
            .size
        end

        # @api private
        def options?
          return @has_options if defined? @has_options

          @has_options = false
        end

        # @api private
        def __define_with__
          seq_names = dry_initializer
            .definitions
            .reject { _2.option }
            .keys
            .join(", ")

          seq_names << ", " unless seq_names.empty?

          undef_method(:with) if method_defined?(:with)

          class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def with(**new_options)                                    # def with(**new_options)
              if new_options.empty?                                    #   if new_options.empty?
                self                                                   #     self
              else                                                     #   else
                self.class.new(#{seq_names}**options, **new_options)   #     self.class.new(attr1, attr2, **options, **new_options)
              end                                                      #   end
            end                                                        # end
          RUBY
        end
      end

      # @api private
      def self.extended(base)
        base.module_eval do
          if method_defined?(:with)
            undef_method(:with)
          end

          extend(::Dry::Initializer)
          extend(DefineWithHook)
          include(InstanceMethods)
        end
      end

      # @api private
      module InstanceMethods
        # Instance options
        #
        # @return [Hash]
        #
        # @api public
        def options
          @__options__ ||= self.class.dry_initializer.definitions.values.each_with_object({}) do |item, obj|
            obj[item.target] = instance_variable_get(item.ivar)
          end
        end

        define_method(:class, Kernel.instance_method(:class))
        define_method(:instance_variable_get, Kernel.instance_method(:instance_variable_get))

        # This makes sure we memoize options before an object becomes frozen
        #
        # @api public
        def freeze
          options
          super
        end
      end
    end
  end
end
