module Democritus
  # Responsible for building a class based on the customization's applied
  # through the #customize method.
  #
  # @see ./spec/lib/democritus/class_builder_spec.rb
  #
  # :reek:UnusedPrivateMethod: { exclude: [ !ruby/regexp /(method_missing|respond_to_missing)/ ] }
  class ClassBuilder
    # @param command_namespaces [Array<Module>] the sequential list of namespaces you want to check for each registered command.
    def initialize(command_namespaces: default_command_namespaces)
      self.customization_module = Module.new
      self.generation_module = Module.new
      self.class_operations = []
      self.instance_operations = []
      self.command_namespaces = command_namespaces
    end

    private

    # The module that receives customized method definitions.
    #
    # @example
    #   Democritus.build do |builder|
    #     builder.a_command
    #     def a_customization
    #     end
    #   end
    #
    # The above #a_customization method is captured in the customization_module and applied as an instance method
    # to the generated class.
    attr_accessor :customization_module

    # The module with the generated code.
    #
    # @example
    #   Democritus.build do |builder|
    #     builder.a_command
    #     def a_customization
    #     end
    #   end
    #
    # The above `builder.a_command` invocation is captured and is applied to the generation_module.
    attr_accessor :generation_module

    # Command operations to be applied as class methods of the generated_class.
    attr_accessor :class_operations

    # The default namespaces in which Democritus will look up commands.
    def default_command_namespaces
      [Democritus::ClassBuilder::Commands]
    end

    # The command namespaces that you want to use. Note, order is important
    attr_reader :command_namespaces

    def command_namespaces=(input)
      @command_namespaces = Array(input).map do |command_namespace|
        case command_namespace
        when Module, Class
          command_namespace
        else
          Object.const_get(command_namespace.to_s)
        end
      end
    end

    # Command operations to be applied as instance methods of the generated_class.
    attr_accessor :instance_operations

    public

    # @api public
    #
    # Responsible for executing the customization block against the
    # customization module with the builder class as a parameter.
    #
    # @yield [Democritus::ClassBuilder] the means to build your custom class.
    #
    # @example
    #   ClassBuilder.new.customize do |builder|
    #     builder.command('paramter')
    #     def to_s; 'parameter'; end
    #   end
    #
    # @return nil
    # @see ./spec/lib/democritus/class_builder_spec.rb
    def customize(&customization_block)
      return unless customization_block
      customization_module.module_exec(self, &customization_block)
      return nil
    end

    # @api public
    #
    # Responsible for generating a Class object based on the customizations
    # applied via a customize block.
    #
    # @example
    #   dynamic_class = Democritus::ClassBuilder.new.generate_class
    #   an_instance_of_the_dynamic_class = dynamic_class.new
    #
    # @return Class object
    #
    # rubocop:disable MethodLength
    # :reek:TooManyStatements: { exclude: [ 'Democritus::ClassBuilder#generate_class' ] }
    def generate_class
      generation_mod = generation_module # get a local binding
      customization_mod = customization_module # get a local binding
      apply_operations(instance_operations, generation_mod)
      generated_class = Class.new do
        # requires the local binding from above
        const_set :GeneratedMethods, generation_mod
        const_set :Customizations, customization_mod
        # because == and case operators are useful
        include DemocritusObjectTag
        include generation_mod

        # customization should be applied last as it allows for "overrides" of generated methods
        include customization_mod
      end
      generated_class
    end
    # rubocop:enable MethodLength

    # @api public
    #
    # When configuring the class that is being built, we don't want to apply all of the modifications at once, instead allowing them
    # to be applied in a specified order.
    #
    # @example
    #   Democritus::ClassBuilder.new.defer(prepend: true) do
    #     define_method(:help) { 'Did you try turning it off and on again?' }
    #   end
    #
    # @param [Hash] options
    # @option options [Boolean] :prepend Is there something about this deferred_operation that should happen first?
    # @param deferred_operation [#call] The operation that will be applied to the generated class
    #
    # @return void
    def defer(**options, &deferred_operation)
      if options[:prepend]
        instance_operations.unshift(deferred_operation)
      else
        instance_operations << deferred_operation
      end
    end

    private

    # :reek:UtilityFunction: { exclude: [ 'Democritus::ClassBuilder#apply_operations' ] }
    def apply_operations(operations, module_or_class)
      operations.each do |operation|
        operation.call(module_or_class)
      end
    end

    # @!group Method Missing

    private

    # @api public
    #
    # The guts of the Democritus plugin system. The ClassBuilder brokers missing methods to registered commands within the
    # CommandNamespace.
    #
    # @param method_name [Symbol] Name of the message being sent to this object
    # @param args Non-keyword arguments for the message sent to this object
    # @param kargs Keyword arguments for the message sent to this object
    # @param block Block argument for the message sent to this object
    # @return void if there is a Command object that is called
    # @return unknown if no Command object is found
    def method_missing(method_name, *args, **kargs, &block)
      command_name = self.class.command_name_for_method(method_name)
      command_namespace = command_namespace_for(command_name)
      if command_namespace
        command_class = command_namespace.const_get(command_name)
        command_class.new(*args, **kargs.merge(builder: self), &block).call
      else
        super
      end
    end

    # @api public
    #
    # A required sibling method when implementing #method_missing
    #
    # @param method_name [Symbol] Name of the message being sent to this object
    # @param args Additional arguments passed to the query
    # @return Boolean
    def respond_to_missing?(method_name, *args)
      respond_to_definition(method_name, :respond_to_missing?, *args)
    end

    # @api private
    def respond_to_definition(method_name, *)
      command_name = self.class.command_name_for_method(method_name)
      command_namespace_for(command_name)
    end

    # @api private
    #
    # Find the first matching the command_namespace that contains the given
    # command_name
    def command_namespace_for(command_name)
      command_namespaces.detect { |cs| cs.const_defined?(command_name) }
    end
    # @!endgroup

    class << self
      # @api public
      #
      # Convert the given :method_name into a "constantized" method name.
      #
      # @example
      #   Democritus::ClassBuilder.command_name_for_method(:test_command) == 'TestCommand'
      #
      # @param method_name [#to_s]
      # @return String
      def command_name_for_method(method_name)
        method_name.to_s.gsub(/(?:^|_)([a-z])/) { Regexp.last_match[1].upcase }
      end
    end
  end
end