# frozen_string_literal: true

module Toys
  ##
  # A mixin definition. Mixin modules should include this module.
  #
  # A mixin is a collection of methods that are available to be called from a
  # tool implementation (i.e. its run method). The mixin is added to the tool
  # class, so it has access to the same methods that can be called by the tool,
  # such as {Toys::Context#get}.
  #
  # ### Usage
  #
  # To create a mixin, define a module, and include this module. Then define
  # the methods you want to be available.
  #
  # If you want to perform some initialization specific to the mixin, you can
  # provide an *initializer* block and/or an *inclusion* block. These can be
  # specified by calling the module methods defined in
  # {Toys::Mixin::ModuleMethods}.
  #
  # The initializer block is called when the tool context is instantiated
  # in preparation for execution. It has access to context methods such as
  # {Toys::Context#get}, and can perform setup for the tool execution itself,
  # such as initializing some persistent state and storing it in the tool using
  # {Toys::Context#set}. The initializer block is passed any extra arguments
  # that were provided to the `include` directive. Define the initializer by
  # calling {Toys::Mixin::ModuleMethods#on_initialize}.
  #
  # The inclusion block is called in the context of your tool class when your
  # mixin is included. It is also passed any extra arguments that were provided
  # to the `include` directive. It can be used to issue directives to define
  # tools or other objects in the DSL, or even enhance the DSL by defining DSL
  # methods specific to the mixin. Define the inclusion block by calling
  # {Toys::Mixin::ModuleMethods#on_include}.
  #
  # ### Example
  #
  # This is an example that implements a simple counter. Whenever the counter
  # is incremented, a log message is emitted. The tool can also retrieve the
  # final counter value.
  #
  #     # Define a mixin by creating a module that includes Toys::Mixin
  #     module MyCounterMixin
  #       include Toys::Mixin
  #
  #       # Initialize the counter. Notice that the initializer is evaluated
  #       # in the context of the runtime context, so has access to the runtime
  #       # context state.
  #       on_initialize do |start = 0|
  #         set(:counter_value, start)
  #       end
  #
  #       # Mixin methods are evaluated in the runtime context and so have
  #       # access to the runtime context state, just as if you had defined
  #       # them in your tool.
  #       def counter_value
  #         get(:counter_value)
  #       end
  #
  #       def increment
  #         set(:counter_value, counter_value + 1)
  #         logger.info("Incremented counter")
  #       end
  #     end
  #
  # Now we can use it from a tool:
  #
  #     tool "count-up" do
  #       # Pass 1 as an extra argument to the mixin initializer
  #       include MyCounterMixin, 1
  #
  #       def run
  #         # Mixin methods can be called.
  #         5.times { increment }
  #         puts "Final value is #{counter_value}"
  #       end
  #     end
  #
  module Mixin
    ##
    # Create a mixin module with the given block.
    #
    # @param block [Proc] Defines the mixin module.
    # @return [Class]
    #
    def self.create(&block)
      mixin_mod = ::Module.new do
        include ::Toys::Mixin
      end
      mixin_mod.module_eval(&block) if block
      mixin_mod
    end

    ##
    # Methods that will be added to a mixin module object.
    #
    module ModuleMethods
      ##
      # Set the initializer for this mixin. This block is evaluated in the
      # runtime context before execution, and is passed any arguments provided
      # to the `include` directive. It can perform any runtime initialization
      # needed by the mixin.
      #
      # @param block [Proc] Sets the initializer proc.
      # @return [self]
      #
      def on_initialize(&block)
        self.initializer = block
        self
      end

      ##
      # The initializer proc for this mixin. This proc is evaluated in the
      # runtime context before execution, and is passed any arguments provided
      # to the `include` directive. It can perform any runtime initialization
      # needed by the mixin.
      #
      # @return [Proc] The iniitiliazer for this mixin.
      #
      attr_accessor :initializer

      ##
      # Set an inclusion proc for this mixin. This block is evaluated in the
      # tool class immediately after the mixin is included, and is passed any
      # arguments provided to the `include` directive.
      #
      # @param block [Proc] Sets the inclusion proc.
      # @return [self]
      #
      def on_include(&block)
        self.inclusion = block
        self
      end

      ##
      # The inclusion proc for this mixin. This block is evaluated in the tool
      # class immediately after the mixin is included, and is passed any
      # arguments provided to the `include` directive.
      #
      # @return [Proc] The inclusion procedure for this mixin.
      #
      attr_accessor :inclusion
    end

    ##
    # @private
    #
    def self.included(mod)
      return if mod.respond_to?(:on_initialize)
      mod.extend(ModuleMethods)
    end
  end
end
