module StateMachines
  # A path represents a sequence of transitions that can be run for a particular
  # object.  Paths can walk to new transitions, revealing all of the possible
  # branches that can be encountered in the object's state machine.
  class Path < Array

    
    # The object whose state machine is being walked
    attr_reader :object
    
    # The state machine this path is walking
    attr_reader :machine
    
    # Creates a new transition path for the given object.  Initially this is an
    # empty path.  In order to start walking the path, it must be populated with
    # an initial transition.
    # 
    # Configuration options:
    # * <tt>:target</tt> - The target state to end the path on
    # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
    #   conditionals defined for each one
    def initialize(object, machine, options = {})
      options.assert_valid_keys(:target, :guard)
      
      @object = object
      @machine = machine
      @target = options[:target]
      @guard = options[:guard]
    end
    
    def initialize_copy(orig) #:nodoc:
      super
      @transitions = nil
    end
    
    # The initial state name for this path
    def from_name
      first && first.from_name
    end
    
    # Lists all of the from states that can be reached through this path.
    # 
    # For example,
    # 
    #   path.to_states  # => [:parked, :idling, :first_gear, ...]
    def from_states
      map {|transition| transition.from_name}.uniq
    end
    
    # The end state name for this path.  If a target state was specified for
    # the path, then that will be returned if the path is complete.
    def to_name
      last && last.to_name
    end
    
    # Lists all of the to states that can be reached through this path.
    # 
    # For example,
    # 
    #   path.to_states  # => [:parked, :idling, :first_gear, ...]
    def to_states
      map {|transition| transition.to_name}.uniq
    end
    
    # Lists all of the events that can be fired through this path.
    # 
    # For example,
    # 
    #   path.events # => [:park, :ignite, :shift_up, ...]
    def events
      map {|transition| transition.event}.uniq
    end
    
    # Walks down the next transitions at the end of this path.  This will only
    # walk down paths that are considered valid.
    def walk
      transitions.each {|transition| yield dup.push(transition)}
    end
    
    # Determines whether or not this path has completed.  A path is considered
    # complete when one of the following conditions is met:
    # * The last transition in the path ends on the target state
    # * There are no more transitions remaining to walk and there is no target
    #   state
    def complete?
      !empty? && (@target ? to_name == @target : transitions.empty?)
    end
    
    private
      # Calculates the number of times the given state has been walked to
      def times_walked_to(state)
        select {|transition| transition.to_name == state}.length
      end
      
      # Determines whether the given transition has been recently walked down in
      # this path.  If a target is configured for this path, then this will only
      # look at transitions walked down since the target was last reached.
      def recently_walked?(transition)
        transitions = self
        if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target}
          transitions = transitions[index(target_transition) + 1..-1]
        end
        transitions.include?(transition)
      end
      
      # Determines whether it's possible to walk to the given transition from
      # the current path.  A transition can be walked to if:
      # * It has not been recently walked and
      # * If a target is specified, it has not been walked to twice yet
      def can_walk_to?(transition)
        !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
      end
      
      # Get the next set of transitions that can be walked to starting from the
      # end of this path
      def transitions
        @transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)}
      end
  end
end
