'''
    File: Lineup.py
    Author: Drew Scott
'''

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

class Player:
    '''
        Public methods:
            generate_pa_outcomes: generates n_games pa outcomes based on the player's stats
            set_pa_outcomes: set the player's pa outcomes to the input (only one game allowed)
    '''

    col_names: List[str] = []

    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']

    outcome_name_to_num_mapping: Dict[str, int] = {}

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

    longest_name: int = 0

    metadata_isset = False

    # main data directory
    data_directory = 'data/' 

    # in main data directory
    lineups_directory = 'teams/'
    outcomes_directory = 'pa_outcomes/'
    stats_filename = 'stats.csv'

    # full paths
    stats_filepath = data_directory + stats_filename

    def __init__(self, stat_line: str):
        if not Player.metadata_isset:
            Player._set_metadata()

        self.player_info: Dict[str, Any] = {} 
        self._set_player_info(stat_line)

        self.player_pa_probabilities: Dict[str, float] = {} 
        self._set_player_pa_probabilities()        

        self.player_pa_thresholds: List[float] = [] 
        self._set_player_pa_thresholds()

        self.pa_outcomes: List[List[int]] = []

    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_outcome_name()
                # 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

    def set_pa_outcomes(self, pa_outcomes: List[str]) -> None:
        '''
            Sets pa_outcomes to the given outcomes, only one game allowed
            (used when simulating a single game with pre-determined outcomes)
        '''

        # TODO: acutally simulate direction
        self.pa_outcomes = [[self._full_outcome_to_num(outcome, 'C') for outcome in pa_outcomes]]

    def _get_outcome_name(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.pa_outcome_directions[2]
        if direction_num < float(self.player_info[Player.pa_outcome_directions[0]]):
            relative_direction = Player.pa_outcome_directions[0]
        elif direction_num < float(self.player_info[Player.pa_outcome_directions[0]]) + \
                float(self.player_info[Player.pa_outcome_directions[1]]):
            relative_direction = Player.pa_outcome_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 _set_player_info(self, stat_line: str) -> None:
        '''
            Sets all of the player's information
        '''

        stat_splits = stat_line[: -1].split(',')

        stats = [int(s) if s.isnumeric() \
                    else float(s) if Player._isfloat(s) \
                    else s \
                for s in stat_splits]

        for col, stat in zip(Player.col_names, stats):
            self.player_info[col] = stat

    def _set_player_pa_probabilities(self) -> None:
        '''
            Sets the probability of each possible plate appearance outcomes for the input player
        '''

        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
            self.player_pa_probabilities[outcome] = prob

    def _set_player_pa_thresholds(self) -> None:
        '''
            Sets the upper threshold (between 0 and 1) for each outcome name.
            Used to decide a plate appearance outcome using a random number
        '''

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

    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'])

    @staticmethod
    def _full_outcome_to_num(outcome_name: 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_name_to_num_mapping[outcome_name]

        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

    @classmethod
    def _set_metadata(cls) -> None:
        '''
            Sets up some static metadata for the Player class
        '''

        stats = pkg_resources.resource_stream(__name__, Player.stats_filepath).read().decode(encoding='utf-8-sig')
        stat_lines = stats.split('\n')

        # get all of the column names
        cls.col_names = stat_lines[0].split(',')

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

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

        # map each outcome name to a number
        for i, outcome in enumerate(cls.pa_outcome_names):
            cls.outcome_name_to_num_mapping[outcome] = i

        # mark metadata set
        Player.metadata_isset = True

    def __repr__(self):
        tab_size = 8
        num_tabs = ((Player.longest_name + 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'
