# frozen_string_literal: true

module StateMachines
  class Machine
    module Configuration
      # Initializes a new state machine with the given configuration.
      def initialize(owner_class, *args, &)
        options = args.last.is_a?(Hash) ? args.pop : {}
        StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions, :async)

        # Find an integration that matches this machine's owner class
        @integration = if options.include?(:integration)
                         options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
                       else
                         StateMachines::Integrations.match(owner_class)
                       end

        if @integration
          extend @integration
          options = (@integration.defaults || {}).merge(options)
        end

        # Add machine-wide defaults
        options = { use_transactions: true, initialize: true }.merge(options)

        # Set machine configuration
        @name = args.first || :state
        @attribute = options[:attribute] || @name
        @events = EventCollection.new(self)
        @states = StateCollection.new(self)
        @callbacks = { before: [], after: [], failure: [] }
        @namespace = options[:namespace]
        @messages = options[:messages] || {}
        @action = options[:action]
        @use_transactions = options[:use_transactions]
        @initialize_state = options[:initialize]
        @action_hook_defined = false
        @async_requested = options[:async]

        self.owner_class = owner_class

        # Merge with sibling machine configurations
        add_sibling_machine_configs

        # Define class integration
        define_helpers
        define_scopes(options[:plural])
        after_initialize

        # Evaluate DSL
        instance_eval(&) if block_given?

        # Configure async mode if requested, after owner_class is set and DSL is evaluated
        configure_async_mode!(true) if @async_requested

        self.initial_state = options[:initial] unless sibling_machines.any?
      end

      # Creates a copy of this machine in addition to copies of each associated
      # event/states/callback, so that the modifications to those collections do
      # not affect the original machine.
      def initialize_copy(orig) # :nodoc:
        super

        @events = @events.dup
        @events.machine = self
        @states = @states.dup
        @states.machine = self
        @callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
        @async_requested = orig.instance_variable_get(:@async_requested)
        @async_mode_enabled = orig.instance_variable_get(:@async_mode_enabled)
      end

      # Sets the class which is the owner of this state machine.  Any methods
      # generated by states, events, or other parts of the machine will be defined
      # on the given owner class.
      def owner_class=(klass)
        @owner_class = klass

        # Create modules for extending the class with state/event-specific methods
        @helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) }
        owner_class.class_eval do
          extend helper_modules[:class]
          include helper_modules[:instance]
        end

        # Add class-/instance-level methods to the owner class for state initialization
        unless owner_class < StateMachines::InstanceMethods
          owner_class.class_eval do
            extend StateMachines::ClassMethods
            include StateMachines::InstanceMethods
          end

          define_state_initializer if @initialize_state
        end

        # Record this machine as matched to the name in the current owner class.
        # This will override any machines mapped to the same name in any superclasses.
        owner_class.state_machines[name] = self
      end

      # Sets the initial state of the machine.  This can be either the static name
      # of a state or a lambda block which determines the initial state at
      # creation time.
      def initial_state=(new_initial_state)
        @initial_state = new_initial_state
        add_states([@initial_state]) unless dynamic_initial_state?

        # Update all states to reflect the new initial state
        states.each { |state| state.initial = (state.name == @initial_state) }

        # Output a warning if there are conflicting initial states for the machine's
        # attribute
        initial_state = states.detect(&:initial)
        has_owner_default = !owner_class_attribute_default.nil?
        has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
        return unless has_owner_default && has_conflicting_default

        warn(
          "Both #{owner_class.name} and its #{name.inspect} machine have defined " \
          "a different default for \"#{attribute}\". Use only one or the other for " \
          'defining defaults to avoid unexpected behaviors.'
        )
      end

      # Gets the attribute name for the given machine scope.
      def attribute(name = :state)
        name == :state ? @attribute : :"#{self.name}_#{name}"
      end
    end
  end
end
