text: """A shot in MPF."""
  from mpf.core.device_monitor import DeviceMonitor
  
  from mpf.core.enable_disable_mixin import EnableDisableMixin
  
  import mpf.core.delays
  from mpf.core.events import event_handler
  from mpf.core.mode import Mode
  from mpf.core.mode_device import ModeDevice
  from mpf.core.player import Player
  
  
  @DeviceMonitor("state", "state_name")
  class Shot(EnableDisableMixin, ModeDevice):
  
      """A device which represents a generic shot."""
  
      config_section = 'shots'
      collection = 'shots'
      class_label = 'shot'
  
      monitor_enabled = False
      """Class attribute which specifies whether any monitors have been registered
      to track shots.
      """
  
      __slots__ = ["delay", "active_sequences", "active_delays", "running_show", "_handlers"]
  
      def __init__(self, machine, name):
          """Initialise shot."""
          # If this device is setup in a machine-wide config, make sure it has
          # a default enable event.
          super(Shot, self).__init__(machine, name)
  
          self.delay = mpf.core.delays.DelayManager(self.machine)
  
          self.active_sequences = list()
          """List of tuples: (id, current_position_index, next_switch)"""
          self.active_delays = set()
          self.running_show = None
          self._handlers = []
  
      def device_loaded_in_mode(self, mode: Mode, player: Player):
          """Add device to a mode that was already started.
  
          Automatically enables the shot and calls the the method
          that's usually called when a player's turn starts since that was missed
          since the mode started after that.
          """
          super().device_loaded_in_mode(mode, player)
          self._update_show()
  
      def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None):
          """Validate and parse shot config."""
          config = super().validate_and_parse_config(config, is_mode_config, debug_prefix)
          for switch in config['switch']:
              if switch not in config['switches']:
                  config['switches'].append(switch)
  
          for switch in config['switches'] + list(config['delay_switch'].keys()):
              if '{}_active'.format(config['playfield'].name) in switch.tags:
                  self.raise_config_error(
                      "Shot '{}' uses switch '{}' which has a "
                      "'{}_active' tag. This is handled internally by the device. Remove the "
                      "redundant '{}_active' tag from that switch.".format(
                          self.name, switch.name, config['playfield'].name,
                          config['playfield'].name), 1)
  
          return config
  
      def _register_switch_handlers(self):
          self._handlers = []
          for switch in self.config['switches']:
              self._handlers.append(self.machine.events.add_handler("{}_active".format(switch.name),
                                                                    self.event_hit, priority=self.mode.priority,
                                                                    blocking_facility="shot"))
  
          for switch in list(self.config['delay_switch'].keys()):
              self._handlers.append(self.machine.events.add_handler("{}_active".format(switch.name),
                                                                    self._delay_switch_hit,
                                                                    switch_name=switch.name,
                                                                    priority=self.mode.priority,
                                                                    blocking_facility="shot"))
  
      def _remove_switch_handlers(self):
          self.delay.clear()
          self.active_delays = set()
  
          self.machine.events.remove_handlers_by_keys(self._handlers)
          self._handlers = []
  
      @event_handler(6)
      def event_advance(self, force=False, **kwargs):
          """Handle advance control event."""
          del kwargs
          self.advance(force)
  
      def advance(self, force=False) -> bool:
          """Advance a shot profile forward.
  
          If this profile is at the last step and configured to loop, it will
          roll over to the first step. If this profile is at the last step and not
          configured to loop, this method has no effect.
          """
          if not self.enabled and not force:
              return False
  
          if not self.player:
              # no player no state
              return False
  
          profile_name = self.config['profile'].name
          state = self._get_state()
  
          self.debug_log("Advancing 1 step. Profile: %s, "
                         "Current State: %s", profile_name, state)
  
          if state + 1 >= len(self.config['profile'].config['states']):
  
              if self.config['profile'].config['loop']:
                  self._set_state(0)
  
              else:
                  return False
          else:
              self.debug_log("Advancing shot by one step.")
              self._set_state(state + 1)
  
          self._update_show()
          return True
  
      def _stop_show(self):
          if not self.running_show:
              return
          self.running_show.stop()
          self.running_show = None
  
      @property
      def can_rotate(self):
          """Return if the shot can be rotated according to its profile."""
          state = self.state_name
          return state not in self.profile.config['state_names_to_not_rotate']
  
      @property
      def state_name(self):
          """Return current state name."""
          if not self.player:
              # no player no state
              return "None"
          return self.config['profile'].config['states'][self._get_state()]['name']
  
      @property
      def state(self):
          """Return current state index."""
          return self._get_state()
  
      @property
      def profile_name(self):
          """Return profile name."""
          return self.config['profile'].name
  
      @property
      def profile(self):
          """Return profile."""
          return self.config['profile']
  
      def _get_state(self):
          if not self.player:
              return 0
          return self.player["shot_{}".format(self.name)]
  
      def _set_state(self, state):
          old = self.player["shot_{}".format(self.name)]
          try:
              old_name = self.state_name
          except IndexError:
              # In this case, the shot profile was changed and the old state index
              # doesn't exist in the new profile. That's okay, but we can't include
              # the old state name in our event.
              old_name = "unknown"
          self.player["shot_{}".format(self.name)] = state
          self.notify_virtual_change("state", old, state)
          self.notify_virtual_change("state_name", old_name, self.state_name)
  
      def _get_profile_settings(self):
          state = self._get_state()
          return self.profile.config['states'][state]
  
      def _update_show(self):
          if not self.enabled and not self.profile.config['show_when_disabled']:
              self._stop_show()
              return
  
          state = self._get_state()
          state_settings = self.profile.config['states'][state]
  
          if state_settings['show']:  # there's a show specified this state
              self._play_show(settings=state_settings)
  
          elif self.profile.config['show']:
              # no show for this state, but we have a profile root show
              self._play_show(settings=state_settings, start_step=state + 1)
  
          # if neither if/elif above happens, it means the current step has no
          # show but the previous step had one. We stop the previous show if there is one
          elif self.running_show:
              self._stop_show()
  
      def _play_show(self, settings, start_step=None):
          manual_advance = settings['manual_advance']
          if settings['show']:
              show_name = settings['show']
              if settings['manual_advance'] is None:
                  manual_advance = False
          else:
              show_name = self.profile.config['show']
              if settings['manual_advance'] is None:
                  manual_advance = True
  
          if settings['show_tokens'] and self.config['show_tokens']:
              show_tokens = dict(settings['show_tokens'])
              show_tokens.update(self.config['show_tokens'])
          elif settings['show_tokens']:
              show_tokens = settings['show_tokens']
          elif self.config['show_tokens']:
              show_tokens = self.config['show_tokens']
          else:
              show_tokens = {}
  
          if show_tokens:
              show_tokens = {k: v.evaluate({})
                             for k, v in show_tokens.items()}
  
          priority = settings['priority'] + self.mode.priority
          if not start_step:
              start_step = settings['start_step']
  
          self.debug_log("Playing show: %s. %s", show_name, settings)
  
          show_config = self.machine.show_controller.create_show_config(
              show_name, priority=priority, speed=settings.get("speed"),
              loops=settings.get("loops", -1), sync_ms=settings.get("sync_ms"), manual_advance=manual_advance,
              show_tokens=show_tokens, events_when_played=settings.get("events_when_played"),
              events_when_stopped=settings.get("events_when_stopped"),
              events_when_looped=settings.get("events_when_looped"),
              events_when_paused=settings.get("events_when_paused"),
              events_when_resumed=settings.get("events_when_resumed"),
              events_when_advanced=settings.get("events_when_advanced"),
              events_when_stepped_back=settings.get("events_when_stepped_back"),
              events_when_updated=settings.get("events_when_updated"),
              events_when_completed=settings.get("events_when_completed"))
          self.running_show = self.machine.show_controller.replace_or_advance_show(self.running_show, show_config,
                                                                                   start_step)
  
      def device_removed_from_mode(self, mode):
          """Remove this shot device.
  
          Destroys it and removes it from the shots collection.
          """
          super().device_removed_from_mode(mode)
          self._remove_switch_handlers()
          if self.running_show:
              self.running_show.stop()
              self.running_show = None
  
      @event_handler(5)
      def event_hit(self, **kwargs):
          """Handle hit control event."""
          success = self.hit()
          if not success:
              return None
  
          if self.profile.config['block']:
              min_priority = kwargs.get("_min_priority", {"all": 0})
              min_shots = min_priority.get("shot", 0)
              min_priority["shot"] = self.mode.priority if self.mode.priority > min_shots else min_shots
              return {"_min_priority": min_priority}
  
          return None
  
      def hit(self) -> bool:
          """Advance the currently-active shot profile.
  
          Note that the shot must be enabled in order for this hit to be
          processed.
  
          Returns true if the shot was enabled or false if the hit has been ignored.
          """
          # mark the playfield active no matter what
          self.config['playfield'].mark_playfield_active_from_device_action()
  
          if not self.enabled or not self.player:
              return False
  
          # Stop if there is an active delay but no sequence
          if self.active_delays:
              return False
  
          profile_settings = self._get_profile_settings()
  
          if not profile_settings:
              return False
  
          state = profile_settings['name']
  
          self.debug_log("Hit! Profile: %s, State: %s",
                         self.profile_name, state)
  
          if self.profile.config['advance_on_hit']:
              self.debug_log("Advancing shot because advance_on_hit is True.")
              advancing = self.advance()
          else:
              self.debug_log('Not advancing shot')
              advancing = False
  
          self._notify_monitors(self.config['profile'].name, state)
  
          self.machine.events.post('{}_hit'.format(self.name),
                                   profile=self.profile_name, state=state, advancing=advancing)
          '''event: (name)_hit
          desc: The shot called (name) was just hit.
  
          Note that there are four events posted when a shot is hit, each
          with variants of the shot name, profile, and current state,
          allowing you to key in on the specific granularity you need.
  
          args:
          profile: The name of the profile that was active when hit.
          state: The name of the state the profile was in when it was hit'''
  
          self.machine.events.post('{}_{}_hit'.format(self.name, self.profile_name),
                                   profile=self.profile_name, state=state, advancing=advancing)
          '''event: (name)_(profile)_hit
          desc: The shot called (name) was just hit with the profile (profile)
          active.
  
          Note that there are four events posted when a shot is hit, each
          with variants of the shot name, profile, and current state,
          allowing you to key in on the specific granularity you need.
  
          Also remember that shots can have more than one active profile at a
          time (typically each associated with a mode), so a single hit to this
          shot might result in this event being posted multiple times with
          different (profile) values.
  
          args:
          profile: The name of the profile that was active when hit.
          state: The name of the state the profile was in when it was hit'''
  
          self.machine.events.post('{}_{}_{}_hit'.format(self.name, self.profile_name, state),
                                   profile=self.profile_name, state=state, advancing=advancing)
          '''event: (name)_(profile)_(state)_hit
          desc: The shot called (name) was just hit with the profile (profile)
          active in the state (state).
  
          Note that there are four events posted when a shot is hit, each
          with variants of the shot name, profile, and current state,
          allowing you to key in on the specific granularity you need.
  
          Also remember that shots can have more than one active profile at a
          time (typically each associated with a mode), so a single hit to this
          shot might result in this event being posted multiple times with
          different (profile) and (state) values.
  
          args:
          profile: The name of the profile that was active when hit.
          state: The name of the state the profile was in when it was hit'''
  
          self.machine.events.post('{}_{}_hit'.format(self.name, state),
                                   profile=self.profile_name, state=state, advancing=advancing)
          '''event: (name)_(state)_hit
          desc: The shot called (name) was just hit while in the profile (state).
  
          Note that there are four events posted when a shot is hit, each
          with variants of the shot name, profile, and current state,
          allowing you to key in on the specific granularity you need.
  
          Also remember that shots can have more than one active profile at a
          time (typically each associated with a mode), so a single hit to this
          shot might result in this event being posted multiple times with
          different (profile) and (state) values.
  
          args:
          profile: The name of the profile that was active when hit.
          state: The name of the state the profile was in when it was hit'''
  
          return True
  
      def _notify_monitors(self, profile, state):
          if Shot.monitor_enabled and "shots" in self.machine.monitors:
              for callback in self.machine.monitors['shots']:
                  callback(name=self.name, profile=profile, state=state)
  
      @event_handler(4)
      def _delay_switch_hit(self, switch_name, **kwargs):
          del kwargs
          self.config['playfield'].mark_playfield_active_from_device_action()
          if not self.enabled:
              return
  
          self.delay.reset(name=switch_name + '_delay_timer',
                           ms=self.config['delay_switch']
                                         [self.machine.switches[switch_name]],
                           callback=self._release_delay,
                           switch=switch_name)
  
          self.active_delays.add(switch_name)
  
      def _release_delay(self, switch):
          self.active_delays.remove(switch)
  
      def jump(self, state, force=True, force_show=False):
          """Jump to a certain state in the active shot profile.
  
          Args:
          ----
              state: int of the state number you want to jump to. Note that states
                  are zero-based, so the first state is 0.
              force: if true, will jump even if the shot is disabled
              force_show: if true, will update the profile show even if the jumped
                  state index is the same as before the jump
  
          """
          self.debug_log("Received jump request. State: %s, Force: %s", state, force)
  
          if not self.enabled and not force:
              self.debug_log("Profile is disabled and force is False. Not jumping")
              return
  
          if not self.player:
              # no player no state
              return
  
          current_state = self._get_state()
          if state == current_state and not force_show:
              self.debug_log("Shot is already in the jump destination state")
              return
  
          self.debug_log("Jumping to profile state '%s'", state)
  
          self._set_state(state)
  
          self._update_show()
  
      @event_handler(1)
      def event_reset(self, **kwargs):
          """Handle reset control event."""
          del kwargs
          self.reset()
  
      def reset(self):
          """Reset the shot profile for the passed mode back to the first state (State 0) and reset all sequences."""
          self.debug_log("Resetting.")
  
          self.jump(state=0)
  
      @event_handler(2)
      def event_restart(self, **kwargs):
          """Handle restart control event."""
          del kwargs
          self.restart()
  
      def restart(self):
          """Restart the shot profile by calling reset() and enable().
  
          Automatically called when one fo the restart_events is called.
          """
          self.reset()
          self.enable()
  
      def _enable(self):
          super()._enable()
          self._register_switch_handlers()
          self._update_show()
  
      def _disable(self):
          super()._disable()
          self._remove_switch_handlers()
          self._update_show()
  