from rengine.exercises import (
    Exercise,
    ExerciseFromTypePreset,
    StrengthExercise,
    pick_random_exercise,
)
import pandas as pd
from rengine.config import (
    TIME_BASED_CONDITIONS,
    EquipmentAvailable,
    ExerciseLoadValues,
    ExerciseType,
    ExerciseLoad,
    ExerciseTypeValues,
    MuscleGroup,
    EXERCISE_DF,
)
from typing import List, Tuple
import dataframe_image as dfi
import random

DF = EXERCISE_DF


def dictionary_addition(dictionaries):
    """Add corresponding values in dictionary."""
    keys = list(set([key for dictionary in dictionaries for key in dictionary]))
    added_dict = dict((key, 0) for key in keys)
    for dictionary in dictionaries:
        for key, value in dictionary.items():
            added_dict[key] += value
    return added_dict


def tbc_set_reduction(tbc: dict, exercise: ExerciseFromTypePreset):
    exercise_type = exercise.exercise_type
    exercise_load = exercise.exercise_load
    if (
        exercise_type in tbc["set_reductions"]
        and exercise_load in tbc["set_reductions"][exercise_type]
    ):
        exercise.sets -= tbc["set_reductions"][exercise_type][exercise_load]


class BaseWorkout:
    def __init__(self, exercises: List[Exercise] = None):
        self.workout = exercises or []

    def create(self):
        self._create()
        self._post_create()

    def _create(self):
        """This is where all the specifics are implemented. Magic happens here!"""
        pass

    def _post_create(self):
        """Runs after _create is called."""
        pass

    def add_exercise(self, exercise: Exercise):
        """Adds specific exercise to workout"""
        self.workout.append(exercise)

    def add_exercises(self, exercises: List[Exercise]):
        for exercise in exercises:
            self.workout.append(exercise)

    def sort_workout_by_intensity(self):
        """
        Sorts workouts by:
        1. First priority is ExerciseType
        2. Second priority is ExerciseLoad
        """
        self.workout = list(
            sorted(
                self.workout,
                key=lambda x: (
                    ExerciseTypeValues[x.exercise_type],
                    ExerciseLoadValues[x.exercise_load],
                ),
                reverse=True,
            )
        )

    def pick_random_exercise(
        self,
        muscle_groups_targeted: List[MuscleGroup],
        exercise_type: ExerciseType,
        allowed_loads: List[ExerciseLoad] = [
            ExerciseLoad.HEAVY,
            ExerciseLoad.MEDIUM,
            ExerciseLoad.LIGHT,
        ],
    ) -> Exercise:
        """Picks single random exercise within rules determined by parameters and time dependencies. <strict> determines if muscle_groups_targeted should exactly match up"""
        return pick_random_exercise(
            muscle_groups_targeted, exercise_type, allowed_loads
        )

    @property
    def total_time(self):
        """Returns total time in minutes of workout"""
        return sum([0] + [exercise.length for exercise in self.workout])

    @property
    def df(self):
        return pd.DataFrame(self.workout_dict)

    @property
    def workout_dict(self):
        return [
            {
                "exercise_name": exercise.exercise_name,
                "muscle_group": exercise.muscle_group,
                "sets": exercise.sets,
                "rep_range": exercise.rep_range,
                "rest_time_range": exercise.rest_time_range,
            }
            for exercise in self.workout
        ]

    @property
    def load_per_muscle_group(self):
        load_dict = dict((muscle_group, 0) for muscle_group in MuscleGroup.ALL)
        for exercise in self.workout:
            load_dict[exercise.muscle_group] += 1
        return load_dict

    @property
    def exercise_names(self):
        return [exercise.exercise_name for exercise in self.workout]

    def __str__(self) -> str:
        return str(self.df) + f"\nTotal Time: {str(int(self.total_time))} minutes"


class AutoGeneratedWorkout(BaseWorkout):
    """Auto generates a workout based on time based conditions, previous workouts, strength exercise."""

    def __init__(
        self,
        target_time,
        muscle_groups: List[MuscleGroup],
        strength_exercise_name: str = None,
        equipment_available=EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None,
    ):
        self.target_time = target_time
        self.strength_exercise_name = strength_exercise_name
        self.muscle_groups = muscle_groups
        self.equipment_available = equipment_available
        self.caps = TIME_BASED_CONDITIONS[target_time]["caps"]
        self.prior_cummative_loads_per_muscle_group = (
            prior_cummative_loads_per_muscle_group
            or dict((muscle_group, 0) for muscle_group in MuscleGroup.ALL)
        )
        self.excluded_exercise_names = excluded_exercise_names or []


        super().__init__(exercises=exercises or [])

    def _create(self):
        self._auto_generate()

    def _post_create(self):
        self.sort_workout_by_intensity()

    def _auto_generate(self):
        """Automatically generates workout based on conditions."""
        tbc = TIME_BASED_CONDITIONS[self.target_time]
        if (
            len(tbc["allowed_strength_loads"]) > 0
            and self.strength_exercise_name != None
        ):
            strength_exercise = StrengthExercise(
                self.strength_exercise_name, allowed_loads=tbc["allowed_strength_loads"]
            )
            tbc_set_reduction(tbc, strength_exercise)
            self.add_exercise(strength_exercise)

        for endurance_probability in tbc["endurance_exercises_probabilities"]:
            n = random.uniform(0, 1)
            if n <= endurance_probability:
                muscle_to_work = self._find_next_muscle_group_to_work()
                endurance_exercise = pick_random_exercise(
                    [muscle_to_work],
                    ExerciseType.ENDURANCE,
                    excluded_exercise_names=self.excluded_exercise_names,
                    equipment_available=self.equipment_available,
                )
                self.excluded_exercise_names.append(endurance_exercise.exercise_name)
                tbc_set_reduction(tbc, endurance_exercise)
                self.add_exercise(endurance_exercise)

        while self.total_time < self.target_time - min(
            self.target_time // 15 * 2.5, 7.5
        ):
            muscle_to_work = self._find_next_muscle_group_to_work()
            hypertrophy_exercise = pick_random_exercise(
                [muscle_to_work],
                ExerciseType.HYPERTROPHY,
                excluded_exercise_names=self.excluded_exercise_names,
                equipment_available=self.equipment_available,
            )
            self.excluded_exercise_names.append(hypertrophy_exercise.exercise_name)
            tbc_set_reduction(tbc, hypertrophy_exercise)
            self.add_exercise(hypertrophy_exercise)

    def _find_next_muscle_group_to_work(self):
        """Finds muscle group with least load"""
        trainable_loads = {
            muscle_group: load
            for muscle_group, load in self.cummulative_load_per_muscle_group.items()
            if muscle_group in self.muscle_groups
            and (
                muscle_group in self.caps
                and self.caps[muscle_group] <= self.load_per_muscle_group[muscle_group]
            )
            == False
        }
        min_muscle = min(trainable_loads, key=trainable_loads.get)
        return min_muscle

    def regenerate_exercise(exercise_ind: int):
        """Regenerates one exercise of chose. Ensures that is same muscle group is worked and exercises use equipment available"""
        pass

    @property
    def cummulative_load_per_muscle_group(self):
        """Total workout load per muscle group for entire week at current state of workout and previous workouts."""
        return dictionary_addition(
            (self.prior_cummative_loads_per_muscle_group, self.load_per_muscle_group)
        )


class UpperBodyWorkout(AutoGeneratedWorkout):
    def __init__(
        self,
        target_time,
        strength_exercise_name: str = None,
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None
    ):
        muscle_groups = [
            MuscleGroup.BACK,
            MuscleGroup.CHEST,
            MuscleGroup.BICEPS,
            MuscleGroup.TRICEPS,
            MuscleGroup.DELTOIDS,
        ]
        super().__init__(
            target_time,
            muscle_groups,
            strength_exercise_name,
            equipment_available,
            excluded_exercise_names or [],
            exercises or [],
            prior_cummative_loads_per_muscle_group or {},
        )


class LowerBodyWorkout(AutoGeneratedWorkout):
    def __init__(
        self,
        target_time,
        strength_exercise_name: str = None,
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None
    ):
        muscle_groups = [MuscleGroup.QUAD, MuscleGroup.HAMSTRINGS, MuscleGroup.CALVES]
        super().__init__(
            target_time,
            muscle_groups,
            strength_exercise_name,
            equipment_available,
            excluded_exercise_names or [],
            exercises or [],
            prior_cummative_loads_per_muscle_group or {},
        )

from rengine.exercises import (
    Exercise,
    ExerciseFromTypePreset,
    StrengthExercise,
    pick_random_exercise,
)
import pandas as pd
from rengine.config import (
    TIME_BASED_CONDITIONS,
    EquipmentAvailable,
    ExerciseLoadValues,
    ExerciseType,
    ExerciseLoad,
    ExerciseTypeValues,
    MuscleGroup,
    EXERCISE_DF,
)
from typing import List, Tuple
import dataframe_image as dfi
import random

DF = EXERCISE_DF


def dictionary_addition(dictionaries):
    """Add corresponding values in dictionary."""
    keys = list(set([key for dictionary in dictionaries for key in dictionary]))
    added_dict = dict((key, 0) for key in keys)
    for dictionary in dictionaries:
        for key, value in dictionary.items():
            added_dict[key] += value
    return added_dict


def tbc_set_reduction(tbc: dict, exercise: ExerciseFromTypePreset):
    exercise_type = exercise.exercise_type
    exercise_load = exercise.exercise_load
    if (
        exercise_type in tbc["set_reductions"]
        and exercise_load in tbc["set_reductions"][exercise_type]
    ):
        exercise.sets -= tbc["set_reductions"][exercise_type][exercise_load]


class BaseWorkout:
    def __init__(self, exercises: List[Exercise] = None):
        self.workout = exercises or []

    def create(self):
        self._create()
        self._post_create()

    def _create(self):
        """This is where all the specifics are implemented. Magic happens here!"""
        pass

    def _post_create(self):
        """Runs after _create is called."""
        pass

    def add_exercise(self, exercise: Exercise):
        """Adds specific exercise to workout"""
        self.workout.append(exercise)

    def add_exercises(self, exercises: List[Exercise]):
        for exercise in exercises:
            self.workout.append(exercise)

    def sort_workout_by_intensity(self):
        """
        Sorts workouts by:
        1. First priority is ExerciseType
        2. Second priority is ExerciseLoad
        """
        self.workout = list(
            sorted(
                self.workout,
                key=lambda x: (
                    ExerciseTypeValues[x.exercise_type],
                    ExerciseLoadValues[x.exercise_load],
                ),
                reverse=True,
            )
        )

    def pick_random_exercise(
        self,
        muscle_groups_targeted: List[MuscleGroup],
        exercise_type: ExerciseType,
        allowed_loads: List[ExerciseLoad] = [
            ExerciseLoad.HEAVY,
            ExerciseLoad.MEDIUM,
            ExerciseLoad.LIGHT,
        ],
    ) -> Exercise:
        """Picks single random exercise within rules determined by parameters and time dependencies. <strict> determines if muscle_groups_targeted should exactly match up"""
        return pick_random_exercise(
            muscle_groups_targeted, exercise_type, allowed_loads
        )

    @property
    def total_time(self):
        """Returns total time in minutes of workout"""
        return sum([0] + [exercise.length for exercise in self.workout])

    @property
    def df(self):
        return pd.DataFrame(self.workout_dict)

    @property
    def workout_dict(self):
        return [
            {
                "exercise_name": exercise.exercise_name,
                "muscle_group": exercise.muscle_group,
                "sets": exercise.sets,
                "rep_range": exercise.rep_range,
                "rest_time_range": exercise.rest_time_range,
            }
            for exercise in self.workout
        ]

    @property
    def load_per_muscle_group(self):
        load_dict = dict((muscle_group, 0) for muscle_group in MuscleGroup.ALL)
        for exercise in self.workout:
            load_dict[exercise.muscle_group] += 1
        return load_dict

    @property
    def exercise_names(self):
        return [exercise.exercise_name for exercise in self.workout]

    def __str__(self) -> str:
        return str(self.df) + f"\nTotal Time: {str(int(self.total_time))} minutes"


class AutoGeneratedWorkout(BaseWorkout):
    """Auto generates a workout based on time based conditions, previous workouts, strength exercise."""

    def __init__(
        self,
        target_time,
        muscle_groups: List[MuscleGroup],
        strength_exercise_name: str = None,
        equipment_available=EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None,
    ):
        self.target_time = target_time
        self.strength_exercise_name = strength_exercise_name
        self.muscle_groups = muscle_groups
        self.equipment_available = equipment_available
        self.caps = TIME_BASED_CONDITIONS[target_time]["caps"]
        self.prior_cummative_loads_per_muscle_group = (
            prior_cummative_loads_per_muscle_group
            or dict((muscle_group, 0) for muscle_group in MuscleGroup.ALL)
        )
        self.excluded_exercise_names = excluded_exercise_names or []


        super().__init__(exercises=exercises or [])

    def _create(self):
        self._auto_generate()

    def _post_create(self):
        self.sort_workout_by_intensity()

    def _auto_generate(self):
        """Automatically generates workout based on conditions."""
        tbc = TIME_BASED_CONDITIONS[self.target_time]
        if (
            len(tbc["allowed_strength_loads"]) > 0
            and self.strength_exercise_name != None
        ):
            strength_exercise = StrengthExercise(
                self.strength_exercise_name, allowed_loads=tbc["allowed_strength_loads"]
            )
            tbc_set_reduction(tbc, strength_exercise)
            self.add_exercise(strength_exercise)

        for endurance_probability in tbc["endurance_exercises_probabilities"]:
            n = random.uniform(0, 1)
            if n <= endurance_probability:
                muscle_to_work = self._find_next_muscle_group_to_work()
                endurance_exercise = pick_random_exercise(
                    [muscle_to_work],
                    ExerciseType.ENDURANCE,
                    excluded_exercise_names=self.excluded_exercise_names,
                    equipment_available=self.equipment_available,
                )
                self.excluded_exercise_names.append(endurance_exercise.exercise_name)
                tbc_set_reduction(tbc, endurance_exercise)
                self.add_exercise(endurance_exercise)

        while self.total_time < self.target_time - min(
            self.target_time // 15 * 2.5, 7.5
        ):
            muscle_to_work = self._find_next_muscle_group_to_work()
            hypertrophy_exercise = pick_random_exercise(
                [muscle_to_work],
                ExerciseType.HYPERTROPHY,
                excluded_exercise_names=self.excluded_exercise_names,
                equipment_available=self.equipment_available,
            )
            self.excluded_exercise_names.append(hypertrophy_exercise.exercise_name)
            tbc_set_reduction(tbc, hypertrophy_exercise)
            self.add_exercise(hypertrophy_exercise)

    def _find_next_muscle_group_to_work(self):
        """Finds muscle group with least load"""
        trainable_loads = {
            muscle_group: load
            for muscle_group, load in self.cummulative_load_per_muscle_group.items()
            if muscle_group in self.muscle_groups
            and (
                muscle_group in self.caps
                and self.caps[muscle_group] <= self.load_per_muscle_group[muscle_group]
            )
            == False
        }
        min_muscle = min(trainable_loads, key=trainable_loads.get)
        return min_muscle

    def regenerate_exercise(exercise_ind: int):
        """Regenerates one exercise of chose. Ensures that is same muscle group is worked and exercises use equipment available"""
        pass

    @property
    def cummulative_load_per_muscle_group(self):
        """Total workout load per muscle group for entire week at current state of workout and previous workouts."""
        return dictionary_addition(
            (self.prior_cummative_loads_per_muscle_group, self.load_per_muscle_group)
        )


class UpperBodyWorkout(AutoGeneratedWorkout):
    def __init__(
        self,
        target_time,
        strength_exercise_name: str = None,
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None
    ):
        muscle_groups = [
            MuscleGroup.BACK,
            MuscleGroup.CHEST,
            MuscleGroup.BICEPS,
            MuscleGroup.TRICEPS,
            MuscleGroup.DELTOIDS,
        ]
        super().__init__(
            target_time,
            muscle_groups,
            strength_exercise_name,
            equipment_available,
            excluded_exercise_names or [],
            exercises or [],
            prior_cummative_loads_per_muscle_group or {},
        )


class LowerBodyWorkout(AutoGeneratedWorkout):
    def __init__(
        self,
        target_time,
        strength_exercise_name: str = None,
        equipment_available: List[EquipmentAvailable] = EquipmentAvailable.ALL,
        excluded_exercise_names: List[str] = None,
        prior_cummative_loads_per_muscle_group: dict = None,
        exercises: List[Exercise] = None
    ):
        muscle_groups = [MuscleGroup.QUAD, MuscleGroup.HAMSTRINGS, MuscleGroup.CALVES]
        super().__init__(
            target_time,
            muscle_groups,
            strength_exercise_name,
            equipment_available,
            excluded_exercise_names or [],
            exercises or [],
            prior_cummative_loads_per_muscle_group or {},
        )

Add corresponding values in dictionary. This is where all the specifics are implemented. Magic happens here! Runs after _create is called. Adds specific exercise to workout Sorts workouts by:         1. First priority is ExerciseType         2. Second priority is ExerciseLoad Picks single random exercise within rules determined by parameters and time dependencies. <strict> determines if muscle_groups_targeted should exactly match up Returns total time in minutes of workout Auto generates a workout based on time based conditions, previous workouts, strength exercise. Automatically generates workout based on conditions. Finds muscle group with least load Regenerates one exercise of chose. Ensures that is same muscle group is worked and exercises use equipment available Total workout load per muscle group for entire week at current state of workout and previous workouts.