text: import abc
  
  import pbpstats
  from pbpstats.resources.enhanced_pbp import Foul
  
  
  class FreeThrow(metaclass=abc.ABCMeta):
      """
      Class for free throw events
      """
  
      shot_type = pbpstats.FREE_THROW_STRING
  
      @abc.abstractproperty
      def is_made(self):
          """
          returns True if shot was made, False otherwise
          """
          pass
  
      @abc.abstractproperty
      def is_ft_1_of_1(self):
          pass
  
      @abc.abstractproperty
      def is_ft_1_of_2(self):
          pass
  
      @abc.abstractproperty
      def is_ft_2_of_2(self):
          pass
  
      @abc.abstractproperty
      def is_ft_1_of_3(self):
          pass
  
      @abc.abstractproperty
      def is_ft_2_of_3(self):
          pass
  
      @abc.abstractproperty
      def is_ft_3_of_3(self):
          pass
  
      @property
      def is_first_ft(self):
          """
          returns True if free throw is first of trip to the free throw line, False otherwise
          """
          return (
              self.is_ft_1_of_1
              or self.is_ft_1_of_2
              or self.is_ft_1_of_3
              or self.is_ft_1pt
              or self.is_ft_2pt
              or self.is_ft_3pt
          )
  
      @property
      def is_end_ft(self):
          """
          returns True if free throw is last of trip to the free throw line, False otherwise
          """
          return (
              self.is_ft_1_of_1
              or self.is_ft_2_of_2
              or self.is_ft_3_of_3
              or self.is_ft_1pt
              or self.is_ft_2pt
              or self.is_ft_3pt
          ) and not self.is_flagrant_ft
  
      @abc.abstractproperty
      def is_technical_ft(self):
          pass
  
      @abc.abstractproperty
      def is_ft_1pt(self):
          pass
  
      @abc.abstractproperty
      def is_ft_2pt(self):
          pass
  
      @abc.abstractproperty
      def is_ft_3pt(self):
          pass
  
      @property
      def shot_value(self):
          """
          returns shot value of a free throw
          Starting in 2019-20 season, the G-League added 2 and 3 point FTs
          """
          if self.is_ft_2pt:
              return 2
          if self.is_ft_3pt:
              return 3
          return 1
  
      @property
      def is_away_from_play_ft(self):
          """
          returns True if free throw is from an away from the play foul, False otherwise.
          """
          foul = self.foul_that_led_to_ft
          if (
              (
                  (self.is_ft_1_of_1 or self.is_ft_1pt)
                  or (self.is_ft_2_of_2 or self.is_ft_2pt)
              )
              and foul is not None
              and foul.is_away_from_play_foul
          ):
              made_shots_at_event_time = []
              fts_by_other_player_at_event_time = []
              events_at_event_time = self.get_all_events_at_current_time()
              for event in events_at_event_time:
                  if hasattr(event, "is_made") and event.is_made:
                      if not isinstance(event, FreeThrow):
                          made_shots_at_event_time.append(event)
                      if (
                          isinstance(event, FreeThrow)
                          and event.player1_id != self.player1_id
                      ):
                          fts_by_other_player_at_event_time.append(event)
  
              if len(made_shots_at_event_time) == 0:
                  # check for made free throw by other player - this is where player is fouled going for rebound on made FT
                  if len(fts_by_other_player_at_event_time) == 0:
                      return True
                  for ft in fts_by_other_player_at_event_time:
                      if ft.team_id != self.team_id:
                          return True
              else:
                  made_shots_at_event_time = sorted(
                      made_shots_at_event_time, key=lambda k: k.order
                  )
                  if (made_shots_at_event_time[0].team_id == foul.team_id) and (
                      self.player1_id != made_shots_at_event_time[0].player1_id
                  ):
                      # make sure player who made shot is not player who shot FT
                      return True
  
          return False
  
      @property
      def is_inbound_foul_ft(self):
          """
          returns True if free throw is from an inbound foul, False otherwise.
          """
          if self.is_ft_1_of_1 or self.is_ft_1pt:
              events_at_event_time = self.get_all_events_at_current_time()
              for event in events_at_event_time:
                  if isinstance(event, Foul) and event.is_inbound_foul:
                      return True
  
          return False
  
      @property
      def foul_that_led_to_ft(self):
          """
          returns :obj:`~pbpstats.resources.enhanced_pbp.foul.Foul` object for the foul
          that resulted in the free throw
          """
          clock = self.clock
          # foul should be before FT so start by going backwards
          event = self
          while (
              event is not None
              and event.clock == clock
              and not (
                  isinstance(event, Foul)
                  and not event.is_technical
                  and not event.is_double_technical
              )
          ):
              event = event.previous_event
  
          if (
              isinstance(event, Foul)
              and not event.is_technical
              and not event.is_double_technical
              and event.clock == clock
          ):
              return event
  
          # bug in pbp where foul is after FT
          event = self
          while (
              event is not None
              and event.clock == clock
              and not (
                  isinstance(event, Foul)
                  and not event.is_technical
                  and not event.is_double_technical
              )
          ):
              event = event.next_event
  
          if (
              isinstance(event, Foul)
              and not event.is_technical
              and not event.is_double_technical
              and event.clock == clock
          ):
              return event
          return None
  
      @property
      def num_ft_for_trip(self):
          """
          returns number of shots for the trip to the free throw line
          """
          if "of 1" in self.description:
              return 1
          elif "of 2" in self.description:
              return 2
          elif "of 3" in self.description:
              return 3
  
      @property
      def free_throw_type(self):
          """
          returns string description of free throw type
          """
          if self.is_technical_ft:
              return "Technical"
          num_fts = self.num_ft_for_trip
          if num_fts is None:
              num_fts = self.shot_value
  
          if num_fts == 1:
              # check for shot before FT at same time as FT
              previous_event = self
              while (
                  previous_event is not None
                  and previous_event.clock == self.clock
                  and not (
                      hasattr(previous_event, "is_made")
                      and previous_event.is_made
                      and not isinstance(previous_event, FreeThrow)
                  )
              ):
                  previous_event = previous_event.previous_event
              if (
                  previous_event is not None
                  and previous_event.clock == self.clock
                  and (
                      hasattr(previous_event, "is_made")
                      and previous_event.is_made
                      and not isinstance(previous_event, FreeThrow)
                  )
              ):
                  and1_shot = previous_event
                  if self.player1_id == and1_shot.player1_id:
                      return f"{and1_shot.shot_value}pt And 1"
                  else:
                      return "1 Shot Away From Play"
              else:
                  return "1 Shot Away From Play"
          foul_event = self.foul_that_led_to_ft
          if foul_event.is_shooting_foul or foul_event.is_shooting_block_foul:
              return f"{num_fts}pt Shooting Foul"
          elif foul_event.is_flagrant:
              if num_fts is None:
                  # assume 2 shot flagrant if num_fts is None
                  num_fts = 2
              return f"{num_fts} Shot Flagrant"
          elif foul_event.is_away_from_play_foul:
              return f"{num_fts} Shot Away From Play"
          elif foul_event.is_inbound_foul:
              return f"{num_fts} Shot Inbound Foul"
          elif foul_event.is_clear_path_foul:
              return f"{num_fts} Shot Clear Path"
          elif num_fts == 3:
              return "3pt Shooting Foul"
          else:
              return "Penalty"
  
      @property
      def event_for_efficiency_stats(self):
          """
          returns :obj:`~pbpstats.resources.enhanced_pbp.foul.Foul` object for the foul
          that resulted in the free throw. Plus/minus points should go to
          the players on the floor at the time of the foul, not the free throw.
          """
          clock = self.clock
          # foul should be before FT so start by going backwards
          event = self
          while (
              event is not None and event.clock == clock and not isinstance(event, Foul)
          ):
              event = event.previous_event
  
          if isinstance(event, Foul) and event.clock == clock:
              return event
  
          # bug in pbp where foul is after FT
          event = self
          while (
              event is not None and event.clock == clock and not isinstance(event, Foul)
          ):
              event = event.next_event
  
          if isinstance(event, Foul) and event.clock == clock:
              return event
          return self
  
      @property
      def event_stats(self):
          """
          returns list of dicts with all stats for event
          """
          stats = []
          team_ids = list(self.current_players.keys())
          is_penalty_event = self.is_penalty_event()
          is_second_chance_event = self.is_second_chance_event()
          lineup_ids = self.event_for_efficiency_stats.lineup_ids
          if self.is_made:
              stats += self._get_made_ft_stat_items(
                  is_second_chance_event, is_penalty_event
              )
              # add plus minus and opponent points - used for lineup/wowy stats to get net rating
              points = self.shot_value
              for (
                  team_id,
                  players,
              ) in self.event_for_efficiency_stats.current_players.items():
                  multiplier = 1 if team_id == self.team_id else -1
                  opponent_team_id = (
                      team_ids[0] if team_id == team_ids[1] else team_ids[1]
                  )
                  for player_id in players:
                      stat_item = {
                          "player_id": player_id,
                          "team_id": team_id,
                          "opponent_team_id": opponent_team_id,
                          "lineup_id": lineup_ids[team_id],
                          "opponent_lineup_id": lineup_ids[opponent_team_id],
                          "stat_key": pbpstats.PLUS_MINUS_STRING,
                          "stat_value": points * multiplier,
                      }
                      stats.append(stat_item)
                      if multiplier == -1:
                          opponent_points_stat_item = {
                              "player_id": player_id,
                              "team_id": team_id,
                              "opponent_team_id": opponent_team_id,
                              "lineup_id": lineup_ids[team_id],
                              "opponent_lineup_id": lineup_ids[opponent_team_id],
                              "stat_key": pbpstats.OPPONENT_POINTS,
                              "stat_value": points,
                          }
                          stats.append(opponent_points_stat_item)
          if self.is_first_ft or self.is_technical_ft:
              stats += self._get_ft_trip_stat_items(
                  is_second_chance_event, is_penalty_event
              )
          if not self.is_made:
              stats += self._get_missed_ft_stat_items(
                  is_second_chance_event, is_penalty_event
              )
  
          opponent_team_id = team_ids[0] if self.team_id == team_ids[1] else team_ids[1]
          for stat in stats:
              if "lineup_id" not in stat.keys():
                  opponent_team_id = (
                      team_ids[0] if stat["team_id"] == team_ids[1] else team_ids[1]
                  )
                  stat["lineup_id"] = lineup_ids[stat["team_id"]]
                  stat["opponent_team_id"] = opponent_team_id
                  stat["opponent_lineup_id"] = lineup_ids[opponent_team_id]
  
          return self.base_stats + stats
  
      def _get_made_ft_stat_items(self, is_second_chance_event, is_penalty_event):
          stats = []
          if self.is_ft_3pt:
              free_throw_key = pbpstats.FT_3_PT_MADE_STRING
          elif self.is_ft_2pt:
              free_throw_key = pbpstats.FT_2_PT_MADE_STRING
          elif self.is_ft_1pt:
              free_throw_key = pbpstats.FT_1_PT_MADE_STRING
          elif self.is_technical_ft:
              free_throw_key = pbpstats.TECHNICAL_FTS_MADE_STRING
          else:
              free_throw_key = pbpstats.FTS_MADE_STRING
  
          stats.append(
              {
                  "player_id": self.player1_id,
                  "team_id": self.team_id,
                  "stat_key": free_throw_key,
                  "stat_value": 1,
              }
          )
          if is_second_chance_event:
              second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_key}"
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": second_chance_stat_key,
                      "stat_value": 1,
                  }
              )
          if is_penalty_event:
              penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_key}"
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": penalty_stat_key,
                      "stat_value": 1,
                  }
              )
              foul_event = self.foul_that_led_to_ft
              if (
                  foul_event is not None
                  and foul_event.is_personal_take_foul
                  and self.seconds_remaining < 60
                  and self.period >= 4
              ):
                  final_minute_take_foul_stat_key = (
                      f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{free_throw_key}"
                  )
                  stats.append(
                      {
                          "player_id": self.player1_id,
                          "team_id": self.team_id,
                          "stat_key": final_minute_take_foul_stat_key,
                          "stat_value": 1,
                      }
                  )
          return stats
  
      def _get_missed_ft_stat_items(self, is_second_chance_event, is_penalty_event):
          stats = []
          if self.is_ft_3pt:
              free_throw_key = pbpstats.FT_3_PT_MISSED_STRING
          elif self.is_ft_2pt:
              free_throw_key = pbpstats.FT_2_PT_MISSED_STRING
          elif self.is_ft_1pt:
              free_throw_key = pbpstats.FT_1_PT_MISSED_STRING
          else:
              free_throw_key = pbpstats.FTS_MISSED_STRING
          stats.append(
              {
                  "player_id": self.player1_id,
                  "team_id": self.team_id,
                  "stat_key": free_throw_key,
                  "stat_value": 1,
              }
          )
          if is_second_chance_event:
              second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_key}"
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": second_chance_stat_key,
                      "stat_value": 1,
                  }
              )
          if is_penalty_event:
              penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_key}"
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": penalty_stat_key,
                      "stat_value": 1,
                  }
              )
              foul_event = self.foul_that_led_to_ft
              if (
                  foul_event is not None
                  and foul_event.is_personal_take_foul
                  and self.seconds_remaining < 60
                  and self.period >= 4
              ):
                  final_minute_take_foul_stat_key = (
                      f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{free_throw_key}"
                  )
                  stats.append(
                      {
                          "player_id": self.player1_id,
                          "team_id": self.team_id,
                          "stat_key": final_minute_take_foul_stat_key,
                          "stat_value": 1,
                      }
                  )
          return stats
  
      def _get_ft_trip_stat_items(self, is_second_chance_event, is_penalty_event):
          stats = []
          free_throw_trip_key = self.free_throw_type + " Free Throw Trips"
          stats.append(
              {
                  "player_id": self.player1_id,
                  "team_id": self.team_id,
                  "stat_key": free_throw_trip_key,
                  "stat_value": 1,
              }
          )
          if is_second_chance_event:
              second_chance_stat_key = (
                  f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_trip_key}"
              )
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": second_chance_stat_key,
                      "stat_value": 1,
                  }
              )
          if is_penalty_event:
              penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_trip_key}"
              stats.append(
                  {
                      "player_id": self.player1_id,
                      "team_id": self.team_id,
                      "stat_key": penalty_stat_key,
                      "stat_value": 1,
                  }
              )
              foul_event = self.foul_that_led_to_ft
              if (
                  foul_event is not None
                  and foul_event.is_personal_take_foul
                  and self.seconds_remaining < 60
                  and self.period >= 4
              ):
                  current_players = self.event_for_efficiency_stats.current_players
                  offense_team_id = self.get_offense_team_id()
                  for team_id, players in current_players.items():
                      final_minute_take_foul_possessions_stat_key = (
                          f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{pbpstats.OFFENSIVE_POSSESSION_STRING}"
                          if team_id == offense_team_id
                          else f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{pbpstats.DEFENSIVE_POSSESSION_STRING}"
                      )
                      for player_id in players:
                          stat_item = {
                              "player_id": player_id,
                              "team_id": team_id,
                              "stat_key": final_minute_take_foul_possessions_stat_key,
                              "stat_value": 1,
                          }
                          stats.append(stat_item)
          return stats
  