'''
    File: Lineup.py
    Author: Drew Scott
    Description: Implementation of a class to store information and statistics about a player.
'''

import random
from typing import List, Dict, Union, Any

class Player:
    '''
        TODO
    '''

    col_names: List[str] = []
    max_name_length: int = 0
    outcome_num_map: Dict[str, int] = {}

    pa_outcome_names = ['b_single', 'b_double', 'b_triple', 'b_home_run', 'b_strikeout', 'b_walk', \
                        'b_catcher_interf', 'b_hit_by_pitch', 'b_out_fly', 'b_out_ground', \
                        'b_out_line_drive', 'b_out_popup']

    directions = ['pull_percent', 'straightaway_percent', 'opposite_percent']

    def __init__(self, stat_line: str):
        self.player_info: Dict[str, Any] = Player.get_info(stat_line)
        self.player_pa_probs: Dict[str, float] = self.get_pa_probs()
        self.player_pa_thresholds: List[float] = self.get_pa_thresholds()
        self.pa_outcomes: List[List[int]] = []

    @classmethod
    def set_metadata(cls, stats_filename: str) -> None:
        '''
            TODO
        '''
        stats = pkg_resources.resource_stream(__name__, 'data/'+stats_filename).read().decode()
#        with open(stats_filename, 'r', encoding='utf-8-sig') as stats_csv:
            # get all of the column names
        stat_lines = stats.split('\n')
            cls.col_names = stat_lines[0].split(',')

            # get the maximum name length
            cls.max_name_length = 0
            for line in stat_lines[1:]:
                last_length = line.index(',')
                first_length = line[last_length+1:].index(',')

                cls.max_name_length = max(cls.max_name_length, last_length + first_length + 1)

        for i, outcome in enumerate(cls.pa_outcome_names):
            cls.outcome_num_map[outcome] = i

    def set_pa_outcomes(self, pa_outcomes: List[str]) -> None:
        '''
            Sets pa_outcomes to the given outcomes
            (used when simulating a single game with pre-determined outcomes
        '''
        # TODO: do direction better
        # all outcomes go to center
        self.pa_outcomes = [[self.full_outcome_to_num(outcome, 'C') for outcome in pa_outcomes]]

    def get_pa_outcome(self) -> str:
        '''
            Returns the name of the plate appearance outcome based on a randomly generated number
        '''
        rand_outcome = random.random()
        for out_threshold, outcome in zip(self.player_pa_thresholds, Player.pa_outcome_names):
            if rand_outcome < out_threshold:
                return outcome

        return Player.pa_outcome_names[-1]

    def get_outcome_direction(self) -> str:
        '''
            Generates the direction of the PA outcome
        '''

        # determine which relative direction the outcome was
        # TODO: make better
        direction_num = random.random() * 100
        relative_direction = Player.directions[2]
        if direction_num < float(self.player_info[Player.directions[0]]):
            relative_direction = Player.directions[0]
        elif direction_num < float(self.player_info[Player.directions[0]]) + \
                float(self.player_info[Player.directions[1]]):
            relative_direction = Player.directions[1]

        # TODO: get pitcher info
        # assume pitcher is a righty, so anyone who switch hits will be lefty
        batter_side = 'R'
        if float(self.player_info['bats_left_pct']) > 0:
            batter_side = 'L'

        # turn relative direction into true direction
        true_direction = 'C'
        if relative_direction == 'pull_percent':
            if batter_side == 'R':
                true_direction = 'L'
            else:
                true_direction = 'R'
        elif relative_direction == 'opposite_percent':
            true_direction = batter_side

        return true_direction

    def generate_pa_outcomes(self, n_games: int) -> None:
        '''
            Sets pa_outcomes to have 10 outcomes per game, generated by the player's thresholds
        '''
        generated_outcomes = []

        for _ in range(n_games):
            game_outcomes = []
            for _ in range(10):
                outcome = self.get_pa_outcome()
                # generate a direction,
                # even if it's not possible (like a strikeout) since it will be ignored
                direction = self.get_outcome_direction()

                game_outcomes.append(self.full_outcome_to_num(outcome, direction))

            generated_outcomes.append(game_outcomes)

        self.pa_outcomes = generated_outcomes

    @staticmethod
    def full_outcome_to_num(outcome: str, direction: str) -> int:
        '''
            Converts the outcome information to a number
        '''

        num = 0

        #outcome_bits = math.ceil(math.log(len(Player.pa_outcome_names), 2))
        outcome_num = Player.outcome_num_map[outcome]

        direction_num = 0
        if direction == 'C':
            direction_num = 1
        elif direction == 'R':
            direction_num = 2

        num = (outcome_num << 2) + direction_num

        return num

    @staticmethod
    def isfloat(string: str) -> bool:
        '''
            Returns true if a str can be read as a float, false otherwise
        '''

        if string.isnumeric():
            return True

        splits = string.split('.')
        if len(splits) == 1 or len(splits) == 2:
            if len(splits[0]) == 0:
                return False

            if splits[0][0] == '-':
                splits[0] = splits[0][1:]

            if len(splits) == 2:
                return splits[0].isnumeric() and splits[1].isnumeric()

            return splits[0].isnumeric()

        return False

    def get_pa_thresholds(self) -> List[float]:
        '''
            Returns the upper threshold (between 0 and 1) for each outcome.
            Used to decide a plate appearance outcome using a random number
        '''
        thresholds = []

        thresholds.append(self.player_pa_probs[Player.pa_outcome_names[0]])
        for i, outcome in enumerate(Player.pa_outcome_names[1:], start=1):
            thresholds.append(thresholds[i-1] + self.player_pa_probs[outcome])

        return thresholds

    def get_pa_probs(self) -> Dict[str, float]:
        '''
            Returns a dict containing only the the probability of each possible plate appearance
            outcomes for the input player
        '''
        probs = {}

        total_pa = int(self.player_info['b_total_pa'])

        for outcome, outcome_count in self.player_info.items():
            if outcome not in Player.pa_outcome_names:
                continue

            prob = int(outcome_count) / total_pa
            probs[outcome] = prob

        return probs

    @classmethod
    def get_info(cls, stats_line: str) -> Dict[str, Any]:
        '''
            Returns a dict of all of the player's information
        '''
        info: Dict[str, Any] = {}

        stats_splits = stats_line[: -1].split(',')
        stats = [int(s) if s.isnumeric() \
                    else float(s) if cls.isfloat(s) \
                    else s \
                for s in stats_splits]

        for col, stat in zip(cls.col_names, stats):
            info[col] = stat

        return info

    def _name_length(self) -> int:
        '''
            Returns the length of the player's name
        '''

        return len(self.player_info['first_name']) + len(self.player_info['last_name'])

    def __repr__(self):
        tab_size = 8
        num_tabs = ((Player.max_name_length + 1 - self._name_length()) // tab_size) + 1
        return f'{self.player_info["first_name"]} {self.player_info["last_name"]}:' + \
                '\t'*num_tabs + \
                f'{self.player_info["on_base_plus_slg"]:.3f} OPS, '+ \
                f'{self.player_info["woba"]:.3f} WOBA'
