import pandas as pd
import re
from .constants import ROSTER_SCHEME, SQUAD_URL
from ..decorators import float_property_decorator, int_property_decorator
from .fb_utils import _lookup_team
from pyquery import PyQuery as pq
from sportsreference.utils import (_get_stats_table,
                                   _parse_field,
                                   _remove_html_comment_tags)
from urllib.error import HTTPError


class SquadPlayer:
    """
    Get player information and stats.

    Given a player ID and data, capture all relevant stats and information for
    the player including name, nationality, goals, assists, expected goal
    difference, nutmegs, and much more.

    Parameters
    ----------
    player_data : PyQuery object
        A PyQuery object containing all fields of information for a single
        player, represented as one long row by concatenating all tables which
        hold values for the requested player.
    player_id : string
        A ``string`` representation of the player's unique 8-digit ID as shown
        on fbref.com.
    """
    def __init__(self, player_data, player_id):
        self._name = None
        self._player_id = player_id
        self._nationality = None
        self._position = None
        self._age = None
        self._matches_played = None
        self._starts = None
        self._minutes = None
        self._goals = None
        self._assists = None
        self._penalty_kicks = None
        self._penalty_kick_attempts = None
        self._yellow_cards = None
        self._red_cards = None
        self._goals_per_90 = None
        self._assists_per_90 = None
        self._goals_and_assists_per_90 = None
        self._goals_non_penalty_per_90 = None
        self._goals_and_assists_non_penalty_per_90 = None
        self._expected_goals = None
        self._expected_goals_non_penalty = None
        self._expected_assists = None
        self._expected_goals_per_90 = None
        self._expected_assists_per_90 = None
        self._expected_goals_and_assists_per_90 = None
        self._expected_goals_non_penalty_per_90 = None
        self._expected_goals_and_assists_non_penalty_per_90 = None
        self._own_goals = None
        # Goalkeeping stats
        self._goals_against = None
        self._own_goals_against = None
        self._goals_against_per_90 = None
        self._shots_on_target_against = None
        self._saves = None
        self._save_percentage = None
        self._wins = None
        self._draws = None
        self._losses = None
        self._clean_sheets = None
        self._clean_sheet_percentage = None
        self._penalty_kicks_attempted = None
        self._penalty_kicks_allowed = None
        self._penalty_kicks_saved = None
        self._penalty_kicks_missed = None
        # Advanced goalkeeping stats
        self._free_kick_goals_against = None
        self._corner_kick_goals_against = None
        self._post_shot_expected_goals = None
        self._post_shot_expected_goals_per_shot = None
        self._post_shot_expected_goals_minus_allowed = None
        self._post_shot_expected_goals_minus_allowed_per_90 = None
        self._launches_completed = None
        self._launches_attempted = None
        self._launch_completion_percentage = None
        self._keeper_passes_attempted = None
        self._throws_attempted = None
        self._launch_percentage = None
        self._average_keeper_pass_length = None
        self._goal_kicks_attempted = None
        self._goal_kick_launch_percentage = None
        self._average_goal_kick_length = None
        self._opponent_cross_attempts = None
        self._opponent_cross_stops = None
        self._opponent_cross_stop_percentage = None
        self._keeper_actions_outside_penalty_area = None
        self._keeper_actions_outside_penalty_area_per_90 = None
        self._average_keeper_action_outside_penalty_distance = None
        # Shooting stats
        self._shots = None
        self._shots_on_target = None
        self._free_kick_shots = None
        self._shots_on_target_percentage = None
        self._shots_per_90 = None
        self._shots_on_target_per_90 = None
        self._goals_per_shot = None
        self._goals_per_shot_on_target = None
        self._expected_goals_non_penalty_per_shot = None
        self._goals_minus_expected = None
        self._non_penalty_minus_expected_non_penalty = None
        # Passing stats
        self._assists_minus_expected = None
        self._key_passes = None
        self._passes_completed = None
        self._passes_attempted = None
        self._pass_completion = None
        self._short_passes_completed = None
        self._short_passes_attempted = None
        self._short_pass_completion = None
        self._medium_passes_completed = None
        self._medium_passes_attempted = None
        self._medium_pass_completion = None
        self._long_passes_completed = None
        self._long_passes_attempted = None
        self._long_pass_completion = None
        self._left_foot_passes = None
        self._right_foot_passes = None
        self._free_kick_passes = None
        self._through_balls = None
        self._corner_kicks = None
        self._throw_ins = None
        self._final_third_passes = None
        self._penalty_area_passes = None
        self._penalty_area_crosses = None
        # Playing time stats
        self._minutes_per_match = None
        self._minutes_played_percentage = None
        self._nineties_played = None
        self._minutes_per_start = None
        self._subs = None
        self._minutes_per_sub = None
        self._unused_sub = None
        self._points_per_match = None
        self._goals_scored_on_pitch = None
        self._goals_against_on_pitch = None
        self._goal_difference_on_pitch = None
        self._goal_difference_on_pitch_per_90 = None
        self._net_difference_on_pitch_per_90 = None
        self._expected_goals_on_pitch = None
        self._expected_goals_against_on_pitch = None
        self._expected_goal_difference = None
        self._expected_goal_difference_per_90 = None
        self._net_expected_goal_difference_per_90 = None
        # Miscellaneous stats
        self._soft_reds = None
        self._fouls_committed = None
        self._fouls_drawn = None
        self._offsides = None
        self._crosses = None
        self._tackles_won = None
        self._interceptions = None
        self._penalty_kicks_won = None
        self._penalty_kicks_conceded = None
        self._successful_dribbles = None
        self._attempted_dribbles = None
        self._dribble_success_rate = None
        self._players_dribbled_past = None
        self._nutmegs = None
        self._dribblers_tackled = None
        self._dribblers_contested = None
        self._tackle_percentage = None
        self._times_dribbled_past = None

        self._parse_player_stats(player_data)

    def _parse_nationality(self, player_data):
        """
        Parse the player's nationality.

        If the nationality is listed for a player, it will contain a URI which
        includes the name of the country the player represents. For example, an
        English player would have a URI of the following:
        "/en/country/ENG/England-Football". Pulling out the country name and
        returning it as a string is a simple solution to pulling someone's
        nationality.

        Parameters
        ----------
        player_data : PyQuery object
            A PyQuery object representing all of the player's stats fields
            combined as a singular row.

        Returns
        -------
        string
            Returns a ``string`` of the player's home country, such as
            'England'.
        """
        country = player_data(ROSTER_SCHEME['nationality'])
        if not country:
            return None
        country = country.attr('href')
        country = re.sub(r'.*\/', '', country)
        country = country.replace('-Football', '')
        return country

    def _parse_player_stats(self, player_data):
        """
        Parse a value for every attribute.

        This method looks through every class attribute with a few exceptions
        and retrieves the value according to the parsing scheme and index of
        the attribute from the passed HTML data. Once the value is retrieved,
        the attribute's value is updated with the returned result.

        Parameters
        ----------
        player_data : string
            A ``string`` representation of all of the player's stats fields
            combined as a singular row.
        player_id : string
            A ``string`` of the player's unique 8-digit ID.
        """
        for field in self.__dict__:
            # The short field truncates the leading '_' in the attribute name.
            short_field = str(field)[1:]
            if short_field == 'player_id':
                continue
            if short_field == 'nationality':
                value = self._parse_nationality(player_data)
            else:
                value = _parse_field(ROSTER_SCHEME, player_data, short_field)
            setattr(self, field, value)

    @property
    def dataframe(self):
        """
        Returns a pandas ``DataFame`` containing all other class properties
        and values. The index for the DataFrame is the player ID.
        """
        fields_to_include = {
            'name': self.name,
            'player_id': self.player_id,
            'nationality': self.nationality,
            'position': self.position,
            'age': self.age,
            'matches_played': self.matches_played,
            'starts': self.starts,
            'minutes': self.minutes,
            'goals': self.goals,
            'assists': self.assists,
            'penalty_kicks': self.penalty_kicks,
            'penalty_kick_attempts': self.penalty_kick_attempts,
            'yellow_cards': self.yellow_cards,
            'red_cards': self.red_cards,
            'goals_per_90': self.goals_per_90,
            'assists_per_90': self.assists_per_90,
            'goals_and_assists_per_90': self.goals_and_assists_per_90,
            'goals_non_penalty_per_90': self.goals_non_penalty_per_90,
            'goals_and_assists_non_penalty_per_90':
            self.goals_and_assists_non_penalty_per_90,
            'expected_goals': self.expected_goals,
            'expected_goals_non_penalty': self.expected_goals_non_penalty,
            'expected_assists': self.expected_assists,
            'expected_goals_per_90': self.expected_goals_per_90,
            'expected_assists_per_90': self.expected_assists_per_90,
            'expected_goals_and_assists_per_90':
            self.expected_goals_and_assists_per_90,
            'expected_goals_non_penalty_per_90':
            self.expected_goals_non_penalty_per_90,
            'expected_goals_and_assists_non_penalty_per_90':
            self.expected_goals_and_assists_non_penalty_per_90,
            'own_goals': self.own_goals,
            'goals_against': self.goals_against,
            'own_goals_against': self.own_goals_against,
            'goals_against_per_90': self.goals_against_per_90,
            'shots_on_target_against': self.shots_on_target_against,
            'saves': self.saves,
            'save_percentage': self.save_percentage,
            'wins': self.wins,
            'draws': self.draws,
            'losses': self.losses,
            'clean_sheets': self.clean_sheets,
            'clean_sheet_percentage': self.clean_sheet_percentage,
            'penalty_kicks_attempted': self.penalty_kicks_attempted,
            'penalty_kicks_allowed': self.penalty_kicks_allowed,
            'penalty_kicks_saved': self.penalty_kicks_saved,
            'penalty_kicks_missed': self.penalty_kicks_missed,
            'free_kick_goals_against': self.free_kick_goals_against,
            'corner_kick_goals_against': self.corner_kick_goals_against,
            'post_shot_expected_goals': self.post_shot_expected_goals,
            'post_shot_expected_goals_per_shot':
            self.post_shot_expected_goals_per_shot,
            'post_shot_expected_goals_minus_allowed':
            self.post_shot_expected_goals_minus_allowed,
            'launches_completed': self.launches_completed,
            'launches_attempted': self.launches_attempted,
            'launch_completion_percentage': self.launch_completion_percentage,
            'keeper_passes_attempted': self.keeper_passes_attempted,
            'throws_attempted': self.throws_attempted,
            'launch_percentage': self.launch_percentage,
            'average_keeper_pass_length': self.average_keeper_pass_length,
            'goal_kicks_attempted': self.goal_kicks_attempted,
            'goal_kick_launch_percentage': self.goal_kick_launch_percentage,
            'average_goal_kick_length': self.average_goal_kick_length,
            'opponent_cross_attempts': self.opponent_cross_attempts,
            'opponent_cross_stops': self.opponent_cross_stops,
            'opponent_cross_stop_percentage':
            self.opponent_cross_stop_percentage,
            'keeper_actions_outside_penalty_area':
            self.keeper_actions_outside_penalty_area,
            'keeper_actions_outside_penalty_area_per_90':
            self.keeper_actions_outside_penalty_area_per_90,
            'average_keeper_action_outside_penalty_distance':
            self.average_keeper_action_outside_penalty_distance,
            'shots': self.shots,
            'shots_on_target': self.shots_on_target,
            'free_kick_shots': self.free_kick_shots,
            'shots_on_target_percentage': self.shots_on_target_percentage,
            'shots_per_90': self.shots_per_90,
            'shots_on_target_per_90': self.shots_on_target_per_90,
            'goals_per_shot': self.goals_per_shot,
            'goals_per_shot_on_target': self.goals_per_shot_on_target,
            'expected_goals_non_penalty_per_shot':
            self.expected_goals_non_penalty_per_shot,
            'goals_minus_expected': self.goals_minus_expected,
            'non_penalty_minus_expected_non_penalty':
            self.non_penalty_minus_expected_non_penalty,
            'assists_minus_expected': self.assists_minus_expected,
            'key_passes': self.key_passes,
            'passes_completed': self.passes_completed,
            'passes_attempted': self.passes_attempted,
            'pass_completion': self.pass_completion,
            'short_passes_completed': self.short_passes_completed,
            'short_passes_attempted': self.short_passes_attempted,
            'short_pass_completion': self.short_pass_completion,
            'medium_passes_completed': self.medium_passes_completed,
            'medium_passes_attempted': self.medium_passes_attempted,
            'medium_pass_completion': self.medium_pass_completion,
            'long_passes_completed': self.long_passes_completed,
            'long_passes_attempted': self.long_passes_attempted,
            'long_pass_completion': self.long_pass_completion,
            'left_foot_passes': self.left_foot_passes,
            'right_foot_passes': self.right_foot_passes,
            'free_kick_passes': self.free_kick_passes,
            'through_balls': self.through_balls,
            'corner_kicks': self.corner_kicks,
            'throw_ins': self.throw_ins,
            'final_third_passes': self.final_third_passes,
            'penalty_area_passes': self.penalty_area_passes,
            'penalty_area_crosses': self.penalty_area_crosses,
            'minutes_per_match': self.minutes_per_match,
            'minutes_played_percentage': self.minutes_played_percentage,
            'nineties_played': self.nineties_played,
            'minutes_per_start': self.minutes_per_start,
            'subs': self.subs,
            'minutes_per_sub': self.minutes_per_sub,
            'unused_sub': self.unused_sub,
            'points_per_match': self.points_per_match,
            'goals_scored_on_pitch': self.goals_scored_on_pitch,
            'goals_against_on_pitch': self.goals_against_on_pitch,
            'goal_difference_on_pitch': self.goal_difference_on_pitch,
            'goal_difference_on_pitch_per_90':
            self.goal_difference_on_pitch_per_90,
            'net_difference_on_pitch_per_90':
            self.net_difference_on_pitch_per_90,
            'expected_goals_on_pitch': self.expected_goals_on_pitch,
            'expected_goals_against_on_pitch':
            self.expected_goals_against_on_pitch,
            'expected_goal_difference': self.expected_goal_difference,
            'expected_goal_difference_per_90':
            self.expected_goal_difference_per_90,
            'net_expected_goal_difference_per_90':
            self.net_expected_goal_difference_per_90,
            'soft_reds': self.soft_reds,
            'fouls_committed': self.fouls_committed,
            'fouls_drawn': self.fouls_drawn,
            'offsides': self.offsides,
            'crosses': self.crosses,
            'tackles_won': self.tackles_won,
            'interceptions': self.interceptions,
            'penalty_kicks_won': self.penalty_kicks_won,
            'penalty_kicks_conceded': self.penalty_kicks_conceded,
            'successful_dribbles': self.successful_dribbles,
            'attempted_dribbles': self.attempted_dribbles,
            'dribble_success_rate': self.dribble_success_rate,
            'players_dribbled_past': self.players_dribbled_past,
            'nutmegs': self.nutmegs,
            'dribblers_tackled': self.dribblers_tackled,
            'dribblers_contested': self.dribblers_contested,
            'tackle_percentage': self.tackle_percentage,
            'times_dribbled_past': self.times_dribbled_past
        }
        return pd.DataFrame([fields_to_include], index=[self.player_id])

    @property
    def name(self):
        """
        Returns a ``string`` of the player's full name, such as 'Harry Kane'.
        """
        return self._name

    @property
    def player_id(self):
        """
        Returns a ``string`` of the player's 8-digit ID, such as '21a66f6a' for
        Harry Kane.
        """
        return self._player_id

    @property
    def nationality(self):
        """
        Returns a ``string`` of the player's home country, such as 'England'.
        """
        return self._nationality

    @property
    def position(self):
        """
        Returns a ``string`` of the player's primary position(s). Multiple
        positions are separated by commas.
        """
        return self._position

    @int_property_decorator
    def age(self):
        """
        Returns an ``int`` of the player's age as of August 1 for winter
        leagues and February 1 for summer leagues for the given season.
        """
        return self._age

    @int_property_decorator
    def matches_played(self):
        """
        Returns an ``int`` of the number of matches the player has participated
        in.
        """
        return self._matches_played

    @int_property_decorator
    def starts(self):
        """
        Returns an ``int`` of the number of games the player has started.
        """
        return self._starts

    @int_property_decorator
    def minutes(self):
        """
        Returns an ``int`` of the number of minutes the player has spent on the
        field in all competitions.
        """
        return self._minutes.replace(',', '')

    @int_property_decorator
    def goals(self):
        """
        Returns an ``int`` of the number of goals the player has scored.
        """
        return self._goals

    @int_property_decorator
    def assists(self):
        """
        Returns an ``int`` of the number of goals the player has assisted.
        """
        return self._assists

    @int_property_decorator
    def penalty_kicks(self):
        """
        Returns an ``int`` of the number of penalty kicks the player has scored
        during regular play.
        """
        return self._penalty_kicks

    @int_property_decorator
    def penalty_kick_attempts(self):
        """
        Returns an ``int`` of the number of penalty kicks the player has
        attempted.
        """
        return self._penalty_kick_attempts

    @int_property_decorator
    def yellow_cards(self):
        """
        Returns an ``int`` of the number of yellow cards the player has
        accumulated during the season.
        """
        return self._yellow_cards

    @int_property_decorator
    def red_cards(self):
        """
        Returns an ``int`` of the number of red cards the player has
        accumulated during the season.
        """
        return self._red_cards

    @float_property_decorator
    def goals_per_90(self):
        """
        Returns a ``float`` of the number of goals the player has scored per
        90 minutes on the field.
        """
        return self._goals_per_90

    @float_property_decorator
    def assists_per_90(self):
        """
        Returns a ``float`` of the number of goals the player has assisted per
        90 minutes on the field.
        """
        return self._assists_per_90

    @float_property_decorator
    def goals_and_assists_per_90(self):
        """
        Returns a ``float`` of the number of goals the player has either scored
        or assisted per 90 minutes on the field.
        """
        return self._goals_and_assists_per_90

    @float_property_decorator
    def goals_non_penalty_per_90(self):
        """
        Returns a ``float`` of the number of non-penalty goals the player has
        scored per 90 minutes on the field.
        """
        return self._goals_non_penalty_per_90

    @float_property_decorator
    def goals_and_assists_non_penalty_per_90(self):
        """
        Returns a ``float`` of the number of non-penalty goals the player has
        either scored or assisted per 90 minutes on the field.
        """
        return self._goals_and_assists_non_penalty_per_90

    @float_property_decorator
    def expected_goals(self):
        """
        Returns a ``float`` of the number of goals the player was expected to
        score based on the quality and quantity of shots taken.
        """
        return self._expected_goals

    @float_property_decorator
    def expected_goals_non_penalty(self):
        """
        Returns a ``float`` of the number of non-penalty goals the player was
        expected to score based on the quality and quantity of shots taken.
        """
        return self._expected_goals_non_penalty

    @float_property_decorator
    def expected_assists(self):
        """
        Returns a ``float`` of the number of goals the player was expected go
        assist based on the quality and quantity of teammate shots taken.
        """
        return self._expected_assists

    @float_property_decorator
    def expected_goals_per_90(self):
        """
        Returns a ``float`` of the player's expected goals per 90 minutes
        played.
        """
        return self._expected_goals_per_90

    @float_property_decorator
    def expected_assists_per_90(self):
        """
        Returns a ``float`` of the player's expected assists per 90 minutes
        played.
        """
        return self._expected_assists_per_90

    @float_property_decorator
    def expected_goals_and_assists_per_90(self):
        """
        Returns a ``float`` of the player's expected goals and assists per 90
        minutes played.
        """
        return self._expected_goals_and_assists_per_90

    @float_property_decorator
    def expected_goals_non_penalty_per_90(self):
        """
        Returns a ``float`` of the player's expected non-penalty goals per 90
        minutes played.
        """
        return self._expected_goals_non_penalty_per_90

    @float_property_decorator
    def expected_goals_and_assists_non_penalty_per_90(self):
        """
        Returns a ``float`` of the player's expected non-penalty goals and
        assists per 90 minutes played.
        """
        return self._expected_goals_and_assists_non_penalty_per_90

    @int_property_decorator
    def own_goals(self):
        """
        Returns an ``int`` of the number of own goals the player has conceded.
        """
        return self._own_goals

    @int_property_decorator
    def goals_against(self):
        """
        Returns an ``int`` of the number of goals a keeper has conceded.
        """
        return self._goals_against

    @int_property_decorator
    def own_goals_against(self):
        """
        Returns an ``int`` of the number of own goals the team scored on a
        keeper.
        """
        return self._own_goals_against

    @float_property_decorator
    def goals_against_per_90(self):
        """
        Returns a ``float`` of the number of goals a keeper has coneceded per
        90 minutes played.
        """
        return self._goals_against_per_90

    @int_property_decorator
    def shots_on_target_against(self):
        """
        Returns an ``int`` of the number of shots on target a keeper has faced.
        """
        return self._shots_on_target_against

    @int_property_decorator
    def saves(self):
        """
        Returns an ``int`` of the number of shots a keeper has saved.
        """
        return self._saves

    @float_property_decorator
    def save_percentage(self):
        """
        Returns a ``float`` of the percentage of shots the keeper saved.
        Percentage ranges from 0-1.
        """
        return self._save_percentage

    @int_property_decorator
    def wins(self):
        """
        Returns an ``int`` of the number of games a keeper has won.
        """
        return self._wins

    @int_property_decorator
    def draws(self):
        """
        Returns an ``int`` of the number of games a keeper has drawn.
        """
        return self._draws

    @int_property_decorator
    def losses(self):
        """
        Returns an ``int`` of the number of games a keeper has lost.
        """
        return self._losses

    @int_property_decorator
    def clean_sheets(self):
        """
        Returns an ``int`` of the number of clean sheets a keeper has
        registered.
        """
        return self._clean_sheets

    @float_property_decorator
    def clean_sheet_percentage(self):
        """
        Returns a ``float`` of the percentage of games a keeper has
        participated in that resulted in a clean sheet.
        """
        return self._clean_sheet_percentage

    @int_property_decorator
    def penalty_kicks_attempted(self):
        """
        Returns an ``int`` of the number of penalty kicks a keeper has faced
        during regular play.
        """
        return self._penalty_kicks_attempted

    @int_property_decorator
    def penalty_kicks_allowed(self):
        """
        Returns an ``int`` of the number of penalty kicks a keeper has conceded
        during regular play.
        """
        return self._penalty_kicks_allowed

    @int_property_decorator
    def penalty_kicks_saved(self):
        """
        Returns an ``int`` of the number of penalty kicks a keeper has saved
        during regular play.
        """
        return self._penalty_kicks_saved

    @int_property_decorator
    def penalty_kicks_missed(self):
        """
        Returns an ``int`` of the number of penalty kicks a keeper has faced
        where the opponent missed the goal.
        """
        return self._penalty_kicks_missed

    @int_property_decorator
    def free_kick_goals_against(self):
        """
        Returns an ``int`` of the number of goals a keeper conceded as a result
        of an opponent's free kick.
        """
        return self._free_kick_goals_against

    @int_property_decorator
    def corner_kick_goals_against(self):
        """
        Returns an ``int`` of the number of goals a keeper conceded as a result
        of an opponent's corner kick.
        """
        return self._corner_kick_goals_against

    @float_property_decorator
    def post_shot_expected_goals(self):
        """
        Returns a ``float`` of the number of goals a keeper was expected to
        concede.
        """
        return self._post_shot_expected_goals

    @float_property_decorator
    def post_shot_expected_goals_per_shot(self):
        """
        Returns a ``float`` of the number of goals a keeper was expected to
        concede per shot faced.
        """
        return self._post_shot_expected_goals_per_shot

    @float_property_decorator
    def post_shot_expected_goals_minus_allowed(self):
        """
        Returns a ``float`` of the number of goals a keeper was expected to
        concede minus the number of goals they actually conceded.
        """
        return self._post_shot_expected_goals_minus_allowed

    @float_property_decorator
    def post_shot_expected_goals_minus_allowed_per_90(self):
        """
        Returns a ``float`` of the number of goals a keeper was expected to
        concede minus the number of goals they actually conceded, per 90
        minutes played.
        """
        return self._post_shot_expected_goals_minus_allowed_per_90

    @int_property_decorator
    def launches_completed(self):
        """
        Returns an ``int`` of the number of passes longer than 40 yards a
        keeper completed.
        """
        return self._launches_completed

    @int_property_decorator
    def launches_attempted(self):
        """
        Returns an ``int`` of the number of passes longer than 40 yards a
        keeper attempted.
        """
        return self._launches_attempted

    @float_property_decorator
    def launch_completion_percentage(self):
        """
        Returns a ``float`` of the percentage of passes longer than 40 yards a
        keeper completed. Percentage ranges from 0-100.
        """
        return self._launch_completion_percentage

    @int_property_decorator
    def keeper_passes_attempted(self):
        """
        Returns an ``int`` of the number of non-goal kick passes a keeper
        attempted.
        """
        return self._keeper_passes_attempted

    @int_property_decorator
    def throws_attempted(self):
        """
        Returns an ``int`` of the number of throws a keeper attempted.
        """
        return self._throws_attempted

    @float_property_decorator
    def launch_percentage(self):
        """
        Returns a ``float`` of the percentage of passes a keeper makes longer
        than 40 yards excluding goal kicks. Percentage ranges from 0-100.
        """
        return self._launch_percentage

    @float_property_decorator
    def average_keeper_pass_length(self):
        """
        Returns a ``float`` of the average pass length for a keeper in yards
        excluding goal kicks.
        """
        return self._average_keeper_pass_length

    @int_property_decorator
    def goal_kicks_attempted(self):
        """
        Returns an ``int`` of the number of goal kicks a keeper attempted.
        """
        return self._goal_kicks_attempted

    @float_property_decorator
    def goal_kick_launch_percentage(self):
        """
        Returns a ``float`` of the percentage of goal kicks a keeper has
        launched further than 40 yards. Percentage ranges from 0-100.
        """
        return self._goal_kick_launch_percentage

    @float_property_decorator
    def average_goal_kick_length(self):
        """
        Returns a ``float`` of the average pass length for goal kicks in yards
        for a keeper.
        """
        return self._average_goal_kick_length

    @int_property_decorator
    def opponent_cross_attempts(self):
        """
        Returns an ``int`` of the number of crosses a keeper has faced.
        """
        return self._opponent_cross_attempts

    @int_property_decorator
    def opponent_cross_stops(self):
        """
        Returns an ``int`` of the number of crosses a keeper has successfully
        stopped.
        """
        return self._opponent_cross_stops

    @float_property_decorator
    def opponent_cross_stop_percentage(self):
        """
        Returns a ``float`` of the percentage of crosses the keeper has
        successfully stopped. Percentage ranges from 0-100.
        """
        return self._opponent_cross_stop_percentage

    @int_property_decorator
    def keeper_actions_outside_penalty_area(self):
        """
        Returns an ``int`` of the number of defensive actions a keeper made
        outside the penalty area.
        """
        return self._keeper_actions_outside_penalty_area

    @float_property_decorator
    def keeper_actions_outside_penalty_area_per_90(self):
        """
        Returns a ``float`` of the number of defensive actions a keeper made
        outside the penalty area per 90 minutes played.
        """
        return self._keeper_actions_outside_penalty_area_per_90

    @float_property_decorator
    def average_keeper_action_outside_penalty_distance(self):
        """
        Returns a ``float`` of the average distance from goal in yards a keeper
        performed a defensive action outside the penalty area.
        """
        return self._average_keeper_action_outside_penalty_distance

    @int_property_decorator
    def shots(self):
        """
        Returns an ``int`` of the number of shots the player has taken.
        """
        return self._shots

    @int_property_decorator
    def shots_on_target(self):
        """
        Returns an ``int`` of the number of shots on target the player has
        taken.
        """
        return self._shots_on_target

    @int_property_decorator
    def free_kick_shots(self):
        """
        Returns an ``int`` of the number of shots the player has taken from
        free kicks.
        """
        return self._free_kick_shots

    @float_property_decorator
    def shots_on_target_percentage(self):
        """
        Returns a ``float`` of the percentage of shots taken by the player that
        were on target. Percentage ranges from 0-100.
        """
        return self._shots_on_target_percentage

    @float_property_decorator
    def shots_per_90(self):
        """
        Returns a ``float`` of the number of shots the player has taken per 90
        minutes played.
        """
        return self._shots_per_90

    @float_property_decorator
    def shots_on_target_per_90(self):
        """
        Returns a ``float`` of the number of shots on target the player has
        taken per 90 minutes played.
        """
        return self._shots_on_target_per_90

    @float_property_decorator
    def goals_per_shot(self):
        """
        Returns a ``float`` of the average number of goals scored per shot
        taken by the player.
        """
        return self._goals_per_shot

    @float_property_decorator
    def goals_per_shot_on_target(self):
        """
        Returns a ``float`` of the average number of goals scored per shot on
        target by the player.
        """
        return self._goals_per_shot_on_target

    @float_property_decorator
    def expected_goals_non_penalty_per_shot(self):
        """
        Returns a ``float`` of the nuber of non-penalty goals the player was
        expected to score per shot.
        """
        return self._expected_goals_non_penalty_per_shot

    @float_property_decorator
    def goals_minus_expected(self):
        """
        Returns a ``float`` of the number of goals scored minus the number of
        goals the player was expected to score.
        """
        return self._goals_minus_expected

    @float_property_decorator
    def non_penalty_minus_expected_non_penalty(self):
        """
        Returns a ``float`` of the number of non-penalty goals scored minus the
        number of non-penalty goals the player was expected to score.
        """
        return self._non_penalty_minus_expected_non_penalty

    @float_property_decorator
    def assists_minus_expected(self):
        """
        Returns a ``float`` of the number of assists the player registered
        minus the actual number of assists the player tallied.
        """
        return self._assists_minus_expected

    @int_property_decorator
    def key_passes(self):
        """
        Returns an ``int`` of the number of passes the player made that
        directly lead to a shot.
        """
        return self._key_passes

    @int_property_decorator
    def passes_completed(self):
        """
        Returns an ``int`` of the total number of passes the player has
        completed.
        """
        return self._passes_completed

    @int_property_decorator
    def passes_attempted(self):
        """
        Returns an ``int`` of the total number of passes the player has
        attempted.
        """
        return self._passes_attempted

    @float_property_decorator
    def pass_completion(self):
        """
        Returns a ``float`` of the player's overall pass completion rating.
        Percentage ranges from 0-100.
        """
        return self._pass_completion

    @int_property_decorator
    def short_passes_completed(self):
        """
        Returns an ``int`` of the total number of passes under 5 yards the
        player has completed.
        """
        return self._short_passes_completed

    @int_property_decorator
    def short_passes_attempted(self):
        """
        Returns an ``int`` of the total number of passes under 5 yards the
        player has attempted.
        """
        return self._short_passes_attempted

    @float_property_decorator
    def short_pass_completion(self):
        """
        Returns a ``float`` of the player's overall pass completion rating for
        passes under 5 yards. Percentage ranges from 0-100.
        """
        return self._short_pass_completion

    @int_property_decorator
    def medium_passes_completed(self):
        """
        Returns an ``int`` of the total number of passes between 5 and 25 yards
        the player has completed.
        """
        return self._medium_passes_completed

    @int_property_decorator
    def medium_passes_attempted(self):
        """
        Returns an ``int`` of the total number of passes between 5 and 25 yards
        the player has attempted.
        """
        return self._medium_passes_attempted

    @float_property_decorator
    def medium_pass_completion(self):
        """
        Returns a ``float`` of the player's overall pass completion rating for
        passes between 5 and 25 yards. Percentage ranges from 0-100.
        """
        return self._medium_pass_completion

    @int_property_decorator
    def long_passes_completed(self):
        """
        Returns an ``int`` of the total number of passes greater than 25 yards
        the player has completed.
        """
        return self._long_passes_completed

    @int_property_decorator
    def long_passes_attempted(self):
        """
        Returns an ``int`` of the total number of passes greater than 25 yards
        the player has attempted.
        """
        return self._long_passes_attempted

    @float_property_decorator
    def long_pass_completion(self):
        """
        Returns a ``float`` of the player's overall pass completion rating for
        passes greater than 25 yards. Percentage ranges from 0-100.
        """
        return self._long_pass_completion

    @int_property_decorator
    def left_foot_passes(self):
        """
        Returns an ``int`` of the number of passes the player made with their
        left foot.
        """
        return self._left_foot_passes

    @int_property_decorator
    def right_foot_passes(self):
        """
        Returns an ``int`` of the number of passes the player made with their
        right foot.
        """
        return self._right_foot_passes

    @int_property_decorator
    def free_kick_passes(self):
        """
        Returns an ``int`` of the number of passes the player made from a free
        kick.
        """
        return self._free_kick_passes

    @int_property_decorator
    def through_balls(self):
        """
        Returns an ``int`` of the number of passes the player made between the
        last line of defenders into open space.
        """
        return self._through_balls

    @int_property_decorator
    def corner_kicks(self):
        """
        Returns an ``int`` of the number of corner kicks the player has taken.
        """
        return self._corner_kicks

    @int_property_decorator
    def throw_ins(self):
        """
        Returns an ``int`` of the number of throw-ins the player took.
        """
        return self._throw_ins

    @int_property_decorator
    def final_third_passes(self):
        """
        Returns an ``int`` of the number of passes the player made into the
        final third.
        """
        return self._final_third_passes

    @int_property_decorator
    def penalty_area_passes(self):
        """
        Returns an ``int`` of the number of passes the player made into the
        opposing penalty area.
        """
        return self._penalty_area_passes

    @int_property_decorator
    def penalty_area_crosses(self):
        """
        Returns an ``int`` of the number of non-set piece crosses the player
        made into the penalty area.
        """
        return self._penalty_area_crosses

    @int_property_decorator
    def minutes_per_match(self):
        """
        Returns an ``int`` of the average number of minutes the player played
        per match.
        """
        return self._minutes_per_match

    @float_property_decorator
    def minutes_played_percentage(self):
        """
        Returns a ``float`` of the percentage of time the player has been on
        the field for all games the team participated in. Percentage ranges
        from 0-100.
        """
        return self._minutes_played_percentage

    @float_property_decorator
    def nineties_played(self):
        """
        Returns a ``float`` of number of the number of minutes the player has
        played divided by 90.
        """
        return self._nineties_played

    @int_property_decorator
    def minutes_per_start(self):
        """
        Returns an ``int`` of the number of minutes the player plays on average
        per game started.
        """
        return self._minutes_per_start

    @int_property_decorator
    def subs(self):
        """
        Returns an ``int`` of the number of times the player has come on as a
        sub.
        """
        return self._subs

    @int_property_decorator
    def minutes_per_sub(self):
        """
        Returns an ``int`` of the average number of minutes the player has
        played per game after coming in as a sub.
        """
        return self._minutes_per_sub

    @int_property_decorator
    def unused_sub(self):
        """
        Returns an ``int`` of the number of times the player was an unused sub
        and spent the entirety of the game on the bench.
        """
        return self._unused_sub

    @float_property_decorator
    def points_per_match(self):
        """
        Returns a ``float`` of the average number of points the team has gained
        per game in which the player participated.
        """
        return self._points_per_match

    @int_property_decorator
    def goals_scored_on_pitch(self):
        """
        Returns an ``int`` of the number of goals the team has scored while the
        player was on the field.
        """
        return self._goals_scored_on_pitch

    @int_property_decorator
    def goals_against_on_pitch(self):
        """
        Returns an ``int`` of the number of goals the team has conceded while
        the player was on the field.
        """
        return self._goals_against_on_pitch

    @int_property_decorator
    def goal_difference_on_pitch(self):
        """
        Returns an ``int`` of the team's goal difference while the player is on
        the field.
        """
        return self._goal_difference_on_pitch

    @float_property_decorator
    def goal_difference_on_pitch_per_90(self):
        """
        Returns a ``float`` of the team's average goal difference while the
        player is on the field, per 90 minutes played.
        """
        return self._goal_difference_on_pitch_per_90

    @float_property_decorator
    def net_difference_on_pitch_per_90(self):
        """
        Returns a ``float`` of the team's goal difference while the player is
        on the pitch minus the team's goal difference while the player is off
        the pitch, per 90 minutes.
        """
        return self._net_difference_on_pitch_per_90

    @float_property_decorator
    def expected_goals_on_pitch(self):
        """
        Returns a ``float`` of the number of goals the team was expected to
        score while the player was on the pitch.
        """
        return self._expected_goals_on_pitch

    @float_property_decorator
    def expected_goals_against_on_pitch(self):
        """
        Returns a ``float`` of the number of goals the team was expected to
        concede while the player was on the pitch.
        """
        return self._expected_goals_against_on_pitch

    @float_property_decorator
    def expected_goal_difference(self):
        """
        Returns a ``float`` of the difference between expected team goals
        scored and conceded while the player was on the pitch.
        """
        return self._expected_goal_difference

    @float_property_decorator
    def expected_goal_difference_per_90(self):
        """
        Returns a ``float`` of the difference between expected team goals
        scored and conceded while the player was on the pitch, per 90 minutes.
        """
        return self._expected_goal_difference_per_90

    @float_property_decorator
    def net_expected_goal_difference_per_90(self):
        """
        Returns a ``float`` of the team's expected goal difference while the
        player is on the pitch minus the team's exepcted goal difference while
        the player is off the pitch, per 90 minutes.
        """
        return self._net_expected_goal_difference_per_90

    @int_property_decorator
    def soft_reds(self):
        """
        Returns an ``int`` of the number of games where the player received two
        yellow cards, resulting in a red, or a "soft red".
        """
        return self._soft_reds

    @int_property_decorator
    def fouls_committed(self):
        """
        Returns an ``int`` of the number of fouls the player has committed.
        """
        return self._fouls_committed

    @int_property_decorator
    def fouls_drawn(self):
        """
        Returns an ``int`` of the number of fouls the player has been the
        victim of.
        """
        return self._fouls_drawn

    @int_property_decorator
    def offsides(self):
        """
        Returns an ``int`` of the number of times the player has been called
        offside.
        """
        return self._offsides

    @int_property_decorator
    def crosses(self):
        """
        Returns an ``int`` of the number of times the player has crossed the
        ball.
        """
        return self._crosses

    @int_property_decorator
    def tackles_won(self):
        """
        Returns an ``int`` of the number of tackles the player has won.
        """
        return self._tackles_won

    @int_property_decorator
    def interceptions(self):
        """
        Returns an ``int`` of the number of times the player has intercepted
        the ball.
        """
        return self._interceptions

    @int_property_decorator
    def penalty_kicks_won(self):
        """
        Returns an ``int`` of the number of times the player has won a penalty
        kick for the team.
        """
        return self._penalty_kicks_won

    @int_property_decorator
    def penalty_kicks_conceded(self):
        """
        Returns an ``int`` of the number of times the player has conceded a
        penalty kick to the opposition.
        """
        return self._penalty_kicks_conceded

    @int_property_decorator
    def successful_dribbles(self):
        """
        Returns an ``int`` of the number of dribbles the player has completed
        successfully.
        """
        return self._successful_dribbles

    @int_property_decorator
    def attempted_dribbles(self):
        """
        Returns an ``int`` of the number of times the player has attempted a
        dribble.
        """
        return self._attempted_dribbles

    @float_property_decorator
    def dribble_success_rate(self):
        """
        Returns a ``float`` of the percentage of attempted dribbles the player
        has successfully completed. Percentage ranges from 0-100.
        """
        return self._dribble_success_rate

    @int_property_decorator
    def players_dribbled_past(self):
        """
        Returns an ``int`` of the number of opponents the player dribbled past.
        """
        return self._players_dribbled_past

    @int_property_decorator
    def nutmegs(self):
        """
        Returns an ``int`` of the number of opponents the player has nutmegged.
        """
        return self._nutmegs

    @int_property_decorator
    def dribblers_tackled(self):
        """
        Returns an ``int`` of the number of opponents who were attempting a
        dribble that the player tackled.
        """
        return self._dribblers_tackled

    @int_property_decorator
    def dribblers_contested(self):
        """
        Returns an ``int`` of the number of opponents who were attempting a
        dribble that the player contested.
        """
        return self._dribblers_contested

    @float_property_decorator
    def tackle_percentage(self):
        """
        Returns a ``float`` of the percentage of opposing dribblers the player
        has successfully tackled. Percentage ranges from 0-100.
        """
        return self._tackle_percentage

    @int_property_decorator
    def times_dribbled_past(self):
        """
        Returns an ``int`` of the number of times the player has been dribbled
        past.
        """
        return self._times_dribbled_past


class Roster:
    """
    Get stats for all players on a roster.

    Request a team's roster for a given season and create instances of the
    Player class for each player, containing a detailed list of the player's
    statistics and information for the season.

    Parameters
    ----------
    squad_id : string
        The team's 8-digit squad ID or the team's name, such as '361ca564' or
        'Tottenham Hotspur'.
    doc : PyQuery object (optional)
        If passed to the class instantiation, this will be used to pull all
        information instead of making another request to the website. If the
        document is not provided, it will be pulled during a later step.
    """
    def __init__(self, squad_id, doc=None):
        self._players = []

        self._squad_id = _lookup_team(squad_id)
        player_data_dict = self._pull_stats(doc)
        if not player_data_dict:
            return None
        self._instantiate_players(player_data_dict)

    def __call__(self, player):
        """
        Return a specified player on the roster.

        Returns a specific player as requested by the passed name or player ID.
        The input string must either match a player's 8-digit unique ID or the
        named listed on fbref.com for the player.

        Parameters
        ----------
        player : string
            A ``string`` of either the player's 8-digit unique ID or the name
            listed on fbref.com for the player.

        Returns
        -------
        Player instance
            If the requested player can be found, their Player instance is
            returned.

        Raises
        ------
        ValueError
            If the requested player cannot be matched with a player in the
            squad.
        """
        for player_instance in self._players:
            if not player_instance.name or not player_instance.player_id:
                continue  # pragma: no cover
            if player.lower() == player_instance.player_id.lower():
                return player_instance
            if player.lower().strip() == player_instance.name.lower().strip():
                return player_instance
        raise ValueError('No player found with the requested name or ID')

    def __repr__(self):
        """
        Returns a ``list`` of all players for the given team.
        """
        return self._players

    def __iter__(self):
        """
        Returns an iterator of all of the players on the given team's roster.
        """
        return iter(self.__repr__())

    def __len__(self):
        """
        Returns the number of player on the given team's roster.
        """
        return len(self.__repr__())

    def _player_id(self, player_data):
        """
        Parse the player's ID from a row.

        The player ID is embedded within the header column of each individual
        player's row within a stats table. The specific ID is in a URL and can
        be easily parsed and returned.

        Parameters
        ----------
        player_data : PyQuery object
            A PyQuery object representing a single row in a stats table for a
            player.

        Returns
        -------
        string
            Returns a ``string`` of the player's unique 8-digit player ID.
        """
        player = player_data('th[data-stat="player"]')
        player_id = player('a').attr('href')
        try:
            player_id = re.sub(r'.*\/players\/', '', player_id)
            player_id = re.sub(r'\/.*', '', player_id)
        except TypeError:
            player_id = None
        return player_id

    def _add_stats_data(self, stats_table, player_data_dict):
        """
        Add each player's stats rows to a dictionary.

        Given the player stats are spread throughout many tables, they should
        be combined by player for a single reference for each player for easier
        lookups.

        Parameters
        ----------
        stats_table : generator
            A generator of all row items in a given table.
        player_data_dict : {str: {'data': str}} dictionary
            A dictionary where every key is the player's ID and every value is
            another dictionary with a 'data' key which contains the string
            version of the row data for the matched player.

        Returns
        -------
        dictionary
            An updated version of the player_data_dict with the passed table
            row information included.
        """
        for player_data in stats_table:
            if 'class="thead"' in str(player_data):
                continue  # pragma: no cover
            player_id = self._player_id(player_data)
            if not player_id:
                continue
            try:
                player_data_dict[player_id]['data'] += player_data
            except KeyError:
                player_data_dict[player_id] = {'data': player_data}
        return player_data_dict

    def _pull_stats(self, doc):
        """
        Download the team page and pull all stats.

        Download the requested team's season page and pull all of the relevant
        stats tables for later parsing.

        Parameters
        ----------
        doc : PyQuery object
            If passed to the class instantiation, this will be used to pull all
            information instead of making another request to the website. If
            the document is not provided, this value will be None.

        Returns
        -------
        dictionary
            Returns a ``dictionary`` where every key is the player's ID and
            every value is another dictionary with a 'data' key which contains
            the string version of the row data for the matched player.
        """
        if not doc:
            try:
                doc = pq(SQUAD_URL % self._squad_id)
                doc = pq(_remove_html_comment_tags(doc))
            except HTTPError:
                return None
        stats_table = []
        player_data_dict = {}

        # Most leagues use the 'stats_*_ks_combined' tag for competitions, but
        # some, like the MLS in North America, use a different table ID.
        for table_id in ['table#stats_standard_ks_combined',
                         'table#stats_keeper_ks_combined',
                         'table#stats_keeper_adv_ks_combined',
                         'table#stats_shooting_ks_combined',
                         'table#stats_passing_ks_combined',
                         'table#stats_playing_time_ks_combined',
                         'table#stats_misc_ks_combined',
                         'table#stats_standard_10090',
                         'table#stats_keeper_10090',
                         'table#stats_keeper_adv_10090',
                         'table#stats_shooting_10090',
                         'table#stats_passing_10090',
                         'table#stats_playing_time_10090',
                         'table#stats_misc_10090']:
            table = _get_stats_table(doc, table_id)
            if not table:
                continue
            player_data_dict = self._add_stats_data(table, player_data_dict)
        return player_data_dict

    def _instantiate_players(self, player_data_dict):
        """
        Create Player instances for each squad member.

        Given the stats information for all players, an instance of the Player
        class should be created and appended to the overall list of players for
        easy future reference.

        Parameters
        ----------
        player_data_dict : {str: {'data': str}} dictionary
            A dictionary where every key is the player's ID and every value is
            another dictionary with a 'data' key which contains the string
            version of the row data for the matched player.
        """
        for player_id, player_data in player_data_dict.items():
            player = SquadPlayer(player_data['data'], player_id)
            self._players.append(player)
