# typed: true
# frozen_string_literal: true

module Micro::Struct
  class Factory
    module CreateStruct
      extend self

      def with(members, features, struct_block)
        struct = create_struct(members)

        ClassScope.def_new(struct, members)

        ClassScope.def_features(struct, features) if features.is_a?(Features::Exposed)
        ClassScope.def_to_proc(struct)            if features.option?(:to_proc)
        ClassScope.def_private_writers(struct)    if features.option?(:readonly)

        InstanceScope.def_with(struct)    if features.option?(:instance_copy)
        InstanceScope.def_to_ary(struct)  if features.option?(:to_ary)
        InstanceScope.def_to_hash(struct) if features.option?(:to_hash)

        ClassScope.evaluate(struct, struct_block)

        struct
      end

      private

      def create_struct(members)
        ::Struct.new(*members.required_and_optional)
      end

      module ClassScope
        def self.def_new(struct, members)
          # The .new() method will require all required keyword arguments.
          # We are doing this because the Struct constructor keyword init option treats everything as optional.
          #
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            class << self
              undef_method :new

              def new(#{members.to_eval.keyword_args})                         # def new(a:, b:, c: nil) do
                instance = allocate                                            #   instance = allocate
                instance.send(:initialize, #{members.to_eval.positional_args}) #   instance.send(:initialize, a, b, c)
                instance                                                       #   instance
              end                                                              # end

              alias __new__ new
            end
          RUBY
        end

        def self.def_features(struct, features)
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            class << self
              attr_accessor :__features__

              alias features __features__
            end
          RUBY

          struct.__features__ = features

          struct.send(:private_class_method, :__features__=)
        end

        def self.def_to_proc(struct)
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def self.to_proc
              ->(hash) { new(**hash) }
            end
          RUBY
        end

        def self.def_private_writers(struct)
          struct.send(:private, :[]=)
          struct.members.each { |member| struct.send(:private, "#{member}=") }
        end

        def self.evaluate(struct, block)
          struct.class_eval(&block) if block
        end
      end

      module InstanceScope
        def self.def_to_ary(struct)
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def to_ary
              to_a
            end
          RUBY
        end

        def self.def_to_hash(struct)
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def to_hash
              to_h
            end
          RUBY
        end

        def self.def_with(struct)
          struct.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def with(**members)
              self.class.new(**to_h.merge(members))
            end
          RUBY
        end
      end
    end

    private_constant :CreateStruct
  end
end
