# frozen_string_literal: true

module StateMachines
  # Represents a collection of events in a state machine
  class EventCollection < NodeCollection
    def initialize(machine) # :nodoc:
      super(machine, index: %i[name qualified_name])
    end

    # Gets the list of events that can be fired on the given object.
    #
    # Valid requirement options:
    # * <tt>:from</tt> - One or more states being transitioned from.  If none
    #   are specified, then this will be the object's current state.
    # * <tt>:to</tt> - One or more states being transitioned to.  If none are
    #   specified, then this will match any to state.
    # * <tt>:on</tt> - One or more events that fire the transition.  If none
    #   are specified, then this will match any event.
    # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
    #   conditionals defined for each one.  Default is true.
    #
    # == Examples
    #
    #   class Vehicle
    #     state_machine :initial => :parked do
    #       event :park do
    #         transition :idling => :parked
    #       end
    #
    #       event :ignite do
    #         transition :parked => :idling
    #       end
    #     end
    #   end
    #
    #   events = Vehicle.state_machine(:state).events
    #
    #   vehicle = Vehicle.new               # => #<Vehicle:0xb7c464b0 @state="parked">
    #   events.valid_for(vehicle)           # => [#<StateMachines::Event name=:ignite transitions=[:parked => :idling]>]
    #
    #   vehicle.state = 'idling'
    #   events.valid_for(vehicle)           # => [#<StateMachines::Event name=:park transitions=[:idling => :parked]>]
    def valid_for(object, requirements = {})
      match(requirements).select { |event| event.can_fire?(object, requirements) }
    end

    # Gets the list of transitions that can be run on the given object.
    #
    # Valid requirement options:
    # * <tt>:from</tt> - One or more states being transitioned from.  If none
    #   are specified, then this will be the object's current state.
    # * <tt>:to</tt> - One or more states being transitioned to.  If none are
    #   specified, then this will match any to state.
    # * <tt>:on</tt> - One or more events that fire the transition.  If none
    #   are specified, then this will match any event.
    # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
    #   conditionals defined for each one.  Default is true.
    #
    # == Examples
    #
    #   class Vehicle
    #     state_machine :initial => :parked do
    #       event :park do
    #         transition :idling => :parked
    #       end
    #
    #       event :ignite do
    #         transition :parked => :idling
    #       end
    #     end
    #   end
    #
    #   events = Vehicle.state_machine.events
    #
    #   vehicle = Vehicle.new                             # => #<Vehicle:0xb7c464b0 @state="parked">
    #   events.transitions_for(vehicle)                   # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
    #
    #   vehicle.state = 'idling'
    #   events.transitions_for(vehicle)                   # => [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
    #
    #   # Search for explicit transitions regardless of the current state
    #   events.transitions_for(vehicle, :from => :parked) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
    def transitions_for(object, requirements = {})
      match(requirements).map { |event| event.transition_for(object, requirements) }.compact
    end

    # Gets the transition that should be performed for the event stored in the
    # given object's event attribute.  This also takes an additional parameter
    # for automatically invalidating the object if the event or transition are
    # invalid.  By default, this is turned off.
    #
    # *Note* that if a transition has already been generated for the event, then
    # that transition will be used.
    #
    # == Examples
    #
    #   class Vehicle < ActiveRecord::Base
    #     state_machine :initial => :parked do
    #       event :ignite do
    #         transition :parked => :idling
    #       end
    #     end
    #   end
    #
    #   vehicle = Vehicle.new                       # => #<Vehicle id: nil, state: "parked">
    #   events = Vehicle.state_machine.events
    #
    #   vehicle.state_event = nil
    #   events.attribute_transition_for(vehicle)    # => nil # Event isn't defined
    #
    #   vehicle.state_event = 'invalid'
    #   events.attribute_transition_for(vehicle)    # => false # Event is invalid
    #
    #   vehicle.state_event = 'ignite'
    #   events.attribute_transition_for(vehicle)    # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
    def attribute_transition_for(object, invalidate = false)
      return unless machine.action

      # TODO, simplify
      # First try the regular event_transition
      transition = machine.read(object, :event_transition)

      # If not found and we have stored transitions by machine (issue #91)
      if !transition && (transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions))
        transition = transitions_by_machine[machine.name]
      end

      transition || if event_name = machine.read(object, :event)
                      if event = self[event_name.to_sym, :name]
                        event.transition_for(object) || begin
                          # No valid transition: invalidate
                          machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
                          false
                        end
                      else
                        # Event is unknown: invalidate
                        machine.invalidate(object, :event, :invalid) if invalidate
                        false
                      end
                    end
    end

    private

    def match(requirements) # :nodoc:
      requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
    end
  end
end
